前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vertx高并发理论原理以及对比SpringBoot

Vertx高并发理论原理以及对比SpringBoot

作者头像
闻说社
发布2024-09-24 11:33:32
640
发布2024-09-24 11:33:32

Vertx定义

Vertx是一个基于Netty响应式工具包,官方没有定义为框架,因为他并不像Spring侵入性那么强,甚至你可以在SpringBoot中使用他。

那什么是响应式

响应式编程,即 Reactive Programming。它是一种基于事件模式的模型。在异步编程模式中,我们描述了两种获得上一个任务执行结果的方式,一个就是主动轮询,我们把它称为 Proactive 方式;另一个就是被动接收反馈,我们称为 Reactive 方式。简单来说,在 Reactive 方式中,上一个任务执行结果的反馈就是一个事件,这个事件的到来会触发下一个任务的执行

所以响应式编程一定是异步的,但异步不一定是响应式。 响应式的核心是事件流Stream(一般包含三种类型的事件:某种类型值,错误,已完成的事件信号)。 熟悉Netty的应该知道,在netty中有两个EventLoop,一个BossEventLoop和一个WorkerEventLoop,分别负责负责监听Socket事件和处理对应的请求

响应式编程解决了什么问题?相比多线程异步的优点?

CPU运行线程代码时如果遇到IO,会将线程挂起,然后运行其他线程,这里会有一次上下文切换,会消耗一些CPU性能。这里要搞清楚是CPU不会被IO阻塞,线程是会被IO阻塞的。也就是说Threa1在执行任务A时遇到了IO块比如查数据库,他会一直阻塞直到这个操作完成,这里用任务会合适点,因为在线程池中需要执行的功能都被包装成任务了。但是golang和java21中的协程可以实现线程不被IO阻塞,但这个也是语言层面做了优化,本身Linux c线程就不支持。

其实响应式编程和异步都是为了解决一个问题:提升CPU的利用率,但是响应式编程相比多线程可以实现更少的线程完成更多的任务,在内存和上下文切换方面开销更小。 先探究一下多线程为啥能提升CPU利用率? 因为磁盘IO,网络IO的速度和CPU处理速度相差巨大。而线程处理任务遇到IO后会导致线程一直等待处于WAITING状态,无法处理其他任务。这样就造成了一个现象:CPU没活干,但有大量任务待处理。我们以单核CPU为例子,假设一个API场景为:

客户端请求服务A的接口/a/test,接口实现逻辑为: A服务收到请求,解析出请求参数aId和bId,然后携带bId请求微服务B获取bId对应详情数据,A收到B的返回后A服务再去自己数据库查询aId数据详情。最后将aId详情和bId详情数据一并返回给客户端。

可以看到一次请求CPU的使用时间片占用较短(实际情况大部分接口占用CPU的时间片比图示更短,下图是理想状态实际难以跑满)。当CPU处理到线程IO时会挂起当前线程然后处理其他线程,当线程比较多就能处理更多任务,使CPU时刻都有任务处理,从而提高了CPU利用率。

多线程.drawio.png
多线程.drawio.png

那么我们是否可以将线程设置很大,这样就能最大限度提高CPU利用率? 不行。两个原因

1. 线程创建有成本代价

首先创建也会耗费CPU资源,同时每个线程也会占用一定内存。 jdk1.4默认的单个线程是占用256k的内存,jdk1.5+默认的单个线程是占用1M的内存,可以通过-Xss参数设定,一般默认就好

2. 线程上下文切换会有增加CPU负担

拿我们上面的单核10线程举例,CPU处理完一个线程的计算任务后等待IO需要切换到另外一个线程。 线程切换就涉及到保存当前线程的上下文(Context)和加载另一个线程的上下文。上下文包括寄存器的值、程序计数器(PC)和栈指针(SP)等。下面是线程切换的主要步骤:

  1. 保存当前线程的上下文:操作系统首先保存当前线程的上下文,将当前线程的寄存器、PC 和 SP 等信息保存到内存中,以便稍后恢复。
  2. 选择下一个线程:操作系统从就绪队列中选择下一个要运行的线程。这个选择可以基于调度算法,如先来先服务(FCFS)或轮转调度(Round Robin)。
  3. 加载下一个线程的上下文:操作系统从内存中加载下一个线程的上下文,包括寄存器的值、PC 和 SP 等。
  4. 切换到下一个线程:操作系统将 CPU 控制权切换到下一个线程,使其继续执行。
  5. 恢复上下文:如果之前保存了当前线程的上下文,操作系统会将其恢复,以便在下次切换回该线程时能够继续执行。

为了节约内存和避免频繁上下文切换带来的系统损耗,就出现了线程池,同时线程池也都具备回收线程的功能来,比如Tomcat就有线程池大小相关配置,而且默认200左右(比较小)

server.tomcat.accept-count:等待队列长度,当可分配的线程数全部用完之后,后续的请求将进入等待队列等待,等待队列满后则拒绝处理,默认100。 server.tomcat.max-connections:最大可被连接数,默认10000 server.tomcat.max-threads:最大工作线程数,默认200 server.tomcat.min-spare-threads:最小工作线程数,初始化分配线程数,默认10

同时这也导致了Tomcat并发度较低,不适合高并发项目,我们可以算一下,在200线程的前提下假如每个请求耗时在20ms左右Tomcat能并发处理的请求在10000左右实际还不到。那么我们可以提高工作线程数量,确实可以,但这样线程上下文切换更加频繁CPU损耗更大,CPU很多时间片都用在了上下文切换而不是处理任务,而且占用内存更多,比如要支持100,0000人在线的IM项目用Tomcat显然不合适。

那么怎样去优化?

优化的原则只有一个,就是用更少的线程去处理更多的任务。而不是更多的线程处理更多的任务,可以看到即使用了多个线程CPU利用率提高了但是每个线程的利用率还是很少!那么有人问了,线程少了可以承担那么多任务吗? 答案是肯定的,node.js就是单线程但是可以处理百万并发(但node.js受限于单线程,如果每个请求都大量耗费CPU会导致性能急剧下降)

那么如何用更少的线程处理更多的任务?

  • 让线程不用等待IO,当遇到IO时线程直接去处理其他任务(jdk21之前还做不到,golang以及jdk21中的协程可以实现)
  • 将大块的IO分散成小块IO(响应式编程和Vertx就是这种思路!!!)

为什么要将大块IO分散成小块IO? 因为大块IO会导致当前线程阻塞太久

假设一个请求场景 用户信息微服务,用户订单微服务,请求操作:

  1. PC端携带用户Id请求用户服务,用户服务接受并从网卡中读取数据(网络IO)
  2. 解析读取请求信息(CPU解析)
  3. 根据解析出的用户Id从用户服务数据库中查出用户A的信息(数据库IO和网络IO)
  4. 用户服务携带用户数据向订单服务请求用户一年消费数据(网络IO)
  5. 根据用户信息和用户A消费数据计算出用户消费指标信息同时生成报表(CPU计算)
  6. 将生成报表发送到oss(网络IO)
  7. 将用户指标信息更新数据库(网络IO和数据库IO)
  8. 同时返回给PC端(网络IO)

因为线程数量是有限的,而且远远小于处理请求数量。我们也知道处理一个接口时CPU占用时间相对IO是很少的,图里我也有体现。但因为线程需要等待IO导致其他需要处理的请求被迫等待,特别是当请求量大的时候越发明显。 如图所示黄色线条中间区域属于被迫等待,明明CPU都没事做了,但是由于5个线程阻塞在IO上导致他无法处理request6和request7导致他们阻塞时间延长了不少,特别当请求更多的时候这个情况愈发严重。传统的Tomcat线程在处理请求时就会面临这种问题,所以导致Tomcat从根本上难以胜任高并发。

线程io分散.drawio.png
线程io分散.drawio.png

要是request1-5中能快速抽出一个线程将request6-7的CPU操作运行完就好了,但是线程又不能再遇到IO时自动挂起处理其他任务,所以这个时候可行的方法是将一个请求的连续的IO拆成单个IO操作(粒度越小越好)封装成任务丢给线程池执行。

优化方法1:拆分大块IO,如图黑色线条

大块IO拆成小块,利于线程快速处理非阻塞任务,原来需要等待数个IO阻塞时间现在只需要等待1个IO阻塞时间了。而且没有了上下文切换,因为每个计算或者IO代码块被包装成了线程任务

优化方法2:使用专门CPU密集型线程池处理非阻塞代码块,如图紫色线条

但是因为IO阻塞时间相对CPU计算时间要大不少,所以还是导致需要CPU的任务块还是需要等待不少时间,那么第二个优化点就是使用专门的非阻塞任务线程池处理CPU计算任务。 其实这里又引出一个问题,阻塞也要耗费线程,CPU计算很快可能导致大量IO任务一起到来,所以这里要根据IO系数调整IO线程池数量。 从整体来看,一个系统如果达到最大并发状态,应该是每个CPU任务块能得到及时响应,每个IO任务块也能得到及时响应。方法1已经做到了后者,但前者没做到,但方法2兼顾了所有。CPU任务块能及时被响应,IO任务块也占满了IO线程池。

拆分.drawio.png
拆分.drawio.png

小结

回过头来看看Vertx以及Java中的响应式编程框架就是采用了上述优化方案。响应式编程是对IO任务块和CPU计算任务块进行了优秀的编排可以更加高效的利用CPU。但其实大多数Web系统从总体上来看瓶颈上是在数据库上,一般的业务没有必要上Vertx,因为JDK21之前写起来属实恶心,那么相比Tomcat,Vertx适合哪些业务场景:

  • 大量连接并发的系统,如IM系统,消息通知中心且基于网络IO的。因为天然基于Netty可以保持大量连接,IO主要是网络IO网卡好点就行了
  • 高性能中间件系统
  • 想用更少的机器支持更大体量的WEB系统
  • 同时Vertx启动非常快,没有SpringBoot臃肿,一些业务相对简单的项目可以试试

那么JDK21中也支持协程了,等到Tomcat优化好了使用响应式框架工具的必要性就更小了,毕竟响应式编程写出来的代码可读性太低了,有的本来代码写的比较乱改成Vertx更加不可看了,特别是业务代码。

在 Spring Boot 3.2 中,启用了虚拟线程后,Tomcat 默认使用的虚拟线程执行器不在需要池化。 也就是说,在 Spring Boot 3.2 以后的版本里,我们不在需要设置 server.tomcat.threads.max 以及 server.tomcat.threads.min-spare 两个属性以控制 Tomcat 线程池的大小了,因为它压根没有使用平台线程池。 对于 Tomcat 来说,引入虚拟线程,不必在为线程池的维护而费心,还能减轻编程的复杂度。 虚拟线程由 JVM 平台负责进行调度,它是廉价且轻量级的,Tomcat 可以使用 “每个请求一个线程” 模型,而不必担心实际需要多少个线程。 就算请求任务在虚拟线程中调用阻塞 I/O 操作,导致运行时虚拟线程被挂起阻塞,但是只要挂起结束后该虚拟线程就可以恢复 使用了虚拟线程后,程序员使用普通的阻塞 API,也可以让程序对硬件的利用达到近乎完美水平,以此提供高水平的并发性,从而实现高吞吐量。 可以说,虚拟线程的引入,以后程序员就算是使用 Java 中阻塞 API 也可以开发出高性能、高吞吐量的应用程序。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Vertx定义
  • 小结
相关产品与服务
腾讯云 BI
腾讯云 BI(Business Intelligence,BI)提供从数据源接入、数据建模到数据可视化分析全流程的BI能力,帮助经营者快速获取决策数据依据。系统采用敏捷自助式设计,使用者仅需通过简单拖拽即可完成原本复杂的报表开发过程,并支持报表的分享、推送等企业协作场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档