对于网关服务来说,需要支撑海量的请求,那必然要使用到多线程,也就不可避免的会导致线程切换。如果可以将这些繁忙的线程绑定到一个cpu核上,可以确保该线程的最大执行速度,实现低延迟,消除操作系统进行调度过程导致线程迁移所造成的抖动影响,还可以避免由于缓存失效而导致的性能开销。

本文以开源项目SONA为例,介绍了一种通过将java线程与CPU绑定的方法,将服务整体性能提升了约25%。本文最后附上开源项目地址。


前言

Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关。

对于网关服务来说,需要支撑海量的请求,那必然要使用到多线程,也就不可避免的会导致线程切换。

线程切换的常见原因:

  1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务。
  2. 当前执行任务碰到IO阻塞, 调度器将挂起此任务, 继续下一任务
  3. 多个任务抢占锁资源, 当前任务没有抢到,被调度器挂起, 继续下一任务
  4. 用户代码挂起当前任务, 让出CPU时间
  5. 中断。硬件中断:外设发送电信号给处理器,异步,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亲和性可以使某些程序提高性能。

JAVA 开源的API网关 java实现网关_java

二、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

JAVA 开源的API网关 java实现网关_JAVA 开源的API网关_02

这里 AffinityStrategies 有多种策略,最好选择 DIFFERENT_CORE

JAVA 开源的API网关 java实现网关_websocket_03

目前 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 开源的API网关 java实现网关_JAVA 开源的API网关_04

在使用 Java-Thread-Affinity 优化后,相同的压测条件下 ,mercury-server 的 CPU使用率从 81.03%降至 65.18%

这里大致说一下 Java-Thread-Affinity的实现:

第一次分配任意空闲的cpu,后续根据策略列表给出另一个与此相关的 affinity lock

net.openhft.affinity.AffinityThreadFactory

JAVA 开源的API网关 java实现网关_后端_05

这里的 bind ,AffinityLock.acquireLock() 传的是 true ,lastAffinityLock.acquireLock(strategies) 传的是 false

JAVA 开源的API网关 java实现网关_websocket_06

进行绑定

JAVA 开源的API网关 java实现网关_实时音视频_07

Linux下使用了sched_setaffinity 函数来实现的 ,这里的 CLibrary 是通过 jna 调用 C 

sched_setaffinity ()设置进程的 CPU 关联掩码

param 1:进程的pid,如果 pid 为0,则使用调用此函数的进程

param 2:cpusetsize 参数是掩码指向的数据的长度(以字节为单位)

param 3:掩码,为cpu_set_t类型

JAVA 开源的API网关 java实现网关_java_08

JAVA 开源的API网关 java实现网关_实时音视频_09

Disruptor 等待策略

这里再额外提一个知识点,disruptor 中的8种等待策略里面,有一个是 BusySpinWaitStrategy(通过不停自旋等待),它的注释上就写了这么一句话:It is best used when threads can be bound to specific CPU cores.  当线程可以被绑定到特定的CPU核心时,用这个策略是最佳的。

JAVA 开源的API网关 java实现网关_java_10


总结

本文详细介绍了SONA长连接网关中是如何实现CPU绑定,提升性能,在后续的系列文章中会对网关中的其他技术细节进行详细的介绍。