进一步推进
虽然我们最初的实验证明 zstandard 流式传输优于 zlib 流式传输,但我们剩下的问题是:“我们能将其推向多远?”我们最初的实验使用了 zstandard 的默认设置,我们想知道通过调整压缩设置,我们可以将压缩率提高到多高。
那么我们进展如何了?
调优
Zstandard 具有高度可配置性,使我们能够调整各种压缩参数。我们将精力集中在我们认为对压缩影响最大的三个参数上:chainlog、hashlog 和 windowlog。这些参数在压缩速度、内存使用量和压缩率之间提供权衡。例如,增加 chainlog 的值通常会提高压缩率,但代价是增加内存使用量和压缩时间。
我们还希望确保在我们决定的设置下,压缩上下文仍然适合我们的主机内存。虽然添加更多主机来吸收额外的内存使用量很简单,但额外的主机需要花钱,而且在某种程度上,收益的回报会递减。
我们确定总体压缩级别为 6,chainlog 和 hashlog 为 16,windowlog 为 18。这些数字略高于 您可以在此处看到的默认设置 并且可以轻松地容纳在网关节点的内存中。
Zstandard 词典
此外,我们想研究是否可以利用 zstandard 的字典支持来进一步压缩数据。通过预先向 zstandard 植入一些信息,它可以更有效地压缩前几千字节的数据。
然而,这样做会增加额外的复杂性,因为压缩器(在本例中为网关节点)和解压缩器(Discord 客户端)都需要具有相同的字典副本才能成功相互通信。
为了生成要使用的字典,我们需要数据…… 还有很多。Zstandard 有一个内置方法来生成字典(zstd——训练) 来自于数据样本,所以我们只需要收集一大堆样本。
值得注意的是,网关支持两种有效载荷编码方法:JSON 和 交易所交易基金,而 JSON 字典在 ETF 上的表现不佳(反之亦然),因此我们必须生成两个字典:每个编码方法一个。
由于字典包含部分训练数据,而且我们必须将字典发送给客户,因此我们需要确保生成字典的样本不包含任何可识别个人身份的用户数据。我们收集了 120,000 条消息的数据,通过 ETF 和 JSON 编码对其进行拆分,然后对其进行匿名化,然后生成我们的字典。
一旦我们的字典建立起来,我们就可以使用收集的数据来快速评估和迭代其功效,而无需部署网关集群。
我们尝试压缩的第一个有效载荷是“READY”。作为发送给用户的第一个(也是最大的)有效载荷之一,READY 包含有关连接用户的大部分信息,例如公会成员身份、设置和已读状态(哪些频道应标记为已读/未读)。我们使用建立基线的默认 zstandard 设置将 2,517,725 字节的单个 READY 有效载荷压缩到 306,745 字节。利用我们刚刚训练的字典,相同的有效载荷被压缩到 306,098 字节——增加了约 600 字节。
最初,这些结果似乎令人沮丧,但我们接下来尝试压缩一个较小的有效载荷,称为 TYPING_START,将其发送到客户端,以便它可以显示“XXX 正在输入…”通知。在这种情况下,636 字节的有效载荷在没有字典的情况下压缩为 466 字节,在有字典的情况下压缩为 187 字节。由于 zstandard 的运作方式,我们在使用字典处理较小有效载荷时看到了更好的结果。
大多数压缩算法都是从已压缩的数据中“学习”的,但对于较小的有效载荷,没有任何数据可供它学习。通过预先通知 zstandard 有效载荷将会是什么样子,它可以在缓冲区完全填充之前就如何压缩前几千字节的数据做出更明智的决定。
对这些发现感到满意后,我们在网关集群中部署了字典支持并开始进行试验。利用暗启动框架,我们将 zstandard 与带有字典的 zstandard 进行了比较。
我们的生产测试得出以下结果:
就绪有效载荷大小
我们特别关注了 READY 负载大小,因为它是通过 websocket 发送的第一批消息之一,并且最有可能从字典中受益。如上表所示,READY 的压缩增益很小,因此我们查看了更多调度类型的结果,希望字典能够为较小的负载提供更多优势。
不幸的是,结果有点混乱。例如,查看我们在这篇文章中一直在比较的消息 create payload size,我们可以看到字典实际上让情况变得更糟。

最终,我们决定不再继续进行字典实验。压缩字典虽然略有改进,但它们会给我们的网关服务和客户端增加额外的复杂性,这抵消了压缩字典带来的一些好处。数据是 Discord 工程设计的重要驱动力,数据说明了一切:不值得投入更多精力。
缓冲区升级
最后,我们探索了在非高峰时段增加 zstandard 缓冲区。Discord 的流量遵循昼夜模式,处理高峰需求所需的内存明显多于一天中其他时间所需的内存。
从表面上看,自动扩展网关集群可以防止我们在非高峰时段浪费计算资源。但是,由于网关连接具有长期存在的性质,传统的自动扩展方法对我们的工作负载并不适用。因此,我们在非高峰时段拥有大量额外的内存和计算资源。拥有所有这些额外的计算资源提出了一个问题:我们能否利用这些资源来提供更大的压缩率?
为了解决这个问题,我们在网关集群中构建了一个反馈循环。此循环将在每个网关节点上运行,并监控连接到它的客户端的内存使用情况。然后,它将确定应升级其 zstandard 缓冲区的新连接客户端的百分比。升级后的缓冲区将使 windowlog、hashlog 和 chainlog 值增加一,并且由于这些参数以 2 的幂表示,因此将这些值增加一将使缓冲区使用的内存使用量大致翻倍。
部署并让反馈循环运行一段时间后,结果并不像我们最初希望的那样好。如下图所示,在 24 小时内,我们的网关节点的升级率相对较低(高达 30%),并且明显低于我们的预期:大约 70%。

经过一番调查,我们发现导致反馈循环表现不佳的主要问题之一是内存碎片:反馈循环查看实际系统内存使用情况,但 BEAM 从系统中分配的内存远远超过处理连接客户端所需的内存。这导致反馈循环认为其可用的内存少于实际内存。
为了尝试缓解这种情况,我们做了一些实验来调整 BEAM 分配器设置——更具体地说, 驱动程序分配 分配器,负责(令人震惊的是) 驱动程序数据分配。网关进程使用的大部分内存是 zstandard 流上下文,它使用 C 语言实现,使用 国家免疫学学会联合会. NIF 内存使用由 driver_alloc。 我们的假设是,如果我们可以调整 驱动程序分配 分配器可以更有效地为我们的 zstandard 上下文分配或释放内存,我们将能够减少碎片并提高整体升级率。
然而,在对分配器设置进行了一些调整后,我们决定恢复反馈循环。虽然我们可能最终会找到正确的分配器设置,但调整分配器所需的工作量加上这给网关集群带来的总体额外复杂性超过了如果成功的话我们会看到的任何收益。
实施与推广
虽然最初的计划是只考虑将 zstandard 提供给移动用户,但带宽改进足以让我们将 zstandard 也提供给桌面用户!由于 zstandard 以 C 库的形式发布,因此只需在目标语言中找到绑定(Android 的 Java、iOS 的 Objective C 和桌面的 Rust)并将它们挂接到每个客户端中即可。对于 Java 来说,实现很简单(中译英)和桌面(zstd-安全),因为绑定已经存在,但是对于 iOS,我们必须编写自己的绑定。
这是一个有风险的变更,一旦出现问题,Discord 可能会完全无法使用,因此此次推出是在实验之后进行的。这次实验有三个目的:如果出现问题,可以快速回滚这些更改,验证我们在“实验室”中看到的结果,并使我们能够判断此更改是否对任何基线指标产生负面影响。
在几个月的时间里,我们成功地向所有平台上的所有用户推出了 zstandard。
1726866238
#Discord #如何将 #Websocket #流量减少
2024-09-20 18:09:15