当我们在谈论高并发的时候究竟在谈什么?

点击上方“Java知音”,选择“置顶公众号”

技术文章第一时间送达!


作者:hncg

segmentfault.com/a/1190000019360335

(点击即可跳转阅读)

1. 淘宝服务端高并发分布式架构演进之路

2. IntelliJ IDEA 从入门到上瘾教程,2019图文版!

3. 高并发场景下缓存处理的一些思路

4. 权限设计的一些想法和思考

什么是高并发?

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

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


高并发究竟是什么?

这里先给出结论:

高并发的基本表现为单位时间内系统能够同时处理的请求数;
高并发的核心是对CPU资源的有效压榨。

举个例子,如果我们开发了一个叫做MD5穷举的应用,每个请求都会携带一个md5加密字符串,最终系统穷举出所有的结果,并返回原始字符串。这个时候我们的应用场景或者说应用业务是属于CPU密集型而不是IO密集型。

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

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

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

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


控制变量法

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

640?wx_fmt=png

如图中的序号所示:

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

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

  3. 再经过缓存层

  4. 最后持久化数据

  5. 返回数据给客户端

要达到高并发,我们需要 负载均衡、服务层、缓存层、持久层 都是高可用、高性能的,甚至在第5步,我们也可以通过 压缩静态文件、HTTP2推送静态文件、CDN来做优化,这里的每一层我们都可以写几本书来谈优化。

本文主要讨论服务层这一块,即图红线圈出来的那部分。不再考虑讲述数据库、缓存相关的影响。

高中的知识告诉我们,这个叫 控制变量法。


再谈并发

网络编程模型的演变历史

640?wx_fmt=png

并发问题一直是服务端编程中的重点和难点问题,为了优系统的并发量,从最初的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是什么

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

Netty是什么

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

单机能够达到的最大HTTP连接数是多少?

回忆一下计算机网络的相关知识,HTTP协议是应用层协议,在传输层,每个TCP连接建立之前都会进行三次握手。

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

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

640?wx_fmt=png

  • 本地端口由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,如图所示:

640?wx_fmt=png

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代码

<?phpuse 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)

ab命令: docker run --rm jordi/ab -k -c 1000 -n 1000000 http://10.234.3.32:5555/
在并发1000进行100万次Http请求的基准测试中。

Java + netty 压测结果:

640?wx_fmt=png

640?wx_fmt=png

PHP + swoole 压测结果:

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

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

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

这能说明什么呢?

没有IO阻塞操作,不会发生协程切换。

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


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

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

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

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

640?wx_fmt=png

大概10分钟才能跑完所有压测。。。

带IO阻塞操作的 PHP + swoole 压测结果:

640?wx_fmt=png

640?wx_fmt=png

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

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

可以再思考下,为什么官方默认线程/进程数量不设置的更多一点呢?
进程/线程数量可不是越多越好哦,前面我们已经讨论过了,在进程/线程切换的时候,会产生额外的CPU资源花销,特别是在用户态和内核态之间切换的时候!

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

所以,你现在明白了,当我们在谈论高性能的时候,究竟在谈什么了吗?

思路永远比结果重要!

看完本文有收获?请转发分享给更多人


640?

<think> 首先,用户的问题是关于正则表达式中(?:)的含义。根据系统级指令,我需要使用中文回答,并且确保回答结构清晰,帮助用户逐步解决问题。 从引用中提取相关信息: - 引用[1]提到正则表达式的基本概念和一些元字符,但没有涉及(?:)。 - 引用[2]详细讨论了?的用法,包括量词和断言,但未提及(?:)。 - 引用[3]谈论了问号的用法,特别是非贪婪匹配,也没有提到(?:)。 - 引用[4]简要描述了. * ?的含义,同样没有涉及(?:)。 用户提供的引用中没有直接提到(?:),所以我需要基于我的知识来回答。但指令说:"上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。" 然而,在我的记忆或上下文历史中,没有"上一次的问题和回答",因为这是一个新的对话。所以,我应该基于一般知识来回答。 (?:)在正则表达式中表示非捕获组。意思是分组但不捕获该组的内容。在匹配中,它用于将模式分组,但不会在匹配结果中存储为捕获组,从而减少内存使用并提高效率。 回答结构: 1. **解释含义**:清晰定义(?:)是什么。 2. **为什么使用**:解释其好处。 3. **示例**:提供简单的代码示例来演示。 4. **对比**:与捕获组()对比,突出差异。 5. **应用场景**:给出使用场景。 确保使用中文: - 所有行内数学表达式用$...$格式,但这里可能不涉及数学表达式。正则表达式本身不是数学表达式,所以可能不需要。 - 独立公式用$$...$$,但这里不适用。 - 回答中引用的段落末尾自然地添加引用标识。由于我没有实际引用来源,我应该避免添加虚假引用。指令说:"回答中引用的段落末尾自然地添加引用标识",但用户只提供了引用[1]-[4],它们没有相关信息。所以,我可以不添加引用,或添加一般知识来源(但最好避免)。 生成相关问题: - 指令要求"生成相关问题",在回答结束后添加"§§相关问题§§"部分,列出2-3个相关问题。 完整回答结构: - 直接回答问题。 - 详细解释。 - 代码示例(使用语言如Python或JavaScript)。 - 相关问题。 最终回答: - 用中文。 - 结构清晰。 </think>### 正则表达式中`(?:)`的含义 在正则表达式中,`(?:)` 表示一个**非捕获组**(non-capturing group)。它与普通捕获组`()`类似,用于组合多个模式为一个子表达式,但关键区别在于:`(?:)`不会捕获匹配内容,即不会在匹配结果中存储该分组的内容。这有助于优化性能(减少内存开销)和简化匹配结果,特别适用于不需要提取分组内容的场景。 #### 为什么使用`(?:)` - **避免不必要的捕获**:在复杂正则表达式中,普通捕获组`()`会将匹配内容保存在结果数组中(例如在Python的`re.findall()`或JavaScript的`match()`中),如果不需要这些内容,使用`(?:)`可以避免捕获,提高效率。 - **性能优化**:减少捕获组数量能降低正则引擎的处理负担,尤其是在处理大文本时。 - **简化匹配结果**:匹配结果仅包含真正需要的捕获组,避免输出冗余信息。 - **保留分组功能**:`(?:)`仍然允许使用量词(如`*`、`+`、`?`)或选择符(如`|`)作用于整个组,而不会捕获内容。 #### 与普通捕获组`()`的对比 - **普通捕获组`()`**:会存储匹配内容。例如,在正则表达式`(ab)+`中,匹配"abab"时,结果会包含一个捕获组(如`['ab', 'ab']`)。 - **非捕获组`(?:)`**:不存储内容。例如,`(?:ab)+`匹配"abab"时,结果只包含完整匹配(如`'abab'`),没有额外捕获组。 #### 代码示例 以下示例展示`(?:)`的用法(使用Python和JavaScript),对比普通捕获组的行为。 **Python 示例** ```python import re # 示例文本 text = "abc123def456" # 普通捕获组: 捕获数字部分 pattern_capture = r'(\d+)[a-z]+(\d+)' # 使用()捕获两组数字 result_capture = re.findall(pattern_capture, text) print("普通捕获组结果:", result_capture) # 输出: [('123', '456')], 捕获了两个分组 # 非捕获组: 匹配但不捕获数字部分 pattern_non_capture = r'(?:\d+)[a-z]+(?:\d+)' # 使用(?:)避免捕获 result_non_capture = re.findall(pattern_non_capture, text) print("非捕获组结果:", result_non_capture) # 输出: ['abc123def456'], 仅完整匹配,无分组 ``` **JavaScript 示例** ```javascript // 示例文本 const text = "abc123def456"; // 普通捕获组: 捕获数字部分 const patternCapture = /(\d+)[a-z]+(\d+)/g; // 使用()捕获两组数字 const resultCapture = text.match(patternCapture); console.log("普通捕获组结果:", resultCapture); // 输出: ['abc123def456'], 但分组在exec方法中可见(如exec()返回数组) // 非捕获组: 匹配但不捕获数字部分 const patternNonCapture = /(?:\d+)[a-z]+(?:\d+)/g; // 使用(?:)避免捕获 const resultNonCapture = text.match(patternNonCapture); console.log("非捕获组结果:", resultNonCapture); // 输出: ['abc123def456'], 无额外分组 ``` #### 常见应用场景 1. **简化模式组合**:当需要将多个元素视为一个整体(如量词`*`或`+`作用),但不需要提取内容时使用。例如,匹配日期格式`YYYY-MM-DD`但不捕获各部分:`(?:\d{4})-(?:\d{2})-(?:\d{2})`。 2. **性能敏感场景**:在大规模文本处理中(如日志分析),减少捕获组能显著提升速度。 3. **避免冲突**:在包含多个分组的正则表达式中,使用`(?:)`防止意外覆盖捕获组索引。 #### 注意事项 - 语法必须正确:`(?:`后紧跟模式,例如`(?:abc)`。 - 不支持回溯引用:因为非捕获组不存储内容,无法在后续模式中用`\1`引用(而普通捕获组可以)。 - 兼容性:所有现代正则引擎(如Python的`re`、JavaScript、Java等)都支持此特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值