作者:莫善
某互联网公司高级 DBA。
本文起源:原创投稿
* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。
一、引言
告警跟运维工作非亲非故,一个好的告警零碎不仅能晋升运维的效率,还能晋升相干人员的工作舒适度及生存品质。相同,如果告警零碎比拟拉胯,那运维的工作就比拟好受了。比方,中午收到无关痛痒的告警信息,又比方这个告警正在解决还始终在发,再比方同一时间产生很多告警,而后不重要的告警把重要的告警刷走了,等等。
本文想分享一下在应用 Alertmanager 的过程中遇到的一些困扰,以及分享一下最近在做的告警零碎革新的我的项目,仅做经验交流。
二、后期筹备
咱们线上采纳 Prometheus + Alertmanager 的架构进行监控告警。所以本文次要是基于 Alertmanager 组件进行介绍。
alertmanager, version 0.17.0 (branch: HEAD, revision: c7551cd75c414dc81df027f691e2eb21d4fd85b2)
build user: [email protected]
build date: 20190503-09:10:07
go version: go1.12.4
1、待处理问题
(1)告警烦扰
因历史遗留问题,咱们线上环境的环境是一个集群一个 prometheus,而后共享一个告警通道 Alertmanager,有时候会呈现 A 集群的告警信息跑到 B 集群的告警中。比方会收到像上面这种状况:
cluster: clusterA
instance: clusterB Node
alert_name: xxx
这个问题始终没找到起因,也没法稳固复现。
(2)告警降级
当初的告警零碎在触发告警时候不会降级。比方优先发给值班人员,其次会依据接管人,告警工夫,告警介质等进行降级。
对于告警降级后文有解释阐明。
(3)告警复原
对于曾经复原的告警,Alertmanager 不会发送一份告警复原的提醒。
(4)告警克制
Alertmanager 针对反复的告警能够做到自定义工夫进行克制,然而不太智能,比方,同一个告警项,后面三次发送距离短点,超过三次的距离能够长点,比方第 1,3,5,10,20,30 分钟发送。另外也不能做到自适应工夫克制,比方工作工夫克制工夫距离能够长一点(同一个告警十分钟发一次),休息时间的克制工夫短一点(同一个告警五分钟发一次)。
(5)告警静默
Alertmanager 反对告警静默性能,然而须要在 Alertmanager 平台进行配置。如果一个机器宕机后,可能触发很多告警须要静默,所以增加及预先删除静默规定的治理比拟麻烦。
另外还有一个比拟头疼的问题,失常通过告警页面能够点击【Silence】是能够将待静默的告警信息带到配置告警静默的页面,然而大部分时候都是不行的(空白页面),须要手动填写须要静默的告警信息,这就很头疼了。
(6)语音告警
Alertmanager 目前不反对语音告警。
这个问题不做为独自的问题进行介绍,会放在告警降级局部。
2、发现新问题
为了解决上述提到的【告警烦扰】问题,咱们采取的办法是将一个 Alertmanager 拆成多个(一个集群一个),这样能解决【告警烦扰问题】,那么又带来了新的问题。
- 如何实现告警收敛?
- 如何治理告警静默?
(1)告警收敛
同一时刻产生多条告警,就会导致相干人员收到多条告警信息,这样即节约告警资源,也对排查问题带来肯定的烦扰。比方单机多实例的场景,一台机器宕机,同时会产生好几十条告警,或者一个集群呈现问题,所有节点都触发告警。针对这种场景如果没有告警收敛,会比拟苦楚。
Alertmanager 自身反对收敛,因为咱们须要解决【告警烦扰】问题,所以咱们革新的时候拆成了一个集群一个 Alertmanager,这样咱们环境就没法用自带的收敛性能了。
(2)告警静默
原本单个 Alertmanager 的告警静默就比拟难治理了,如果多个告警项,可能是多个 Alertmanager 须要静默,静默的治理就更加麻烦了。
针对以上问题,上面会一一介绍一下解决思路,局部解决方案不见得适宜所有环境,其余环境或者有更好的解决方案,这里仅供参考。
三、告警革新
1、告警烦扰问题
如前文所述,通过将一个 Alertmanager 拆成 n 个,这种形式看起来比拟笨,然而却无效,如果有一个上帝视角将所有 Alertmanager 治理起来,就当作一个数据库实例去看待,其实也很不便。这类组件也不须要很多的系统资源,应用虚拟机齐全够用。另外其实也有一个益处,退出说 Alertmanager 呈现问题,比方过程失常,然而不会发告警了,这样也不至于团灭。
咱们平台的监控、告警都曾经实现自动化,并且都是通过平台进行治理,用一个 Alertmanager 跟多个,在部署装置上老本差不多,然而不太好治理(这个后文有阐明)。
2、告警降级问题
发送告警的介质次要分几种,邮件,企业微信(其余即时通讯工具),短信,电话 / 语音。从左到右老本顺次增高,所以为了告警资源不被节约,尽可能的节俭短信 / 电话这种告警介质,所以咱们心愿咱们的告警零碎是能自适应的进行调整。这个降级分如下几个维度:
- 第一 告警介质的降级。邮件 –> 企业微信 –> 短信 –> 电话(同一个告警项发送超过 3 次就向上降级一个介质,具体能够依据需要定义)。
- 第二 告警接管人降级。一级 –> 二级 –> leader。
- 第三 依照工夫降级。工作工夫通过邮件 / 企业微信发送告警,工作工夫之外通过短信 / 电话发送告警。
这个问题想通过 Alertmanager 来解决如同不可行,所以只能通过其余伎俩进行曲线救国了。咱们采取的形式是开发一个脚本读取 Alertmanager 的告警信息,而后通过脚本进行发送告警信息。
其实也能够间接读取 prometheus 的告警信息,原理上差不太多。
Send a detailed message to the DBA by Mail #只有是告警就会通过邮件告知
if now_time > 8 and now_time < 22 :
Send a simple message to the DBA by WX
else #依照告警工夫降级告警介质
if alert_count > 3 and phone_count < 3 :
Send a simple simple message to the DBA by phone #短信告警降级电话告警
elif alert_count > 3 and phone_count > 3 :
Send a simple message to the leader by phone #接管告警人员降级
else
Send a simple message to the DBA by SMS #告警介质降级
承受告警的人员蕴含值班 DBA 及我的项目负责人,如果接管告警的对象无奈收到告警信息(联系方式异样,到职),就会读取通讯录获取一位同组的人员进行从新发送告警。
- 能够定义,屡次告警后进行接管人员的告警降级,同理也能够定义告警工夫,告警介质等告警降级。
- 只有是告警就会通过邮件告知,起因是短信 / 语音这种介质老本比拟高,也不不便查阅,所以只会发送简略的告警提醒,具体的须要查阅邮件。
3、告警收敛 / 克制问题
首先须要一个表来保留发送告警的记录,蕴含告警项(要求全局惟一,倡议应用 ip:port),告警状态,累计告警次数,最初告警工夫,整个零碎的好几个性能都须要这个表。
CREATE TABLE `tb_alert_for_task` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`alert_task` varchar(100) DEFAULT ''COMMENT' 告警我的项目 ',
`alert_state` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态, 0 示意曾经复原, 1 示意正在告警',
`alert_count` int(11) NOT NULL DEFAULT '0' COMMENT '告警的次数, 单个告警项发送的告警次数是 10 次(每天至少发送十次)',
`u_time` datetime NOT NULL DEFAULT '2021-12-08 00:00:00' COMMENT '下一次发送告警的工夫',
`alert_remarks` varchar(50) NOT NULL DEFAULT ''COMMENT' 告警内容 ',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_alert_task` (`alert_task`)
) ENGINE=InnoDB AUTO_INCREMENT=7049 DEFAULT CHARSET=utf8mb4;
通过查阅文档咱们能够晓得调用上面的告警信息 api 是能够获取以后告警信息。
api/v1/alerts?silenced=false&inhibited=false
一条告警信息长上面这样,我认为有用的就是【alertname】【cluster】【instance(主机局部)】这三个属性,能够将这三个属性作为收敛维度。
{
'labels': {
'alertname': 'TiDB_server_is_down',
'cluster': 'cluster_name_001',
'env': 'cluster_name_001',
'expr': 'probe_success{group="tidb"}==0','group':'tidb','instance':'192.168.168.1:4000','job':'tidb_port_probe','level':'emergency','monitor':'prometheus'
}
}
这里省略了其余信息,仅保留了 labels 局部
(1)克制
克制逻辑是下一次发送告警工夫小于以后工夫,或者告警发送次数大于 10 次。为什么是这个逻辑?这须要联合收敛局部的代码介绍,所以前面解释。
#这是克制的逻辑, 将以后告警异样项都读出来, 如果以后 alertmanager 的告警曾经在这外面就视为克制对象, 因为这些告警还不满足再次发送的条件
select_sql = "select alert_task from tb_tidb_alert_for_task where alert_state = 1 and (u_time > now() or alert_count > 10);"
state, skip_instance = connect_mysql(opt = "select", sql = {"sql" : select_sql})
skip_instance 是个列表,在遍历告警信息的时候会用到,如果在这个列表外面就疏忽这个告警,以此起到克制的成果。
(2)收敛
收敛思路大略是这样,遍历每一条告警信息,遍历的时候会在【tb_tidb_alert_for_task】表记录一条记录,蕴含:
- instance 信息,这是全局惟一。
- 告警状态,区别是正在告警,还是曾经复原告警,复原告警的时候用到。
- 发送告警次数,这个值会作为收敛参考数据。
- 下一次发送告警工夫,这是不便解决克制。
next_time = 0
if now_time > 8 and now_time < 22 :
next_time = max_alert_count * 2 + 1
else :
next_time = max_alert_count
insert_sql[instance_name] = """replace into tb_tidb_alert_for_task(alert_task,alert_state,alert_count,u_time,alert_remarks)
select '"""+ instance_name +"""',1,"""+ str(max_alert_count) +""", date_add(now(), INTERVAL + """+ str(next_time) +""" MINUTE),
'tidb 集群告警';"""state, _ = connect_mysql(opt ="insert", sql = insert_sql)
下面这部分逻辑是服务于克制性能,所以能够解释的通,上文将下一次发送告警工夫要求小于以后工夫。
- 工作工夫,发送一次告警后,下一次告警工夫是距离 2n+1 min(n 示意告警次数)。
- 非工作工夫,发送一次告警后,下一次告警工夫是距离 n+1 min(n 示意告警次数)
上面这部分逻辑是遍历 Alertmanager 的所有处于 active 状态的告警信息,而后做剖析解决。
def f_get_alert_to_msg(url) :
try :
res = json.loads(requests.get(url, headers = header, timeout = 10).text) #读取 alertmanager 的告警状态
except Exception as err :
return {"code" : 1, "info" : str(err)}
for temp in res["data"] :
cluster_name = temp["labels"]["cluster"]
alert_name = temp["labels"]["alertname"]
instance_name = cluster_name + ":all" #非凡状况没有 instance 信息,这种时候是集群告警,而不是某个节点告警
if "instance" in temp["labels"] : instance_name = temp["labels"]["instance"]
if len(global_instance_list) == 0 : global_instance_list = []
if len(instance_name) > 0 : global_instance_list.append(instance_name) #这个列表在判断是否是告警曾经复原用到
if instance_name in skip_instance : continue # 合乎克制条件就疏忽这个告警
if cluster_name not in global_alert_cluster.keys() : global_alert_cluster[cluster_name] = {}
if alert_name not in global_alert_name.keys() : global_alert_name[alert_name] = {}
if instance[0] not in global_alert_host.keys() : global_alert_host[instance[0]] = {}
if alert_name not in global_alert_cluster[cluster_name].keys() : global_alert_cluster[cluster_name][alert_name] = []
if cluster_name not in global_alert_name[alert_name].keys() : global_alert_name[alert_name][cluster_name] = []
if alert_name not in global_alert_host[instance[0]].keys() : global_alert_host[instance[0]][alert_name] = []
if instance_name not in global_alert_cluster[cluster_name][alert_name] : global_alert_cluster[cluster_name][alert_name].append(instance_name)
if instance_name not in global_alert_name[alert_name][cluster_name] : global_alert_name[alert_name][cluster_name].append(instance_name)
if cluster_name + ":" + instance[1] not in global_alert_host[instance[0]][alert_name] : global_alert_host[instance[0]][alert_name].append(cluster_name + ":" + instance[1])
return {"code" : 0, "info" : "ok"}
将【alertname】【cluster】【instance】别离保留到【global_alert_name】【global_alert_cluster】【global_alert_host】这三个字典。
上面这个逻辑是遍历 alertmanager 的 url,依据 url 去扫对应的 alertmanager 的告警信息,能够看到代码中有一个判断 Alertmanager 状态的代码,能够起到监控 Alertmanager 的作用,会将拜访异样的发送进去。
for url in url_list :
status = f_get_alert_to_msg(url) #读取 alertmanager 告警信息
if status == 1 :
error_list.append(url)
if len(error_list) > 0 :
info = "Alertmanager 拜访异样 :" + ",".join(error_list)
status = f_alert_sms_to_user(tel,info)
最终依据【global_alert_name】【global_alert_cluster】【global_alert_host】三个字典的长度判断以哪个维度进行收敛,即哪个字典最短就以哪个维度为收敛对象。
if len(global_alert_cluster.keys()) < len(global_alert_host.keys()) and len(global_alert_cluster.keys()) < len(global_alert_name.keys()) :
alert = global_alert_cluster
info_tmp = "告警集群 :"
elif len(global_alert_name.keys()) < len(global_alert_host.keys()) :
alert = global_alert_name
info_tmp = "告警名称 :"
else :
alert = global_alert_host
info_tmp = "告警主机 :"
4、告警复原问题
#读取告警状态是 1, 且比以后工夫还早的条目
sql = """select alert_task from tb_tidb_alert_for_task
where alert_state = 1 and alert_remarks = 'tidb 集群告警' and u_time < date_add(now(), INTERVAL - 1 MINUTE);"""state, alert_instance = connect_mysql(opt ="select", sql = {"sql" : sql})
alert_ok = []
for instance in alert_instance :
if instance not in global_instance_list : #global_instance_list 是在遍历告警信息的时候记录了所有 instance 信息
alert_ok.append(instance)
update_sql[instance] = """update tb_tidb_alert_for_task set alert_state = 0, alert_count = 0
where alert_state = 1 and alert_remarks = 'tidb 集群短信告警' and alert_task = '"""+ instance +"""';"""state, _ = connect_mysql(opt ="update", sql = update_sql)
如果表里处在告警状态的记录不在以后正在告警的列表中,就阐明告警曾经复原,这时候就会变更告警状态,且将告警次数置为 0。
5、告警静默问题
(1)增加静默
/api/v1/silences
try :
expi_time = int(expi_time) #小时
except Exception as err :
return {"code" : 1, "info" : str(err)}
if expi_time > 24 :
return {"code" : 1, "info" : "The alarm cannot be silent for more than 24 hours"}
local_time = f_get_time() #以后工夫 2022-01-01 00:00:00
local_time = dt.datetime.strptime(local_time,'%Y-%m-%d %H:%M:%S')
start_time = local2utc(local_time).strftime("%Y-%m-%dT%H:%M:%S.000Z") #换成 UTC 工夫
end_time = dt.datetime.strptime(((dt.datetime.now() - timedelta(hours = -expi_time)).strftime("%Y-%m-%d %H:%M:%S")),"%Y-%m-%d %H:%M:%S")
end_time = local2utc(end_time).strftime("%Y-%m-%dT%H:%M:%S.000Z")
dic = {
"id" : "","createdBy": user,"comment": comment,"startsAt": start_time ,"endsAt": end_time,"matchers" : [
{
"name" : name,
"value" : value
}
]
}
try :
res = json.loads(requests.post(url, json = dic).text)
except Exception as err :
res = {"code" : 1, "info" : str(err)}
return res
这里须要留神,增加静默肯定须要让用户提供静默超时工夫,比方 2h,下限是 24 小时,这样能够防止因工夫过长,而后忘记规定,导致告警始终被静默。
另外用户提供的是静默小时数,而增加静默是须要一个开始工夫和完结工夫,且须要 UTC 工夫,所以须要通过计算非凡解决一下。
最初还须要留神,这里增加静默是繁多规定,即只有一个 name-value 对,不反对与条件及正则。如果须要多个条件能够追加 matchers 列表的值。
"matchers" : [
{
"name" : name1,
"value" : value1
},
{
"name" : name2,
"value" : value2
}
]
(2)删除静默
这部分须要用到两个 api,第一个是先获取规定 id,通过 id 进行删除。
/api/v1/silences?silenced=false&inhibited=false
/api/v1/silence/id
url = "http://xxx/api/v1/silences?silenced=false&inhibited=false"
try :
id_info = json.loads(requests.get(url).text)
except Exception as err :
return {"code" : 1, "info" : str(err)}
for item in id_info["data"] :
if item["status"]["state"] != "active" : continue #非 active 状态的间接疏忽
if item["matchers"][0]["name"] != name or item["matchers"][0]["value"] != value : continue #不满足条件的也间接疏忽
try : #先要获取 id 能力进行删除规定
url = "http://xxx/api/v1/silence/" + item["id"]
res = json.loads(requests.delete(url).text)
except Exception as err : #拜访失败
res = {"code" : 1, "info" : str(err)}
return res
能够看到告警静默的治理还是比拟麻烦的,所以咱们曾经将这部分治理性能整合到平台,能够通过平台的告警治理列表页进行告警静默的治理,能够通过 ip,instance,cluster,role,alert_name 这几个维度进行治理,也反对告警信息展现,能够通过展现页面一一告警增加静默,也能够将所有告警一键静默,这样就解决了告警静默难治理的问题。
四、注意事项
- 如果没有平台进行治理,不倡议应用这样的形式运维告警零碎。
- 增加告警静默的时候强烈建议增加超时工夫,且不宜过长,防止增加后忘记。
- 增加静默的时候肯定要做到心里有数,避免出现故障告警被顺带增加静默而又未进行解决的状况。
五、写在最初
本文所有内容仅供参考,因各自环境不同,并非通用计划,且在应用文中代码时可能碰上未知的问题。<font color=’red’> 如有线上环境操作需要,请在测试环境充沛测试。</font>