考虑到传统分析器的工作原理,为这一挑战提出一个通用的免费软件解决方案应该是直截了当的。
让我们把问题分成两部分:
收集数据
假设我们可以将 Web 应用程序分解为各个组成部分(API、函数)并测量完成每个部分所花费的时间。每个部分每天被调用数千次,因此我们可以在一整天左右的时间里在多个主机上收集这些数据。当一天结束时,我们将拥有一个相当大且相关的数据集。
顿悟#1:用' URL '替换' function ' ,我们现有的网络日志就是“它”。我们需要的数据已经存在:
- Web API 的每个部分都由请求 URL 定义(可能带有一些参数)
- 往返时间(通常以微秒为单位)出现在每一行我们有一天,(周,月)的行,这些数据很方便
因此,如果我们可以访问 Web 应用程序的所有分布式部分的标准 Web 日志,我们的问题的第一部分(收集数据)就解决了。
呈现数据
现在我们有了一个大数据集,但仍然没有真正的洞察力。我们如何获得洞察力?
顿悟#2:直接可视化我们的(多个)网络服务器日志。
一张图片值一千字。我们可以使用哪张图片?
我们需要将成千上万行或数百万行的多个 Web 服务器日志压缩成一个简短的摘要,这将讲述有关我们性能的大部分故事。换句话说:目标是直接从我们的网络日志中生成类似分析器的报告,甚至更好:图形分析器报告。
想象一下,我们可以映射:
- 一维的调用延迟
- 对另一个维度的调用次数,以及
- 颜色的函数标识(本质上是第三维)
一张这样的图片:下面是 API 延迟的堆积密度图表(函数名称是为了说明目的而编造的)。
图表:
![1000 字的故事:通过 API 对 Web 应用程序的堆叠延迟分布](https://i.stack.imgur.com/Eh38R.png)
这个例子的一些观察
- 在我们的应用程序中,我们有一个代表 3 个完全不同的“世界”的三模态分布:
- 最快的响应以大约 300 微秒的延迟为中心。这些响应来自我们的清漆缓存
- 第二快,平均不到 0.01 秒,来自我们的中间层 Web 应用程序(Apache/Tomcat)提供的各种 API
- 最慢的响应,集中在 0.1 秒左右,有时需要几秒钟才能响应,涉及到我们的 SQL 数据库的往返。
我们可以看到应用程序上的缓存效果有多么显着(请注意,x 轴是 log10 刻度)
我们可以明确地看到哪些 API 往往是快的还是慢的,所以我们知道应该关注什么。
我们可以看到每天最常调用哪些 API。我们还可以看到其中一些很少被调用,甚至很难在图表上看到它们的颜色。
怎么做?
第一步是预处理并从日志中提取所需数据的子集。在这里,在多个日志上使用像 Unix ' cut ' 这样的简单实用程序可能就足够了。您可能还需要将多个相似的 URL 折叠成更短的字符串来描述功能/API,例如“注册”或“购买”。如果你有一个由负载均衡器生成的多主机统一日志视图,这个任务可能会更容易。我们只提取 API (URL) 的名称及其延迟,因此我们最终得到一个包含一对列的大文件,由 TAB 分隔
*API_Name Latency_in_microSecs*
func_01 32734
func_01 32851
func_06 598452
...
func_11 232734
现在我们在结果数据对上运行下面的 R 脚本来生成想要的图表(使用 Hadley Wickham 的精彩ggplot2库)。
瞧!
生成图表的代码
最后,这是从 API+Latency TSV 数据文件生成图表的代码:
#!/usr/bin/Rscript --vanilla
#
# Generate stacked chart of API latencies by API from a TSV data-set
#
# ariel faigon - Dec 2012
#
.libPaths(c('~/local/lib/R',
'/usr/lib/R/library',
'/usr/lib/R/site-library'
))
suppressPackageStartupMessages(library(ggplot2))
# grid lib needed for 'unit()':
suppressPackageStartupMessages(library(grid))
#
# Constants: width, height, resolution, font-colors and styles
# Adapt to taste
#
wh.ratio = 2
WIDTH = 8
HEIGHT = WIDTH / wh.ratio
DPI = 200
FONTSIZE = 11
MyGray = gray(0.5)
title.theme = element_text(family="FreeSans", face="bold.italic",
size=FONTSIZE)
x.label.theme = element_text(family="FreeSans", face="bold.italic",
size=FONTSIZE-1, vjust=-0.1)
y.label.theme = element_text(family="FreeSans", face="bold.italic",
size=FONTSIZE-1, angle=90, vjust=0.2)
x.axis.theme = element_text(family="FreeSans", face="bold",
size=FONTSIZE-1, colour=MyGray)
y.axis.theme = element_text(family="FreeSans", face="bold",
size=FONTSIZE-1, colour=MyGray)
#
# Function generating well-spaced & well-labeled y-axis (count) breaks
#
yscale_breaks <- function(from.to) {
from <- 0
to <- from.to[2]
# round to 10 ceiling
to <- ceiling(to / 10.0) * 10
# Count major breaks on 10^N boundaries, include the 0
n.maj = 1 + ceiling(log(to) / log(10))
# if major breaks are too few, add minor-breaks half-way between them
n.breaks <- ifelse(n.maj < 5, max(5, n.maj*2+1), n.maj)
breaks <- as.integer(seq(from, to, length.out=n.breaks))
breaks
}
#
# -- main
#
# -- process the command line args: [tsv_file [png_file]]
# (use defaults if they aren't provided)
#
argv <- commandArgs(trailingOnly = TRUE)
if (is.null(argv) || (length(argv) < 1)) {
argv <- c(Sys.glob('*api-lat.tsv')[1])
}
tsvfile <- argv[1]
stopifnot(! is.na(tsvfile))
pngfile <- ifelse(is.na(argv[2]), paste(tsvfile, '.png', sep=''), argv[2])
# -- Read the data from the TSV file into an internal data.frame d
d <- read.csv(tsvfile, sep='\t', head=F)
# -- Give each data column a human readable name
names(d) <- c('API', 'Latency')
#
# -- Convert microseconds Latency (our weblog resolution) to seconds
#
d <- transform(d, Latency=Latency/1e6)
#
# -- Trim the latency axis:
# Drop the few 0.001% extreme-slowest outliers on the right
# to prevent them from pushing the bulk of the data to the left
Max.Lat <- quantile(d$Latency, probs=0.99999)
d <- subset(d, Latency < Max.Lat)
#
# -- API factor pruning
# Drop rows where the APIs is less than small % of total calls
#
Rare.APIs.pct <- 0.001
if (Rare.APIs.pct > 0.0) {
d.N <- nrow(d)
API.counts <- table(d$API)
d <- transform(d, CallPct=100.0*API.counts[d$API]/d.N)
d <- d[d$CallPct > Rare.APIs.pct, ]
d.N.new <- nrow(d)
}
#
# -- Adjust legend item-height &font-size
# to the number of distinct APIs we have
#
API.count <- nlevels(as.factor(d$API))
Legend.LineSize <- ifelse(API.count < 20, 1.0, 20.0/API.count)
Legend.FontSize <- max(6, as.integer(Legend.LineSize * (FONTSIZE - 1)))
legend.theme = element_text(family="FreeSans", face="bold.italic",
size=Legend.FontSize,
colour=gray(0.3))
# -- set latency (X-axis) breaks and labels (s.b made more generic)
lat.breaks <- c(0.00001, 0.0001, 0.001, 0.01, 0.1, 1, 10)
lat.labels <- sprintf("%g", lat.breaks)
#
# -- Generate the chart using ggplot
#
p <- ggplot(data=d, aes(x=Latency, y=..count../1000.0, group=API, fill=API)) +
geom_bar(binwidth=0.01) +
scale_x_log10(breaks=lat.breaks, labels=lat.labels) +
scale_y_continuous(breaks=yscale_breaks) +
ggtitle('APIs Calls & Latency Distribution') +
xlab('Latency in seconds - log(10) scale') +
ylab('Call count (in 1000s)') +
theme(
plot.title=title.theme,
axis.title.y=y.label.theme,
axis.title.x=x.label.theme,
axis.text.x=x.axis.theme,
axis.text.y=y.axis.theme,
legend.text=legend.theme,
legend.key.height=unit(Legend.LineSize, "line")
)
#
# -- Save the plot into the png file
#
ggsave(p, file=pngfile, width=WIDTH, height=HEIGHT, dpi=DPI)