前滴滴出行技术专家,现任OPPO 文档数据库 mongodb 负责人,负责 oppo 千万级峰值 TPS/ 十万亿级数据量文档数据库 mongodb 研发和运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。后续持续分享《 MongoDB 内核源码设计、性能优化、最佳运维实践》, Github 账号地址 : https://github.com/y123456yz
前言:
服务端通常需要支持高并发业务访问,如何设计优秀的服务端网络IO
工作线程
/
进程模型对业务的高并发访问需求起着至关重要的核心作用。
本文总结了了不同场景下的多种网络IO 线程 / 进程模型,并给出了各种模型的优缺点及其性能优化方法,非常适合服务端开发、中间件开发、数据库开发等开发人员借鉴。

说明:
1. 所有网络 IO 事件 (accept 事件、读事件、写事件 ) 注册到 epoll 事件集
2. 主循环中通过 epoll_wait 一次性获取内核态收集到的 epoll 事件信息,然后轮询执行各个事件对应的回调。
3. 事件注册、 epoll_wait 事件获取、事件回调执行全部由一个线程处理
一个完整的请求处理过程主要包含以下几个部分:
步骤1 :通过 epoll_wait 一次性获取网络 IO 事件
步骤2 :读取数据及协议解析
步骤3 :解析成功后进行业务逻辑处理,然后应答客户端
1. 所有工作都由一个线程执行,包括 epoll 事件获取、事件处理 ( 数据读写 ) 、只要任一一个请求的事件回调处理阻塞,其他请求都会阻塞。例如 redis 的 hash 结构,如果 filed 过多,假设一个 hash key 包含数百万 filed ,则该 Hash key 过期的时候,整个 redis 阻塞。
2. 单线程工作模型, CPU 会成为瓶颈,如果 QPS 过高,整个 CPU 负载会达到 100% ,时延抖动厉害。
1. redis 缓存
2. 推特缓存中间件twemproxy
1. while (1) {
2. //epoll_wait等待网络事件,如果有网络事件则返回,或者超时范围
3. size_t numevents= epoll_wait();
4.
5. //遍历前面epoll获取到的网络事件,执行对应事件回调
6. for (j = 0; j < numevents; j++) {
7. if(读事件) {
8. //读数据
9. readData()
10. //解析
11. parseData()
12. //读事件处理、读到数据后的业务逻辑处理
13. requestDeal()
14. } else if(写事件) {
15. //写事件处理,写数据逻辑处理
16. writeEentDeal()
17. } else {
18. //异常事件处理
19. errorDeal()
20. }
21. }
22. }
说明: 后续多线程/ 进程模型中,每个线程 / 进程的主流程和该 while() 流程一致。
由于之前工作需要,需要对redis 内核做二次优化开发,因此对整个 redis 代码做了部分代码注释,同时把 redis 的网络模块独立出来做成了简单 demo ,该 demo 对理解 epoll 网络事件处理及 Io 复用实现会有帮助,代码比较简短,可以参考如下地址 :
该线程模型图如下图所示:

说明:
1. listener 线程负责接受所有的客户端链接
2. listener 线程每接收到一个新的客户端链接产生一个新的 fd ,然后通过分发器发送给对应的工作线程 (hash 方式 )
3. 工作线程获取到对应的新链接 fd 后,后续该链接上的所有网络 IO 读写都由该线程处理
4. 假设有 32 个链接,则 32 个链接建立成功后,每个线程平均处理 4 个链接上的读写、报文处理、业务逻辑处理
1.
进行
accept
处理的
listener
线程只有一个,在瞬间高并发场景容易成为瓶颈
2.
一个线程通过
IO
复用方式处理多个链接
fd
的数据读写、报文解析及后续业务逻辑处理,这个过程会有严重的排队现象,例如某个链接的报文接收解析完毕后的内部处理时间过长,则其他链接的请求就会阻塞排队
memcache 缓存,适用于内部处理比较快的缓存场景、代理中间场景。 memcache 源码实现中文分析可以详见 : memcache 源码实现分析
该模型原型图如下:

说明:
1. Linux kernel 3.9 开始支持 reuseport 功能,内核协议栈每获取到一个新链接自动均衡分发给用户态 worker 线程。
2. 该模型解决了模型一的 listener 单点瓶颈问题 , 多个进程/ 线程同时做为 listener ,都可以 accept 客户端新链接。
3.1
该网络进程
/
线程模型缺陷
reuseport
支持后,内核通过负载均衡的方式分发不同新链接到多个用户态
worker
进程
/
线程
,
每个进程/
线程
通过IO
复用方式处理多个
客户端新
链接fd
的数据读写、报文解析、解析后的业务逻辑处理
。
每个工作进程/
线程同时处理多个链接的请求
,
如果
某个链接的报文接收解析完毕后的内部处理时间
过长
,则其他链接的请求就会阻塞排队
。
该模型虽然解决了 listener 单点瓶颈问题 , 但是工作线程内部的排队问题没有解决。
不过,Nginx 作为七层转发代理,由于都是内存处理,所以内部处理时间比较短,所以适用于该模型。
1. nginx(nginx 用的是进程,模型原理一样 ) ,该模型适用于内部业务逻辑简单的场景,如 nginx 代理等
2. reuseport 支持性能提升过程可以参考我另一篇分享: https://my.oschina.net/u/4087916/blog/3016162
Nginx 多进程高并发、低时延、高可靠机制在缓存 (redis 、 memcache)twemproxy 代理中的应用
该线程模型图如下图:

说明:
1. listener 线程负责接受所有的客户端链接
2. listener 线程每接收到一个新的客户端链接就创建一个线程,该线程只负责处理该链接上的数据读写、报文解析、业务逻辑处理。
1. 一个链接创建一个线程,如果 10 万个链接,那么就需要 10 万个线程,线程数太多,系统负责、内存消耗也会很多
2. 当链接关闭的时候,线程也需要销毁,频繁的线程创建和消耗进一步增加系统负载
1. mysql 默认方式、 mongodb 同步线程模型配置,适用于请求处理比较耗时的场景,如数据库服务
2. Apache web 服务器,该模型限制了 apache 性能, nginx 优势会更加明显
该线程模型图如下图所示:

说明:
1. listener 线程接收到一个新链接 fd 后,把该 fd 交由线程池处理,后续该链接的所有读写、报文解析、业务处理都由线程池中多个线程处理。
2. 该模型把一次请求转换为多个任务 ( 网络数据读写、 读取数据 后的业务逻辑处理) 入队到全局队列,线程池中的线程从队列中获取任务执行。
3. 同一个请求访问被拆分为多个任务,一次请求可能由多个线程处理。
3. 当任务太多,系统压力大的时候,线程池中线程数动态增加
4. 当任务减少,系统压力减少的时候,线程池中线程数动态减少
T1: 调用底层 asio 库接收一个完整 mongodb 报文的时间
T2: 接收到报文后的后续所有处理 ( 含报文解析、认证、引擎层处理、发送数据给客户端等 )
T3: 线程等待数据的时间 ( 例如:长时间没有流量,则现在等待读取数据 )
线程运行总时间=T1 + T2 +T3 ,其中 T3 是无用等待时间。如果 T3 的无用等待时间占比很大,则说明线程比较空闲。工作线程每一次循环处理后判断有效时间占比,如果小于指定阀值,则自己直接 exit 退出销毁
控制线程专门用于判断线程池中工作线程的压力情况,以此来决定是否在线程池中创建新的工作线程来提升性能。
控制线程每过一定时间循环检查线程池中的线程压力状态,实现原理就是简单的实时记录线程池中的线程当前运行情况,为以下两类计数:总线程数_threadsRunning 、当前正在运行 task 任务的线程数 _threadsInUse 。如果 _threadsRunning=_threadsRunning ,说明所有工作线程当前都在处理 task 任务,线程池中线程压力大,这时候控制线程就开始增加线程池中线程数。
该模型详细源码实现过程更多细节详见:https://my.oschina.net/u/4087916/blog/4295038
1. 线程池获取任务执行,有锁竞争,这里就会成为系统瓶颈
mongodb 动态 adaptive 线程模型,适用于请求处理比较耗时的场景,如数据库服务
该模型详细源码优化分析实现过程参考:
https://my.oschina.net/u/4087916/blog/4295038
Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计
该线程模型图如下:

说明:
把一个全局队列拆分为多个队列,任务入队的时候按照hash 散列到各自的队列,工作线程获取获取任务的时候,同理通过 hash 的方式去对应的队列获取任务,通过这种方式减少锁竞争,同时提升整体性能。
mongodb 内核多队列 adaptive 线程模型优化,性能有很好的提升,适用于请求处理比较耗时的场景,如数据库服务。该模型详细源码优化分析实现过程参考:
Mongodb 网络传输处理源码实现及性能调优 - 体验内核性能极致设计
答: 实际上所有服务都可以利用这一特性,包括数据库服务(mongodb 、 mysql 等 ) 。但是因为数据库服务访问时延一般都是 ms 级别,如果 reuseport 特性利用起来,时延会有几十 us 的性能提升,这相比数据库内部处理的 ms 级时延,这几十 us 的性能提升,基本上可以忽略掉,这也是大部分数据库服务没有支持该功能的原因。
缓存,代理等中间件,由于本身内部处理时间就比较小,也是us 级别,所以需要充分利用该特性。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69984922/viewspace-2727336/ ,如需转载,请注明出处,否则将追究法律责任。