ITPub博客

首页 > 数据库 > SQL on Hadoop > 每秒超过 7 亿个事件:我们如何理解海量数据

每秒超过 7 亿个事件:我们如何理解海量数据

SQL on Hadoop 作者:ITPUB社群 时间:1970-01-01 08:33:45 0 删除 编辑


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 估计器已正确实现,并且我们使用了正确的采样过程。

使用 Cloudflare 的分析 API 查询采样数据

我们已经用采样数据为大多数分析数据集提供支持。例如,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;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-11-21

  • 博文量
    1090
  • 访问量
    546760