如何深入理解 StatsD 與 Graphite ?

眾所周知,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 與 Graphite
理解 StatsD 與 Graphite

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 官方博客。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容