以此文章来记录我的nginx核心知识学习过程

nginx基础架构

nginx请求处理流程

nginx请求处理流程

中间为称之为状态机的原因是因为nginx的非阻塞的时间驱动处理引擎epoll,epoll提供的3个方法都不是异步的,而是同步的。

nginx进程架构

nginx进程架构

nginx主要由master进程来管理底下的worker进程,那么为什么是多进程结构而不是多线程结构,主要原因是,因为nginx要保证它的高可用性和高可靠性,多线程结构会使用同一个线程池,而如果因为某个第三方模块引发了一个地址空间越界或者其他错误时,那么就会导致整个nginx进程全部挂掉,所以是多进程结构。

所有的worker来处理真正的请求,master进程只做监控worker进程,cache loader做缓存的载入,cache manager做缓存的管理,进程间的通信都是使用共享内存来进行解决。那么为什么worker进程会很多,是因为nginx采用了四列驱动模型,希望每个worker进程独享一个CPU,所以worker进程的数量一般为CPU的个数,同时可能需要对各个worker进程绑定它的CPU

nginx中的信号

nginx中的信号

  • TERM,INT信号代表立刻结束
  • QUIT代表优雅地结束,也就是等待当前链接处理完成后再关闭
  • HUP代表master进程会关闭旧worker进程,开启一个新的worker进程。也就是reload
  • USR1一般用来做日志切割,等于nginx命令中的reopen
  • CHLD信号,是master进程管理worker进程的方式,因为linux中规定当子进程终止时,会向父进程发送CHLD信号

一般是通过向master进程发起信号来管理worker进程,而不是直接向worker进程发送信号~

nginx reload的流程

reload流程reload流程图

nginx热升级流程

nginx热升级
nginx热升级

其中新的master进程是老的master进程的子进程,所以当老的master进程关闭时,新的master进程会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。所以老master推出后,新的master并不会退出。

worker进程如何优雅地关闭

worker的优雅关闭

所谓优雅地关闭是指nginx的worker进程能够识别出当前正在处理的请求,等待处理完成后再把连接进行关闭,对于有些协议nginx无法做到,例如websocket请求,tcp层反向代理。但是对于http请求,nginx是能够做到识别的,所以优雅地关闭仅针对http请求。

worker_shutdown_timeout这个功能默认不打开

网络收发与nginx事件的对应关系

网络传输
tcp流与报文

MTU默认为1500个字节,MSS=MTU-20-20,MTU处于网络层,MSS处于传输层
TCP协议与非阻塞接口

nginx事件驱动模型

nginx事件循环

其中图中的方框代表从kernel中获取等待处理的事件,那么nginx是使用的epoll网络事件收集器的模型,如图所示为性能测试图标

性能测试
epoll

假设有100万个并发连接,可能活跃的连接只有几百个,select或者poll的实现会将100万个连接全部扔给操作系统,让系统判断哪些连接是有事件发生的。而epoll则是利用活跃连接比较小的特性,减少了进程间切换的次数,遍历循环及内存拷贝的工作,利用红黑树和链表的数据结构,链表中仅仅只有活跃的连接,所以取活跃连接的时候只需要遍历这个链表

所以总结来说,select和poll最大的问题是,每次都需要传递全部并发fd,而实际只有少量的fd数据是活跃的,需要处理的。所以它效率低下,而epoll通过epoll_ctl和epoll_wait分解了这个问题,效率大幅提高。

nginx请求切换

请求切换

传统服务在处理时,每一个进程同一时间只处理一个请求,阻塞类的进程必然会导致进程的来回切换,每做一次切换会消耗一定的时间,大约是5微秒,当并发连接增大时,这是一个指数级的增长,会消耗大部分计算资源。所以传统服务依赖的是操作系统进程调度的方法来实现并发连接数。

nginx在蓝色处理请求不满足时,直接在用户态就切换到了绿色的处理请求,这样就少了进程间切换的成本,除非是worker使用的时间片到了。往往会把worker的优先级提高,来加大他的时间片。

用户态简单来说是操作系统为提升可靠性,将应用程序操作内存成为用户态,OS本身为内核态,边缘出发相对于水平触发而言,是epoll的两种触发方式。

同步&异步,阻塞&非阻塞

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。


阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

总结:
- 同步异步更加倾向于相对于操作结果来说,会不会等待结果返回。
- 阻塞非阻塞更加倾向于可不可以去干其他事,线程是否被阻塞

阻塞调用
非阻塞调用

nginx模块

nginx模块

我们可以到nginx的官方文档查看到nginx提供的官方模块的详细文档。

nginx编译通过--add-module来添加模块,在objs/ngx_modules.c中的ngx_module_t *ngx_modules[]中可以看到已经所有已经编译进nginx的模块

nginx模块

ngx_module_t是每个模块必须具备的数据结构,那么根据业务场景的不同需要划分不同的子模块,ngx_module_t中有个成员为type,定义了这个模块是属于哪个类型的模块
nginx模块分类

在每个module中会把这个模块通用的逻辑写在_core中,例如event_core,http_core_module等。
操作查看module

我们可以看到core只是nginx核心框架代码,并不是指nginx的core_module,而event,http,mail等则是指所有子类型的模块都处在这里。这里我们进入http中,那么每个子模块中都有一个核心模块,定义了这个模块的工作方式,ngx_http.c为http模块的核心模块,我们看下它的源码。

http模块核心文件代码

可以看到它属于的模块类型为ngx_core_module,在http目录下都是框架类的代码,而官方提供的非框架类代码都在http/modules目录下。

nginx如何通过连接池处理网络请求

连接请求

在右侧的ngx_cycle_t中,每次worker进程都有个独立的ngx_cycle_t数据结构,其中有connections数组,这就是所谓的连接池数组,这个数据的大小可以在worker connections这个配置项中观察到,其中可以看到worker_connections 512;也就是默认会有个512大小的数组来处理连接,每个数组就代表一个连接,nginx处理的请求数量一般都是以万,百万为单位计算的,往往这个值需要修改

通过这句话It should be kept in mind that this number includes all connections (e.g. connections with proxied servers, among others), not only connections with clients.我们可以得知,这个连接数不仅仅局限于客户端的连接,也包括与上游服务器之间的连接。

回到上面那张图,每个连接对应着一个读写事件,read_events和write_events。三个数据的连接是通过序号进行对应,所以我们在考虑nginx能够释放多大的性能时,首先要保证worker_connections足够使用,但是worker_connections所指向的数组同时影响了我们打开的内存,当我们配置了更大的worker_connections意味着系统使用了更大的内存。所以每个connections连接具体使用了多少内存呢?

核心数据结构

每个connections就是ngx_connection_s这个结构体,这个结构体在64位操作系统中大约占用了232字节,具体根据nginx版本的不同可能会有差异。nginx中每个事件对应的结构体叫做ngx_event_s,他所占用的字节数为96字节,所以当我们建立一个连接时,大约消耗的字节数为232+96x2=424 (读事件和写事件),这个数字描述的是连接的结构体需要的内存。

其中我们来关注图片中bytes_sent这个变量,可以在这个页面中看到官方对于他的定义number of bytes sent to a client代表发送给客户端的字节数,如果开启了gzip,则代表了压缩后的字节。

内存池对性能的影响

内存池

在http_core_module中我们可以看到这个两个参数connection_pool_sizerequest_pool_size前者大小为256(32bit)|512(64bit),分配大的内存池可以有效减少分配内存的次数;后者请求内存大小默认大小为4k,之所以差距这么大,因为对于连接而言,需要保存的上下文信息非常少,只需要帮助后续的请求,读最初的一部分字节即可,而对于请求而言,需要保存大量的上下文信息,比如所有读取到的url或者header

所有worker进程协同工作的关键:共享内存池

nginx进程间的通讯方式

如果需要做数据的同步,就需要共享内存。所谓共享内存就是打开了一块内存,多个worker进程之间可以同时访问它,读取或者写入。但是这必然会导致一个问题,脏数据,所以需要引入锁机制。现在的nginx版本中锁都是自旋锁,而不是基于早起linux的一种信号量基础(会导致进程进入休眠状态,也就是发生了主动切换)。自旋锁是当锁的条件没有满足,例如一块内存被1号worker进程使用时,那么2号worker进程需要去获取锁的时候,只要1号进程没有释放锁,那么2号进程会一直不停地去请求这把锁。所以使用自旋锁要求所有的nginx模块都要快速地使用共享内存,也就是快速取得锁,快速释放锁,一旦某个第三方模块不遵守这样的约定时,就会出现死锁,性能下降的问题。

同时共享内存会引来第二个问题,因为是多个进程使用同一个内存池,那么我们在程序中手动将内存分配给程序是十分繁琐的,所以我们用到Slab内存管理器来解决这个问题。

下图为nginx哪些官方模块使用了共享内存:
共享内存的模块

其中ngx_http_lua_api为openresty核心模块,下图为openresty共享内存代码:

# 使用红黑树来保存key-value,使用链表来保证当应用业务代码超过10M的限制时,lua的shared_dict处理方式是lru淘汰,也就是最早的节点如果长时间不用,会优先被淘汰掉

http{
    lua_shared_dict dogs 10m;
    server{
        location /set{
            content_by_lua_block{
                local dogs = ngx.shared.dogs
                dogs:set("Jim", 8)
                ngx.say("STORED")
            }
        }
        location /get{
            content_by_lua_block{
                local dogs = ngx.shared.dogs
                ngx.say(dogs:get("Jim"))
            }
        }
    }
}

持续更新中。。。

版权声明:本文为原创文章,版权归 heroyf 所有
本文链接: https://heroyf.club/2019/03/nginx-100/


“苹果是给那些为了爱选择死亡的人的奖励”