Nginx 再续前缘

大家好,我是Leo。

聊一下Nginx,文章分类主要是MySQLRedis秒杀系统RocketMQ计算机网络NginxMybatis设计模式大厂面试人生理财。先整理一下。方便粉丝更好的阅读,同时也方便自己不断的复习沉淀。

学习从未停止,技术永无止境,我们一起加油让学习成为习惯。如果阅读过程中有任何疑问,可以关注下面公众号联系我。

网络收发与Nginx对应关系

Nginx是以事件驱动的框架,事件主要指网络事件,每次连接都对应两个事件,写事件读事件

我们看一下下列的网络拓扑的流程图,主机A母庸质疑就是客户端A,主机B也是最终到达的地方也就是服务器B。重点来聊一下中间的路由器。

网络拓扑

1)从主机A的应用层处理到传输层(应用层发了请求,传输层会记录浏览器的端口与Nginx的端口)

2)传输层处理到网络层(网络层会记录目标主机的IP,NGINX的公网IP)

3)网络层处理到链路层(链路层会处理到我们家的路由器)

4)由以太网转发出去到设备运营商(运营商会根据设备里面的目标IP转发到下一层)

5)设备运营商之间的基站通信也可以说是广域网

6)最后回到以太网找到服务器B

7)到了服务器B再以相同的方式解析回去 (类似Tomcat)

路由器数据流

里面的细节可以参考 3万字聊聊计算机网络(一)

前面我们聊到了Nginx是一个以事件驱动的框架,那么必不可少的就是 事件收集,分发者(简称转发模块) 以及 各种类型 消费者。Nginx的所有操作都会向转发模块写入事件,这里也包括读事件。

事件堆积在转发模块之后,消费者会实时或者不定时的消费其中的事件。

Nginx也可以说是 生产者

事件收集,分发者也可以说是Linux的内核 kernel

Nginx事件模型

根据上述的关系,我们可以继续看一下Nginx是如何处理这些事件的。

Nginx刚启动时,是处于 WAIT FOR EVENTS ON COMMECTIONS

当请求过来时,不会直接让Nginx来处理,而是先经过操作系统,当kernel为用户处理完TCP的三次握手之后,再去通知Nginx的相关函数,告诉Nginx你可以继续处理。

通知完之后Nginx就走到了 RECEIVE QUEUE OF NEWEVENTS 。它开始处理事件的时候上述我们也介绍过了,会有一个转发模块来统一处理,他会向kernel索要相关的事件,kernel会把所有的事件推向消息队列,Nginx会一直消费消息队列中的待处理事件。

Nginx拿到事件之后会走到 PROCESS THE EVENTS QUEUE IN CYCLE ,开始处理事件。处理事件之后可以参考右面这张图。

Nginx处理事件模型

它会循环判断当前事件是否为空,不为空时,会处理这个事件,如果在处理过程中生成新的小事件会放入下面那个消息队列等待处理(比如具有延时效应的关闭事件)。

当前所有的事件都处理完成之后就会返回到最初的 WAIT FOR EVENTS ON COMMECTIONS

epoll的优劣及原理

上述事件模型我们介绍过了Nginx不断的从kernel内核中获取事件,那么这个是如何实现的呢?

它是通过 epoll 实现的。epoll 是 Linux 内核的可扩展 I/O 事件通知机制,其最大的特点就是性能优异。

为什么性能优异,优异在哪里? 暂且先打个问号。

阻塞

先看一下 阻塞的由来。以网卡接收数据的过程为例,为了方便理解,我们简化一下技术细节,把数据接收分为4个步骤

1)NIC 接收到数据,通过 DMA 方式写入内存(Ring Buffer 和 sk_buff)。

2)NIC 发出 IRQ,告诉内核有新的数据过来了。

3)Linux 内核响应中断,系统切换为内核态,处理 Interrupt Handler,从RingBuffer 拿出一个 Packet, 并处理协议栈,填充 Socket 并交给用户进程。

4)系统切换为用户态,用户进程处理数据内容。

【DMA:直接内存访问】【NIC:网卡】【IRQ:中断请求】【Interrupt Handler:终端处理程序】【Packet:数据包】

网卡何时接收到数据是依赖发送方和传输路径的,这个延迟通常都很高,是毫秒(ms)级别的。而应用程序处理数据是纳秒(ns)级别的。也就是说整个过程中,内核态等待数据,处理协议栈是个相对很慢的过程。这么长的时间里,用户态的进程是无事可做的,因此用到了 阻塞

阻塞是进程调度的关键一环,指的是进程在等待某事件发生之前的等待状态。当进程被阻塞时,是不会占用CPU资源的。

换个角度来讲。为了支持多任务,Linux 实现了进程调度的功能(CPU 时间片的调度)。而这个时间片的切换,只会在“可运行状态”的进程间进行。因此阻塞的进程是不占用 CPU 资源的。

阻塞恢复

内核当然可以很容易的修改一个进程的状态,问题是网络 IO 中内核该修改那个进程的状态。

socket 结构体,包含了两个重要数据:进程 ID端口号

进程 ID 存放的就是执行 connect,send,read 函数,被挂起的进程。在 socket 创建之初,端口号就被确定了下来,操作系统会维护一个端口号到 socket 的数据结构。

当网卡接收到数据时,数据中一定会带着端口号,内核就可以找到对应的 socket,并从中取得“挂起”进程的 ID。将进程的状态修改为“可运行状态”(加入到工作队列)。此时内核代码执行完毕,将控制权交还给用户态。通过正常的“CPU 时间片的调度”,用户进程得以处理数据。

上面介绍的整个过程,基本就是 BIO(阻塞 IO)的基本原理了。用户进程都是独立的处理自己的业务,这其实是一种符合进程模型的处理方式。

上下文切换优化

上面介绍的过程中,有两个地方 会造成频繁的上下文切换,效率可能会很低。

1)如果频繁的收到数据包,NIC 可能频繁发出中断请求(IRQ)。CPU 也许在用户态,也许在内核态,也许还在处理上一条数据的协议栈。但无论如何,CPU 都要尽快的响应中断。这么做实际上非常低效,造成了大量的上下文切换,也可能导致用户进程长时间无法获得数据。(即使是多核,每次协议栈都没有处理完,自然无法交给用户进程)

2)每个 Packet 对应一个 socket,每个 socket 对应一个用户态的进程。这些用户态进程转为“可运行状态”,必然要引起进程间的上下文切换。

通过 NAPI机制 + 单线程IO多路复用机制解决上下文切换问题

NAPI机制

NAPI机制是网卡驱动用来解决频繁 IRQ 的技术 。原理其实特别简单,把 Interrupt Handler 分为两部分。

1)函数名为 napi_schedule,专门快速响应 IRQ,只记录必要信息,并在合适的时机发出软中断 softirq。

2)函数名为 netrxaction,在另一个进程中执行,专门响应 napi_schedule 发出的软中断,批量的处理 RingBuffer 中的数据。

通过NAPI机制的优化 ,接收数据的流程如下

1)NIC 接收到数据,通过 DMA 方式写入内存(Ring Buffer 和 sk_buff)。

2)NIC 发出中断请求(IRQ),告诉内核有新的数据过来了。

3)driver 的 napi_schedule 函数响应 IRQ,并在合适的时机发出软中断(NET_RX_SOFTIRQ)

4)driver 的 net_rx_action 函数响应软中断,从 Ring Buffer 中批量拉取收到的数据。并处理协议栈,填充 Socket 并交给用户进程。

5)系统切换为用户态,多个用户进程切换为“可运行状态”,按 CPU 时间片调度,处理数据内容。

IO多路复用

IO 多路复用思路和 NAPI 是很接近的。每个 socket 不再阻塞读写它的进程,而是用一个专门的线程,批量的处理用户态数据,这样就减少了线程间的上下文切换。

作为 IO 多路复用的一个实现,select 的原理也很简单。所有的 socket 统一保存执行 select 函数的(监视进程)进程 ID。任何一个 socket 接收了数据,都会唤醒“监视进程”。内核只要告诉“监视进程”,那些 socket 已经就绪,监视进程就可以批量处理了。

对于内核,同时处理的 socket 可能有很多,监视进程也可能有多个。所以监视进程每次“批量处理数据”,都需要告诉内核它“关心的 socket”。内核在唤醒监视进程时,就可以把“关心的 socket”中,就绪的 socket 传给监视进程。

换句话说,在执行系统调用 select 或 epoll_create 时,入参是“关心的 socket”,出参是“就绪的 socket”。

而 select 与 epoll 的区别在于:

select (一次O(n)查找)

1)每次传给内核一个用户空间分配的 fd_set 用于表示“关心的 socket”。其结构(相当于 bitset)限制了只能保存1024个 socket。

2)每次 socket 状态变化,内核利用 fd_set 查询O(1),就能知道监视进程是否关心这个 socket。

3)内核是复用了 fd_set 作为出参,返还给监视进程(所以每次 select 入参需要重置)。然而监视进程必须遍历一遍 socket 数组O(n),才知道哪些 socket 就绪了。

epoll (全是O(1)查找)

1)每次传给内核一个实例句柄。这个句柄是在内核分配的红黑树 rbr+双向链表 rdllist。只要句柄不变,内核就能复用上次计算的结果。

2)每次 socket 状态变化,内核就可以快速从 rbr 查询O(1),监视进程是否关心这个 socket。同时修改 rdllist,所以 rdllist 实际上是“就绪的 socket”的一个缓存。

3)内核复制 rdllist 的一部分或者全部(LT 和 ET),到专门的 epoll_event 作为出参。所以监视进程,可以直接一个个处理数据,无需再遍历确认。

epoll 是对 select 和 poll 的改进,解决了“性能开销大”和“文件描述符数量少”这两个缺点,是性能最高的多路复用实现方式,能支持的并发量也是最大。

总结

  1. 基于数据收发的基本原理,系统利用阻塞提高了 CPU 利用率。
  2. 为了优化上线文切换,设计了“IO 多路复用”(和 NAPI)。
  3. 为了优化“内核与监视进程的交互”,设计了三个版本的 API(select,poll,epoll)

篇幅问题,这里没有介绍poll

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值