文章详细介绍了快手基于 Dragonwell 社区 Wisp 协程自研的 Java 17 **透明协程**技术。针对传统线程模型和异步模型的局限性,快手选择了协程这一“同步编程,异步执行”的方案。文章深入分析了快手在协程落地过程中遇到的挑战,包括低负载下 CPU 消耗高、长任务抢占开销大以及 IO 查询不及时等问题。针对这些问题,快手团队对调度器、抢占机制和 IO 模型进行了深度优化和重构。通过采用 Handshake 和 HandOff 机制降低抢占开销,并优化 IO 管理模块,解决了 JNI 长任务抢占和 IO 查询不及时的问题。最终,快手 Java 协程架构在 Java 17 上得以实现,服务 QPS 提升 30% 以上,并节省了数千万的服务器成本。**该方案对业务具有非侵入性,是其一个重要优势。**
导读
对于开发者而言,传统线程模型逻辑直观但性能受限,而异步模型虽性能高却复杂性大。协程以“同步编程,异步执行”平衡两者,成为现代语言标配。结合自身业务需求,快手基于社区开源版本自研了Java17透明协程技术,实现对业务无侵入的同时,吞吐能力提升30%以上。本文将深入剖析快手协程技术的背后原理与架构演进。


-
云原生高负载环境:服务进程在CPU资源受限的情况下频繁遭遇节流。
-
线程上下文切换频繁:服务进程具有IO密集或锁密集的特点,导致线程上下文切换频繁。

-
运行效率提升:协程在提升QPS方面的卓越表现,结合系统软件优化的规模化效应,将为快手带来可观的成本节省收益。
-
编程效率提升:快手各业务线层进行了部分不彻底的异步化改造,导致框架代码复杂度增加,可维护性降低,架构演进受阻。透明协程的引入有助于提升快手高并发架构的开发效率。
-
云原生架构演进:协程将补齐Java语言的短板,助力快手的技术架构更好地适应云原生场景,为长远技术规划奠定基础。
2.1 Java 协程方案选型

目前Java业界具有代表性的方案有两类:Oralce官方的Loom协程,阿里Dragonwell社区的Wisp协程。二者特点如下:
-
透明性:Loom不支持透明协程,这意味着业务方在引入Loom时需要对原有代码进行一定的改造与适配。相比之下,Wisp协程则提供了透明协程的支持,使得业务方能够在几乎不感知协程存在的情况下轻松使用。
-
切换性能:Loom的切换性能相对较低,这主要源于其对栈序列化的处理。而Wisp则凭借高效的切换机制,实现了更高的切换性能。理论上,更高的切换性能意味着能够支持更高的QPS,从而带来更好的系统性能与用户体验。
-
并发数:由于Loom协程的栈是按需使用的,因此它占用的物理内存较少,同时对StopTheWorld事件的影响也更小。这使得Loom能够支持更高的并发数,并通过结构化并发的策略,进一步降低服务的响应时间(RT)。
综合考虑快手Java服务的庞大体量以及业务适配改造的成本,最终选择基于Dragonwell社区的Wisp协程方案进行改造优化。
2.2 快手Java 协程架构演讲

2.2.1 社区协程架构
Dragonwell社区的原生协程架构作为快手协程架构的雏形,整体如下:

社区Java协程架构分为调度器、IO管理模块、Timer管理模块和Locker管理模块4个主体。具体来说:调度器的工作线程是WispCarrier,其数量和CPU核数相当,主要职责包括轮询RunQueue获取任务进行执行,查询IO管理/Timer管理/Locker管理模块获取就绪任务到RunQueue,Steal任务等。如果WispCarrier处于空闲状态则会进入休眠,让出CPU资源;IO管理模块主要负责维护所有FD和阻塞Task的映射关系,基于Epoll机制提供IO就绪状态的查询能力;Timer管理模块则专注于定时器的全面管理,每个定时器对应一个阻塞Task,该模块提供定时器到期Task查询能力;Locker管理模块同样不可或缺,负责统筹管理所有因锁而阻塞的任务,并具备高效查询锁就绪状态下相关任务的能力。
由于快手的Java服务场景相对复杂,上述架构模块内部的一些机制缺陷在落地过程中逐步暴露出来,成为快手Java透明协程规模化落地的主要障碍。缺陷主要集中在如下几个方面(对应上面架构图红色标记部分):
-
调度器缺陷:原生调度实现策略在低负载工况下CPU消耗偏高,无法满足客户的需求。
-
抢占缺陷:原生架构下协程长任务抢占机制开销大,且无法实现JNI长任务的及时抢占,导致部分服务的长尾延时高,影响服务可用性。
-
IO管理缺陷:原生IO管理机制在部分场景下IO查询不及时,导致服务的平均延时严重劣化。
快手需要通过持续的Java透明协程架构升级演进,来解决上述一系列制约Java透明协程技术规模化落地的障碍。
2.2.2 调度CPU优化

为了攻克Wisp调度器在低负载下的CPU效率难题,核心在于优化Context-Switch频率。然而,我们面临两大挑战:一是Wisp原生的认主模式导致任务均匀分散在所有WispCarrier上,难以实现任务集中执行以降低切换开销;二是低负载时,Wisp需依赖WispCarrier0和WispCarrier1(作为IO Poller)两个线程协同工作,这进一步加剧了协程间的Context-Switch频率。
针对上述的问题,我们提炼出调度器设计的通用原则:

在新的架构下,WispCarrier的CPU资源分布呈现出一个倒金字塔状集中分布,完美契合了我们的设计初衷,成功消除了Wisp协程在低负载工况下相对于传统协程的CPU效率劣势。具体见下图:

2.2.3 调度抢占优化
协程的抢占长尾延时高一直是业界面临的难题,协程的切换时机完全依赖于用户代码行为,如果遇到长任务(用户代码长时间运行非阻塞代码不释放WispCarrier),就会造成业务长尾延时高。为了缓解该问题,社区Wisp协程基于Safepoint机制实现了调度抢占,但该机制存在如下问题:
-
Java长任务抢占代价高:为了抢占一个业务协程,Safepoint需要打断所有的用户线程进入昂贵的StopTheWorld,导致所有线程的业务RT都会发生抖动,影响面大。
-
JNI长任务无法抢占:快手大量使用JNI,而JNI是无法被Safepoint打断的,因此JNI长任务的长尾延时劣化无法解决。
对于Java长任务的抢占,我们抛弃了昂贵的全局Safepoint,改用Java17引入的Handshake机制来实现抢占。Handshake机制能够实现特定线程的打断,将StopTheWorld改为StopTheThread,避免了StopTheWorld导致的所有线程的暂停。
为了实现JNI长任务的抢占,我们重新思考了抢占的本质。抢占的本质目的在于消除长任务执行对其它任务的影响,虽然JNI没有类似Handshake的机制能够打断特定任务,但如果将受影响的任务及时转交给其它的WispCarrier进行补偿,同样也可以消除长任务的影响。基于思路的转换,我们针对JNI长任务设计了HandOff调度抢占机制(Wisp社区存在HandOff机制的原型,但很遗憾并没有最终完全实现):当调度器发现某个JNI任务执行时间过长需要触发抢占时,我们将对应的WispCarrier中受影响的任务全部HandOff移交给其它空闲WispCarrier来执行,这样JNI长任务就被限制在一个单独的WispCarrier里独立运行,不会影响其它WispTask。HandOff抢占机制整体架构图如下:

对比旧的抢占机制,新的任务抢占机制有着显著的优点:
-
能够抢占JNI长任务:HandOff通过补偿空闲线程的方式,巧妙得实现了对于JNI长任务的抢占。
-
抢占代价低:对于Java长任务,Handshake抢占代价显著优于Safepoint抢占;对于JNI长任务,基于一系列关键数据结构的重构(WP分离),HandOff仅仅是唤醒空闲线程和交换指针,抢占开销非常小。
基于新的调度器抢占设计,我们解决了JNI长任务造成的长尾延时劣化,扩大了协程优化的落地适用范围,并提升了抢占的性能。优化效果如下:

2.2.4 IO模型优化
针对Wisp IO模型在生产环境中推广时所暴露的缺陷,我们进行了IO模型的重构,旨在解决以下问题:
-
查询不及时:Wisp进行IO查询的响应速度不足,导致响应时间过长。
-
设计低效:Wisp原生的IO管理模块采用基于HashMap的集中式FD到WispTask的关系映射设计存在激烈临界态竞争。并且Epoll采用EPOLLONESHOT模式,系统调用过多。
-
堆外内存膨胀:Wisp的Socket劫持实现完全拷贝Java8的旧的Socket,相比于Java17的线程模型下NIOSocket,其ThreadLocal DirectBuffer堆外缓存不设容量上限,堆外内存资源消耗较多。
这些缺陷在某些服务工况下,使得Wisp的RT相对于线程模型显著劣化。为了克服这些挑战,我们遵循以下协程IO模型的设计原则进行了优化:
-
非阻塞IO补偿:在适当时机进行非阻塞IO补偿,以规避timedEpoll执行优先级较低可能带来的IO延时风险。
-
复用内核结构:尽可能复用内核数据结构,简化用户态设计,同时采用边沿触发代替EPOLLONESHOT模式,一次性为FD注册所有IO事件,从而实现系统调用作用范围的复用,大幅度减少epoll_ctl系统调用的频率。
-
资源缓存限定:对于ThreadLocal DirectBuffer等缓存资源,设定合理的上限,以防止资源消耗失控。
下图是基于上述原则重构后的IO架构图:


2.2.5 快手Java协程架构

-
调度器缺陷:通过调度策略的重新设计,我们有效控制了低负载工况下协程的CPU消耗,满足了客户的需求。
-
抢占机制缺陷:通过新的Handshake和HandOff机制,我们显著降低了Java长任务抢占开销,并实现了JNI长任务的及时抢占,降低了服务可用性风险。
-
IO管理缺陷:通过IO管理模块关键数据结构的重构改造,消除了IO查询不及时导致的服务平均延时劣化。
-
与Loom的深度融合:Loom作为OpenJDK社区的官方协程实现,我们将努力推动其与Wisp协程的和谐共存,以实现技术的互补与协同。
-
调度策略与调度器的解耦:为了提供更加灵活高效的协程管理,我们将进一步将调度策略与调度器设计进行解耦,允许用户根据自身需求自定义协程策略。这将有助于用户实现更具针对性的、性能更优的调度方案,从而进一步提升服务效能。
