時序資料庫-Graphite
Graphite就屬於一種時序資料庫,作用是儲存和聚合監控資料並繪製圖標,不負責資料的收集。之所以想寫一篇關於Graphite的博文主要是因為這是我接觸到的另一種新型資料庫,其特點和功能讓人眼前一亮。但是需要強調的是,這裡所謂的時序資料庫只是Graphite的一部分(WhisperDB的),而Graphite不僅僅是資料庫那麼簡單,它還包含有監控、資料計算、生成圖示等功能。
Graphite包括3個元件:
Carbon:守護程序,監聽時序資料
Whisper:資料儲存,graphite內建的DB
graphite webapp:用Django開發的UI,用於圖示的渲染,提供API
架構圖:
Carbon:
Carbon是一組守護程序,更詳細的說是一組python程式碼編寫的監聽資料服務的程序。
最簡潔的安裝只需要carbon-cache.py,更高階的安裝還需要carbon-relay.py和carbon-aggregator.py,我們就來看下這3個python程序的詳細功能。
carbon-cache.py,根據不同策略接受資料,並儘可能高效地將它們寫到磁碟上。主要功能有2部分:1快取資料;2寫入磁碟。涉及到的配置檔案也是圍繞這兩部分:carbon.conf中的[cache]定義了監聽的埠和快取策略;
carbon-relay.py,複製和分片,為分散式而服務。涉及到的配置檔案:carbon.conf中的[relay]定義了監聽的主機、埠和分發模式;relay-rules.conf定義了在rules模式下詳細的分發策略。
caibon-aggregator.py做資料的聚合,通過聚合減少資料庫壓力防止磁碟爆掉。涉及到的配置檔案:carbon.conf中的[aggregator]定義了接收和分發的主機;aggregation-rules.conf定義的是聚合策略。
carbon-aggregator-cache.py
從上文可以看得出carbon.conf是最重要的配置檔案,carbon-cache和carbon-relay可以安裝在同一個主機,carbon.conf中配置如下:
LINE_RECEIVER_INTERFACE = 0.0.0.0
LINE_RECEIVER_PORT = 2003
PICKLE_RECEIVER_INTERFACE = 0.0.0.0
PICKLE_RECEIVER_PORT = 2004
storage-schemas.conf也是個很重要的配置,配置裡可以配置多個策略,每個策略只有3行。
1 name,策略標識
2 以pattern=開頭的一個正則表示式
3 一個時間的配置規則
例如我現在的配置是這樣的;
[default]
pattern = my.com$
retentions = 10s:8d,1m:31d,10m:1y,1h:5y
pattern配置的是必須以my.com結尾,這個好理解,難點在retentions的配置。
Retentions中配置規則如下:
s - second
m - minute
h - hour
d - day
w - week
y - year
10S:8d表示每10秒記錄一次保留8天,以此類推剩下的配置,這裡為什麼要配置不同的策略?其實從粒度上可以看得出來越來越粗曠,這是充分考慮了我們的現實應用、磁碟空間等因素,取其平衡。本質是資料不斷插入又不斷整合的過程。 storage-aggregation.conf這是個非必須的配置檔案,功能與上面的storage-schemas.conf類似,字面意思看得出是做資料從高精度向低精度聚合而用的。雖然是非必須,並不是說聚合是非必需的,因為如果沒有聚合Whisper會爆掉,Graphite提供了預設的聚合規則,所以沒有storage-aggregation.conf配置也是可以的。 有3種方式可以向Graphite傳送資料:文字、Pickle和AMQP
最簡單直接的方式是傳送文字,大批量的資料通過Pickle,併發太大隻能通過AMQP非同步方式來削峰填谷。
純文字方式格式:
<metric path> <metric value> <metric timestamp>
<metric path>是名稱空間,是用逗號分隔開的keys
<metric value>是收集的value
<metric timestamp>是時間戳,距離1970年1月1日0點的秒數 問題:Graphite明確表示不做收集,我們是通過什麼方式把需要的資料吐給Graphite的呢?
我能想到主要有2種模式:純push模式、pull+push模式。
Push模式,就是在被監控例項上通過指令碼主動講資料吐流給graphite,這種模式理解起來簡單,但是缺點是要在每一個數據提供者上做定製化處理。
Pull+push模式是在中間伺服器上主動去被監控例項上獲取資料,然後吐流給graphite。這種方式的優點是對於被檢控方沒有汙染,是完全綠色的方案。缺點也有,不能太頻繁的去pull。
例如我們可以通過python寫一個指令碼,利用雲平臺的sdk週期性請求獲得想要的資料,然後吐流給Graphite。 Web App:
Web app提供了多種資料表現方式:
url以http://GRAPHITE_HOST:GRAPHITE_PORT/render開始,後面根據實際需求拼接不同的連線。
1 資料統計範圍
Graphite的範圍分為空間和時間兩個維度。
空間,代表要統計哪些target,target對應的值是名稱空間。我們把對應的以逗點隔開的名稱空間定義為一個path,*代表0-多個字元;{A,B,C}代表字元取值列表,[0-9]中括號代表取值範圍。
時間,代表統計的時間範圍,由&from=-8d&until=-7d這樣來限制,格式比較靈活,也可以&from=20091201&until=20091231這樣
From預設是24小時之前,until預設是now。 2 返回型別
graphite的返回型別幾乎涵蓋了所有的型別:
&format=png
&format=raw
&format=csv
&format=json
&format=svg
&format=pdf
&format=dygraph
&format=rickshaw
呼叫者根據自己的需要來決定以何種方式來獲取response資料。最常用的當然是json,因為其靈活度最高,雖然graphite提供了png格式的報表,但是建議慎用,因為比較醜。。。我一般是json獲取資料然後用echart之類的展示。
3 常用統計運算函式
絕對值:
&target=absolute(Server.instance01.threads.busy)
聚合:
&target=aggregate(host.cpu-[0-7].cpu-{user,system}.value,"sum")
第一個引數是名稱空間表示式,第二個引數支援average, median, sum, min, max, diff, stddev, count, range, multiply, last
平均值:
前面聚合裡包含了普通平均值的計算方式,avg()就是aggregate的average方式,這裡介紹的是帶有過濾條件的更復雜的平均值函式。
&target=averageAbove(server*.instance*.threads.busy,25)
平均值大於某個值的指標才會顯示。同理還有averageBelow
別名:
&target=alias(Sales.widgets.largeBlue,"LargeBlue Widgets")
百分比:
&target=asPercent(Server*.connections.{failed,succeeded},Server*.connections.attempted, 0)
求每臺機器連線的成功和失敗率,預設為0
第二個引數是總數,第三個引數是預設值,如果沒有指定總數百分比的分母就是第一個引數value自動求和。
積累:
&target=cumulative(Sales.widgets.largeBlue)
統計個數:
&target=countSeries(carbon.agents.*.*)
&target=currentAbove(server*.instance*.threads.busy,50)
只繪製大於某個值的記錄,同理還有currentBelow
名稱空間剔除
&target=exclude(servers*.instance*.threads.busy,"server02")
Value條件剔除:
&target=filterSeries(system.interface.eth*.packetsSent,'max', '>', 1000)
括號內參數是<名稱空間><運算> <比較符> <比較值>
運算子支援:average, median, sum, min, max, diff, stddev, range, multiply & last
比較符支援:=, !=, >, >=, < & <=
Graphite自帶了Dashboard,訪問url:http://my.graphite.host/dashboard
在Dashboard中可以編輯自己的儀表盤,這裡不再詳細介紹,因為我們一般是通過前面的API介面來獲取Graphite的Json資料或者圖表嵌入到自己專案中。
Whisper Database:
資料庫分為關係資料庫、NoSql資料庫、RRD時序資料庫
RRD資料庫的特性是:環形、大小固定、無需運維
RRD refers to Round Robin Database
Graphite為什麼不用已有的RRDtool而是選擇自開發一個whisper?
原因一:Graphite給自己的定位是運維專用時序資料庫,所以要貼合業務,普通RRD的預設設定不能滿足運維的要求,例如延時,如果預設為0肯定不符合我們的預期。
原因二:對接上千個數據結點時RRD過於頻繁的儲存(例如每秒鐘入庫1次)對IO壓力過大,所以開發了whisper,可以將資料進行處理後延時入庫(例如收集上千個數據結點資料後10分鐘後聚合一下再入庫),這種設計為了減少IO的壓力。PS:graphite在設計時當時的RRD並不能滿足需求,whisper開發出來之後其它RRD也有了聚合插入的功能,但原因一仍然是Graphite要單獨開發一套whisperDB的根本原因。
Whisper的資料在聚合時要求配置的精準度是倍數關係。例如現在配置了大小兩個粒度10s和1m,因為1m是10s的整數倍,所以這是合理的。但是如果兩個粒度是40s和1m,這樣是錯誤的。
資料庫層在聚合時的策略支援以下幾種(預設是avarage):
average
sum
last
max
min
Whisper Database的效能比傳統RRD慢2-5倍,並不是因為whisper的設計不如RRD優秀,而是因為whisper是用python實現的,而RRD是由C實現的,python的執行效率比C要低的多。
Whisper Database是Graphite的預設儲存DB,但並不是唯一的DB,我們也可以使用自己的資料庫以及儲存方式。Graphite WebApp獲取資料的API是通過“finder”介面來實現的,預設的是對接whisper的實現。
STORAGE_FINDERS = (
'graphite.finders.remote.RemoteFinder',
'graphite.finders.standard.StandardFinder',
)
如果使用自己的finder,就需要自己寫python指令碼,繼承CustomFinder,實現裡面的find_函式
class CustomFinder(object):
def find_nodes(self, query):
#
這些是非常高階的用法了,我還是推薦用預設的whisper DB來作為Graphite的儲存更方便快捷。
看下我自己程式碼的例子:
def get_aggrate_metric_data(self, metrics, from_, until,
period, alias, func=None, series_func=None):
# http://172.28.209.218/render?target=alias(summarize(sumSeries(alicloud.*.*.lb.*.network_traffic_in), "1hour"),'total')&format=csv
if func is None:
func = 'avg'
if series_func is None:
series_func = 'sumSeries'
expression = self.alias_template.format(
func=func,
pattern=metrics,
period=period,
alias=alias,
series_func=series_func
)
return self.__get_metric_data(alias, from_, until, expression, period)
def __get_metric_data(self, target, from_, until, expression,
period='1day', func='summarize', format='json'):
'''
@params:
target: graphite metric name # ex: stats.counter.foo
from_: start time # 20181201
util: end time # 20181203
expression: summarize(stats.gauges.aliyun.ggg,"20min")
format: data format # json,xml,csv etc.
@return:
formated json for echart
{
'time': [201911,201912...],
'value': [20,30,...],
}
'''
url = urljoin(self.endpoint, '/render')
print "expression ===", expression
payload = {'target': expression, 'format': format, 'until': until, 'from': from_}
payload_str = "&".join("%s=%s" % (k, v) for k, v in payload.items())
response = requests.get(url, params=payload_str)
data = response.json()
period_format = {'day': '%Y%m%d', 'hour': '%Y%m%d%H', 'min': '%Y%m%d%H%M'}
m = re.search(r'\d+(\w+)', period)
key = m.group(1)
try:
formatter = period_format[key]
except KeyError:
raise ValueError("not supported period: %s" % period)
ret = {}
for entry in data:
ret[target] = []
ret['time'] = []
for point in entry['datapoints']:
value = point[0]
if value is None:
value = 0
m = re.search(r'\d+(\w+)', period)
key = m.group(1)
try:
formatter = period_format[key]
except KeyError:
raise ValueError("not support this period: %s" % period)
x = datetime.datetime.fromtimestamp(
int(point[1])).strftime(formatter)
ret['time'].append(x)
ret[target].append(int(value))
return ret
返回的資料的Json格式如下:
<type 'list'>: [{u'target': u'aliyun.*.*.lb.*.network_traffic_in', u'datapoints': [[52741453320.0, 1530144000], [67990254390.0, 1530147600], [75203783520.0, 1530151200], [84519116310.0, 1530154800], [97207351410.0, 1530158400], [85248715260.0, 1530162000], [77570771850.0, 1530165600], [78157443840.0, 1530169200], [82784259870.0, 1530172800], [93145906620.0, 1530176400], [98472522750.0, 1530180000], [104994759480.0, 1530183600], [113430028800.0, 1530187200], [111290674350.0, 1530190800], [88019724510.0, 1530194400], [59661109860.0, 1530198000], [43395220110.0, 1530201600], [28912292760.0, 1530205200], [22186935360.0, 1530208800], [18953705880.0, 1530212400], [17860729380.0, 1530216000], [21559443900.0, 1530219600], [28923104520.0, 1530223200], [40697908200.0, 1530226800], [57158022330.0, 1530230400], [69985946970.0, 1530234000], [79089390270.0, 1530237600], [87491228640.0, 1530241200], [100534699350.0, 1530244800], [90967098120.0, 1530248400], [83598698910.0, 1530252000], [84985892520.0, 1530255600], [14350187070.0, 1530259200]]}]