在事件循环中使用暂停器
Chronicle的开源暂停器可以帮助在资源使用和响应性、低延迟、低抖动的应用之间提供平衡。
通常在低延迟开发中,必须在最小化延迟和避免过度使用CPU之间做出权衡。本文探讨了Chronicle的Pausers--一个开源产品--如何在没有数据需要处理的时候自动应用back-off策略,在资源使用和响应式、低延迟、低抖动的应用之间提供平衡。
问题的描述
在一个典型的应用堆栈中,多个线程被用于服务事件、处理数据、管道化等等。一个重要的设计考虑是线程如何意识到有工作要做,一些一般的方法包括:
- 信号/通知:在这种情况下,接收线程产生(即被添加到一个等待队列),直到被另一个线程通知。这样做的好处是资源消耗低;然而,有一个相对较高的延迟,至少20-50微秒(可能更多--见下文),以响应信号而重新安排线程。
- 忙碌的等待:在这种情况下,接收线程不断地旋转,检查是否有工作要做。这样做的好处是,当有工作要做时,可以快速响应(低延迟);然而,它的代价是CPU的高使用率,在没有工作的时候浪费了周期。此外,持续的高CPU使用率反过来会导致明显的高功率需求和相关的冷却负荷。
- 固定睡眠:在这种情况下,当没有进一步的工作要做时,接收线程在再次检查是否有更多的工作之前,会睡眠一段时间。这样做的好处是资源使用率低,但这种策略的明显缺点是,最坏情况下的延迟至少和睡眠时间一样大。
睡眠的问题,以及如何睡得安稳
当线程请求睡眠时的实际行为不仅在不同的平台上有所不同,而且在同一平台的不同版本和使用模式上也有所不同。
例如,POSIX要求睡眠调用总是产生CPU,而Linux允许睡眠实现(包括sleep、usleep、nanosleep和类似)在某些情况下忙于等待。对于有固定定时器刻度(通常是100Hz、250Hz或1000Hz)的旧版Linux来说,当屈服于调度器时,会有一个相对较大的惩罚,这就鼓励了在睡眠调用内部短时间内使用忙碌等待。相比之下,最近版本的Linux有更复杂的调度器,使用动态tick,这使得与睡眠线程的短时期互动更加准确,这在很大程度上消除了忙于等待以实现低睡眠期的需要。
对于标准进程(即在标准调度器下以正常权限运行的进程),以下经验法则通常适用于最近的Linux版本:
- 睡眠请求~1us,原则上可以合理准确地提供服务
- 一般来说,即使是很短的睡眠时间也不会忙于等待,但极短的时间几乎肯定会等待。
- 与繁忙等待(100%)相比,
1ms和1us的睡眠请求分别将CPU使用率降低到1%和10%。
虽然上述情况表明,即使是相对较短的~1us的睡眠也有可能在延迟和资源使用之间提供一个有用的折衷,但主要的问题是调度--只要睡眠进程完全关闭一个核心的上下文,重新调度的开销就会比预定的睡眠时间高几个数量级。
在这里,对于系统将如何表现,同样没有单一的答案。关键是要尽可能地偏重于这种情况,以避免线程被从一个核心中切换出来,在这种情况下,使用线程亲和力(以避免线程被转移到另一个核心)和CPU隔离(以避免另一个进程/线程与线程竞争)可以非常有效。(其他选择包括使用实时优先级运行;然而,我们希望尽可能地将本文件的重点放在标准设置上)。
谨慎地使用亲和力、隔离和短暂的睡眠期可以带来响应迅速、低抖动的环境,与繁忙的等待相比,它使用的CPU资源要少得多。
什么是暂停器?
Chronicle的暂停器--一个开源产品--提供了一个介于上述信号/通知、固定睡眠和忙于等待的极端行为的滑动尺度,它使用了一个智能的回避策略,能够进行更细致的控制,以更好地平衡低延迟和资源利用。
一般的策略是在没有工作要做的时候,先忙着等待一小段时间,然后逐步退到越来越长的暂停时间(消耗的CPU数量越来越少)。根据任务的不同,有不同的策略(暂停模式),使用暂停器的典型方式是:
while (running) {
if (pollForWork()) // pollForWork returns true if work was done
pauser.reset(); // minimal or no pause path
else
pauser.pause(); // incrementally back off
}
暂停模式
本表说明了几种不同的保释模式,以及使用每种模式的好处和坏处。
表1.暂停器模式
Chronicle Pausers允许优化CPU负载,以达到一定的响应速度和延迟水平。这种权衡可以配置得很精确,而不需要对你的应用程序代码进行重大修改。例如,如果你意识到一个特定的线程需要更多的响应,你可以把它的Pauser从back-off Pauser改为busy Pauser,反之亦然。
值得注意的是,用于最低延迟的Busy Pauser在内部使用繁忙等待,因此会消耗一个核心的100%。因此,重要的是要确保忙碌的暂停者不争夺同一个核心,在使用忙碌的暂停者来控制这方面的问题时,应该考虑CPU亲和力和隔离。关于CPU隔离及其在事件循环中的好处的更多信息可以在这里找到。
暂停器模式的性能
下图绘制了等待事件的时间(X轴)与暂停/响应时间的对比,选择了一些暂停器。
忙碌、定时忙碌、屈服和Millis Pausers显示了平坦的响应时间,无论线程等待多久接收事件,但由于不同的屈服策略与CPU使用率,响应时间各不相同。在许多情况下,特别是TimedBusy在低延迟和CPU使用率之间提供了一个很好的折中。
Sleepy和Balanced策略显示了响应时间的阶梯式变化和稳定增长,反映了线程等待接收事件的时间越长,响应时间就越长的增量。
图1.暂停模式的性能
总结
本文探讨了Chronicle Pausers的使用,以及如何使用它们来构建响应迅速、低延迟、低抖动的应用程序,并使CPU利用率相对较低。这反过来又有助于最大限度地提高硬件利用率,同时也降低了功耗,有助于降低企业的成本。