对高并发的思考

近期很长的一段时间,都在学习总结,什么是高并发,怎么样实现高并发,接触了解的内容包括Nginx工作原理,Tomcat 的三种运营模式,Pistache并发机制,CGI/Fast-CGI,nodejs及其底层的异步IO库linuv,Linux的事件轮询机制Epoll,以及Go语言的GMP调度模型。自认为形成一套自圆其说的逻辑,所以记录总结一下。

首先谈一下并发,在OS的课程上对于并发和并行有严格的区分:

  • 并发指的是多个任务同一个core上一起执行,通过调度算法,给用户以所有的任务正在同时的进行“错觉”,但是本质上,任务是串行的
  • 并行指的是多个任务在多个core上同时执行

而我们这里讨论的并发可以认为是上述两种情况的融合,即所谓高并发是指,如何在有限核的服务器上尽可能多的处理请求或任务(任务数>>核数),同时还要追求低延时(平均延时、尾部延时)、高资源利用率(主要考虑CPU利用率)等性能指标。

这里先给出结论吧,要想实现最极致的并发,核心在于满足以下三点:

  1. 执行任务的线程等于核数
    线程数等于核数,旨在减少线程切换的开销,显然这个开销并不是特大,因此超过核数引入的开销其实并不大,但是引入过多的线程(如启动上万线程)开销就无法忽视了,其次线程切换是需要进行用户态/内核态的转化的,而且CPU的调度算法是O(n)的。
  2. CPU的利用率为100%
    CPU的利用率为100%,这是为了不让CPU空闲,我们都知道,一个请求的执行一般可以划分为两种情况:执行计算、执行IO,其中后者是不会使用CPU的。到这里我们知道,一般来说前两点是矛盾的,如果线程等于核数,那么CPU利用率一定不会高,因为CPU时间被IO任务占据。
  3. 任务完全公平的使用CPU
    关于这点,我关注的比较少,其实,实现前两点就是高并发了,能做到公平性的也只有Go语言了,不过由于用户级线程是无法被内核感知的,其公平性也是无法达到CFS算法的水准。我们给的公平下一个定义的话:假设当前有N个优先级相同的任务在同一个core上运行,那么在一个调度周期T时间内,每个任务的得到的执行时间是(T/N),那么可以认为是公平的。

显然要想严格实现上述三点是不可能的,对于第三点,关于什么是完全的公平,本身也是一个需要讨论的话题。我们要做的只是向上面三点靠拢。

下面我将从BIO、NIO、Nodejs、Go,四个方面,以递进的方式,阐述,我对于实现高并的的理解。

一、BIO

对于一个web服务器而言,是基于TCP + HTTP协议的。TCP与UDP不同之处在于,在使用TCP进行数据传送之前,必须要在客户端和服务端之间建立TCP连接,然后再进行数据传输,过程及涉及的系统调用如下图所示:HTTP请求的底层流程因此在服务器端,有两种阻塞的情况

  • accept()
    如果任何客户端没有进行connect()操作,那么执行accept()将会阻塞
  • read()
    如果客户端没有通过write()发送数据(即HTTP请求,HTTP请求就是遵循了HTTP协议的消息),那么执行read()将会阻塞

BIO的模式是这样的:

  1. 首先,由一个主线程来监听server-fd(listen-fd)
  2. 执行accept()阻塞等待新连接,当新连接到达时创建client-fd
  3. 从线程池中选择一个工作线程,讲client-fd交给线程处理,然后执行循环执行2、3
  4. 工作线程执行read(),阻塞等待HTTP请求的到达,然后解析、处理、封装HTTP请求,发送响应
  5. 如果是长连接(一般都是长连接),那么将循环执行4,知道断开连接,释放线程

BIO模式要求,每一个线程处理一个连接,这样同时处理的连接数将取决于线程池的大小,那我们假设启动一个合理大小的线程池,使之不会过多的消耗系统资源(如,是当前核数的2倍):

要点表现解释
线程数目尚可因为我们假设启动一个不大的线程池
CPU利用率很低因为每个线程会在阻塞等待http请求的到达,在次期间,CPU无法做任何事情,且长连接会让情况更糟,因为这意味着CPU长时间的阻塞
公平性很差因为处理的连接数目受限与线程池的大小,新的连接只能等待前面的连接处理完毕,长连接模式显然让这个问题更加严重

二、NIO

BIO的主要症结在于阻塞等待新连接和阻塞等待请求,而基于IO复用机制的epoll是解决网络IO阻塞的灵丹妙药,基于epoll,每个线程可以处理多个连接:

  1. 主线程维护一个epoll,用于监听listen-fd
  2. 当新连接到达时,调用accept()创建一个client-fd,然后将client-fd放到主线程的epoll中进行监听
  3. 当epoll监听的client-fd有先请求到达时,会返回产生事件的client-fd
  4. 主线程将HTTP请求交给工作线程处理,然后继续执行epoll的监听
  5. 工作线程执行解析、处理、封装HTTP请求,发送响应,处理完毕后,等待主线程的任务派发

注,以上只是一个NIO的一种简单模式

与NIO显著不同的一点在于,工作线程对应于一个HTTP请求,而非一个连接,这样我们每个线程都在执行HTTP请求,而不会像BIO那样阻塞等待请求的到达。也就是说我们使用epoll实现了网络IO的异步

但是显然,同时处理http请求的数目依然收到线程池大小的限制,我们再次假设启动一个合理大小的线程池:

要点表现解释
线程数目尚可因为我们假设启动一个不大的线程池
CPU利用率有提高,但还不够提高是因为实现了网络IO的异步,不需要阻塞等待HTTP请求,在长连接的情况下改进很大;不够是因为,用户的处理函数中进行的IO操作依然会阻塞线程,如需要访问数据库或本地的文件等。
公平性有提高,但同样不够有提高是因为新连接可以马上被处理,且任何一个连接的HTTP请求都遵循FIFO的原则;不够的原因依然是FIFO,只能一个个的处理HTTP请求

三、Nodejs

NIO的主要问题在于,仅仅将处理TCP连接和HTTP请求的部分实现了异步,但用户处理函数中的IO操作,依然会阻塞线程。

于是nodejs,使用libuv封装了所有的IO操作(当然了主要就是网络IO和文件IO),实现了完全的异步IO。

其中网络IO是基于epoll的,而文件IO是基于线程池的。

这里需要注意一点,关于网络IO和文件IO可以看此图:

真正的IO操作,是由DMA完成的,由网卡或磁盘到socket_Buff或pageCache的这一部分,这一部分CPU是不会参与的,但是将数据再从socket_Buff或pageCache读到用户空间,是CPU操作。
当epoll感知到可读时,数据已经达了socket_Buff了,因此epoll实现异步网络IO可谓绝配;但显然文件IO是无法使用epoll的,而Linux的AIO操作又不成熟,因此只能使用多线程来实现异步了,Go也是这么做的。

而使用线程实现异步文件IO结束的标志是数据已经读取到用户空间(即read(2)操作返回),因此整个过程其实是包含了CPU操作的,当然了epoll机制实现的异步网络IO虽然不包括CPU的copy过程,但是也要内核执行中断处理程序、调用协议栈处理packet。但是IO的时间肯定是占大头,因此我们可以认为执行异步IO的线程,几乎不参与CPU调度的竞争,这是我们满足1. 执行任务的线程等于核数的一种变通方式!!!

要点表现解释
线程数目接近完美虽然我们启动实现异步文件IO的线程,但是他几乎不会参与线程的CPU调度
CPU利用率完美一切IO都是异步的,CPU将专注与计算任务
公平性又有提高,但是依然不够因为事件循环机制,每个HTTP任务都被以回调函数的形式拆分,谁的异步IO完成,谁得到执行;但是毕竟是单线程的模式,一旦某个请求的计算任务很长,那么也会指导处理完才能处理别的

四、Go

现在只剩下公平性的问题了。

其实上面的优化,总结一下就是:异步I/O

Go语言使用了另一种方法:协程或者叫纤程或者叫用户级线程。

每个任务都以协程的方式,在线程上执行,也就是PMG模型!这里就不展开PMG的运行原理了。

Go在执行到IO这样阻塞的任务时,同Nodejs一样,对于网络IO,使用epoll机制,用一个线程运行net-poller来等待;对于文件IO,则也是启动新的线程(M)来处理,当然了,不光是IO操作,但凡要执行长时间的系统调用,Go都会启动一个线程来执行等待,然后切换执行其他的协程任务。

此外Go语言还是实现了,基于抢占的协程调度,具体的方法是当协程要调用一个函数时,在函数的入口处,由编译器插入检查代码,如果已经运行了足够的时间,那就切换到其他线程,显然这种方法和CFS调度相比还是弱。

所以我认为,Go在线程数目和CPU利用率上与nodejs差不多,在公平性上比Nodejs又进了一步,但是依然不是完美的!

后记

以上所有,都是一些不够成熟的见解,在描述上也很欠缺,以后随着我的理解深入,一定会进一步的更新!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HUST-Kingdo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值