实时数据捕获的百分比
Posted
技术标签:
【中文标题】实时数据捕获的百分比【英文标题】:Percentiles of Live Data Capture 【发布时间】:2010-11-17 22:36:04 【问题描述】:我正在寻找一种算法来确定实时数据捕获的百分位数。
例如,考虑开发一个服务器应用程序。
服务器的响应时间可能如下: 17 毫秒 33 毫秒 52 毫秒 60 毫秒 55 毫秒 等等
报告第 90 个百分位响应时间、第 80 个百分位响应时间等很有用。
天真的算法是将每个响应时间插入到一个列表中。当请求统计时,对列表进行排序并在适当的位置获取值。
内存使用量与请求数量呈线性关系。
在有限的内存使用情况下,是否有一种算法可以产生“近似”百分位数统计信息?例如,假设我想以一种处理数百万个请求的方式解决这个问题,但只想使用一千字节的内存进行百分比跟踪(放弃对旧请求的跟踪不是一种选择,因为百分比应该是适用于所有请求)。
还要求没有关于分布的先验知识。例如,我不想提前指定任何范围的存储桶。
【问题讨论】:
【参考方案1】:@thkala 从一些文献引用开始。让我扩展一下。
实现
2019 年催款论文中的 T-digest 在 Java 中具有参考实现,并且该页面上的端口为 Python、Go、javascript、C++、Scala、C、Clojure、C#、Kotlin 和 C++ port by facebook,以及进一步的 rust port of that C++ port Spark 将 2001 年 Greenwald/Khanna 论文中的“GK01”用于近似分位数 Beam: org.apache.beam.sdk.transforms.ApproximateQuantiles 有近似分位数 Java:Guava:com.google.common.math.Quantiles 实现精确分位数,因此占用更多内存 Rust:quantiles crate 实现了 2001 GK 算法“GK01”和 2005 CKMS 算法。 (注意:我发现 CKMS 实现很慢 - issue) C++:boost quantiles 有一些代码,但我没看懂。 我对 Rust [link] 中多达 1 亿个项目的选项进行了一些分析,发现 GK01 最好,T-digest 第二,“在优先级队列中保留 1% 的最高值”第三。文学
2001:Space-efficient online computation of quantile summaries(作者:Greenwald,Khanna)。在 Rust 中实现:quantiles::greenwald_khanna。
2004:Medians and beyond: new aggregation techniques for sensor networks(作者:Shrivastava、Buragohain、Agrawal、Suri)。引入“q-digests”,用于固定宇宙数据。
2005: Effective computation of biased quantiles over data streams (by Cormode, Korn, Muthukrishnan, Srivastava)... 用 Rust 实现:quantiles::ckms 指出 IEEE 演示文稿是正确的,但自行发布的演示文稿存在缺陷。通过精心设计的数据,空间可以随着输入大小线性增长。 “有偏见”意味着它关注 P90/P95/P99 而不是所有的百分位数)。
2006: Space-and time-efficient deterministic algorithms for biased quantiles over data streams (by Cormode, Korn, Muthukrishnan, Srivastava)...在 2005 年的论文中改进了空间限制
2007:A fast algorithm for approximate quantiles in high speed data streams(作者:张、王)。声称比 GK 加速 60-300 倍。下面的 2020 年文献综述称,这具有最先进的空间上限。
2019 Computing extremely accurate quantiles using t-digests(邓宁,Ertl)。介绍 t-digests、O(log n) 空间、O(1) 更新、O(1) 最终计算。它的简洁功能是您可以构建部分摘要(例如每天一个)并将它们合并为几个月,然后将几个月合并为几年。这就是大型查询引擎所使用的。
2020 A survey of approximate quantile computation on large-scale data (technical report)(陈章)。
2021 The t-digest: Efficient estimates of distributions - 一篇关于 t-digests 的平易近人的总结论文。
这听起来很愚蠢,但如果我想计算 10M float64s 的 P99,我刚刚创建了一个包含 100k float32s 的优先级队列(需要 400k)。它只占用“GK01”的 4 倍空间,而且速度更快。对于 5M 或更少的项目,它比 GK01 占用更少的空间!!
struct TopValues
values: std::collections::BinaryHeap<std::cmp::Reverse<ordered_float::NotNan<f32>>>,
impl TopValues
fn new(count: usize) -> Self
let capacity = std::cmp::max(count / 100, 1);
let values = std::collections::BinaryHeap::with_capacity(capacity);
TopValues values
fn render(&mut self) -> String
let p99 = self.values.peek().unwrap().0;
let max = self.values.drain().min().unwrap().0;
format!("TopValues, p99=:.4, max=:.4", p99, max)
fn insert(&mut self, value: f64)
let value = value as f32;
let value = std::cmp::Reverse(unsafe ordered_float::NotNan::new_unchecked(value) );
if self.values.len() < self.values.capacity()
self.values.push(value);
else if self.values.peek().unwrap().0 < value.0
self.values.pop();
self.values.push(value);
else
【讨论】:
【参考方案2】:你可以试试下面的结构:
接受输入n,即。 n = 100.
我们将保留一个范围数组 [min, max],按 min 和 count 排序。
插入值 x – 对 x 的 min 范围进行二分搜索。如果未找到,则取前面的范围(其中 min )。如果值属于范围 (x ),则增加 count。否则插入新范围 [min = x, max = x, count = 1]。
如果范围数达到 2*n - 通过从奇数和 中获取 min 将数组折叠/合并为 n(一半) >max 来自偶数条目,将它们的 count 相加。
获取即。 p95 从最后对计数求和直到下一次加法将达到阈值 sum >= 95%,取 p95 = min + (max - min) * partial。
它将基于动态测量范围。 n 可以修改为内存(在较小程度上 cpu)的交易准确性。如果您使值更加离散,即。通过在插入前四舍五入到 0.01 - 它会更快地稳定在范围内。
您可以通过不假设每个范围包含均匀分布的条目来提高准确性,即。像值总和这样便宜的东西会给你avg = sum / count,这将有助于从它所在的范围读取更接近的p95值。
您也可以旋转它们,即。在 m = 1 000 000 个条目开始填充新数组并将 p95 作为数组中计数的加权和之后(如果数组 B 具有 A 计数的 10%,那么它对 p95 值的贡献为 10%)。
【讨论】:
【参考方案3】:我曾经发表过一篇关于这个主题的博文。该博客现已失效,但该文章已完整包含在下面。
基本思想是减少对精确计算的要求,以支持“95% 的响应需要 500ms-600ms 或更短时间”(对于 500ms-600ms 的所有精确百分位数)。
由于我们最近开始感觉到我们的一个 web 应用的响应时间变得更糟,因此我们决定花一些时间来调整应用的性能。作为第一步,我们希望彻底了解当前的响应时间。对于性能评估,使用最小、最大或平均响应时间是一个坏主意:“‘平均值’是性能优化的弊端,通常与‘医院病人的平均体温’一样有用”(mysql Performance Blog)。相反,性能调谐器应该查看percentile:“百分位数是某个变量的值,低于该值的观察百分比”(***)。换句话说:第 95 个百分位是 95% 的请求完成的时间。因此,与百分位数相关的性能目标可能类似于“第 95 个百分位数应低于 800 毫秒”。设定这样的性能目标是一回事,但为实时系统有效地跟踪它们是另一回事。
我花了很长时间寻找现有的百分位数计算实现(例如here 或here)。所有这些都需要存储每个请求的响应时间,并按需计算百分位数或按顺序添加新的响应时间。这不是我想要的。我希望有一个解决方案可以为数十万个请求提供内存和 CPU 高效的实时统计信息。存储数十万个请求的响应时间并按需计算百分位数既不利于 CPU 也不利于内存。
我所希望的这种解决方案似乎根本不存在。转念一想,我想到了另一个想法:对于我正在寻找的绩效评估类型,没有必要获得确切的百分位数。像“第 95 个百分位在 850 毫秒和 900 毫秒之间”这样的近似答案就足够了。以这种方式降低要求使实现变得非常容易,尤其是在可能结果的上下边界已知的情况下。例如,我对超过几秒的响应时间不感兴趣——不管是 10 秒还是 15 秒,它们都非常糟糕。
所以这是实现背后的想法:
-
定义任意随机数的响应时间段(例如
0-100ms
、100-200ms
、200-400ms
、400-800ms
、800-1200ms
、...)
计算响应数和每个桶的响应数(对于 360 毫秒的响应时间,增加 200 毫秒 - 400 毫秒桶的计数器)
通过对存储桶的计数器求和来估计第 n 个百分位数,直到总和超过总数的 n%
就这么简单。还有here is the code。
一些亮点:
public void increment(final int millis)
final int i = index(millis);
if (i < _limits.length)
_counts[i]++;
_total++;
public int estimatePercentile(final double percentile)
if (percentile < 0.0 || percentile > 100.0)
throw new IllegalArgumentException("percentile must be between 0.0 and 100.0, was " + percentile);
for (final Percentile p : this)
if (percentile - p.getPercentage() <= 0.0001)
return p.getLimit();
return Integer.MAX_VALUE;
这种方法每个桶只需要两个 int 值(= 8 字节),允许使用 1K 内存跟踪 128 个桶。对于使用 50 毫秒的粒度分析 Web 应用程序的响应时间已经绰绰有余)。此外,出于性能考虑,我特意在没有任何同步的情况下实现了这一点(例如,使用 AtomicIntegers),因为我知道某些增量可能会丢失。
顺便说一句,使用 Google 图表和 60% 计数器,我能够在收集的一小时响应时间中创建一个漂亮的图表:
【讨论】:
虽然某些应用程序需要更复杂的分桶算法,但这确实是一种非常酷的显示百分位数数据的方式! 我刚刚更改了图表的颜色(原为j.mp/kj6sW),结果更酷了。现在很容易获得应用程序响应的最后 60 分钟的近似百分位数。可能是某些应用程序需要准确的数据。不过,对于大多数 Web 应用程序(和类似的服务器)来说,它应该已经足够了。 太棒了!正在寻找这样的 Java 算法,谢谢!【参考方案4】:(问这个问题已经有一段时间了,但我想指出一些相关的研究论文)
在过去几年中,对数据流的近似百分位数进行了大量研究。一些包含完整算法定义的有趣论文:
A fast algorithm for approximate quantiles in high speed data streams
Space-and time-efficient deterministic algorithms for biased quantiles over data streams
Effective computation of biased quantiles over data streams
所有这些论文都提出了具有亚线性空间复杂度的算法,用于计算数据流上的近似百分位数。
【讨论】:
【参考方案5】:尝试使用论文“同时估计几个百分位数的顺序过程”(Raatikainen) 中定义的简单算法。它速度很快,需要 2*m+3 个标记(对于 m 个百分位数),并且可以快速获得准确的近似值。
【讨论】:
【参考方案6】:如果您想在获得越来越多的数据时保持内存使用量不变,那么您将不得不以某种方式resample 该数据。这意味着您必须应用某种rebinning 方案。您可以等到获得一定数量的原始输入后再开始重新分箱,但您不能完全避免它。
所以您的问题实际上是在问“动态分箱数据的最佳方式是什么”?有很多方法,但是如果您想最小化您对可能收到的值的范围或分布的假设,那么一种简单的方法是对固定大小 k 的桶进行平均,宽度呈对数分布.例如,假设您想随时在内存中保存 1000 个值。为 k 选择一个大小,例如 100。选择您的最小分辨率,例如 1ms。那么
第一个存储桶处理 0-1ms (width=1ms) 之间的值 第二个桶:1-3ms (w=2ms) 第三个桶:3-7ms(w=4ms) 第四个桶:7-15ms (w=8ms) ... 第十个桶:511-1023ms(w=512ms)这种log-scaled 方法类似于hash table algorithms 中使用的分块系统,被一些文件系统和内存分配算法使用。当您的数据具有较大的动态范围时,它会很好地工作。
随着新值的出现,您可以根据自己的要求选择重新采样的方式。例如,您可以跟踪moving average、使用first-in-first-out 或其他更复杂的方法。请参阅Kademlia 算法了解一种方法(由Bittorrent 使用)。
归根结底,重新装箱必须让您丢失一些信息。您对分箱的选择将决定丢失哪些信息的细节。另一种说法是,恒定大小的内存存储意味着dynamic range 和sampling fidelity 之间的权衡;如何权衡取舍取决于您,但与任何抽样问题一样,这个基本事实无法回避。
如果您真的对利弊感兴趣,那么这个论坛上的任何答案都可能是足够的。您应该查看sampling theory。有大量关于这个主题的研究可用。
对于它的价值,我怀疑您的服务器时间将具有相对较小的动态范围,因此更宽松的缩放以允许对常见值进行更高的采样可能会提供更准确的结果。
编辑:为了回答您的评论,这里有一个简单的分箱算法示例。
您将 1000 个值存储在 10 个 bin 中。因此,每个 bin 包含 100 个值。假设每个 bin 都作为一个动态数组(Perl 或 Python 术语中的“列表”)实现。当一个新值进来时:
根据您选择的 bin 限制确定应将其存储在哪个 bin 中。 如果 bin 未满,请将值附加到 bin 列表中。 如果 bin 已满,请删除 bin 列表顶部的值,并将新值附加到 bin 列表底部。这意味着旧值会随着时间的推移而被丢弃。要找到第 90 个百分位,请对 bin 10 进行排序。第 90 个百分位是排序列表中的第一个值(元素 900/1000)。
如果您不喜欢丢弃旧值,那么您可以实施一些替代方案来代替。例如,当一个 bin 变满时(在我的示例中达到 100 个值),您可以取最旧的 50 个元素(即列表中的前 50 个)的平均值,丢弃这些元素,然后将新的平均元素附加到bin,留下一个包含 51 个元素的 bin,现在有空间容纳 49 个新值。这是一个简单的重组示例。
另一个rebinning的例子是downsampling;例如,丢弃排序列表中的每 5 个值。
我希望这个具体的例子有所帮助。要带走的关键点是有很多方法可以实现恒定的内存老化算法;根据您的要求,只有您可以决定什么是令人满意的。
【讨论】:
感谢您的深刻见解,但我无法从中收集到足够的信息来实际进行实施。您提供的链接没有提到百分位数或“重新组合”。您不会碰巧知道任何专门针对当前主题的参考资料吗? @binarycoder:我在答案中添加了一个示例,试图让我所说的更加具体。希望对您有所帮助。 在我看来,您的示例并不能很好地工作。它假定您已经完美地调整了存储桶的大小并且每个存储桶有 100 个值。这是一个相当强的假设。您的存储桶的大小不太可能接收完全相同数量的值,因此第 10 个存储桶的最小值可能不是您的第 90 个百分位数。【参考方案7】:我相信有很多很好的近似算法可以解决这个问题。一个好的方法是简单地使用一个固定大小的数组(比如 1K 的数据)。修正一些概率 p。对于每个请求,以概率 p 将其响应时间写入数组(替换其中最旧的时间)。由于数组是实时流的子采样,并且由于子采样保留了分布,因此对该数组进行统计将为您提供完整实时流的统计信息的近似值。
这种方法有几个优点:它不需要先验信息,并且易于编码。您可以快速构建它并通过实验确定,对于您的特定服务器,在什么时候增加缓冲区对答案的影响可以忽略不计。这就是近似值足够精确的点。
如果您发现需要太多内存才能提供足够精确的统计信息,那么您将不得不进一步挖掘。好的关键字是:“流计算”、“流统计”,当然还有“百分位数”。你也可以试试“ire and curses”的方法。
【讨论】:
我不知道。这种替换算法似乎清楚地引入了对旧数据的偏见。这就是为什么我非常欣赏关于任何解决方案的稳健性的适当数学论证。 如果实时数据取自某个分布 D,那么子采样(任何子采样)也将从 D 派生。如果实时数据不是取自某个分布,则百分位数列表可能不是最有启发性的东西。 关键词很有帮助。搜索“quantile”和“stream”会引发关于这个主题的各种研究!所有技术似乎比这里建议的任何算法都涉及更多。这就是为什么我不愿将任何东西标记为“答案”。 我接受这是“最佳”答案。但是要进行无偏的“水库采样”,p 必须是水库大小/总样本SoFar。此外,必须随机选择要驱逐的元素(不是最旧的)。 这是一个不错的实用方法! 喜欢【参考方案8】:使用大整数的动态数组 T[] 或 T[n] 计算响应时间为 n 毫秒的次数。如果您真的在对服务器应用程序进行统计,那么可能 250 毫秒的响应时间是您的绝对限制。因此,您的 1 KB 在 0 到 250 之间的每毫秒中保存一个 32 位整数,并且您有一些空间可以用于溢出箱。 如果您想要具有更多 bin 的东西,请为 1000 个 bin 使用 8 位数字,当计数器溢出时(即在该响应时间的第 256 个请求),您将所有 bin 中的位向下移动 1。(有效地将值减半所有垃圾箱)。这意味着您忽略所有捕获少于访问次数最多的 bin 捕获的延迟的 1/127 的 bin。
如果您真的非常需要一组特定的垃圾箱,我建议您使用第一天的请求来提供一套合理的固定垃圾箱。在实时的、性能敏感的应用程序中,任何动态的东西都是非常危险的。如果你选择这条路,你最好知道你在做什么,或者有一天你会被叫下床解释为什么你的统计跟踪器突然占用生产服务器上 90% 的 CPU 和 75% 的内存。
至于其他统计数据:对于均值和方差,有一些 nice recursive algorithms 占用很少的内存。这两个统计数据本身对于许多分布可能足够有用,因为central limit theorem 指出由足够多的自变量产生的分布接近正态分布(完全由均值和方差定义),您可以使用最后一个 N 上的normality tests 之一(其中 N 足够大但受您的内存要求的限制)来监控正常假设是否仍然成立。
【讨论】:
我对收集更多种类的统计数据很感兴趣,而不仅仅是响应时间。确定适当的界限并不总是那么容易。所以,我正在寻找一个通用的解决方案。谢谢。以上是关于实时数据捕获的百分比的主要内容,如果未能解决你的问题,请参考以下文章