Cloudflare 的网络为客户提供了大量服务。我们以事件日志和聚合分析的形式收集相关数据并将其交付给客户。截至 2024 年 12 月,我们的数据管道每秒摄取高达 7.06 亿个由 Cloudflare 服务生成的事件,这代表自我们 2018 年数据管道博客文章以来增长了 100 倍。
在峰值时,我们每秒传输 107 GiB 的压缩数据,要么直接推送给客户,要么进行额外的排队和批处理。
所有这些数据流为 Logs、Analytics 和计费等产品提供支持,也为其他产品提供支持,例如训练用于机器人检测的机器学习模型。本博客文章重点介绍我们用于高效且准确地处理为 Analytics 产品摄取的大量数据的技术。之前的一篇博客文章更深入地介绍了 Logs 的数据管道。
该管道大致可以用下图描述。

数据管道有多个阶段,由于硬件故障或配置错误,每个阶段都可能并且自然会中断或变慢。当这种情况发生时,数据量太大,无法长时间全部缓冲。最终一些数据会被丢弃,导致分析出现缺口并降低产品体验,除非有适当的缓解措施。
当每秒超过五亿个事件中有些必须被丢弃时,如何保留其中有价值的信息?通过降采样以受控的方式丢弃。
以下视觉类比展示了不受控数据丢失与降采样之间的区别。在两种情况下,交付的像素数量相同。一个是仅展示一幅著名画作一小部分的高分辨率视图,另一个则展示整幅画作,尽管模糊且高度像素化。

如上所述,管道中的任何点都可能失败,因此我们希望能够在任何需要的地方进行降采样。一些服务会在数据到达 Logfwdr 之前主动在源头进行降采样。这会使从该数据中提取的信息有些模糊,但比原本可能交付的内容有用得多:原始数据的随机片段之间存在缺口,甚至什么都没有。“模糊”的程度不在我们的控制范围内(我们会尽最大努力交付完整数据),但有一种稳健的方法来估计它,如下一节所述。
当缓冲区溢出时,Logfwdr 可以决定对缓冲区中的数据降采样。Logfwdr 同时处理许多数据流,因此我们需要通过为每个数据流分配权重来对它们进行优先级排序,然后应用最大最小公平性以更好地利用缓冲区。它允许每个数据流按需存储,只要整个缓冲区未饱和。一旦饱和,各流将根据其加权大小公平地划分缓冲区。
在我们的实现(Go)中,每个数据流由一个 goroutine 驱动,它们通过 channel 协作。每次分配和释放内存时,它们都会查询同一个 tracker 对象。该 tracker 使用最大堆来始终了解最重的参与者是谁以及总使用量是多少。每当总使用量超过限制时,tracker 会反复向最重的参与者发送“请卸载一些负载”的信号,直到使用量再次低于限制。
这样做的效果是,健康的流只缓冲极少量数据,可以按需分配而不丢失。但任何滞后的流则会公平地瓜分剩余的内存配额。
我们大致均匀地进行降采样,总是从缓冲区中取出一些降采样程度最低的批次(使用最小堆来找到它们),并在降采样时将它们合并在一起。

合并使各批次保持大致相同的大小,并将其数量控制在合理范围内。
降采样成本很低,但由于缓冲区中的数据是压缩的,降采样会导致重新压缩,而这是我们对数据所做的最昂贵的操作。但在系统承受重负载时,使用额外的 CPU 时间是你最不想做的事情!为了补偿重新压缩的成本,每当流处于“卸载负载”状态时,我们也会开始对新鲜数据进行降采样(在它第一次被压缩之前)。
我们将这种方法称为“无底缓冲区”,因为你可以向其中塞入实际上无限量的数据,而它会自动被稀释。无底缓冲区类似于蓄水池采样(reservoir sampling),其中缓冲区就是蓄水池,而总体则来自输入流。但也有一些区别。首先,在我们的管道中,输入数据流永远不会结束,而蓄水池采样假设输入流会结束以最终确定样本。其次,生成的样本也永远不会结束。
让我们看看管道中的下一个阶段:Logreceiver。它位于分布式队列的前面。Logreceiver 的目的是按某个键对每个数据流进行分区,以便 Logpush、Analytics 插入器或其他进程更容易消费。
Logreceiver 主动对分析数据进行自适应采样。这提高了小型客户(每天接收约 10 个事件)的分析准确性,同时对大型客户(每秒数百万个事件)进行更积极的降采样。然后,Logreceiver 将相同的数据以多种分辨率(100%、10%、1% 等)推送到分布式队列中的不同主题。这使它能够在队列过载时仍然推送一些数据而不是什么都不推,只需跳过写入高分辨率样本数据即可。
插入器也是如此:它们可以跳过读取或写入高分辨率数据。Analytics API 可以跳过读取高分辨率数据。分析数据库可能因为过载、集群状态降级,或者仅仅因为要读取的数据太多(时间范围非常宽或客户非常大)而无法读取高分辨率数据。自适应地降低到较低分辨率,使得 API 在所有这些情况下都能返回一些结果。
好了,我们在分析数据库中有一些降采样数据。它看起来像原始数据,但缺少了一些行。我们如何理解它?我们如何知道结果是否可信?
让我们来看看数学。
到目前为止,我们知道如何使用 HT 估计器来进行 SUM、COUNT 和 AVG。
我们不知道 N,但有一个变通方法:同时置信区间。分别构造 SUM 和 COUNT 的置信区间,然后将它们组合成 AVG 的置信区间。这被称为 Bonferroni 方法。它需要为 SUM 和 COUNT 生成更宽的(一半的“不置信度”)区间。这里是一个简化的可视化表示,但实际的估计器必须考虑到橙色区域可能低于零的情况。

在 SQL 中,估计器和置信区间看起来像这样:
WITH sum (x * _sample_interval) AS t, sum (x * x * _sample_interval * (_sample_interval - 1 )) AS vt, sum (_sample_interval) AS c, sum (_sample_interval * (_sample_interval - 1 )) AS vc, -- ClickHouse does not expose the erf⁻¹ function , so we precompute some magic numbers , -- ( only for 95% confidence , will be different otherwise ) : -- 1.959963984540054 = Φ ⁻¹(( 1 + 0.950 )/ 2 ) = √ 2 * erf ⁻¹( 0.950 ) -- 2.241402727604945 = Φ ⁻¹((1+0.975)/2) = √2 * erf ⁻¹(0.975) 1.959963984540054 * sqrt ( vt ) AS err950_t , 1.959963984540054 * sqrt ( vc ) AS err950_c , 2.241402727604945 * sqrt ( vt ) AS err975_t , 2.241402727604945 * sqrt ( vc ) AS err975_c SELECT t - err950_t AS lo_total , t AS est_total , t + err950_t AS hi_total , c - err950_c AS lo_count , c AS est_count , c + err950_c AS hi_count , ( t - err975_t ) / ( c + err975_c ) AS lo_average , t / c AS est_average , ( t + err975_t ) / ( c - err975_c ) AS hi_average FROM ...
为时间序列上的每个时间段构造置信区间,你就得到了一个置信带,清晰地展示了分析的准确性。下图显示了这样一个置信带的示例,以线条周围的阴影表示。

我们开始在我们的内部仪表板上使用置信带,一段时间后注意到了一些可怕的事情:系统性误差!对于某个特定网站,“总服务字节数”的估计值高于从汇总中获得的真实控制值,而且置信带偏差很大。见下图,其中真实值(蓝线)始终在黄色置信带之外。

我们检查了存储的数据是否有损坏,数据是完好的。我们检查了查询中的数学计算,也是正确的。只有在阅读了所有负责采样的系统的源代码之后,我们才找到了根本原因的候选者。
我们在所有地方都使用了简单随机采样,基本上是对每个事件“抛硬币”,但在 Logreceiver 中采样方式不同。它不是随机采样,而是执行系统采样,从批次中的第一个事件开始,以等间隔选取事件。

为什么这会是个问题?
import
itertools
def take_every(src, period):
for i, x in enumerate(src):
if i % period == 0:
yield x
pattern = [10, 1, 1, 1, 1, 1]
sample_interval = 10 # bad if it has common factors with len(pattern)
true_mean = sum(pattern) / len(pattern)
orig = itertools.cycle(pattern)
sample_size = 10000
sample = itertools.islice(take_every(orig, sample_interval), sample_size)
sample_mean = sum(sample) / sample_size
print(f
"{true_mean=} {sample_mean=}"
)在上面的代码中尝试了 pattern 和 sample_interval 的不同值之后,我们意识到了偏差来自哪里。
想象一个人打开一个巨大的生成的 HTML 页面,其中有许多小的/缓存的资源,比如图标。第一个响应会很大,紧接着是一连串的小响应。如果网站访问量不大,响应往往会全部集中在 Logfwdr 中一个批次的开始处。Logreceiver 不切割批次,只是将它们连接起来。第一个响应仍然是第一个,所以它总是被选中,从而将估计值向上偏移。

我们利用碰巧拥有的原始未采样数据来验证这个假设,因为那个特定网站也在使用其中一个 Logs 产品。我们取给定时间范围内的所有事件,并按至少一分钟的间隔进行分组。在每个组中,我们按时间对所有事件进行排序,查看感兴趣的变量(响应大小,以字节为单位),并将其与组内的排名绘制成散点图。

一个清晰的模式!第一个响应比平均值大得多的可能性要高得多。
我们通过让 Logreceiver 在采样前打乱数据修复了该问题。随着我们推出该修复,估计值与真实值趋于一致。

现在,经过一段时间的实战测试,我们确信 HT 估计器已正确实现,并且我们使用了正确的采样过程。
我们已经用采样数据为大多数分析数据集提供支持。例如,Workers Analytics Engine 在 SQL 中暴露采样间隔,使我们的客户能够构建带有置信区间的仪表板。在 GraphQL API 中,所有名称中包含“Adaptive”的数据节点都基于采样数据,采样间隔也作为字段暴露在那里,尽管仅凭这一点无法构建置信区间。我们正在努力在 GraphQL API 中暴露置信区间,并作为实验将其添加到了 httpRequestsAdaptiveGroups 节点上的 count 和 edgeResponseBytes(sum)字段中。这可通过 confidence(level: X) 使用。
一个示例 GraphQL 查询:
query
HTTPRequestsWithConfidence
(
$accountTag: string
$zoneTag: string
$datetimeStart: string
$datetimeEnd: string
) {
viewer {
zones
(filter: { zoneTag: $zoneTag }) {
httpRequestsAdaptiveGroups
(
filter: {
datetime_geq: $datetimeStart
datetime_leq: $datetimeEnd
}
limit:
100
) {
confidence
(level:
0.95
) {
level
count {
estimate
lower
upper
sampleSize
}
sum {
edgeResponseBytes {
estimate
lower
upper
sampleSize
}
}
}
}
}
}上述查询请求 SUM(edgeResponseBytes) 和 COUNT 的估计值及 95% 置信区间。结果还会显示样本大小,了解这一点很有用,因为我们依赖中心极限定理来构建置信区间,因此小样本效果不太好。
此查询的响应:
{
"data"
: {
"viewer"
: {
"zones"
: [
{
"httpRequestsAdaptiveGroups"
: [
{
"confidence"
: {
"level"
:
0.95
,
"count"
: {
"estimate"
:
96947
,
"lower"
:
"96874.24"
,
"upper"
:
"97019.76"
,
"sampleSize"
:
96294
},
"sum"
: {
"edgeResponseBytes"
: {
"estimate"
:
495797559
,
"lower"
:
"495262898.54"
,
"upper"
:
"496332219.46"
,
"sampleSize"
:
96294
}
}
}
}
]
}
]
}
},
"errors"
:
null
}响应显示估计计数为 96947,我们有 95% 的把握认为真实计数位于 96874.24 到 97019.76 之间。同样,还提供了响应字节总和的估计值和范围。
这些估计基于 96294 行的样本大小,这有足够的样本来计算良好的置信区间。
我们讨论了是什么让我们的数据管道在每 1.5 年规模翻倍的情况下仍保持可扩展性和弹性、其数学原理如何,以及它为何容易出错。我们一直在努力寻找更好的方法,以保持数据管道及基于它的产品对客户有用。如果你有兴趣做这类事情,并想帮助我们构建更好的互联网,请查看我们的招聘页面。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Cloudflare Blog
原创地址:https://blog.cloudflare.com/how-we-make-sense-of-too-much-data
原文作者:Cloudflare Blog;如有侵权,请联系管理员删除。