APP下载

吃透这篇 你也能搭建出一个高并发和高效能的系统

消息来源:baojiabao.com 作者: 发布时间:2024-05-21

报价宝综合消息吃透这篇 你也能搭建出一个高并发和高效能的系统

什么是高并发?高并发是互联网分散式系统架构的效能指标之一,它通常是指单位时间内系统能够同时处理的请求数,简单点说,就是 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 编排如下:

# 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 程式码:

2019-12-22 21:08:00

相关文章