背景

一般情况下,吞吐量和延迟之间是有成正比的,即吞吐量越大,任务的延迟也就越大,反之亦然。在许多的应用场景下,更希望的是吞吐量大,同时也希望保证低延迟的。
如果是为了满足吞吐量,但是对延迟的要求并不是特别高,只需要保证最终一致性,则多线程即可保证。但是为了保证低延迟,则更希望JVM/MicroSerivce/Machine只专注一件事。
实际上,单线程在专注一个任务时,效率是非常高的。为此,要保证整个系统的低延迟,单个CPU/CPU Core绑定一个JVM/MicroSerivce/Machine是一个非常好的解决方案。那如何解决呢?

方案

服务之间使用高速总线/队列通信,或者其他队列或者总线,具体有Kafka,RabbitMQ, RocketMQ等等。

Archtecture
Archtecture

每个服务都是单线程。OK,可能这个时候有人问了,那么在这种情况下,多个服务就类似于多线程了,跟原来单机的多线程岂不是没有区别?实际上这是个概念问题,即并行和并发的区别。
简单讲一下并发和并行的区别。

  • 并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。

  • 并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。

总结:可以看到,在一个时间点上,并发只做一个任务,并行在做多个任务。

  1. 并发是单CPU操作,牺牲线程的调度时间,完成一个多线程完成多任务的操作,主要牺牲时间。
  2. 并行是多CPU操作,系统CPU的数量,完成多个CPU完成多任务的操作,主要牺牲物理资源。
并行和并发
并行和并发

所以总结以上,在提高任务的处理效率,降低其延时的瓶颈都落到了高速总线上了,但这不是本节所讲的重点。回归重点,如何将一个线程绑定到一个CPU上,只专注做一件事情呢?我在GitHub上找到了一个开源库Java-Thread-Affinity,就能做到这件事。

代码

在我仔细研读Java-Thread-Affinity的使用介绍后,其核心主要是两点:

  1. 在Linux环境下,隔离CPU,可参考:Get Started
  2. 使用上述代码库,可参考:Using AffinityLock

比如在我前文事件分发器(订阅/发布模式)的实现中提到的事件分发器,我在其中设计一个定时事件,但是又想保住其任务的准时,定时器事件不受队列排队影响导致过多的延时,这时,CPU隔离就显得格外重要,活不多说,coding please。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
timer = new Thread(() -> {
AffinityLock al = AffinityLock.acquireLock();
try {
while (isActive) {
try {
TimeUnit.MILLISECONDS.sleep(interval);
publishTimer();
} catch (Exception e) {
logger.error("Failed to generate timer event", e);
}
}
} finally {
al.release();
}
});

可以看到我使用AffinityLock.acquireLock()获取一个CPU锁,这样既可以完成对CPU的隔离,并实时分发事件。