我曾经发表过一篇关于这个主题的博文。该博客现已失效,但该文章已完整包含在下面。
基本思想是减少对精确计算的要求,以支持“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% 计数器,我能够在收集的一小时响应时间中创建一个漂亮的图表: