1、背景
项目开发中经常会用到很多外部组件,比如mongo、mysql、redis等,虽然在公有环境中使用云上的组件一般都会有完整的监控视图,但是有些项目是部署在私有环境中,使用的都是自建组件,没有完整的监控视图,其次,业务侧也希望将所有组件收归到一起进行统一的监控管理,这样就需要业务侧有一个多组件的监控平台,并且能够方便进行扩展。像笔者目前所在的项目组开发的大数据处理平台,使用了很多外部组件,元数据存储方面有:mongo、mysql、elasticsearch、redis、postgres,大数据存储方面有:hadoop(spark、hive、hbase、hdfs、yarn),为了更好的发现和定位问题,我们需要一个统一的监控管理中心。
2、选型
统一监控平台采用业界广泛使用的框架:prometheus grafana,其在功能和效果方面完全能达到业务侧需求,并且针对这个选型网上有很多开源组件的exporter和对应grafana视图,避免了很多自定义视图的繁琐工作。比如在这里可以找到很多不错的exporter和视图:https://promcat.io/、https://prometheus.io/docs/instrumenting/exporters/。
3、初始方案
Prometheus grafana工作原理是由prometheus定期拉取其配置的exporter进程的监控数据,然后grafana视图读取prometheus中的监控数据展示出来,如图1所示。既然我们可以从网上下载到个组件exporter和grafana视图,那就只需要启动各组件exporter进程,将进程地址配置到prometheus中进行定期拉取即可,方案很简单也不需要额外写代码,但是缺点是不方便新增和管理组件,像我们平台使用到了很多组件,我们就需要维护很多exporter进程。
4、改进方案
上面方案因为prometheus直接对接exporter导致维护成本增加,如果在prometheus和exporter之间增加一个中间层进行管理就可以解决这个问题,如图2所示。
4.1 管理exporter
将所有exporter、grafana视图放在monitor启动目录中,用于自动导入grafana视图、启动exporter进程、检测exporter进程存活状态。
设计monitor的配置如下:
代码语言:javascript复制[Plugin]
[[Plugin.Instances]]
Enable = false
Folder = "Hadoop"
Title = "Hadoop"
DashboardDir = "../tools/hadoop"
Port = 9001
Command = "nohup python3 %v -cluster %v -nns %v -rms %v -host 0.0.0.0 -port %v &"
Parameters = [
"../tools/hadoop/hadoop_jmx_exporter-master/hadoop_jmx_exporter.py",
"jm-test-emr",
"http://127.0.0.1:4008/jmx http://127.0.0.1:4008/jmx",
"http://127.0.0.1:5004/jmx http://127.0.0.1:5004/jmx",
"9001",
]
...
4.2 聚合监控数据
Prometheus拉取数据有特定的协议,因此monitor需要汇聚所有exporter的监控数据后按照同样的协议发送给prometheus。
正常情况我们在写业务侧exporter暴露给prometheus时可以通过注册自己的Collector来收集监控数据,但是这里我们需要对接的是其他exporter返回的特定格式数据,为了避免解析数据然后重新格式化,我们可以直接在http层进行聚合然后返回给prometheus。
为了在http层聚合,我们先看看prometheus拉取接口是怎么处理的。下载prometheus的golang库,prometheus拉取监控数据流程为:
- 注册prometheus请求地址
...
http.NewServeMux().Handle("/metrics", promhttp.Handler())
...
- 默认请求处理句柄
promhttp.Handler() {
return InstrumentMetricHandler(
prometheus.DefaultRegisterer, HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}),
)
}
promhttp.HandlerFor() {
...
//收集数据
mfs, err := reg.Gather()
//获取回包编码
enc := expfmt.NewEncoder(w, contentType)
//编码所有数据返回prometheus
for _, mf := range mfs {
if enc.Encode(mf) {
return
}
}
...
}
从上面的流程可以看出,我们只需要重写HandlerFor,将各组件exporter的回包拼接后返回prometheus就可以完成数据汇聚,简化代码如下:
代码语言:javascript复制promhttp.HandlerFor() {
...
//收集数据
mfs, err := reg.Gather()
//获取回包编码
enc := expfmt.NewEncoder(w, contentType)
//编码所有数据返回prometheus
for _, mf := range mfs {
if enc.Encode(mf) {
return
}
}
//加上其他exporter数据回包并返回
var wg sync.WaitGroup
var rsps [][]byte
for _, p := range plugins {
wg.Add(1)
go func(plugin *Plugin) {
defer wg.Done()
body, e := HttpGet(plugin.Url, nil, nil, nil, 0)
if e == nil && len(body) > 0 {
body = append(body, 'n')
rsps = append(rsps, body)
}
}(p)
}
wg.Wait()
for _, rsp := range rsps {
w.Write(rsp)
}
...
}