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发信号,不是进程间数据通信
- 这个人的相关文章也推荐,风格很喜欢,清晰明了值得学习
-
USER2
和WINCH
是通过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_size
和request_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模块,学习模块指令的使用,理解变量的强大作用