V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
sanbenweiyang
V2EX  ›  程序员

Socket Server 的 N 种并发模型汇总

  •  
  •   sanbenweiyang · 2020-04-06 10:06:08 +08:00 · 1562 次点击
    这是一个创建于 1711 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原创声明 作者: 刘丹冰 Aceld,微信公众号同名

    本文主要介绍常见的 Server 的并发模型,这些模型与编程语言本身无关,有的编程语言可能在语法上直接透明了模型本质,所以开发者没必要一定要基于模型去编写,只是需要知道和了解并发模型的构成和特点即可。

    那么在了解并发模型之前,我们需要两个必备的前置知识:

    • socket 网络编程
    • 多路 IO 复用机制
    • 多线程 /多进程等并发编程理论

    模型一、单线程 Accept (无 IO 复用)

    (1) 模型结构图

    (2) 模型分析

    ① 主线程main thread执行阻塞 Accept,每次客户端 Connect 链接过来,main thread中 accept 响应并建立连接

    ② 创建链接成功,得到Connfd1套接字后, 依然在main thread串行处理套接字读写,并处理业务。

    ③ 在②处理业务中,如果有新客户端Connect过来,Server无响应,直到当前套接字全部业务处理完毕。

    ④ 当前客户端处理完后,完毕链接,处理下一个客户端请求。

    (3) 优缺点

    优点

    • socket 编程流程清晰且简单,适合学习使用,了解 socket 基本编程流程。

    缺点

    • 该模型并非并发模型,是串行的服务器,同一时刻,监听并响应最大的网络请求量为1。 即并发量为1

    • 仅适合学习基本 socket 编程,不适合任何服务器 Server 构建。


    模型二、单线程 Accept+多线程读写业务(无 IO 复用)

    (1) 模型结构图

    (2) 模型分析

    ① 主线程main thread执行阻塞 Accept,每次客户端 Connect 链接过来,main thread中 accept 响应并建立连接

    ② 创建链接成功,得到Connfd1套接字后,创建一个新线程thread1用来处理客户端的读写业务。main thead依然回到Accept阻塞等待新客户端。

    thread1通过套接字Connfd1与客户端进行通信读写。

    ④ server 在②处理业务中,如果有新客户端Connect过来,main threadAccept依然响应并建立连接,重复②过程。

    (3) 优缺点

    优点

    • 基于模型一:单线程 Accept (无 IO 复用) 支持了并发的特性。
    • 使用灵活,一个客户端对应一个线程单独处理,server处理业务内聚程度高,客户端无论如何写,服务端均会有一个线程做资源响应。

    缺点

    • 随着客户端的数量增多,需要开辟的线程也增加,客户端与 server 线程数量1:1正比关系,一次对于高并发场景,线程数量收到硬件上限瓶颈。
    • 对于长链接,客户端一旦无业务读写,只要不关闭,server 的对应线程依然需要保持连接(心跳、健康监测等机制),占用连接资源和线程开销资源浪费。
    • 仅适合客户端数量不大,并且数量可控的场景使用。

    仅适合学习基本 socket 编程,不适合任何服务器 Server 构建。


    模型三、单线程多路 IO 复用

    (1) 模型结构图

    (2) 模型分析

    ① 主线程main thread创建listenFd之后,采用多路 I/O 复用机制(如:select 、epoll)进行 IO 状态阻塞监控。有Client1客户端Connect请求,I/O 复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1加入到监听 I/O 集合中。

    Client1再次进行正常读写业务请求,main thread多路 I/O 复用机制阻塞返回,会触该套接字的读 /写事件等。

    ③ 对于Client1的读写业务,Server 依然在main thread执行流程提继续执行,此时如果有新的客户端Connect链接请求过来,Server 将没有即时响应。

    ④ 等到 Server 处理完一个连接的Read+Write操作,继续回到多路 I/O 复用机制阻塞,其他链接过来重复 ②、③流程。

    (3) 优缺点

    优点

    • 单流程解决了可以同时监听多个客户端读写状态的模型,不需要1:1与客户端的线程数量关系。
    • 多路 I/O 复用阻塞,非忙询状态,不浪费 CPU 资源,CPU 利用率较高。

    缺点

    • 虽然可以监听多个客户端的读写状态,但是同一时间内,只能处理一个客户端的读写操作,实际上读写的业务并发为 1 。
    • 多客户端访问 Server,业务为串行执行,大量请求会有排队延迟现象,如图中⑤所示,当Client3占据main thread流程时,Client1,Client2流程卡在IO 复用等待下次监听触发事件。

    模型四、单线程多路 IO 复用+多线程读写业务(业务工作池)

    (1) 模型结构图

    (2) 模型分析

    ① 主线程main thread创建listenFd之后,采用多路 I/O 复用机制(如:select 、epoll)进行 IO 状态阻塞监控。有Client1客户端Connect请求,I/O 复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1加入到监听 I/O 集合中。

    ② 当connFd1有可读消息,触发读事件,并且进行读写消息

    main thread按照固定的协议读取消息,并且交给worker pool工作线程池, 工作线程池在 server 启动之前就已经开启固定数量的thread,里面的线程只处理消息业务,不进行套接字读写操作。

    ④ 工作池处理完业务,触发connFd1写事件,将回执客户端的消息通过main thead写给对方。

    (3) 优缺点

    优点

    • 对于模型三, 将业务处理部分,通过工作池分离出来,减少多客户端访问 Server,业务为串行执行,大量请求会有排队延迟时间。
    • 实际上读写的业务并发为 1,但是业务流程并发为 worker pool 线程数量,加快了业务处理并行效率。

    缺点

    • 读写依然为main thread单独处理,最高读写并行通道依然为 1.
    • 虽然多个 worker 线程处理业务,但是最后返回给客户端,依旧需要排队,因为出口还是main threadRead + Write

    模型五、单线程 IO 复用+多线程 IO 复用(链接线程池)

    (1) 模型结构图

    (2) 模型分析

    ① Server 在启动监听之前,开辟固定数量(N)的线程,用Thead Pool线程池管理

    ② 主线程main thread创建listenFd之后,采用多路 I/O 复用机制(如:select 、epoll)进行 IO 状态阻塞监控。有Client1客户端Connect请求,I/O 复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1分发给Thread Pool中的某个线程进行监听。

    Thread Pool中的每个thread都启动多路 I/O 复用机制(select 、epoll),用来监听main thread建立成功并且分发下来的 socket 套接字。

    ④ 如图, thread监听ConnFd1 、ConnFd2, thread2监听ConnFd3,thread3监听ConnFd4. 当对应的ConnFd有读写事件,对应的线程处理该套接字的读写及业务。

    (3) 优缺点

    优点

    • main thread的单流程读写,分散到多线程完成,这样增加了同一时刻的读写并行通道,并行通道数量NN为线程池Thread数量。
    • server 同时监听的ConnFd 套接字数量几乎成倍增大,之前的全部监控数量取决于main thread多路 I/O 复用机制的最大限制***(select 默认为 1024,epoll 默认与内存大小相关,约 3~6w 不等)***,所以理论单点 Server 最高响应并发数量为N*(3~6W)(N为线程池Thread数量,建议与 CPU 核心成比例 1:1)。
    • 如果良好的线程池数量和 CPU 核心数适配,那么可以尝试 CPU 核心与 Thread 进行绑定,从而降低 CPU 的切换频率,提升每个Thread处理合理业务的效率,降低 CPU 切换成本开销。

    缺点

    • 虽然监听的并发数量提升,但是最高读写并行通道依然为N,而且多个身处同一个 Thread 的客户端,会出现读写延迟现象,实际上每个Thread的模型特征与模型三:单线程多路 IO 复用一致。

    模型五(进程版)、单进程多路 I/O 复用+多进程多路 I/O 复用(进程池)

    (1) 模型结构图

    (2) 模型分析

    五、单线程 IO 复用+多线程 IO 复用(链接线程池)无大差异。

    不同处

    • 进程和线程的内存布局不同导致,main process(主进程)不再进行Accept操作,而是将Accept过程分散到各个子进程(process)中.
    • 进程的特性,资源独立,所以main process如果 Accept 成功的 fd,其他进程无法共享资源,所以需要各子进程自行 Accept 创建链接
    • main process只是监听ListenFd状态,一旦触发读事件(有新连接请求). 通过一些 IPC(进程间通信:如信号、共享内存、管道)等, 让各自子进程Process竞争Accept完成链接建立,并各自监听。
    (3) 优缺点

    五、单线程 IO 复用+多线程 IO 复用(链接线程池)无大差异。

    不同处:

    多进程内存资源空间占用稍微大一些

    多进程模型安全稳定型较强,这也是因为各自进程互不干扰的特点导致。


    模型六、单线程多路 I/O 复用+多线程多路 I/O 复用+多线程

    (1) 模型结构图

    (2) 模型分析

    ① Server 在启动监听之前,开辟固定数量(N)的线程,用Thead Pool线程池管理

    ② 主线程main thread创建listenFd之后,采用多路 I/O 复用机制(如:select 、epoll)进行 IO 状态阻塞监控。有Client1客户端Connect请求,I/O 复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1分发给Thread Pool中的某个线程进行监听。

    Thread Pool中的每个thread都启动多路 I/O 复用机制(select 、epoll),用来监听main thread建立成功并且分发下来的 socket 套接字。一旦其中某个被监听的客户端套接字触发I/O 读写事件,那么,会立刻开辟一个新线程来处理I/O 读写业务。

    ④ 但某个读写线程完成当前读写业务,如果当前套接字没有被关闭,那么将当前客户端套接字如:ConnFd3重新加回线程池的监控线程中,同时自身线程自我销毁。

    (3) 优缺点

    优点

    • 模型五、单线程 IO 复用+多线程 IO 复用(链接线程池)基础上,除了能够保证同时响应的最高并发数,又能解决读写并行通道局限的问题。

    • 同一时刻的读写并行通道,达到最大化极限,一个客户端可以对应一个单独执行流程处理读写业务,读写并行通道与客户端数量1:1关系。

    缺点

    • 该模型过于理想化,因为要求 CPU 核心数量足够大。
    • 如果硬件 CPU 数量可数(目前的硬件情况),那么该模型将造成大量的 CPU 切换成本浪费。因为为了保证读写并行通道与客户端1:1的关系,那么 Server 需要开辟的Thread数量就与客户端一致,那么线程池中做多路 I/O 复用的监听线程池绑定 CPU 数量将变得毫无意义。
    • 如果每个临时的读写Thread都能够绑定一个单独的 CPU,那么此模型将是最优模型。但是目前 CPU 的数量无法与客户端的数量达到一个量级,目前甚至差的不是几个量级的事。

    总结

    综上,我们整理了 7 中 Server 的服务器处理结构模型,每个模型都有各自的特点和优势,那么对于多少应付高并发和高 CPU 利用率的模型,目前多数采用的是模型五(或模型五进程版,如 Nginx 就是类似模型五进程版的改版)。

    至于并发模型并非设计的约复杂越好,也不是线程开辟的越多越好,我们要考虑硬件的利用与和切换成本的开销。模型六设计就极为复杂,线程较多,但以当今的硬件能力无法支撑,反倒导致该模型性能极差。所以对于不同的业务场景也要选择适合的模型构建,并不是一定固定就要使用某个来应用。


    ###关于作者:

    mail: [email protected] github: https://github.com/aceld 原创书籍 gitbook: http://legacy.gitbook.com/@aceld

    创作不易, 共同学习进步, 欢迎关注作者, 回复"zinx"有好礼

    作者微信公众号


    文章推荐

    开源软件作品

    (原创开源)Zinx-基于 Golang 轻量级服务器并发框架-完整版(附教程视频)

    (原创开源)Lars-基于 C++负载均衡远程调度系统-完整版

    精选文章

    典藏版-Golang 调度器 GMP 原理与调度全分析

    典藏版-Golang 三色标记、混合写屏障 GC 模式图文全分析

    最常用的调试 golang 的 bug 以及性能问题的实践方法?

    Golang 中的 Defer 必掌握的 7 知识点

    Golang 中的局部变量“何时栈?何时堆?”

    使用 Golang 的 interface 接口设计原则

    流? I/O 操作?阻塞? epoll?

    深入浅出 Golang 的协程池设计

    Go 语言构建微服务一站式解决方案


    3 条回复    2020-04-07 00:24:33 +08:00
    hhhsuan
        1
    hhhsuan  
       2020-04-06 10:50:47 +08:00   ❤️ 1
    看不到图?
    paoqi2048
        2
    paoqi2048  
       2020-04-06 20:23:26 +08:00
    两个模型五
    Meltdown
        3
    Meltdown  
       2020-04-07 00:24:33 +08:00 via Android
    默认 tcp 么,UDP 怎么多线程呢…
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3132 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 05:00 · PVG 13:00 · LAX 21:00 · JFK 00:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.