当我们在谈论高并发的时候究竟在谈什么?
什么是高并发?
高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,
简单点说,就是QPS(Queries per second)。
那么我们在谈论高并发的时候,究竟在谈些什么东西呢?
高并发究竟是什么?
这里先给出结论: 高并发
的基本表现为单位时间内系统能够同时处理的请求数,高并发
的核心是对CPU资源的有效压榨。
举个例子,如果我们开发了一个叫做MD5穷举
的应用,每个请求都会携带一个md5加密字符串,最终系统穷举出所有的结果,并返回原始字符串。这个时候我们的应用场景或者说应用业务是属于CPU密集型
而不是IO密集型
。这个时候CPU一直在做有效计算,甚至可以把CPU利用率跑满,这时我们谈论高并发并没有任何意义。(当然,我们可以通过加机器也就是加CPU来提高并发能力,这个是一个正常猿都知道废话方案,谈论加机器没有什么意义,没有任何高并发是加机器解决不了,如果有,那说明你加的机器还不够多!🐶)
对于大多数互联网应用来说,CPU不是也不应该是系统的瓶颈,系统的大部分时间的状况都是CPU在等I/O (硬盘/内存/网络) 的读/写操作完成。
这个时候就可能有人会说,我看系统监控的时候,内存和网络都很正常,但是CPU利用率却跑满了这是为什么?
这是一个好问题,后文我会给出实际的例子,再次强调上文说的 '有效压榨' 这4个字,这4个字会围绕本文的全部内容!
控制变量法
万事万物都是互相联系的,当我们在谈论高并发的时候,系统的每个环节应该都是需要与之相匹配的。我们先来回顾一下一个经典C/S的HTTP请求流程。

如图中的序号所示:
1 我们会经过DNS服务器的解析,请求到达负载均衡集群
2 负载均衡服务器会根据配置的规则,想请求分摊到服务层。服务层也是我们的业务核心层,这里可能也会有一些PRC、MQ的一些调用等等
3 再经过缓存层
4 最后持久化数据
5 返回数据给客户端
要达到高并发,我们需要 负载均衡、服务层、缓存层、持久层 都是高可用、高性能的,甚至在第5步,我们也可以通过 压缩静态文件、HTTP2推送静态文件、CDN来做优化,这里的每一层我们都可以写几本书来谈优化。
本文主要讨论服务层这一块,即图红线圈出来的那部分。不再考虑讲述数据库、缓存相关的影响。
高中的知识告诉我们,这个叫 控制变量法
。
再谈并发
- 网络编程模型的演变历史

并发问题一直是服务端编程中的重点和难点问题,为了优系统的并发量,从最初的Fork进程开始,到进程池/线程池,再到epoll事件驱动(Nginx、node.js反人类回调),再到协程。
从上中可以很明显的看出,整个演变的过程,就是对CPU有效性能压榨的过程。
什么?不明显?
- 那我们再谈谈上下文切换
在谈论上下文切换之前,我们再明确两个名词的概念。
并行:两个事件同一时刻完成。
并发:两个事件在同一时间段内交替发生,从宏观上看,两个事件都发生了。
线程是操作系统调度的最小单位,进程是资源分配的最小单位。由于CPU是串行的,因此对于单核CPU来说,同一时刻一定是只有一个线程在占用CPU资源的。因此,Linux作为一个多任务(进程)系统,会频繁的发生进程/线程切换。
在每个任务运行前,CPU都需要知道从哪里加载,从哪里运行,这些信息保存在CPU寄存器
和操作系统的程序计数器
里面,这两样东西就叫做 CPU上下文
。
进程是由内核来管理和调度的,进程的切换只能发生在内核态,因此 虚拟内存、栈、全局变量等用户空间的资源,以及内核堆栈、寄存器等内核空间的状态,就叫做 进程上下文
。
前面说过,线程是操作系统调度的最小单位。同时线程会共享父进程的虚拟内存和全局变量等资源,因此 父进程的资源加上线上自己的私有数据就叫做线程的上下文
。
对于线程的上下文切换来说,如果是同一进程的线程,因为有资源共享,所以会比多进程间的切换消耗更少的资源。
现在就更容易解释了,进程和线程的切换,会产生CPU上下文
切换和进程/线程上下文
的切换。而这些上下文切换
,都是会消耗额外的CPU的资源的。
- 进一步谈谈协程的上下文切换
那么协程就不需要上下文切换了吗?需要,但是不会产生 CPU上下文切换
和进程/线程上下文
的切换,因为这些切换都是在同一个线程中,即用户态中的切换,你甚至可以简单的理解为,协程上下文
之间的切换,就是移动了一下你程序里面的指针,CPU资源依旧属于当前线程。
需要深刻理解的,可以再深入看看Go的GMP模型
。
最终的效果就是协程进一步压榨了CPU的有效利用率。
回到开始的那个问题
这个时候就可能有人会说,我看系统监控的时候,内存和网络都很正常,但是CPU利用率却跑满了这是为什么?
注意本篇文章在谈到CPU利用率的时候,一定会加上有效
两字作为定语,CPU利用率跑满,很多时候其实是做了很多低效的计算。
以”世界上最好的语言”为例,典型PHP-FPM的CGI模式,每一个HTTP请求:
都会读取框架的数百个php文件,
都会重新建立/释放一遍MYSQL/REIDS/MQ连接,
都会重新动态解释编译执行PHP文件,
都会在不同的php-fpm进程直接不停的切换切换再切换。
php的这种CGI运行模式,根本上就决定了它在高并发上的灾难性表现。
找到问题,往往比解决问题更难。当我们理解了当我们在谈论高并发究竟在谈什么
之后,我们会发现高并发和高性能并不是编程语言限制了你,限制你的只是你的思想。