1. 程式人生 > >時序資料庫-Graphite

時序資料庫-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.pycarbon-aggregator.py,我們就來看下這3python程序的詳細功能。

 

carbon-cache.py,根據不同策略接受資料,並儘可能高效地將它們寫到磁碟上。主要功能有2部分:1快取資料;2寫入磁碟。涉及到的配置檔案也是圍繞這兩部分:carbon.conf中的[cache]定義了監聽的埠和快取策略;

storage-schemas.conf定義了資料如何儲存。

 

carbon-relay.py,複製和分片,為分散式而服務。涉及到的配置檔案:carbon.conf中的[relay]定義了監聽的主機、埠和分發模式;relay-rules.conf定義了在rules模式下詳細的分發策略。

 

caibon-aggregator.py做資料的聚合,通過聚合減少資料庫壓力防止磁碟爆掉。涉及到的配置檔案:carbon.conf中的[aggregator]定義了接收和分發的主機;aggregation-rules.conf定義的是聚合策略。

 

carbon-aggregator-cache.py

carbon-cache.pycarbon-aggregator.py的整合,將兩個程序合併成一個程序,本質上要做的事情不變,涉及到的配置也不變。

 

從上文可以看得出carbon.conf是最重要的配置檔案,carbon-cachecarbon-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()就是aggregateaverage方式,這裡介紹的是帶有過濾條件的更復雜的平均值函式。

&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,訪問urlhttp://my.graphite.host/dashboard

Dashboard中可以編輯自己的儀表盤,這裡不再詳細介紹,因為我們一般是通過前面的API介面來獲取GraphiteJson資料或者圖表嵌入到自己專案中。

 

 Whisper Database:

資料庫分為關係資料庫、NoSql資料庫、RRD時序資料庫

RRD資料庫的特性是:環形、大小固定、無需運維

RRD refers to Round Robin Database

 

Graphite為什麼不用已有的RRDtool而是選擇自開發一個whisper

原因一:Graphite給自己的定位是運維專用時序資料庫,所以要貼合業務,普通RRD的預設設定不能滿足運維的要求,例如延時,如果預設為0肯定不符合我們的預期。

原因二:對接上千個數據結點時RRD過於頻繁的儲存(例如每秒鐘入庫1次)對IO壓力過大,所以開發了whisper,可以將資料進行處理後延時入庫(例如收集上千個數據結點資料後10分鐘後聚合一下再入庫),這種設計為了減少IO的壓力。PSgraphite在設計時當時的RRD並不能滿足需求,whisper開發出來之後其它RRD也有了聚合插入的功能,但原因一仍然是Graphite要單獨開發一套whisperDB的根本原因。

 

Whisper的資料在聚合時要求配置的精準度是倍數關係。例如現在配置了大小兩個粒度10s1m,因為1m10s的整數倍,所以這是合理的。但是如果兩個粒度是40s1m,這樣是錯誤的。

資料庫層在聚合時的策略支援以下幾種(預設是avarage):

average

sum

last

max

min

Whisper Database的效能比傳統RRD2-5倍,並不是因為whisper的設計不如RRD優秀,而是因為whisper是用python實現的,而RRD是由C實現的,python的執行效率比C要低的多。

 

Whisper DatabaseGraphite的預設儲存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]]}]