如何设计监控平台的告警组件(补档)

当业务发展到一定程度的时候,开发人员会开始考虑在系统中引入监控系统来对系统/业务进行监控。绝大多数监控系统都有两大核心功能,一个是工程师通过这个监控系统,能够对整个系统的运行情况一目了然,另外一个,就是当发生意外情况的时候,监控系统能将事件通知到人手上,毕竟人不可能24小时都在工作。这篇文章将要介绍的,就是第二个核心功能的承担着,告警组件。

项目背景

我们公司目前的监控系统采用的是TICK架构中的TI,即用Telegraf采集数据,Influxdb做存储。C我们用了更加流行的Grafana做了替换。至于我们为什么选择了Influxdb,可以参考这篇文章:InfluxDB与Prometheus用于监控系统上的对比

剩下K,即Kapacitor,我们最后抛弃了它,主要还是因为Kapacitor的太过于臃肿,上手和维护成本太高,很多功能我们都用不上,还不如自己开发一个。而Grafana的报警功能其实还可以,但是对于我们来说有个不大不小的缺陷,这里就不提了。于是自己心里先把实现思路过了一遍,觉得能Hold得住,就向领导请示想自己开发,接下来轮子就造起来了。

需求与设计原则

作为一个核心组件,我给自己先定了一个最基本的目标:稳定。功能多不多、炫不炫要让位给稳定性。

接下来开始思考告警组件的两个基本需求:通知,和异常事件的发现。

通知方面,邮件的方式是必不可少的。另外因为我们公司有自己的IM产品线,所以支持Webhook也是要留在考虑项里面。至于短信这些和客户的需求耦合度比较高,所以暂不考虑。

而异常事件的发现,我选择参考Kapacitor的方式,主动去DB做查询,拿到数据再做触发的判断。这种方式有个缺点,如果数据库挂了,那么数据的流入端就断了,这为系统的可用性增加了一个不确定性因素。但我们在Influxdb前面使用relay做了高可用网关,而在我们集群中relay也是高可用的,这可以抵消掉一些上面的不确定性因素。

但是反过来,如果参考Prometheus或者Open-Falcon的方式,在数据送入数据库之前,先经过告警组做判断,我们目前的监控系统就需要增加一个网关,或者将告警功能嵌入relay里面,这样一来相当于监控的数据流在进入DB前会经过两扇门,每一扇都会降低整个系统一定的吞吐量。还有一个点不得不考虑,对配置的每一次修改都需要重启程序,这势必会造成数据的丢失。为了解决这个问题,Prometheus和Open-Falcon都是将待进入DB的数据复制一份,导到告警组件这里来,而这又需要对采集组件的配合。所以基于上面基点的考虑,我就选择了主动去DB做查询的方式。

说完上面两个基本需求,还有一个定制化的需求也要考虑,我们希望能在我们的虚拟机管理平台上像主流的公有云厂商一样能够让用户配置告警的策略,所以这引入另外一个需求:对外暴露易操作的API,让前端妹子调用。

内部设计

明确了基本需求后,接下来开始内部的设计。为了理清思路,我写下了一份我(从使用者的角度)想要的配置文件(yaml格式),来帮助我对事物进行抽象:

alert:
- name: 宿主机监控
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: 内存使用率
sql: SELECT used_percent FROM mem WHERE time > now() - 1m
op: ">="
threshold: 50

notifier:
- name: 测试组
enable: true
type: mail 
host: smtp.163.com
port: 25
username: qq912293672@163.com
password: xxxxxx
from: qq912293672@163.com
to: [912293672@qq.com]

从配置文件上可以看到,我对告警组件抽象出了两个大的划分,一个是alert,抽象了数据的获取。一个是notifier,抽象了事件的通知。

在alert里面,我赋予了alert几个属性,其中type用来标识数据库的类型,因为我希望这个告警组件能支持多个存储后端。接下来是query,sql属性让用户自定义数据的查询方式,并且用threshold和op表示触发的阀值以及如何触发。总的来说,我采用了将多个查询实体组合成一个告警单位。这是经过思考的结果,目的是为了避免通知风暴:即很多机器很不幸都出异常的时,多个事件将聚合成一个告警,而不是发出多个邮件,而每个邮件的内容却很少。

而notifier的配置,我特意添加了type,也是为了支持多种通知方式,以及一个开关enable。

将notifier与alert分开,以及alert中包含query的设计,其实也是从Grafana和prometheus中学到的思路。在此感谢下今天的开源文化,让我等普通人有机会学到别人优秀的设计理念。

抽象得差不多后,可以考试编码了,按照分类,将代码主要分为3个模块,notify,alert,service。其中service对应我们上面的第三个需求,对外暴露API。

接口设计

开发语言上我使用的是Go,我们将会有多个query在执行,这刚好对上的Go的强项,并发。下面看下几个主要的接口:

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

因为alert其实可以当做一个获取数据的执行单位,所以我在这里又抽象出了一个执行器Executor,接下来我们只需要让我们Alert实现该接口,就能被调用执行。

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

Analyzer接口实现对各个监控系统数据处理。

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

Result接口抽象了监控系统的返回数据,屏蔽掉各个监控系统之间的数据差异。

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

通知接口

接下来我们需要实现一个调度器,实现了对上面Executor的调度和控制:

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

核心调度逻辑:

runFn := func(schItem *scheduleItem) {
        bf := bytes.NewBufferString("")
        ticker := time.NewTicker(schItem.executor.Interval())
        notiMap := make(map[string]int)
        mutex := &sync.RWMutex{}        for {            select {            // 定时器到期
            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 {                    // 对通知数进行累加
                    mutex.Lock()
                    notiMap[result.QueryName()]++
                    mutex.Unlock()                    // 通知数已经超过了限制
                    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()                // 内容为空则跳过通知
                if msgBody == "" {                    continue
                }                // 遍历通知器将报警发送出去
                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())
                    }()
                }            // 收到退出信号
            case <-schItem.closeCh:
                ticker.Stop()
                log.Infof("Executor %s exit", schItem.executor.Config().Name)                return
            }
        }
    }

上面的调度控制中,为了避免某个异常事件在短时间没有解决时,我实现了自己的一个控制逻辑:当同一个query的通知已经连续超过3次时,我会让它定制通知半小时。若半小时异常还继续,则再发三次通知给接受者,如此循环下去。

最后还有HTTP API的实现以及对数据的存储。这属于常规的开发逻辑,和我们这个告警组件的关系不是很大,就不一一介绍了。

总结

写这篇文章主要是总结下设计的思路,尤其是在几个核心问题上。在设计之初,除了告诉自己要保持住稳定性之外,还特别注意了如何对代码做到恰到好处的抽象,这也是最近半年看了那么多优秀开源项目的代码后的想法。老实说我这一次又对自己做得不满意,有机会我重构下整个组件。另外其实还有一个比较棘手的问题,就是如何让告警组件做到高可用(这无法通过简单部署多个服务就能实现,这样会导致通知事件的重复发送),这个问题最近正在解决,可以期待下一篇文章。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266