C# 多线程猜想
公司分配给我一个活,让我给Kong网关做一个获取设置的站点。Kong网关号称几万的QPS的神器,我有点慌,如果因为我的站点拖累了Kong我就是千古罪人。
配合Kong的站点必须要经过性能测试,在性能测试的时候就发现个很有意思的现象,如果我用25条线程压我的站点,那么结果是这样的。
如果我用50条线程去压站点,结果是这样的
现象就是,我提高了并发数量,我的QPS其实并没有什么变化,但是我的单次平均响应时间缺提高了一倍。其实这种现象还是比较好解释的。首先,我们来了解一下,IIS的大概处理逻辑。
其实IIS维护了这么几个东西,首先一个是队列,用来提高服务器的同时处理请求数用的。这么说吧,假设我现在程序很原始很简陋,我一次只能处理一条请求,那么,我在处理一条请求的过程中,第二条请求过来了,那么这个时候我显然不应该告诉他,我现在正忙,没空搭理他,而应该是告诉他,你先等会,我马上来处理你。让他等会,其实就是相当于把他放到队列里边,一会再来处理。
另外一个概念叫做,同时处理数。刚才我的假设是我一次只能处理一条数据。但是我存在多个核,就算是一核在一个时间点上只能处理一条数据,那么,现在我机器是4核的,那么我最起码也应该能处理4条数据,假设现在一次性来了4条数据,那么这4条数据基本上可以认为是同时在处理的,但是如果同时来了8条数据,那么就是4条在处理,4条在等待。
现在来解释一下,为什么会出现50并发比25并发,提升了等待时间,但是QPS并没有提高。我想可以这么解释,其实QPS在25并发的时候已经接近于极限了,这个极限应该怎么算呢,大概就应该是1秒 * 同时处理数 / 每个请求的真实处理时间。可以看出来这个极限其实跟客户端的并发数没有什么直接联系。那么50并发的时候,为什么等待的时间反而变长了呢?那是因为,客户端并发数大于服务器同时处理数的时候,有一部分固定数量的请求在请求队列里,他必须等待已经进入处理逻辑的部分处理完,然后再处理自己,所以就造成了QPS并没有提升但是响应时间变长的现象发生。
因为是这样的多倍叠加的模式,所以,有时候,你会发现,你的接口,如果只是几毫秒响应的话,大家都很快。但是一旦你慢下来,响应时间是成指数级的增长。原因也很简单,主要有以下几个。
- 等待的队列边长了(因为前边处理的很慢,所以等的人越来越多)
- 等待的单次边长了(但是变长的人不止是你自己呀,还有你等待的其他人)
这几个情况一综合那可不是乘法运算嘛,那可不就是指数级增加嘛。
提问,那么究竟多少并发,才是最理想的状态呢?
之前考虑这个问题的时候,可能理所当然的认为,这个东西嘛,应该是跟CPU核数有关系,应该跟核数一样多就是最优解了吧。但是现实经常啪啪打脸。经过实测,一般是要比CPU核数多不少才是CPU不累,处理效率很高的状态。那么为什么会出现这种情况呢?我觉得这个问题有点大,我们需要拆开来看。
1、一个核心真的是一条线程执行的最快吗?
这个问题嘛,其实也对,也不对。说他对是因为,其实如果存在多条线程,那么多条线程之间切换的时候,其实也挺消耗资源的。但是多线程的为什么需要多线程呢?我觉得这个问题也可以拆成两个问题。在拆问题之前先给介绍两个概念。计算密集型、IO密集型,计算密集型就是你在做运算,加减乘除也好,比对也要,加密解密也好,这种主要依赖于CPU叫计算密集型线程。如果你的线程大部分时间都消耗在了读取网络数据,读取本地数据,或者驱动硬件等待返回这种情况叫做IO密集型。
1.1 一个核心真的是一个计算密集型最快吗?
是的。因为线程切换本身也是需要消耗资源的,频繁的切换其实对于计算密集型线程没有任何好处,因为计算量并没有变少反而变多了。
1.2 一个核心真的是一个IO密集型线程最快吗?
不对。多个IO密集型线程肯定比一个IO密集型线程要快,因为大部分时间,其实跟CPU没有关系,CPU大部分时间都是在等而已。所以让CPU一次性处理多个,反而更加占有优势。
2、为什么不是并发量跟同时处理数相等时是最优解。
真实的业务场景,一条线程并不是纯粹的IO或者计算,更多的时候是处于两者都有的情况。那么对于这种线程的话。反正不是一核一个最快,因为它毕竟是存在IO的情况。他们肯定要多处理几个才划算。
这样服务器等待客户端请求的时间就太长了,如果并发数量跟处理数量相等的话,那么对于一个并发来说,就相当于客户端发起请求、发送网络数据、服务器处理、发送网络数据、客户端接收网络数据,然后进行下一轮处理。这样的话就相当于客户端与服务器端处于同一个线程,单线程工作,并且中间存在了大量的等待的时间,所以服务器的QPS并不会上来。
最理想的状态应该是,以下的状态
- 等待队列中始终存在数据(不会让处理线程等待客户端请求)
- 客户端的请求进入等待队列后立马被处理(不会因为别的请求而造成响应时间过长,而引发下一步的等待队列过长)
根据上边总结的多线程的相关结论,一般一个核心肯定要处理多个线程,并且等待队列中存在并且存在不了多少数据。
那么最佳并发的结论应该是,核心数 * N(单核心同时处理线程数) + M(等待队列中存在的少数请求)。
题外话:为什么Golang号称利用协成能够更好的利用CPU 达到更高的运算效率呢?
我猜应该是将IO型线程中的多线程切换部分性能节省下来,用作于更多的CPU计算来提高了整体性能。