眾所周知,StatsD 負(fù)責(zé)收集并聚合測(cè)量值。之后,它會(huì)將數(shù)據(jù)傳給 Graphite,后者以時(shí)間序列為依據(jù)存儲(chǔ)數(shù)據(jù),并繪制圖表。但是,我們不知道,基于 http 訪問(wèn)的圖表在展示時(shí),是基于每秒鐘的請(qǐng)求數(shù),每次留存的平均請(qǐng)求數(shù)還是其它。讓我們就以此為目標(biāo),來(lái)一探究竟吧!本文系 OneAPM 工程師編譯整理。

StatsD
為了全面了解 StatsD 的工作原理,我閱讀了它的源碼。之前我就耳聞 StatsD 是一種簡(jiǎn)單的應(yīng)用,但讀過(guò)源碼后才發(fā)現(xiàn)它竟如此簡(jiǎn)單!在主腳本文件只有300多行代碼,而 Graphite 的后端代碼只有150行左右。
StatsD 中的概念
在這個(gè)文檔中,列出了一些需要理解的 StatsD 概念。
Buckets
當(dāng)一個(gè) Whisper 文件被創(chuàng)建,它會(huì)有一個(gè)不會(huì)改變的固定大小。在這個(gè)文件中可能有多個(gè) "buckets" 對(duì)應(yīng)于不同分別率的數(shù)據(jù)點(diǎn),每個(gè) bucket 也有一個(gè)保留屬性指明數(shù)據(jù)點(diǎn)應(yīng)該在 bucket 中應(yīng)該被保留的時(shí)間長(zhǎng)度,Whisper 執(zhí)行一些簡(jiǎn)單的數(shù)學(xué)計(jì)算來(lái)計(jì)算出多少數(shù)據(jù)點(diǎn)會(huì)被實(shí)際保存在每個(gè) bucket 中。
Values
每個(gè) stat 都有一個(gè) value,該值的解釋方式依賴于 modifier。通常,values 應(yīng)該是整數(shù)。
Flush Interval
在 flush interval (沖洗間隔,通常為10秒)超時(shí)之后,stats 會(huì)聚集起來(lái),傳送到上游的后端服務(wù)。
測(cè)量值類別
計(jì)數(shù)器
計(jì)數(shù)器很簡(jiǎn)單。它會(huì)給 bucket 加 value,并存儲(chǔ)在內(nèi)存中,直到 flush interval 超時(shí)。
讓我們看一下生成計(jì)數(shù)器 stats 的源碼,該 stats 會(huì)被推送到后端。
for (key in counters) {
var value = counters[key];
var valuePerSecond = value / (flushInterval / 1000); // calculate "per second" rate
statString += 'stats.'+ key + ' ' + valuePerSecond + ' ' + ts + "\n";
statString += 'stats_counts.' + key + ' ' + value + ' ' + ts + "\n";
numStats += 1;
}
首先,StatsD 會(huì)迭代它收到的所有計(jì)數(shù)器,對(duì)每個(gè)計(jì)數(shù)器它都會(huì)分配兩個(gè)變量。一個(gè)變量用于存儲(chǔ)計(jì)數(shù)器的 value,另一個(gè)存儲(chǔ) per-second value。之后,它會(huì)將 values 加至 statString,同時(shí)增加 numStats 變量的值。
如果你使用默認(rèn)的 flush interval(10秒),并在每個(gè)間隔通過(guò)某個(gè)計(jì)數(shù)器給 StatsD 傳送7個(gè)增量。則計(jì)時(shí)器的 value 為 7,而 per-second value 為 0.7。
計(jì)時(shí)器
計(jì)時(shí)器用于收集數(shù)字。他們不必要包含時(shí)間值。你可以收集某個(gè)存儲(chǔ)器中的字節(jié)數(shù)、對(duì)象數(shù)或任意數(shù)字。計(jì)時(shí)器的一大好處在于,你可以得到平均值、總值、計(jì)數(shù)值和上下限值。給 StatsD 設(shè)置一個(gè)計(jì)時(shí)器,就能在數(shù)據(jù)傳送給 Graphite 之前自動(dòng)計(jì)算這些量。
計(jì)時(shí)器的源碼比計(jì)數(shù)器的源碼要稍微復(fù)雜一些。
for (key in timers) {
if (timers[key].length > 0) {
var values = timers[key].sort(function (a,b) { return a-b; });
var count = values.length;
var min = values[0];
var max = values[count - 1];
var cumulativeValues = [min];
for (var i = 1; i < count; i++) {
cumulativeValues.push(values[i] + cumulativeValues[i-1]);
}
var sum = min;
var mean = min;
var maxAtThreshold = max;
var message = "";
var key2;
for (key2 in pctThreshold) {
var pct = pctThreshold[key2];
if (count > 1) {
var thresholdIndex = Math.round(((100 - pct) / 100) * count);
var numInThreshold = count - thresholdIndex;
maxAtThreshold = values[numInThreshold - 1];
sum = cumulativeValues[numInThreshold - 1];
mean = sum / numInThreshold;
}
var clean_pct = '' + pct;
clean_pct.replace('.', '_');
message += 'stats.timers.' + key + '.mean_' + clean_pct + ' ' + mean + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.upper_' + clean_pct + ' ' + maxAtThreshold + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.sum_' + clean_pct + ' ' + sum + ' ' + ts + "\n";
}
sum = cumulativeValues[count-1];
mean = sum / count;
message += 'stats.timers.' + key + '.upper ' + max + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.lower ' + min + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.count ' + count + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.sum ' + sum + ' ' + ts + "\n";
message += 'stats.timers.' + key + '.mean ' + mean + ' ' + ts + "\n";
statString += message;
numStats += 1;
}
}
如果在默認(rèn)的 flush interval 內(nèi),你將下列計(jì)數(shù)器 values 傳給 StatsD:
- 450
- 120
- 553
- 994
- 334
- 844
- 675
- 496
StatsD 將會(huì)計(jì)數(shù)下面的 values:
- mean_90 496
- upper_90 844
- sum_90 3472
- upper 994
- lower 120
- count 8
- sum 4466
- mean 558.25
Gauges
一個(gè) guage 代表著時(shí)間段內(nèi)某點(diǎn)的任意 vaule,是 StatsD 中最簡(jiǎn)單的類型。你可以給它傳任意值,它會(huì)傳給后端。
Gauge stats 的源碼只有短短四行。
for (key in gauges) {
statString += 'stats.gauges.' + key + ' ' + gauges[key] + ' ' + ts + "\n";
numStats += 1;
}
給 StatsD 傳一個(gè)數(shù)字,它會(huì)不經(jīng)處理地將該數(shù)字傳到后端。值得注意的是,在一個(gè) flush interval 內(nèi),只有 gauge 最后的值會(huì)傳送到后端。因此,如果你在一個(gè) flush interval 內(nèi),將下面的 gauge 值傳給 StatsD:
- 643
- 754
- 583
會(huì)傳到后端的值只有583而已。該 gauge 的值會(huì)一直存儲(chǔ)在內(nèi)存中,直到 flush interval 結(jié)束才傳值。
Graphite
現(xiàn)在,我們已經(jīng)了解數(shù)據(jù)是怎樣從 StatsD 傳出來(lái)的,讓我們看看它在 Graphite 里是如何存儲(chǔ)并處理的。
總覽
在 Graphite 文檔里,我們可以找到 Graphite 概覽,此概覽總結(jié)了 Graphite 的兩個(gè)要點(diǎn):
- Graphite 存儲(chǔ)數(shù)值型帶有時(shí)間序列的數(shù)據(jù)。
- Graphite 按需繪制圖表。
Graphite 由三部分組成:
- carbon :監(jiān)聽(tīng)時(shí)間序列的數(shù)據(jù)的后臺(tái)程序。
- whisper:一個(gè)簡(jiǎn)單的數(shù)據(jù)庫(kù)庫(kù),用來(lái)存儲(chǔ)時(shí)間序列數(shù)據(jù)。
- webapp: Django webapp,使用 Cairo 來(lái)根據(jù)需要呈現(xiàn)圖形。
Graphite 當(dāng)做時(shí)間序列數(shù)據(jù)的格式如下:
<key> <numeric value> <timestamp>
存儲(chǔ)方案
Graphite 采用可配置的存儲(chǔ)方案用以定義所存數(shù)據(jù)的留存率。它會(huì)給數(shù)據(jù)路徑匹配特定的模式,從而決定所存數(shù)據(jù)的頻率和來(lái)歷。
以下配置示例截取自 StatsD 文檔。
[stats]
pattern = ^stats\..*
retentions = 10:2160,60:10080,600:262974
該示例表明,匹配上述樣式的數(shù)據(jù)都會(huì)套用這些留存。留存的格式為 frequency: history。所以,該配置允許我們將10秒鐘的數(shù)據(jù)存儲(chǔ)6個(gè)小時(shí),1分鐘的數(shù)據(jù)存儲(chǔ)1周,10分鐘的數(shù)據(jù)存儲(chǔ)5年。
在 Graphite 顯示計(jì)時(shí)器
了解了這么多,我們來(lái)看看一個(gè)簡(jiǎn)單的 ruby 腳本,該腳本能收集 HTTP 請(qǐng)求的時(shí)間。
#!/usr/bin/env ruby
require 'rubygems' if RUBY_VERSION < '1.9.0'
require './statsdclient.rb'
require 'typhoeus'
Statsd.host = 'localhost'
Statsd.port = 8125
def to_ms time
(1000 * time).to_i
end
while true
start_time = Time.now.to_f
resp = Typhoeus::Request.get 'http://www.example.org/system/information'
end_time = Time.now.to_f
elapsed_time = (1000 * end_time) - (to_ms start_time)
response_time = to_ms resp.time
start_transfer_time = to_ms resp.start_transfer_time
app_connect_time = to_ms resp.app_connect_time
pretransfer_time = to_ms resp.pretransfer_time
connect_time = to_ms resp.connect_time
name_lookup_time = to_ms resp.name_lookup_time
Statsd.timing('http_request.elapsed_time', elapsed_time)
Statsd.timing('http_request.response_time', response_time)
Statsd.timing('http_request.start_transfer_time', start_transfer_time)
Statsd.timing('http_request.app_connect_time', app_connect_time)
Statsd.timing('http_request.pretransfer_time', pretransfer_time)
Statsd.timing('http_request.connect_time', connect_time)
Statsd.timing('http_request.name_lookup_time', name_lookup_time)
sleep 10
end
讓我們看看該數(shù)據(jù)生成的 Graphite 圖。該數(shù)據(jù)來(lái)自 2 分鐘前,而 elapsed_time 則來(lái)自前面的腳本。
圖像生成
Render URL
下面圖片的 Render URL
/render/?width=586&height=308&from=-2minutes&target=stats.timers.http_request.elapsed_time.sum
Graphite 生成的圖片
該圖片簡(jiǎn)單地描繪了 http 請(qǐng)求在一段時(shí)間內(nèi)的 elapsed_time 值。
JSON-data
Render URL
下面 JSON-data 的 Render URL
/render/?width=586&height=308&from=-2minutes&target=stats.timers.http_request.elapsed_time.sum&format=json
來(lái)自 Graphite 的 JSON-output
在下面的結(jié)果中,我們可以查看來(lái)自 Graphite 的源數(shù)據(jù)。這些數(shù)據(jù)來(lái)自12個(gè)不同的數(shù)據(jù)點(diǎn),也即 StatsD 10 秒 flush internal 的兩分鐘。Graphite 繪制數(shù)據(jù)就是如此簡(jiǎn)單。
此外,借助 JSONLint,JSON-data 的數(shù)據(jù)顯示更加美觀。
[
{
"target": "stats.timers.http_request.elapsed_time.sum",
"datapoints": [
[
53.449951171875,
1343038130
],
[
50.3916015625,
1343038140
],
[
50.1357421875,
1343038150
],
[
39.601806640625,
1343038160
],
[
41.5263671875,
1343038170
],
[
34.3974609375,
1343038180
],
[
36.3818359375,
1343038190
],
[
35.009033203125,
1343038200
],
[
37.0087890625,
1343038210
],
[
38.486572265625,
1343038220
],
[
45.66064453125,
1343038230
],
[
null,
1343038240
]
]
}
]
在 Graphite 繪制 gauge 圖像
下面的簡(jiǎn)單腳本能將 gauge 傳送給 StatsD,模擬用戶注冊(cè)的過(guò)程。
#!/usr/bin/env ruby
require './statsdclient.rb'
Statsd.host = 'localhost'
Statsd.port = 8125
user_registrations = 1
while true
user_registrations += Random.rand 128
Statsd.gauge('user_registrations', user_registrations)
sleep 10
end
圖像顯示——用戶注冊(cè)數(shù)量
Render URL
下面圖片的 Render URL
/render/?width=586&height=308&from=-20minutes&target=stats.gauges.user_registrations
來(lái)自 Graphite 的圖片
另一個(gè)簡(jiǎn)單的圖片,展示總的注冊(cè)數(shù)。
圖片顯示——每分鐘的用戶注冊(cè)數(shù)
使用 Graphite 的衍生函數(shù),可以獲得每分鐘的用戶注冊(cè)數(shù)量。
Render URL
下面圖片的 Render URL
/render/?width=586&height=308&from=-20minutes&target=derivative(stats.gauges.user_registrations)
來(lái)自 Graphite 的圖片
該圖片所用的數(shù)據(jù)跟之前的圖片一致,但是使用了衍生函數(shù)從而顯示每分鐘的注冊(cè)率。
結(jié)論
深入了解 StatsD 與 Graphite 的工作原理,能讓我們更加明白 StatsD 所傳送的數(shù)據(jù)種類,如何傳送,以及怎樣更有效地根據(jù) Graphite 讀取數(shù)據(jù)。
原文地址:https://blog.pkhamre.com/understanding-statsd-and-graphite/
OneAPM 是應(yīng)用性能管理領(lǐng)域的新興領(lǐng)軍企業(yè),Cloud Insight 能幫助企業(yè)用戶和開(kāi)發(fā)者輕松實(shí)現(xiàn):監(jiān)控各項(xiàng)基礎(chǔ)組件以及對(duì)數(shù)據(jù)進(jìn)行聚合、過(guò)濾和篩選的功能,致力于打造一個(gè)更為強(qiáng)大的數(shù)據(jù)管理平臺(tái)。想閱讀更多技術(shù)文章,請(qǐng)?jiān)L問(wèn) OneAPM 官方博客。