性能文章>【译】如何用Chronicle 的开源Pauser,平衡低延迟和CPU使用率?>

【译】如何用Chronicle 的开源Pauser,平衡低延迟和CPU使用率?转载

1周前
203715

通常在低延迟开发中,必须在最小化延迟和避免过度使用 CPU 之间进行权衡。本文探讨了如何使用Chronicle 的Pausers(一种开源产品)在没有要处理的数据时设置自动退避策略,从而在资源使用和响应式、低延迟、低抖动的应用程序之间取得平衡。

问题描述

在典型的应用程序堆栈中,多个线程用于服务事件、处理数据、流水线等。一个重要的设计考虑是线程如何意识到自己有工作要做,一些通用方法包括:

  • 信号/通知:在这种情况下,接收线程让步(即被添加到等待队列中)直到被另一个线程通知。这样做的好处是资源消耗低;然而,为了响应信号重新调度线程,至少有 20-50 微秒(可能更多——见下文)的相对较高的延迟。
  • Busy Waiting:在这种情况下,接收线程不断地旋转,检查是否有一些迹象表明有工作要做。当线程有工作要做时,这具有快速响应(低延迟)的好处;但是,它的代价是 CPU 使用率很高,在无事可做时会浪费周期。此外,持续的高 CPU 使用率反过来会导致明显更高的功率需求和相关的冷却负载。
  • 固定休眠:在这种情况下,当没有进一步的工作要做时,接收线程会休眠一段固定的时间,然后再次检查是否有更多的工作。这具有资源使用率低的好处,但该策略的明显缺点是最坏情况下的延迟至少与睡眠周期一样大。

线程睡眠问题

线程请求睡眠时的实际行为不仅因平台而异,而且因同一平台的不同版本和使用模式而异。

例如,POSIX 要求 sleep 调用总是让出 CPU,而 Linux 允许 sleep 实现(包括 sleep、usleep、nanosleep 等)在某些情况下忙于等待。对于具有固定计时器滴答声(通常为 100Hz、250Hz 或 1000Hz)的旧版本 Linux,在向调度程序让步时会产生相对较大的损失,这鼓励在睡眠调用中短时间使用内部忙等待。相比之下,较新的 Linux 版本具有使用动态滴答的更复杂的调度程序,可以与睡眠线程进行更准确的短周期交互,这在很大程度上消除了为实现低睡眠周期而进行的忙等待的需要。

以下经验法则通常适用于标准进程的最新 Linux 版本(即在标准调度程序下以正常权限运行的那些):

  • 睡眠请求 ~1us 原则上可以以合理的精度得到服务
  • 一般来说,即使是很短的睡眠时间也不会忙于等待,尽管极短的睡眠时间几乎肯定会
  • 与忙等待 (100%) 相比,~1ms 和 ~1us 的睡眠请求将 CPU 使用率分别降低到 ~1% 和 ~10%

虽然以上表明,即使是相对较短的约 1us 睡眠也可能会在延迟和资源使用之间提供有用的折衷,但主要问题是调度——一旦睡眠进程完全上下文关闭内核,重新调度的开销可能是比预期的睡眠时间高几个数量级。

同样,对于系统的行为方式,没有单一的答案。关键是尽可能的偏向情况,避免线程从一个核切换,使用线程亲和(避免线程被移动到另一个核)和CPU隔离(避免另一个进程/线程争用)线程)在这种情况下可能非常有效。(其他选项包括以实时优先级运行;但是,我们希望尽可能将本文档的重点放在标准设置上。)

仔细使用亲和性、隔离性和较短的睡眠时间可以产生响应迅速、低抖动的环境,与忙碌的等待相比,这些环境使用的 CPU 资源要少得多。

什么是暂停器?

Chronicle 的 Pausers — 一种开源产品 — 通过使用智能退避策略在上述极端信号/通知、固定睡眠和忙碌等待之间提供可滑动的行为规模,从而实现更细微的控制以更好地平衡低延迟和资源利用率。

一般的策略是在没有工作要做的时候,先忙着等待一段时间,然后再逐步退回到越来越长的暂停(消耗的 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 负载。这种权衡可以高精度配置,而无需对应用程序代码进行重大更改。例如,如果您意识到某个特定线程需要更具响应性,您可以将其暂停从退避暂停更改为繁忙暂停,反之亦然。 

值得注意的是,用于最低延迟的 Busy Pauser 在内部使用忙等待,因此将消耗 100% 的一个内核。因此,确保 Busy Pauser 不会争用同一个内核非常重要,并且在使用 Busy Pauser 控制这方面时应考虑 CPU 亲和性和隔离性。有关 CPU 隔离及其在事件循环中的好处的更多信息,请参见此处

暂停模式的性能

下图绘制了等待事件的时间(x 轴)与所选暂停者的暂停/响应时间。

Busy、TimedBusy、Yielding 和 Millis Pauser 显示平坦的响应时间,无论线程等待接收事件多长时间,但由于不同的 yield 策略和 CPU 使用率,响应时间不同。在许多情况下,TimedBusy 特别是在低延迟和 CPU 使用率之间提供了极好的折衷。

Sleepy 和 Balanced 策略显示响应时间的阶跃变化和稳定增长,反映了线程等待接收事件的时间越长,增量回退。

图 1. 暂停模式性能

结论

本文探讨了 Chronicle 的 Pausers 的使用,以及它们如何用于构建响应式、低延迟、低抖动且 CPU 利用率相对较低的应用程序。这反过来又有助于最大限度地提高硬件利用率,同时降低功耗,帮助降低组织成本。

 

点赞收藏
金色梦想

终身学习。

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】(大表小技巧)有时候 2 小时的 SQL 操作,可能只要 1 分钟

【全网首发】(大表小技巧)有时候 2 小时的 SQL 操作,可能只要 1 分钟

一次java内存top res高排查记录

一次java内存top res高排查记录

干货!Java代码优化必知的30个小技巧!

干货!Java代码优化必知的30个小技巧!

Java 异步调用原理与实战

Java 异步调用原理与实战

代码改成多线程,竟有9大问题

代码改成多线程,竟有9大问题

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

5
1