目录
欢迎作者总结高并发进程上下文切换线程上下文切换协作上下文切换性能测试结果。优质的文章都在这里等着。
文章来源:
多亏了作者的优质文章,作者从操作系统原理的角度深刻解释了什么是高并发性。
笔者在相关文章中看过明确的解释,值得一读。
什么是高并发?
高并发性是互联网分布式系统体系结构的性能指标之一,通常是指系统在单位时间内可以同时处理的请求数。
简而言之,Queries per second(QPS)。
那么,当我们谈论高并发性的时候,到底在说什么呢?
高并发性究竟是什么?
我先下结论:
高并发性的基本表现是系统在单位时间内可以同时处理的请求数。
高并发性的核心是对CPU资源的有效压榨。
例如,如果开发一个名为MD5的应用程序,每个请求都会携带MD5加密字符串,最后系统会琢磨所有结果并返回原始字符串。此时,我们的应用方案或应用程序业务是CPU密集型的,而不是IO密集型的。此时,CPU已经进行了有效的计算,CPU利用率可能会充满。这个时候谈论高并发是没有意义的。
(当然,我们可以通过添加机器,即CPU来提高同步能力,这是正常猴子知道可笑的程序,谈论附加机器是没有意义的,高并发是附加机器不能解决的,如果有的话,那就意味着你添加的机器不够用!)。
在大多数internet应用程序中,CPU不是系统的瓶颈,在大多数情况下,CPU将完成等待I/O(硬盘/内存/网络)的读/写操作。
此时看系统监控时,为什么内存和网络正常,CPU利用率却满了?
这是个好问题。稍后我再强调一下实际例子上面提到的“有效挤压”四个字。这四个字将包围这篇文章的全部内容!
控制变量法
万事万物相互联系。当我们谈论高并发性时,系统的所有部分都必须与之一致。首先,让我们看一下经典的C/S的HTTP请求过程。
图中序列号显示的:
我们要求通过DNS服务器的确认,到达负载平衡集群负载平衡服务器。我想根据配置的规则请求服务层分担。服务层也是我们的业务核心层,这里可能有一些PRC、MQ的一些调用等。然后经过缓存层,最后将数据返回给客户端,实现高并发,负载平衡、服务层、缓存层、持久层都需要高可用性和高性能。即使在步骤5中,也可以通过静态文件压缩、HTTP2执行静态文件推送。
本文主要讨论服务层的这一部分,即图片红色线圈出现的部分。不再考虑数据库、缓存的相关影响。
高中的知识告诉我们这叫控制变量法。
相声并行
网络编程模型的演化历史
并发问题一直是服务器端编程的重点和难点问题。为了优化系统的并发性,从初始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运行模式,根本上就决定了它在高并发上的灾难性表现。
找到问题,往往比解决问题更难。当我们理解了当我们在谈论高并发究竟在谈什么 之后,我们会发现高并发和高性能并不是编程语言限制了你,限制你的只是你的思想。
找到问题,解决问题!当我们能有效压榨CPU性能之后,能达到什么样的效果?
下面我们看看 php+swoole的HTTP服务 与 Java高性能的异步框架netty的HTTP服务之间的性能差异对比。
性能对比前的准备
- swoole是什么
- Netty是什么
- 单机能够达到的最大HTTP连接数是多少?
回忆一下计算机网络的相关知识,HTTP协议是应用层协议,在传输层,每个HTTP请求都会进行三次握手,并建立一个TCP连接。
每个TCP连接由 本地ip,本地端口,远端ip,远端端口,四个属性标识。
本地端口由16位组成,因此本地端口的最多数量为 2^16 = 65535个。
远端端口由16位组成,因此远端端口的最多数量为 2^16 = 65535个。
同时,在linux底层的网络编程模型中,每个TCP连接,操作系统都会维护一个File descriptor(fd)文件来与之对应,而fd的数量限制,可以由ulimt -n 命令查看和修改,测试之前我们可以执行命令: ulimit -n 65536修改这个限制为65535。
因此,在不考虑硬件资源限制的情况下,
本地的最大HTTP连接数为: 本地最大端口数65535 * 本地ip数1 = 65535 个。
远端的最大HTTP连接数为:远端最大端口数65535 * 远端(客户端)ip数+∞ = 无限制~~ 。
PS: 实际上操作系统会有一些保留端口占用,因此本地的连接数实际也是达不到理论值的。
性能对比
测试资源
各一台docker容器,1G内存+2核CPU
docker-compose编排如下:
# java8 version: "2.2" services: java8: container_name: "java8" hostname: "java8" image: "java:8" volumes: - /home/cg/MyApp:/MyApp ports: - "5555:8080" environment: - TZ=Asia/Shanghai working_dir: /MyApp cpus: 2 cpuset: 0,1 mem_limit: 1024m memswap_limit: 1024m mem_reservation: 1024m tty: true # php7-sw version: "2.2" services: php7-sw: container_name: "php7-sw" hostname: "php7-sw" image: "mileschou/swoole:7.1" volumes: - /home/cg/MyApp:/MyApp ports: - "5551:8080" environment: - TZ=Asia/Shanghai working_dir: /MyApp cpus: 2 cpuset: 0,1 mem_limit: 1024m memswap_limit: 1024m mem_reservation: 1024m tty: true- php代码
- Java关键代码
源代码来自,
public static void main(String[] args) throws Exception { // Configure SSL. final SslContext sslCtx; if (SSL) { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = S(), ()).build(); } else { sslCtx = null; } // Configure the server. EventLoopGroup bossGroup = new NioEventLoopGroup(2); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.option, 1024); b.group(bossGroup, workerGroup) .channel) .handler(new LoggingHandler)) .childHandler(new HttpHelloWorldServerInitializer(sslCtx)); Channel ch = b.bind(PORT).sync().channel(); Sy("Open your web browser and navigate to " + (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync(); } finally { bo(); workerGroup.shutdownGracefully(); } }因为我只给了两个核心的CPU资源,所以两个服务均只开启连个work进程即可。
5551端口表示PHP服务。
5555端口表示Java服务。
- 压测工具结果对比:ApacheBench (ab)
ab命令: docker run --rm jordi/ab -k -c 1000 -n 1000000
在并发1000进行100万次Http请求的基准测试中,
Java + netty 压测结果:
PHP + swoole 压测结果:
服务QPS响应时间ms(max,min)内存(MB)Java + ne(11,25)600+php + (9,25)30+
ps: 上图选择的是三次压测下的最佳结果。
总的来说,性能差异并不大,PHP+swoole的服务甚至比Java+netty的服务还要稍微好一点,特别是在内存占用方面,java用了600MB,php只用了30MB。
这能说明什么呢?
没有IO阻塞操作,不会发生协程切换。
这个仅仅只能说明 多线程+epoll的模式下,有效的压榨CPU性能,你甚至用PHP都能写出高并发和高性能的服务。
性能对比——见证奇迹的时刻
上面代码其实并没有展现出协程的优秀性能,因为整个请求没有阻塞操作,但往往我们的应用会伴随着例如 文档读取、DB连接等各种阻塞操作,下面我们看看加上阻塞操作后,压测结果如何。
Java和PHP代码中,我都分别加上 slee) //秒的代码,模拟0.01秒的系统调用阻塞。
代码就不再重复贴上来了。
大概10分钟才能跑完所有压测。。。
带IO阻塞操作的 PHP + swoole 压测结果:
服务QPS响应时间ms(max,min)内存(MB)Java + ne(52,160)100+php + (9,25)30+
从结果中可以看出,基于协程的php+ swoole服务比 Java + netty服务的QPS高了6倍。
当然,这两个测试代码都是官方demo中的源代码,肯定还有很多可以优化的配置,优化之后,结果肯定也会好很多。
可以再思考下,为什么官方默认线程/进程数量不设置的更多一点呢?
进程/线程数量可不是越多越好哦,前面我们已经讨论过了,在进程/线程切换的时候,会产生额外的CPU资源花销,特别是在用户态和内核态之间切换的时候!
总结
对于这些压测结果来说,我并不是针对Java,我是指 只要明白了高并发的核心是什么,找到这个目标,无论用什么编程语言,只要针对CPU利用率做有效的优化(连接池、守护进程、多线程、协程、select轮询、epoll事件驱动),你也能搭建出一个高并发和高性能的系统。
所以,你现在明白了,当我们在谈论高性能的时候,究竟在谈什么了吗?
思路永远比结果重要!
您的转发+关注就是对笔者最大的支持,欢迎关注。
对大厂架构设计,BAT等厂家面试题解读,编程语言理论或者互联网圈逸闻趣事这些感兴趣,欢迎关注笔者,没有错,干货文章都在这里。