对于网关服务来说,需要支撑海量的请求,那必然要使用到多线程,也就不可避免的会导致线程切换。如果可以将这些繁忙的线程绑定到一个cpu核上,可以确保该线程的最大执行速度,实现低延迟,消除操作系统进行调度过程导致线程迁移所造成的抖动影响,还可以避免由于缓存失效而导致的性能开销。
本文以开源项目SONA为例,介绍了一种通过将java线程与CPU绑定的方法,将服务整体性能提升了约25%。本文最后附上开源项目地址。
前言
Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关。
对于网关服务来说,需要支撑海量的请求,那必然要使用到多线程,也就不可避免的会导致线程切换。
线程切换的常见原因:
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务。
- 当前执行任务碰到IO阻塞, 调度器将挂起此任务, 继续下一任务
- 多个任务抢占锁资源, 当前任务没有抢到,被调度器挂起, 继续下一任务
- 用户代码挂起当前任务, 让出CPU时间
- 中断。硬件中断:外设发送电信号给处理器,异步,IRQ(interrupt request);软中断:异常情况 or 特殊指令集,软中断可以用来实现system call。
之前的文章也介绍过,为了支撑海量请求 ,SONA网关使用了三层的网络处理模型,其中最重要的就是 Netty 的 boss 与work eventloop (千万不要在 eventloop 中做任何可能导致阻塞的操作,否则会严重影响系统的 qps),如果可以将这些繁忙的线程绑定到一个cpu核上,可以确保该线程的最大执行速度,实现低延迟,消除操作系统进行调度过程导致线程迁移所造成的抖动影响,还可以避免由于缓存失效而导致的性能开销。
一、CPU绑定
在Linux系统中,进程的调度切换是由内核自动完成的,在多核CPU上,进程有可能在不同的CPU核上来回切换执行,这对CPU的缓存不是很有利。
因为在多核CPU结构中,每个核心有各自的L1、L2缓存,而L3缓存是共用的。如果一个进程在核心间来回切换,各个核心的缓存命中率就会受到影响。相反如果进程不管如何调度,都始终可以在一个核心上执行,那么其数据的L1、L2 缓存的命中率可以显著提高。
另外使用CPU绑定可以将关键的进程隔离开,对于部分实时进程调度优先级提高,可以将其绑定到一个指定CPU核上,可以保证实时进程的调度,也可以避免其他CPU上进程被该实时进程干扰。我们可以手动地为其分配CPU核,而不会过多的占用同一个CPU,所以设置CPU亲和性可以使某些程序提高性能。
二、CPU亲和性
CPU 亲和性(affinity)就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。
软亲和性(affinity): 是进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他处理器,Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。
硬亲和性(affinity):是利用linux内核提供给用户的API,强行将进程或者线程绑定到某一个指定的cpu核运行。
在 Linux 内核中,所有的进程都有一个相关的数据结构,称为 task_struct 。其中与 亲和性(affinity)相关度最高的是cpus_allowed 位掩码。这个位掩码由n位组成,与系统中的n个逻辑处理器一一对应。 具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程,那么这个系统就有一个 8 位的位掩码。
如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 CPU 上运行。因此,如果一个进程可以在任何 CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1。这也是 Linux 中进程的缺省状态。
三、Java-Thread-Affinity
实现CPU 绑核需要使用到Linux内核的库,对于JAVA应用来说,并不能直接调用。好在Chronicle Software 开源了一个java 库 Java-Thread-Affinity,它的实现原理是通过JNA(Java Native Access)调用底层的C函数,设置CPU亲和性。这个工具能够识别独立的CPU,基于最优效果原则,根据开发人员提供的规则将线程绑定到合适的CPU上。
pom.xml中需增加以下依赖
<dependency>
<groupId>net.openhft</groupId>
<artifactId>affinity</artifactId>
<version>3.0.6</version>
</dependency>
创建一个特定策略的AffinityThreadFactory
这里 AffinityStrategies 有多种策略,最好选择 DIFFERENT_CORE
目前 mercury 网关服务用的 阿里云 ECS ,配置是 4核8g。
部署在Linux上的话,mercury 默认使用的是 epoll 模式。所以这里boss eventloop设置的 1, work eventloop设置的 4,观察下来效果是比较好的
bootstrap = new ServerBootstrap();
bossGroup = NettyFactory.eventLoopGroup(1, "bossLoopGroup");
workerGroup = NettyFactory.eventLoopGroup(4, "workerLoopGroup");
在自定义的业务线程池里也使用了
在使用 Java-Thread-Affinity 优化后,相同的压测条件下 ,mercury-server 的 CPU使用率从 81.03%降至 65.18%
这里大致说一下 Java-Thread-Affinity的实现:
第一次分配任意空闲的cpu,后续根据策略列表给出另一个与此相关的 affinity lock
net.openhft.affinity.AffinityThreadFactory
这里的 bind ,AffinityLock.acquireLock() 传的是 true ,lastAffinityLock.acquireLock(strategies) 传的是 false
进行绑定
Linux下使用了sched_setaffinity 函数来实现的 ,这里的 CLibrary 是通过 jna 调用 C
sched_setaffinity ()设置进程的 CPU 关联掩码
param 1:进程的pid,如果 pid 为0,则使用调用此函数的进程
param 2:cpusetsize 参数是掩码指向的数据的长度(以字节为单位)
param 3:掩码,为cpu_set_t类型
Disruptor 等待策略
这里再额外提一个知识点,disruptor 中的8种等待策略里面,有一个是 BusySpinWaitStrategy(通过不停自旋等待),它的注释上就写了这么一句话:It is best used when threads can be bound to specific CPU cores. 当线程可以被绑定到特定的CPU核心时,用这个策略是最佳的。
总结
本文详细介绍了SONA长连接网关中是如何实现CPU绑定,提升性能,在后续的系列文章中会对网关中的其他技术细节进行详细的介绍。