nginx架构

  • 简介
  • 请求处理流程
  • 进程结构
  • 进程管理
  • 网络事件
  • 请求切换
  • 相关概念
  • nginx模块
  • 内存池
  • 进程通信
  • slab内存管理器
  • nginx容器
  • 哈希表
  • 红黑树
  • 动态模块
  • 小结


简介

  • 上一波总结了nginx的基本使用方法
  • nginx作为边缘节点所要承受的压力可能比业务服务器大几个数量级,意味着会把普通场景下的问题放大数倍
  • 这里进一步了解nginx处理流程,如何控制进程,解决并发难题

请求处理流程

  • nginx进程采用Master-Worker架构模型?
  • worker进程的数量和CPU核数匹配,为何这样设计?
  • 带着这两个问题,先来了解请求处理流程:
  • 核心在非阻塞的事件驱动处理引擎(epoll),需要状态机正确识别处理请求
  • 如果服务器内存不足,退化为阻塞调用,需要线程池(那堆白色波浪线)

进程结构

  • 单进程结构(不常用)
  • 多进程结构
  • 使用进程而不是线程,是因为线程共享同一进程的资源空间,如果某一线程出现类似地址越界的错误,此进程挂掉,其他线程也没了
  • 进程结构图:
  • master进程负责监控,worker负责处理请求
  • 缓存是进程间共享的,也交由worker处理,CacheM和CacheL负责管理缓存
  • worker和cache管理进程都是master的子进程
  • worker的数量一般要和CPU核数匹配,每个worker都能绑定一个核
  • 回顾进程命令
  • ps -ef |grep nginx 查看nginx的进程号
  • nginx -s reload 优雅重启nginx/重新加载配置文件
  • nginx -s stop nginx停止服务
  • nginx -s quit 优雅停止nginx,有连接时会等连接请求完成再杀死worker进程
  • 本质都是向Master进程发送信号

进程管理

  • 进程管理主要是进程通信,可以使用管道、共享内存或者信号,nginx使用信号
  • 注:这里相当于master对worker发信号,不是进程间数据通信
  • 这个人的相关文章也推荐,风格很喜欢,清晰明了值得学习
  • USER2WINCH是通过Linux的kill命令针对进程pid控制父进程的
  • 通常不对子进程进行上述命令,都是通过Master
  • nginx的命令行会在logs文件夹中找到pid执行
  • reload流程
  • 不停止服务(处理请求的同时),把配置文件平滑更新,进本流程如下:
  • 如果之前的worker进程出问题没有退出,一般会设置shutdown timeout,强制退出
  • 热部署完整流程
  • 旧的master进程一般不会立即关闭,方便回滚,具体流程如下:
  • 新的master进程还是旧master的子进程,但是用新的nginx二进制文件启动
  • 优雅的quit
  • 针对worker:

网络事件

  • nginx是事件驱动的,这里的事件主要是网络请求
  • 先了解一下网络请求的大致过程:(计算机网络需要掌握)
  • 如下图,从上到下会在各层协议的控制下添加头部传递,从下到上会识别各层头部信息将数据送到上一次处理
  • 在传输层获取TCP报文,这里规定了对应的端口号(应用的端口间通信,例如80)
  • 每收到一个报文,其实就是一个事件,可以分为读事件和写事件
  • 在任何异步事件的处理框架中(例如nginx),都会有一个事件收集分发器,会定义每一类事件的消费者(处理者):
  • 概念开始抽象起来了,此时就需要具象的理解一下,让自己先接受;
  • 之前说了TCP报文,里面包含的信息会交给对应端口的应用,这里交给nginx,在使用我们的应用配置之前(nginx.conf),要先对来的这波信息分析,干什么?谁干?然后才是怎么干!
  • 这是事件驱动下的异步处理框架的关键所在(大佬处理的是这部分)
  • 下面是通过wireshark抓包得到的TCP三次握手过程,感受一下读事件:
  • 这个软件可以安装一下,也可以看到nginx是如何响应建立链接的
  • nginx事件循环
  • 使用epoll,事件分发(创建进程)后会有事件队列(等着worker将其出队处理)
  • 问题来了,为什么要事件循环,岂不是有违事件驱动的思想?
  • 其实很简单,因为一个worker可能陆续接到多个事件请求,此时的关键不再是谁驱动,而是怎么调度CPU处理好所有的请求
  • 我们在nginx应用层面避免这种“轮询”是因为边缘可能压力巨大,节省性能
  • epoll介绍
  • 既然提到epoll,回顾一下,核心主要有两点:就绪列表和功能分离
  • 功能分离指,相对于select,将维护等待队列(epoll_ctl)和阻塞进程(epoll_wait)分开处理
  • epoll是网络编程中的多路复用技术,是协调进程处理网络请求
  • socket是文件系统管理的对象,由进程创建,或者说socket对象中有进程的引用,方便唤醒此进程来处理接收到消息的socket
  • 流程如下:
  • epoll_create在内核创建eventpoll对象(就绪列表rdlist是其成员)
  • 可以用epoll_ctl添加或删除所要监听的socket(活跃链接)到eventpoll
  • 某个socket收到数据后,中断程序会操作eventpoll对象,并给rdlist添加此socket引用
  • 当程序执行到epoll_wait时,如果rdlist已经引用了socket(需要相关进程被唤醒处理socket),那么epoll_wait直接返回(不执行),如果rdlist为空,阻塞进程(释放CPU资源)
  • 将epoll当成一段处理进程的程序,eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态
  • 小结:nginx通过epoll运行自己的事件驱动框架(框架分发,epoll处理),epoll是管理进程多路复用的对象,主要思想是维护队列、进程阻塞、就序列表

请求切换

  • 使用上面的框架,带来的益处有哪些呢?首先体现在请求切换方面
  • 给nginx-worker分配较大的时间片(提高优先级),在用户态代码即可完成请求的切换(单核CPU分配,epoll协调)
  • 下图是普通的进程间切换(左)和用户态切换(右)流程:

相关概念

  • 阻塞调用
  • 非阻塞下的异步调用:
  • 一般情况下,会使用openresty或nginx的JavaScript模块实现非阻塞
  • 这里不是很懂,涉及到使用Lua语言,先记录!

nginx模块

  • 有很多开发者为nginx开发第三方模块,了解一个nginx模块从以下几方面入手:
  • 编译进nginx
  • 提供哪些配置项
  • 使用场景
  • 提供哪些变量
  • 进入官网可以看到所有模块的详细说明,这里就说说怎么看编译后的模块信息吧:
  • ngx_http_gzip_module为例
  • 进入编译后的objs目录,会看到ngx_modules.c,查看之,能找到一个引用数组ngx_module_t *ngx_modules[],就包含了所有被编译进来的模块
  • 再进入src/http/modules目录,可以查看ngx_http_gzip_filter_module.c文件,这是源文件,主要看以下几部分
  • 名为ngx_command_t的结构体,顾名思义,定义了指令名
  • ngx_http_gzip_filter_module_ctx定义此模块上下文
  • 每个模块都有所属的应用,由此将模块分为不同类型的子模块,每种模块会具体化ctx(context,上下文,即在此应用场景下遵循的标准配置)
  • 可以发现,所有模块都会注册在ngx_module_t(源码中都包含此结构体)
  • 这里的ctx_index顺序在一定程度上决定了优先级
  • type就划分了模块属于哪个应用
  • 所有模块都可以支持基本的nginx指令,例如reload、quit、stop等
  • nginx是如何划分模块的呢?
  • 从上图可以看出,nginx的模块结构很灵活,如果有新的应用分类,可以集成
  • 也可以称为四个基本应用,原生nginx框架只定义了核心模块CORE和配置模块CONF,故将其他的属于应用模块的称为子模块
  • 进入src目录可以看到四大应用模块
  • 里面可有可无的模块放在modules文件夹
  • 名称中,http/event/mail/stream即所属应用;带有filter即过滤响应请求模块,带有upstream即负载均衡模块,其他都是为生成响应的
  • 核心数据结构
  • 模块数据结构决定了在使用时占用的内存空间,在官网Core functionality的worker_connections中可以看到:
Syntax:	worker_connections number;
Default:	
worker_connections 512;
Context:	events
  • 连接和事件相对应
  • 在处理events时建立的连接(socket)需要初始化结构体数据ngx_connection_s,每个约占232字节,调用模块处理时ngx_event_s约占96字节,即连接数量越多占用越大
  • 一般高并发场景时,我们需要将连接数量设置的足够大
  • 模块中的变量会在access等log中频繁使用,可在官网Embedded Variables了解

内存池

  • 内存池可以预分配小块内存,可有效减少内存碎片,nginx中将小块内存碎片链接起来
  • 请求内存池:处理请求时使用,一般4K,响应结束释放
  • 连接内存池:建立连接时使用,一般256|512,链接关闭时释放
  • 在ngx_http_core_module中可以看到connection_pool_sizerequest_pool_size
  • 内存池在开发第三方模块时很常用,恰当配置很重要

进程通信

  • worker进程之间通信通过共享内存的方式,之前的进程管理使用信号的方式
  • 共享内存是worker之间最有效的通信手段,主要应用在流控等场景
  • nginx中使用共享内存的模块:
  • 共享内存就一定会出现竞争问题,需要加锁:

slab内存管理器

  • 将共享内存分为多个固定大小的slot(例如16K),类似于操作系统中虚拟内存的页式分配
  • slot大小定为多少合适呢?需要监控业务场景决定,淘宝开源了类似OpenResty的基于nginx的Web平台Tengine,可以将其中的ngx_slab_stat编译进来
  • 但并没有独立提供,下载其源码包,进入modules找到slab,将其编译到OpenResty(别搞错)
./configure --add-module=/tmp/tengine-2.3.2/modules/ngx_slab_stat
gmake  # 不用管Error 2
mv /usr/local/openresty/nginx/sbin/nginx /usr/local/openresty/nginx/sbin/nginx.old
cp /tmp/openresty-1.19.3.1/build/nginx-1.19.3/objs/nginx /usr/local/openresty/nginx/sbin/
  • 测试代码:编写nginx.conf
# gzip on;
lua_shared_dict cat 10m;
server {
	......
	location =/slab_stat {
            slab_stat;
    }

    location /set {
            content_by_lua_block {
                    local cat=ngx.shared.cat
                    cat:set("Roy",8)
                    ngx.say("STORED")
            }
    }

    location /get {
            content_by_lua_block {
                    local cat=ngx.shared.cat
                    ngx.say(cat:get("Roy"))
            }
    }
    ......
}

nginx容器







哈希表

  • 哈希表可以提高查询效率
  • 定义在结构体ngx_hash_t,指向连续的一组键,每个键指向用户结构体数据,每个这样的键值对称为ngx_hash_elt_t
  • 哈希表只为静态不变内容服务,bucket_size需要考虑CPU的cache_line对齐问题,一般是64字节
  • 以下是常见的模块对哈希表的配置:

红黑树

  • nginx在共享内存时常用红黑树管理对象,当然不止于此
  • 使用数据结构ngx_rbtree_t描述,红黑树是一棵自平衡二叉查找树(BST)
  • 红黑树里提供了很多方法,可以直接调用不用担心性能问题

动态模块

  • 仅仅编译动态库而不用替换整个二进制文件(sbin/nginx),在包含大量第三方模块热部署时非常有用,替换掉动态库即可,然后reload
  • 如何实现呢?
  • ./configure开始,只有部分模块支持动态
  • 搞个刚解压的nginx包,预备…
./configure --with-http_image_filter_module=dynamic	# 以图片动态库为例
make
make install
  • 安装目录中会多出个modules,里面就是动态库,以.so结尾
  • 配置文件nginx.conf,做个简单的图片缩放:
load_module modules/ngx_http_image_filter_module.so;
......
server {
	......
	location / {
		root test;	# 图片放在test目录,按图片名访问
		image_filter resize 15 10;
	}
	......
	# 如果没效果禁用浏览器缓存
}
  • 如何重新编译动态库呢?还是先configure dynamic,此时将objs中生成的.so文件copy到安装目录的modules中即可(反正是load嘛,很灵活!),无需make;

小结

  • 主要了解了nginx的请求处理流程、进程结构、进程管理。关键在于理解事件驱动下的events分发和多路复用进程管理方法epoll
  • 下一节将梳理繁多的nginx模块,学习模块指令的使用,理解变量的强大作用