什么是高并发?高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,简单点说,就是 QPS(Queries Per Second)。

那么我们在谈论高并发的时候,究竟在谈些什么东西呢?高并发究竟是什么?

这里先给出结论:高并发的基本表现为单位时间内系统能够同时处理的请求数,高并发的核心是对 CPU 资源的有效压榨。

举个例子,如果我们开发了一个叫做 MD5 穷举的应用,每个请求都会携带一个 MD5 加密字符串,最终系统穷举出所有的结果,并返回原始字符串。

这个时候我们的应用场景或者说应用业务是属于 CPU 密集型而不是 IO 密集型。

这个时候 CPU 一直在做有效计算,甚至可以把 CPU 利用率跑满,这时我们谈论高并发并没有任何意义。(当然,我们可以通过加机器也就是加 CPU 来提高并发能力,这个是一个正常猿都知道的废话方案,谈论加机器没有什么意义,没有任何高并发是加机器解决不了,如果有,那说明你加的机器还不够多!)

对于大多数互联网应用来说,CPU 不是也不应该是系统的瓶颈,系统的大部分时间的状况都是 CPU 在等 I/O (硬盘/内存/网络) 的读/写操作完成。

这个时候就可能有人会说,我看系统监控的时候,内存和网络都很正常,但是 CPU 利用率却跑满了这是为什么?

这是一个好问题,后文我会给出实际的例子,再次强调上文说的 '有效压榨' 这 4 个字,这 4 个字会围绕本文的全部内容!

控制变量法

万事万物都是互相联系的,当我们在谈论高并发的时候,系统的每个环节应该都是需要与之相匹配的。我们先来回顾一下一个经典 C/S 的 HTTP 请求流程。

如图中的序号所示:

  • 我们会经过 DNS 服务器的解析,请求到达负载均衡集群。

  • 负载均衡服务器会根据配置的规则,将请求分摊到服务层。服务层也是我们的业务核心层,这里可能也会有一些 RPC、MQ 的一些调用等等。

  • 再经过缓存层。

  • 最后持久化数据。

  • 返回数据给客户端。

要达到高并发,我们需要负载均衡、服务层、缓存层、持久层都是高可用、高性能的。

甚至在第 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/Redis/MQ连接

  • 都会重新动态解释编译执行 PHP 文件

  • 都会在不同的 php-fpm 进程直接不停的切换切换再切换

PHP 的这种 CGI 运行模式,根本上就决定了它在高并发上的灾难性表现。

找到问题,往往比解决问题更难。当我们理解了当我们在谈论高并发究竟在谈什么之后,我们会发现高并发和高性能并不是编程语言限制了你,限制你的只是你的思想。

找到问题,解决问题!当我们能有效压榨 CPU 性能之后,能达到什么样的效果?

下面我们看看 PHP+Swoole 的 HTTP 服务与 Java 高性能的异步框架 Netty 的 HTTP 服务之间的性能差异对比。

性能对比前的准备

Swoole 是什么

Swoole 是一个为 PHP 开发人员用 C 和 C++ 编写的基于事件的高性能异步&协程并行网络通信引擎。

Netty 是什么

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

单机能够达到的最大 HTTP 连接数是多少?回忆一下计算机网络的相关知识,HTTP 协议是应用层协议,在传输层,每个 TCP 连接建立之前都会进行三次握手。

每个 TCP 连接由本地 IP,本地端口,远端 IP,远端端口,四个属性标识。

TCP 协议报文头如上图(图片来自维基百科):

  • 本地端口由 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 编排如下:

# java8version: "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-swversion: "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 代码:

<?php use Swoole\Server;use Swoole\Http\Response;$http = new swoole_http_server("0.0.0.0", 8080);$http->set(['worker_num' => 2]);$http->on("request", function ($request, Response $response) {//go(function () use ($response) {// Swoole\Coroutine::sleep(0.01);        $response->end('Hello World');//});});$http->on("start", function (Server $server) {    go(function () use ($server) {echo "server listen on 0.0.0.0:8080 \n";    });});$http->start();

Java 关键代码:(源代码来自:https://github.com/netty/netty)

    public static void main(String[] args) throws Exception {        // Configure SSL.        final SslContext sslCtx;        if (SSL) {            SelfSignedCertificate ssc = new SelfSignedCertificate();            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();        } else {            sslCtx = null;        }

        // Configure the server.        EventLoopGroup bossGroup = new NioEventLoopGroup(2);        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            ServerBootstrap b = new ServerBootstrap();            b.option(ChannelOption.SO_BACKLOG, 1024);            b.group(bossGroup, workerGroup)             .channel(NioServerSocketChannel.class)             .handler(new LoggingHandler(LogLevel.INFO))             .childHandler(new HttpHelloWorldServerInitializer(sslCtx));

            Channel ch = b.bind(PORT).sync().channel();

            System.err.println("Open your web browser and navigate to " +                    (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();        } finally {            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }

因为我只给了两个核心的 CPU 资源,所以两个服务均只开启两个 Work 进程即可:

  • 5551 端口表示 PHP 服务。

  • 5555 端口表示 Java 服务。

压测工具结果对比

ApacheBench (ab)命令:

docker run --rm jordi/ab -k -c 1000 -n 1000000 http://10.234.3.32:5555/

在并发 1000 进行 100 万次 HTTP 请求的基准测试中,Java + Netty 压测结果:

PHP + Swoole 压测结果:

PS:上图选择的是三次压测下的最佳结果。

总的来说,性能差异并不大,PHP+Swoole 的服务甚至比 Java+Netty 的服务还要稍微好一点,特别是在内存占用方面,Java 用了 600MB,PHP 只用了 30MB。

这能说明什么呢?没有 IO 阻塞操作,不会发生协程切换。

这个仅仅只能说明多线程+Epoll 的模式下,有效的压榨 CPU 性能,你甚至用 PHP 都能写出高并发和高性能的服务。

性能对比:见证奇迹的时刻

上面代码其实并没有展现出协程的优秀性能,因为整个请求没有阻塞操作,但往往我们的应用会伴随着例如文档读取、DB 连接/查询等各种阻塞操作,下面我们看看加上阻塞操作后,压测结果如何。

Java 和 PHP 代码中,我都分别加上 Sleep(0.01) //秒的代码,模拟 0.01 秒的系统调用阻塞,代码就不再重复贴上来了。

带 IO 阻塞操作的 Java + Netty 压测结果:

大概 10 分钟才能跑完所有压测,带 IO 阻塞操作的 PHP + Swoole 压测结果:

从结果中可以看出,基于协程的 PHP + Swoole 服务比 Java + Netty 服务的 QPS 高了 6 倍。

当然,这两个测试代码都是官方 Demo 中的源代码,肯定还有很多可以优化的配置,优化之后,结果肯定也会好很多。

可以再思考下,为什么官方默认线程/进程数量不设置的更多一点呢?

进程/线程数量可不是越多越好哦,前面我们已经讨论过了,在进程/线程切换的时候,会产生额外的 CPU 资源花销,特别是在用户态和内核态之间切换的时候!

对于这些压测结果来说,我并不是针对 Java,我是指只要明白了高并发的核心是什么,找到这个目标,无论用什么编程语言,只要针对 CPU 利用率做有效的优化(连接池、守护进程、多线程、协程、Select 轮询、Epoll 事件驱动),你也能搭建出一个高并发和高性能的系统。

所以,你现在明白了,当我们在谈论高性能的时候,究竟在谈什么了吗?思路永远比结果重要!

作者:hncg

编辑:陶家龙、孙淑娟

出处:https://segmentfault.com/a/1190000019360335

精彩文章推荐:

百万并发下的Nginx优化,看这一篇就够了!

有人要将“高并发”拉下“神坛”!

此文若说不清Epoll原理,那就过来掐死我!

一个springboot能支持多少并发_吃透这篇,你也能搭建出一个高并发和高性能的系统...相关推荐

  1. php ssc 源码_吃透这篇,你也能搭建出一个高并发和高性能的系统

    什么是高并发?高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,简单点说,就是 QPS(Queries Per Second). 那么我们在谈论高并发的时候,究 ...

  2. qps多少才算高并发_要大到什么程度?才算高并发?

    文章目录 前言 在这里插入图片描述 一.什么是高并发? 定义: 关键指标: 关键指标的维度: 并发 引申指标: 二.多大算高并发 场景1: 场景2: 三.高并发的本质 对比包括: 前提包括: 总结 前 ...

  3. 假设一动态集合S用一个长度为m的直接寻址表T来表示。请给出一个查找S中最大元素的过程。(算法导论第十一章11.1-1)

    假设一动态集合S用一个长度为m的直接寻址表T来表示.请给出一个查找S中最大元素的过程.你所给的过程在最坏情况下的运行时间是多少. (算法导论第十一章11.1-1) #include "Key ...

  4. 多少并发量算高并发_如何理解:程序、进程、线程、并发、并行、高并发?

    作者:大宽宽 链接:http://tinyurl.com/wx5xxho 在这里你可以了解: 为啥大家说的进程的意思有出入? 为啥并发那么难理解? 为啥高并发不仅仅是"高"+&qu ...

  5. h5画三角形_如何利用css或html5画出一个三角形?两种不同的制作三角形方法(代码实例)...

    我们在平时的前端开发的时候,有时候是需要一些小图形来丰富一下页面效果,比如:下拉列表的倒三角图形.那么这样的一个三角形是如何制作出来的,本章给大家介绍如何利用css或html画出一个三角形?两种不同的 ...

  6. java每秒向mysql写一条记录_【Java】mysql一条记录在高并发场景下读写?

    CREATE TABLE `user_info` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `balance` decimal(10,2) DE ...

  7. html点击按钮弹出悬浮窗_点击文字或按钮弹出一个DIV窗口(DIV悬浮窗口)

    以下是弹出窗口代码: 点击文字弹出一个DIV层窗口代码 .black_overlay{ display: none; position: absolute; top: 0%; left: 0%; wi ...

  8. python四瓣花图形_如何使用Inscape设计工具设计出一个四瓣花朵图

    在使用矢量图形时,利用Inscape设计工具绘制多个圆形,并利用圆形的特性设计出一个四瓣图形,然后使用图形的形态交叉光滑效果,形成一个花瓣图形.下面利用一个实例说明,操作如下: 工具/原料 Insca ...

  9. 电商那些年,我摸爬打滚出的高并发架构实战精髓(续)

    一.分层,分割,分布式 大型网站要很好地支撑高并发,需要长期的规划设计.在初期,需要把系统进行分层,在发展过程中把核心业务进行拆分成模块单元,根据需求进行分布式部署,可以进行独立团队维护开发. 分层: ...

最新文章

  1. 1数字图像获取:1.1图像数字化
  2. CVPR 2022 | 模型难复现不一定是作者的错,最新研究发现模型架构要背锅
  3. 安卓定时启动软件app_便签app排行榜前十名安卓手机哪个高颜值便签软件好用?...
  4. 结构之美——优先队列基本结构(四)——二叉堆、d堆、左式堆
  5. Akka入门(二)Akka的Actor模型如何满足现代分布式系统需求
  6. Java Collection类型的forEach方法
  7. 安全方向比路由交换难吗_「网工进阶」路由交换:链路聚合的配置,你都会吗...
  8. java跳转_java servlet 几种页面跳转的方法
  9. 零基础科普:4种简单推荐算法背后的原理
  10. Linux查看显示编辑文本文件
  11. OpenShift 4 - Ingress、Route与Shard
  12. mysql之冷备和mysqldump、mydumper、xtrabackup备份
  13. miou 代码 VOC2012
  14. 编写Test3.jsp,在JSP页面中静态包含文件Sqrt.jsp(该页面计算数据的算术平方根)。要求程序有两个文件,主文件静态包含一个能够计算数据的算术平方根的页面。
  15. SQL Server无法以local 登陆问题的解决方法provider: Named Pipes Provider, error: 40 - Could not open a connection
  16. VLDB 2022最佳研究论文:克服通信挑战,新框架SANCUS实现GNN高效训练
  17. form表单字段默认值
  18. Ubuntu系统安装微信(解决高分辨率屏幕问题及图标显示问题)
  19. [真诚的思考](http://simplemind.info/blog/?p=423)
  20. include静态引入jsp与动态引入jsp的区别

热门文章

  1. ALV列(Column)换到行(Row) 之 列上限不固定篇
  2. 用数据可视化解读:为何2亿国人爱钓鱼
  3. python如何计算整数和_Python中整数和浮点数
  4. hdfs user 连接_通过API访问HDFS
  5. java 脚本引擎性能_Java SE 6 入门之脚本引擎加大程序性能
  6. python得到一个10位随机数的方法及拓展
  7. 首次登录kk服务器信息,kk云服务器设置
  8. Python flask出现jinja2.exceptions.TemplateNotFound错误(修复host就好了???)
  9. 【深度学习的数学】单层感知机是什么?
  10. Intel Realsense D435 管道配置文件类(pipeline_profile)与流配置文件类(config)