有时多即是少。 当减少负载导致延迟增加时

如在 大多数帖子,一个分布式服务出现了问题,我们称这个服务为Alvin。 这次不是我自己发现问题,是客户端的人告诉我的。

有一天,我醒来时收到一封不满的电子邮件,因为我们计划在不久的将来推出 Alvin,原因是长时间拖延。 具体来说,客户的 99% 延迟时间在 50 毫秒左右,远高于我们的延迟预算。 这是令人惊讶的,因为我对该服务进行了广泛的测试,特别是在延迟方面,这是一个常见的抱怨。

在将 Alvin 投入测试之前,我以每秒 40k 查询 (QPS) 的速度进行了大量实验,所有结果都显示延迟小于 10 毫秒。 我准备声明我不同意他们的结果。 但再看一遍这封信,我发现了一些新的东西:我没有准确测试他们提到的条件,他们的 QPS 比我低得多。 我测试的 QPS 为 40k,但他们只有 1k。 我又进行了一次实验,这次 QPS 较低,只是为了安抚他们。

由于我在博客中讨论了这一点,您可能已经发现他们的数字是正确的。 我一遍又一遍地测试我的虚拟客户端,结果都是一样:请求数量少不仅会增加延迟,而且会增加延迟超过 10 毫秒的请求数量。 换句话说,如果在 40k QPS 时每秒大约有 50 个请求超过 50 毫秒,那么在 1k QPS 时每秒有 100 个请求超过 50 毫秒。 悖论!

有时多即是少。 当减少负载导致延迟增加时

缩小搜索范围

当在具有许多组件的分布式系统中面临延迟问题时,第一步是创建一个简短的嫌疑人列表。 让我们更深入地研究一下 Alvin 的架构:

有时多即是少。 当减少负载导致延迟增加时

一个好的起点是已完成的 I/O 转换列表(网络调用/磁盘查找等)。 让我们尝试找出延迟在哪里。 除了与客户端进行明显的 I/O 之外,Alvin 还采取了额外的步骤:访问数据存储。 然而,该存储与 Alvin 在同一集群中运行,因此那里的延迟应该小于客户端的延迟。 那么,嫌疑人名单如下:

  1. 从客户端到 Alvin 的网络调用。
  2. 从 Alvin 到数据存储的网络调用。
  3. 在数据存储中的磁盘上搜索。
  4. 从数据仓库到 Alvin 的网络调用。
  5. Alvin 到客户端的网络调用。

让我们试着划掉一些要点。

数据存储与此无关

我做的第一件事是将 Alvin 转换为不处理请求的 ping-ping 服务器。 当它收到请求时,它返回一个空响应。 如果延迟减少,那么 Alvin 或数据仓库实现中的错误并不是闻所未闻的。 在第一个实验中,我们得到下图:

有时多即是少。 当减少负载导致延迟增加时

正如您所看到的,使用 ping-ping 服务器时没有任何改进。 这意味着数据仓库不会增加延迟,并且嫌疑人列表减少了一半:

  1. 从客户端到 Alvin 的网络调用。
  2. Alvin 到客户端的网络调用。

伟大的! 该名单正在迅速缩小。 我想我已经差不多明白原因了。

远程过程调用

现在是时候向您介绍一位新玩家了: 远程过程调用。 这是 Google 的一个开源库,用于进程内通信 RPC的。 虽然 gRPC 优化良好且广泛使用,这是我第一次在这种规模的系统上使用它,我预计我的实现至少可以说是次优的。

可用性 gRPC 在堆栈中产生了一个新问题:也许是我的实现或我自己 gRPC 造成延迟问题? 将新嫌疑人添加到名单中:

  1. 客户致电图书馆 gRPC
  2. 图书馆 gRPC 对客户端上的库进行网络调用 gRPC 在服务器上
  3. 图书馆 gRPC 联系Alvin(乒乓服务器情况下无操作)

为了让您了解代码的样子,我的客户端/Alvin 实现与客户端-服务器实现没有太大区别 异步示例.

注意:上面的列表有点简化,因为 gRPC 可以使用您自己的(模板?)线程模型,其中执行堆栈是交织在一起的 gRPC 和用户实施。 为了简单起见,我们将坚持使用这个模型。

分析将解决一切问题

划掉数据存储后,我以为我快完成了:“现在很简单! 让我们应用该配置文件并找出延迟发生的位置。” 我 精密仿形的忠实粉丝,因为 CPU 速度非常快,而且通常不是瓶颈。 大多数延迟发生在处理器必须停止处理以执行其他操作时。 准确的 CPU 分析就是这样做的:它准确地记录一切 上下文切换 并明确哪里发生了延误。

我在客户端和服务器端获取了四个配置文件:具有高 QPS(低延迟)和具有低 QPS(高延迟)的乒乓服务器。 为了以防万一,我还获取了一个示例处理器配置文件。 在比较配置文件时,我通常会寻找异常的调用堆栈。 例如,在高延迟的坏方面,会有更多的上下文切换(10 次或更多)。 但就我而言,上下文切换的次数几乎相同。 令我恐惧的是,那里没有任何重要的东西。

额外的调试

我很绝望。 我不知道我还可以使用哪些其他工具,我的下一个计划本质上是用不同的变化重复实验,而不是清楚地诊断问题。

如果

从一开始,我就担心具体的 50ms 延迟。 这是一个非常重要的时刻。 我决定从代码中删除一些块,直到我能够准确地找出导致此错误的部分。 然后进行了一项有效的实验。

和往常一样,事后看来,一切都是显而易见的。 我将客户端放置在与 Alvin 相同的计算机上 - 并向 localhost。 并且延迟的增加消失了!

有时多即是少。 当减少负载导致延迟增加时

网络出现问题。

学习网络工程师技能

我必须承认:我对网络技术的了解很糟糕,尤其是考虑到我每天都与它们一起工作。 但网络是主要嫌疑人,我需要学习如何调试它。

幸运的是,互联网喜爱那些想要学习的人。 ping 和tracert 的组合似乎是调试网络传输问题的一个足够好的开始。

首先,我推出了 到 Alvin 的 TCP 端口。 我使用默认设置 - 没什么特别的。 在 10 多个 ping 中,除了第一个用于预热的 ping 之外,没有一个超过 50 毫秒。 这与在第 99 个百分位数处观察到的 100 毫秒延迟增加相反:在那里,对于每 50 个请求,我们应该看到大约一个请求的延迟为 XNUMX 毫秒。

然后我尝试了 tracert命令:Alvin 和客户端之间的路线上的某个节点可能存在问题。 但追踪者也空手而归。

因此,导致延迟的不是我的代码、gRPC 实现或网络。 我开始担心我永远无法理解这一点。

现在我们使用什么操作系统

gRPC 在 Linux 上广泛使用,但在 Windows 上却很奇特。 我决定尝试一个实验,结果很有效:我创建了一个 Linux 虚拟机,为 Linux 编译了 Alvin,然后部署了它。

有时多即是少。 当减少负载导致延迟增加时

发生的事情是这样的:尽管数据源没有什么不同,但 Linux 乒乓球服务器没有与类似的 Windows 主机相同的延迟。 事实证明,问题出在 Windows 的 gRPC 实现上。

内格尔算法

一直以来我都以为我丢了一面旗帜 gRPC。 现在我明白它到底是什么了 gRPC Windows 标志丢失。 我发现了一个内部 RPC 库,我相信它可以很好地适用于所有标志集 Winsock的。 然后我将所有这些标志添加到 gRPC 中,并在 Windows 上部署 Alvin,在修补的 Windows 乒乓服务器中!

有时多即是少。 当减少负载导致延迟增加时

几乎 完成:我开始一次删除一个添加的标志,直到回归返回,以便我可以查明原因。 这是臭名昭著的 TCP_NODELAY,Nagle的算法切换。

内格尔算法 尝试通过延迟消息传输直到数据包大小超过一定字节数来减少通过网络发送的数据包数量。 虽然这对于普通用户来说可能很好,但对于实时服务器来说却是具有破坏性的,因为操作系统会延迟一些消息,导致低 QPS 上的滞后。 U gRPC 该标志是在 TCP 套接字的 Linux 实现中设置的,但在 Windows 中没有设置。 我是这个 修正.

结论

低QPS下的较高延迟是由操作系统优化引起的。 回想起来,分析并没有检测到延迟,因为它是在内核模式下完成的,而不是在 用户模式。 我不知道 Nagle 的算法是否可以通过 ETW 捕获来观察,但这会很有趣。

至于 localhost 实验,它可能没有触及实际的网络代码,并且 Nagle 的算法没有运行,因此当客户端通过 localhost 到达 Alvin 时,延迟问题就消失了。

下次当您看到延迟随着每秒请求数的减少而增加时,Nagle 的算法应该出现在您的嫌疑人名单上!

来源: habr.com

添加评论