Nginx作为高性能服务器的缘由以及请求过程
阅读原文时间:2022年04月23日阅读:1

Nginx作为高性能服务器的缘由以及请求过程

简介: Nginxx采用的是多进程(单线程)&多路IO复用模型,使用I/O多路复用技术的Nginx,就成了"并发事件驱动"的服务器,同时使用sendfile等技术,最终实现了高性能,主要从以下几个方面讲述Nginx高性能机制:

  • IO多路复用机制
  • Nginx master-worker进程机制
  • sendfile零拷贝机制

Nginx master-worker进程机制

web请求处理机制

  • 多进程方式:服务器每接收到一个客户端请求就有服务器的主进程生成一个子进程响应客户端,直

到用户关闭连接,这样的优势是处理速度快,子进程之间相互独立,但是如果访问过大会导致服务

器资源耗尽而无法提供请求。

  • 多线程方式:与多进程方式类似,但是每收到一个客户端请求会有服务进程派生出一个线程和此客

户端进行交互,一个线程的开销远远小于一个进程,因此多线程方式在很大程度减轻了web服务器

对系统资源的要求,但是多线程也有自己的缺点,即当多个线程位于同一个进程内工作的时候,可

以相互访问同样的内存地址空间,所以他们相互影响,一旦主进程挂掉则所有子线程都不能工作

了,IIS服务器使用了多线程的方式,需要间隔一段时间就重启一次才能稳定。

Nginx是多进程组织模型,而且是一个由Master主进程和Worker工作进程组成。

主进程 ( master process ) 的功能:

#对外接口:接收外部的操作(信号)
#对内转发:根据外部的操作的不同,通过信号管理 Worker
#监控:监控 worker 进程的运行状态,worker 进程异常终止后,自动重启 worker 进程
#读取Nginx 配置文件并验证其有效性和正确性
#建立、绑定和关闭socket连接
#按照配置生成、管理和结束工作进程
#接受外界指令,比如重启、升级及退出服务器等指令
#不中断服务,实现平滑升级,重启服务并应用新的配置
#开启日志文件,获取文件描述符
#不中断服务,实现平滑升级,升级失败进行回滚处理
#编译和处理perl脚本

工作进程 ( worker process ) 的功能

#所有 Worker 进程都是平等的
#实际处理:网络请求,由 Worker 进程处理
#Worker进程数量:一般设置为核心数,充分利用CPU资源,同时避免进程数量过多,导致进程竞争CPU资源,增加上下文切换的损耗
#接受处理客户的请求
#将请求依次送入各个功能模块进行处理
#I/O调用,获取响应数据
#与后端服务器通信,接收后端服务器的处理结果
#缓存数据,访问缓存索引,查询和调用缓存数据
#发送请求结果,响应客户的请求
#接收主程序指令,比如重启、升级和退出等

IO多路复用机制

在介绍IO多路复用前先介绍一下网络IO:

​ 网络通信就是网络协议栈到用户空间进程的IO就是网络IO

网络IO过程详解:

1. 客户端发送请求,与服务端建立三次握手,将数据报文发送到网卡
2. 网卡把数据报文copy到内核空间
3. 内核空间把数据copy到nginx的进程空间(假设是nginx的web服务)
4. nginx对请求数据进行分析,如需要得到某个磁盘文件(应用程序无权限得到磁盘数据),继续将数据发送给内核空间,让内核帮忙去得到磁盘的数据
5. 内核发送指令,通过DMA直接把磁盘的数据加载到内核空间
6. 内核空间copy到用户空间
7. 用户空间封装响应报文头部再把数据返回到内核空间
8. 内核空间再把数据通过网卡发送出去

网络I/O 处理过程:

获取请求数据,客户端与服务器建立连接发出请求,服务器接受请求(1-3)
构建响应,当服务器接收完请求,并在用户空间处理客户端的请求,直到构建响应完成(4)
返回数据,服务器将已构建好的响应再通过内核空间的网络 I/O 发还给客户端(5-7)

不论磁盘和网络I/O:

每次I/O,都要经由两个阶段:
第一步:将数据从文件先加载至内核内存空间(缓冲区),等待数据准备完成,时间较长
第二步:将数据从内核缓冲区复制到用户空间的进程的内存中,时间较短

多路复用IO指一个线程可以同时(实际是交替实现,即并发完成)监控和处理多个文件描述符对应各自

的IO,即复用同一个线程

一个线程之所以能实现同时处理多个IO,是因为这个线程调用了内核中的SELECT,POLL或EPOLL等系统调

用,从而实现多路复用IO

过程详解:

1.客户端发送的请求交给select(相当于代理,可以是poll也可以是epoll),select把所有请求发给内核,等待select完成(没完成前阻塞在select)
2.内核返回信息后再经过系统调用把数据从内核复制到用户空间

I/O multiplexing 主要包括:select,poll,epoll三种系统调用,select/poll/epoll的好处就在于单个

process就可以同时处理多个网络连接的IO。

它的基本原理就是select/poll/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数

据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,

当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从

kernel拷贝到用户进程。

Apache prefork是此模式的select,worker是poll模式。

IO多路复用(IO Multiplexing) :是一种机制,程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”
IO多路复用一般和NIO一起使用的。NIO和IO多路复用是相对独立的。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用,可以只用IO多路复用 + BIO,这时还是当前线程被卡住。IO多路复用和NIO是要配合一起使用才有实际意义
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,就通知该进程多个连接共用一个等待机制,本模型会阻塞进程,但是进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的IO操作上
用户首先将需要进行IO操作添加到select中,同时等待select系统调用返回。当数据到达时,IO被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视IO,以及调用select函数的额外操作,效率更差。并且阻塞了两次,但是第一次阻塞在select上时,select可以监控多个IO上是否已有IO操作准备就绪,即可达到在同一个线程内同时处理多个IO请求的目的。而不像阻塞IO那种,一次只能监控一个IO
虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只是注册自己需要的IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO模型,而非真正的异步IO

优缺点

  • 优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述

符一个线程),这样可以大大节省系统资源

  • 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处

理需要 2 次系统调用,占用时间会有增加

IO多路复用适用如下场合:

  • 当客户端处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用
  • 当一个客户端同时处理多个套接字时,此情况可能的但很少出现
  • 当一个服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用
  • 当一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
  • 当一个服务器要处理多个服务或多个协议,一般要使用I/O复用

模型性能比较

性能差异的原因

Select:
POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理
缺点
单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义FD_SETSIZE,再重新编译内核实现,但是这样也会造成效率的降低单个进程可监视的fd数量被限制,默认是1024,修改此值需要重新编译内核对socket是线性扫描,即采用轮询的方法,效率较低
select 采取了内存拷贝方法来实现内核将 FD 消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:
本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态其没有最大连接数的限制,原因是它是基于链表来存储的大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd select是边缘触发即只通知一次
epoll: 在Linux 2.6内核中提出的select和poll的增强版本支持水平触发LT和边缘触发ET,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次
使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
优点:
没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口),具体查看/proc/sys/fs/file-max,此值和系统内存大小相关
效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关
内存拷贝,利用mmap(Memory Mapping)加速与内核空间的消息传递;即epoll使用mmap减少复制开销
总结: 1、epoll只是一组API,比起select这种扫描全部的文件描述符,epoll只读取就绪的文件描述符,再加入
基于事件的就绪通知机制,所以性能比较好
2、基于epoll的事件多路复用减少了进程间切换的次数,使得操作系统少做了相对于用户任务来说的无用功。
3、epoll比select等多路复用方式来说,减少了遍历循环及内存拷贝的工作量,因为活跃连接只占总并发连
接的很小一部分。

范例: 最大并发连接数和内存有直接关系

#内存1G
[root@centos8 ~]#free -h
             total       used       free     shared buff/cache   available
Mem:         952Mi       168Mi       605Mi       12Mi       178Mi       629Mi
Swap:         2.0Gi         0B       2.0Gi
[root@centos8 ~]#cat /proc/sys/fs/file-max
92953
#内存2G
[root@centos8 ~]#free -h
             total       used       free     shared buff/cache   available
Mem:          1.9Gi       258Mi       1.3Gi       12Mi       341Mi       1.6Gi
Swap:         2.0Gi         0B       2.0Gi
[root@centos8 ~]#cat /proc/sys/fs/file-max
195920

范例: 内核限制

[root@centos8 ~]#grep -R FD_SETSIZE linux-5.8/*
linux-5.8/Documentation/userspace-api/media/v4l/func-select.rst:  
``FD_SETSIZE``.
linux-5.8/include/uapi/linux/posix_types.h:#undef __FD_SETSIZE
linux-5.8/include/uapi/linux/posix_types.h:#define __FD_SETSIZE 1024 #单个进程能够监视的文件描述符的文件最大数量
linux-5.8/include/uapi/linux/posix_types.h: unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
linux-5.8/tools/include/nolibc/nolibc.h:#define FD_SETSIZE 256
linux-5.8/tools/include/nolibc/nolibc.h:typedef struct { uint32_t
fd32[FD_SETSIZE/32]; } fd_set;
linux-5.8/tools/include/nolibc/nolibc.h: if (fd < 0 || fd >= FD_SETSIZE)
linux-5.8/tools/testing/selftests/net/nettest.c: rc = select(FD_SETSIZE,
  • sendfile零拷贝机制

传统的数据访问方式:

传统的 Linux 系统的标准 I/O 接口(read、write)是基于数据拷贝的,也就是数据都是 copy_to_user 或者 copy_from_user,这样做的好处是,通过中间缓存的机制,减少磁盘 I/O 的操作,但是坏处也很明显,大量数据的拷贝,用户态和内核态的频繁切换,会消耗大量的 CPU 资源,严重影响数据传输的性能,统计表明,在Linux协议栈中,数据包在内核态和用户态之间的拷贝所用的时间甚至占到了数据包整个处理流程时间的57.1%

MMAP技术(Memory Mapping):

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问。

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。

实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间

对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。内存映射减少数据在用户空间和内核空间之间的拷贝操作,适合大量数据传输

上面左图为传统读写,右图为MMAP.两者相比mmap要比普通的read系统调用少了一次copy的过程。因为read调用,进程是无法直接访问kernel space的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(page cache)。

sendfile拷贝机制:

过程:

直接复制到内核空间,内核空间在复制到sockert缓存,然后通过缓存发送到网卡

http uwsgi