如何設(shè)計(jì)監(jiān)控平臺(tái)的告警組件(補(bǔ)檔)

當(dāng)業(yè)務(wù)發(fā)展到一定程度的時(shí)候,開(kāi)發(fā)人員會(huì)開(kāi)始考慮在系統(tǒng)中引入監(jiān)控系統(tǒng)來(lái)對(duì)系統(tǒng)/業(yè)務(wù)進(jìn)行監(jiān)控。絕大多數(shù)監(jiān)控系統(tǒng)都有兩大核心功能,一個(gè)是工程師通過(guò)這個(gè)監(jiān)控系統(tǒng),能夠?qū)φ麄€(gè)系統(tǒng)的運(yùn)行情況一目了然,另外一個(gè),就是當(dāng)發(fā)生意外情況的時(shí)候,監(jiān)控系統(tǒng)能將事件通知到人手上,畢竟人不可能24小時(shí)都在工作。這篇文章將要介紹的,就是第二個(gè)核心功能的承擔(dān)著,告警組件。

項(xiàng)目背景

我們公司目前的監(jiān)控系統(tǒng)采用的是TICK架構(gòu)中的TI,即用Telegraf采集數(shù)據(jù),Influxdb做存儲(chǔ)。C我們用了更加流行的Grafana做了替換。至于我們?yōu)槭裁催x擇了Influxdb,可以參考這篇文章:InfluxDB與Prometheus用于監(jiān)控系統(tǒng)上的對(duì)比

剩下K,即Kapacitor,我們最后拋棄了它,主要還是因?yàn)镵apacitor的太過(guò)于臃腫,上手和維護(hù)成本太高,很多功能我們都用不上,還不如自己開(kāi)發(fā)一個(gè)。而Grafana的報(bào)警功能其實(shí)還可以,但是對(duì)于我們來(lái)說(shuō)有個(gè)不大不小的缺陷,這里就不提了。于是自己心里先把實(shí)現(xiàn)思路過(guò)了一遍,覺(jué)得能Hold得住,就向領(lǐng)導(dǎo)請(qǐng)示想自己開(kāi)發(fā),接下來(lái)輪子就造起來(lái)了。

需求與設(shè)計(jì)原則

作為一個(gè)核心組件,我給自己先定了一個(gè)最基本的目標(biāo):穩(wěn)定。功能多不多、炫不炫要讓位給穩(wěn)定性。

接下來(lái)開(kāi)始思考告警組件的兩個(gè)基本需求:通知,和異常事件的發(fā)現(xiàn)。

通知方面,郵件的方式是必不可少的。另外因?yàn)槲覀児居凶约旱腎M產(chǎn)品線,所以支持Webhook也是要留在考慮項(xiàng)里面。至于短信這些和客戶的需求耦合度比較高,所以暫不考慮。

而異常事件的發(fā)現(xiàn),我選擇參考Kapacitor的方式,主動(dòng)去DB做查詢,拿到數(shù)據(jù)再做觸發(fā)的判斷。這種方式有個(gè)缺點(diǎn),如果數(shù)據(jù)庫(kù)掛了,那么數(shù)據(jù)的流入端就斷了,這為系統(tǒng)的可用性增加了一個(gè)不確定性因素。但我們?cè)贗nfluxdb前面使用relay做了高可用網(wǎng)關(guān),而在我們集群中relay也是高可用的,這可以抵消掉一些上面的不確定性因素。

但是反過(guò)來(lái),如果參考Prometheus或者Open-Falcon的方式,在數(shù)據(jù)送入數(shù)據(jù)庫(kù)之前,先經(jīng)過(guò)告警組做判斷,我們目前的監(jiān)控系統(tǒng)就需要增加一個(gè)網(wǎng)關(guān),或者將告警功能嵌入relay里面,這樣一來(lái)相當(dāng)于監(jiān)控的數(shù)據(jù)流在進(jìn)入DB前會(huì)經(jīng)過(guò)兩扇門(mén),每一扇都會(huì)降低整個(gè)系統(tǒng)一定的吞吐量。還有一個(gè)點(diǎn)不得不考慮,對(duì)配置的每一次修改都需要重啟程序,這勢(shì)必會(huì)造成數(shù)據(jù)的丟失。為了解決這個(gè)問(wèn)題,Prometheus和Open-Falcon都是將待進(jìn)入DB的數(shù)據(jù)復(fù)制一份,導(dǎo)到告警組件這里來(lái),而這又需要對(duì)采集組件的配合。所以基于上面基點(diǎn)的考慮,我就選擇了主動(dòng)去DB做查詢的方式。

說(shuō)完上面兩個(gè)基本需求,還有一個(gè)定制化的需求也要考慮,我們希望能在我們的虛擬機(jī)管理平臺(tái)上像主流的公有云廠商一樣能夠讓用戶配置告警的策略,所以這引入另外一個(gè)需求:對(duì)外暴露易操作的API,讓前端妹子調(diào)用。

內(nèi)部設(shè)計(jì)

明確了基本需求后,接下來(lái)開(kāi)始內(nèi)部的設(shè)計(jì)。為了理清思路,我寫(xiě)下了一份我(從使用者的角度)想要的配置文件(yaml格式),來(lái)幫助我對(duì)事物進(jìn)行抽象:

alert:
- name: 宿主機(jī)監(jiān)控
type: influxdb
url: http://120.25.127.4:8086
db: telegraf
interval: 2s
query:
- name: cpu空閑值
sql: SELECT usage_idle FROM cpu WHERE time > now() - 1m
threshold: 100
op: "<="
- name: 內(nèi)存使用率
sql: SELECT used_percent FROM mem WHERE time > now() - 1m
op: ">="
threshold: 50

notifier:
- name: 測(cè)試組
enable: true
type: mail 
host: smtp.163.com
port: 25
username: qq912293672@163.com
password: xxxxxx
from: qq912293672@163.com
to: [912293672@qq.com]

從配置文件上可以看到,我對(duì)告警組件抽象出了兩個(gè)大的劃分,一個(gè)是alert,抽象了數(shù)據(jù)的獲取。一個(gè)是notifier,抽象了事件的通知。

在alert里面,我賦予了alert幾個(gè)屬性,其中type用來(lái)標(biāo)識(shí)數(shù)據(jù)庫(kù)的類型,因?yàn)槲蚁M@個(gè)告警組件能支持多個(gè)存儲(chǔ)后端。接下來(lái)是query,sql屬性讓用戶自定義數(shù)據(jù)的查詢方式,并且用threshold和op表示觸發(fā)的閥值以及如何觸發(fā)??偟膩?lái)說(shuō),我采用了將多個(gè)查詢實(shí)體組合成一個(gè)告警單位。這是經(jīng)過(guò)思考的結(jié)果,目的是為了避免通知風(fēng)暴:即很多機(jī)器很不幸都出異常的時(shí),多個(gè)事件將聚合成一個(gè)告警,而不是發(fā)出多個(gè)郵件,而每個(gè)郵件的內(nèi)容卻很少。

而notifier的配置,我特意添加了type,也是為了支持多種通知方式,以及一個(gè)開(kāi)關(guān)enable。

將notifier與alert分開(kāi),以及alert中包含query的設(shè)計(jì),其實(shí)也是從Grafana和prometheus中學(xué)到的思路。在此感謝下今天的開(kāi)源文化,讓我等普通人有機(jī)會(huì)學(xué)到別人優(yōu)秀的設(shè)計(jì)理念。

抽象得差不多后,可以考試編碼了,按照分類,將代碼主要分為3個(gè)模塊,notify,alert,service。其中service對(duì)應(yīng)我們上面的第三個(gè)需求,對(duì)外暴露API。

接口設(shè)計(jì)

開(kāi)發(fā)語(yǔ)言上我使用的是Go,我們將會(huì)有多個(gè)query在執(zhí)行,這剛好對(duì)上的Go的強(qiáng)項(xiàng),并發(fā)。下面看下幾個(gè)主要的接口:

// Executor :type Executor interface {
    Execute() ([]Result, error)
    Interval() time.Duration
    Config() Config
    Close() error
}

因?yàn)閍lert其實(shí)可以當(dāng)做一個(gè)獲取數(shù)據(jù)的執(zhí)行單位,所以我在這里又抽象出了一個(gè)執(zhí)行器Executor,接下來(lái)我們只需要讓我們Alert實(shí)現(xiàn)該接口,就能被調(diào)用執(zhí)行。

type Analyzer interface {
    Analyze(string, interface{}, QueryConfig) (Result, bool)
}

Analyzer接口實(shí)現(xiàn)對(duì)各個(gè)監(jiān)控系統(tǒng)數(shù)據(jù)處理。

type Result interface {
    String() string
    QueryName() string}

Result接口抽象了監(jiān)控系統(tǒng)的返回?cái)?shù)據(jù),屏蔽掉各個(gè)監(jiān)控系統(tǒng)之間的數(shù)據(jù)差異。

type Notifier interface {
    Send(content string) error
    Name() string
    Type() string
    To() []string
    Enable() bool
    Config() *Config
}

通知接口

接下來(lái)我們需要實(shí)現(xiàn)一個(gè)調(diào)度器,實(shí)現(xiàn)了對(duì)上面Executor的調(diào)度和控制:

type Scheduler interface {
    Run()
    AddExecutor(executor.Executor)
    RemoveExecutor(name string)
    ExecutorExist(name string) bool
    Stop()
}

核心調(diào)度邏輯:

runFn := func(schItem *scheduleItem) {
        bf := bytes.NewBufferString("")
        ticker := time.NewTicker(schItem.executor.Interval())
        notiMap := make(map[string]int)
        mutex := &sync.RWMutex{}        for {            select {            // 定時(shí)器到期
            case <-ticker.C:
                results, err := schItem.executor.Execute()                if err != nil {
                    log.Error(err)                    continue
                }
                notifiers, err := adaper.ReadAllNotifier()                if err != nil {
                    log.Error(err)                    continue
                }                for _, result := range results {                    // 對(duì)通知數(shù)進(jìn)行累加
                    mutex.Lock()
                    notiMap[result.QueryName()]++
                    mutex.Unlock()                    // 通知數(shù)已經(jīng)超過(guò)了限制
                    log.Debugf("notiMap:%+v", notiMap)
                    result := result                    if notiMap[result.QueryName()] > notiSeqCount {                        if notiMap[result.QueryName()] == notiSeqCount+1 {                            go func(name string) {
                                time.Sleep(notiSleepDuration)
                                mutex.Lock()
                                notiMap[name] = 0
                                mutex.Unlock()
                            }(result.QueryName())
                        }                        continue
                    }                    if _, err := bf.WriteString(result.String()); err != nil {
                        log.Error(err)
                    }
                    bf.WriteString("<br>")
                }
                msgBody := bf.String()
                bf.Reset()                // 內(nèi)容為空則跳過(guò)通知
                if msgBody == "" {                    continue
                }                // 遍歷通知器將報(bào)警發(fā)送出去
                for _, notifier := range notifiers {
                    log.Debugf("bool:%v", notifier.Enable())                    if !notifier.Enable() {                        continue
                    }
                    notifier := notifier                    go func() {
                        err := notifier.Send(msgBody)                        if err != nil {
                            log.Errorf("Send %s notify to %s fail:%s", notifier.Type(), notifier.To(), err.Error())                            return
                        }
                        log.Infof("Send %s notify to %s success", notifier.Type(), notifier.To())
                    }()
                }            // 收到退出信號(hào)
            case <-schItem.closeCh:
                ticker.Stop()
                log.Infof("Executor %s exit", schItem.executor.Config().Name)                return
            }
        }
    }

上面的調(diào)度控制中,為了避免某個(gè)異常事件在短時(shí)間沒(méi)有解決時(shí),我實(shí)現(xiàn)了自己的一個(gè)控制邏輯:當(dāng)同一個(gè)query的通知已經(jīng)連續(xù)超過(guò)3次時(shí),我會(huì)讓它定制通知半小時(shí)。若半小時(shí)異常還繼續(xù),則再發(fā)三次通知給接受者,如此循環(huán)下去。

最后還有HTTP API的實(shí)現(xiàn)以及對(duì)數(shù)據(jù)的存儲(chǔ)。這屬于常規(guī)的開(kāi)發(fā)邏輯,和我們這個(gè)告警組件的關(guān)系不是很大,就不一一介紹了。

總結(jié)

寫(xiě)這篇文章主要是總結(jié)下設(shè)計(jì)的思路,尤其是在幾個(gè)核心問(wèn)題上。在設(shè)計(jì)之初,除了告訴自己要保持住穩(wěn)定性之外,還特別注意了如何對(duì)代碼做到恰到好處的抽象,這也是最近半年看了那么多優(yōu)秀開(kāi)源項(xiàng)目的代碼后的想法。老實(shí)說(shuō)我這一次又對(duì)自己做得不滿意,有機(jī)會(huì)我重構(gòu)下整個(gè)組件。另外其實(shí)還有一個(gè)比較棘手的問(wèn)題,就是如何讓告警組件做到高可用(這無(wú)法通過(guò)簡(jiǎn)單部署多個(gè)服務(wù)就能實(shí)現(xiàn),這樣會(huì)導(dǎo)致通知事件的重復(fù)發(fā)送),這個(gè)問(wèn)題最近正在解決,可以期待下一篇文章。

https://mp.weixin.qq.com/s/qr8WyroAWqx4D85J89RbvQ

最后編輯于
?著作權(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ù)。

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