zabbix告警优化

目前新zabbix系统添加了1300多台监控设备,3W多个触发器,每天的告警也是满天飞,造成了有用的信息通常淹没在了告警风暴当中。由于目前都采用的短信告警,成本上也是一笔不小的开支,所以就很必要对告警进行优化。

告警依赖

有时候一台主机的可用性依赖于另一台主机。如果一台路由器宕机,则路由器后端的服务器将变得不可用。如果这两者都设置了触发器,你可能会收到关于两个主机宕机的通知,然而只有路由器是真正故障的。

这就是主机之间某些依赖关系可能有用的地方,设置依赖关系的通知可能会被抑制,而只发送根本问题的通知。

虽然Zabbix不支持主机之间的直接依赖关系,但是它们可以定义另外一种更加灵活的方式 – 触发器依赖关系。一个触发器可以有一个或多个依赖的触发器。

例如,主机位于路由器2后面,路由器2在路由器1后面。

Zabbix – 路由器1 – 路由器2 – 主机
如果路由器1宕机,显然主机和路由器2也不可达,然而我们不想收到主机、路由器1和路由器2都宕机的3条通知。

因此,在这种情况下我们定义了两个依赖关系:

‘主机宕机’ 触发器依赖于 ‘路由器2宕机’ 触发器
‘路由器2宕机’ 触发器依赖于 ‘路由器1宕机’ 触发器
在改变“主机宕机”触发器的状态之前,Zabbix将会检查相应触发器的依赖关系,如果找到,并且一个触发器处于“异常”状态,则触发器状态不会发生改变,因此不会执行动作,也不会发送通知。

Zabbix递归执行此检查,如果路由器1或路由器2是不可达的状态,那么主机触发器则不会更新。

所以根据zabbix提供的这个功能,对部分有依赖性的触发器之间做了关联,可以减少一部分告警。

告警升级

我给每个不同系统的群组分配了不同的权限,每个人收到的告警相对不会太多,但是作为领导或者权限更大的人来说,收到的告警数量数十倍增加,这个时候就需要对告警进行升级配置。

第一次出现告警的时候消息只发给一线运维人员,如果5分钟还没处理完才会发给级别或权限更大的人员。

告警去重合并

我实际工作中碰过很多次因为网络抖动造成的大面积agent无法访问的告警,或者因为某些原因服务无法访问但是下一次监控周期即恢复正常的情况,这样会导致大量的误报。

针对于此,上面的两种办法都不能有效的应对这种场景,于是想到通过数据库的层面进行一些处理,主要思路是将生成的alert写库以后,再定期写入到一张临时表当中,对于告警发送周期范围内已经恢复正常的告警直接删除。对于agent大面积无法访问的误报进行告警合并。

对于动作的内容有点要求,这里用#符号作为分隔,方便数据库层面去处理字符串,只用配置subject即可

#Operations:
{EVENT.NAME}#{HOST.NAME}#{HOST.IP}#{ITEM.NAME}:{ITEM.VALUE}#{EVENT.DATE} {EVENT.TIME}

#Recovery operations:
{EVENT.NAME}#{HOST.NAME}#{HOST.IP}#{ITEM.NAME}:{ITEM.VALUE}#{EVENT.RECOVERY.DATE} {EVENT.RECOVERY.TIME}

新建两张表

CREATE TABLE `his_alerts` (
  `eventid` bigint(20) DEFAULT NULL,
  `clock` int(20) DEFAULT NULL,
  `sendto` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `subject` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `p_eventid` bigint(20) DEFAULT NULL,
  `status` varchar(2) COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

create index idx_his_alerts_eventid on his_alerts(eventid);

CREATE TABLE `tmp_alerts` (
  `eventid` bigint(20) DEFAULT NULL,
  `clock` int(20) DEFAULT NULL,
  `sendto` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `subject` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `p_eventid` bigint(20) DEFAULT NULL,
  `status` varchar(2) COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `all_alerts` (
  `sendto` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `subject` varchar(255) COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

在mysql库中新建存储过程

drop PROCEDURE IF EXISTS resize_alerts;
delimiter $$
CREATE PROCEDURE resize_alerts ( ) 
BEGIN
    DECLARE
        v_eventid INT ( 20 );
    DECLARE
        v_cnt INT ( 2 );
    DECLARE
        v_status INT ( 2 );
    DECLARE
        v_sendto VARCHAR ( 255 );
    declare v_name varchar(128);
    DECLARE
        loop_done INT DEFAULT 0;

    start transaction;
    # 归档告警数据
    INSERT INTO his_alerts SELECT
    * 
    FROM
        tmp_alerts;

    # 清空临时表
    TRUNCATE TABLE tmp_alerts;
    truncate table all_alerts;

    # 初始化要处理的告警数据
    INSERT INTO tmp_alerts 
    SELECT
        a.eventid,
        a.clock,
        a.sendto,
        a.`subject`,
        a.p_eventid,
    CASE
            WHEN a.p_eventid IS NULL THEN
            1 ELSE 0 
    END STATUS 
    FROM
        alerts a,
        `events` b 
    WHERE a.clock > UNIX_TIMESTAMP( date_add( now( ), INTERVAL - 2 minute ) ) 
      AND a.eventid not in ( SELECT eventid FROM his_alerts )
        AND STATUS = 1 
        AND mediatypeid = 3 
        AND a.eventid = b.eventid 
        order by 1;


# 删除一分钟内已经恢复的告警
    delete from tmp_alerts where eventid in(select eventid from (       
SELECT a.eventid FROM tmp_alerts a WHERE eventid IN ( SELECT p_eventid FROM tmp_alerts )
union all
SELECT a.eventid FROM tmp_alerts a WHERE p_eventid IN ( SELECT eventid FROM tmp_alerts )) aa);

# 合并多个agent无法访问的告警
    INSERT INTO all_alerts (sendto, subject)
    SELECT
    sendto,
    concat( CASE WHEN STATUS = '0' THEN '【恢复OK】有大量zabbix客户端恢复访问! ' ELSE '【故障PROBLEM】有大量zabbix客户端无法访问,请检查网络! ' END, '主机数量:', cnt ) 
    FROM
        (
        SELECT
            sendto,
            STATUS,
            count( 1 ) cnt 
        FROM
            tmp_alerts 
        WHERE
            `subject` like '%Zabbix agent 无法访问%' 
        GROUP BY
            sendto,
        STATUS 
        HAVING
            count( 1 ) > 2 
        ) aa;

DELETE from tmp_alerts where eventid in(select eventid from (   
    SELECT EVENTID 
    FROM
        tmp_alerts a 
    WHERE
        EXISTS (
        SELECT
            1 
        FROM
            ( SELECT sendto, STATUS FROM tmp_alerts WHERE `subject` like '%Zabbix agent 无法访问%'  GROUP BY sendto, STATUS HAVING count( 1 ) > 2 ) b 
        WHERE
            a.sendto = b.sendto 
            AND a.STATUS = b.STATUS 
        ) 
        AND a.`subject` like '%Zabbix agent 无法访问%' ) aa);

    # 合并相同主机多个告警

    insert into all_alerts (sendto, subject, eventtime)
    select sendto,subject,substr(`subject`,instr(subject,'告警时间:')+5) eventtime from (
    select sendto,
                    concat(case  when status='0' then '【恢复OK】: ' else '【故障PROBLEM】: ' end ,triggername,
                    ' 主机:',hostname,
                    ' 问题详情:',itemvalue,
                    ' 告警时间:',eventtime) subject
    from (select sendto,group_concat(triggername order by eventtime) triggername,hostname,min(itemvalue) itemvalue,min(eventtime) eventtime,status from (
                SELECT 
                        a.sendto,
                        substring_index(subject,'#',1) triggername,
                        substring_index(substring_index(subject,'#',2),'#',-1) hostname,
                        substring_index(substring_index(subject,'#',4),'#',-1) itemvalue,
                        substring_index(subject,'#',-1) eventtime,
                        a.STATUS 
                    FROM
                        tmp_alerts a) dd
        group by sendto,hostname,status
      having count(1) >1
        order by eventtime) aa ) bb;

 # 删除已经合并的相同主机多个告警

    delete from tmp_alerts where eventid in(
select eventid from (
select eventid from tmp_alerts a where EXISTS(select 1 from (
select group_concat(eventid) eventid,sendto,group_concat(triggername) triggername,hostname,min(itemvalue) itemvalue,min(eventtime) eventtime,status from (
                SELECT 
                        a.sendto,
                        eventid,
                        substring_index(subject,'#',1) triggername,
                        substring_index(substring_index(subject,'#',2),'#',-1) hostname,
                        substring_index(substring_index(subject,'#',4),'#',-1) itemvalue,
                        substring_index(subject,'#',-1) eventtime,
                        a.STATUS 
                    FROM
                        tmp_alerts a) dd
        group by sendto,hostname,status
      having count(1) >1
        order by eventtime) aa where a.sendto=aa.sendto and a.status=aa.status and instr(aa.eventid,a.eventid) > 0)) mm);


# 合并剩余的多个主机相同告警

insert into all_alerts (sendto, subject, eventtime)
select sendto,subject,substr(`subject`,instr(subject,'告警时间:')+5) eventtime from (
    select sendto,
                    concat(case  when status='0' then '【恢复OK】: ' else '【故障PROBLEM】: ' end ,triggername,
                    ' 主机:',hostname,case when cnt='1' then '' else concat(' 等',cnt,'台主机') end,
                    ' 问题详情:',itemvalue,
                    ' 告警时间:',eventtime) subject
    from (select sendto,triggername,max(hostname) hostname,min(itemvalue) itemvalue,min(eventtime) eventtime,status,count(1) cnt from (
                SELECT 
                        a.sendto,
                        substring_index(subject,'#',1) triggername,
                        substring_index(substring_index(subject,'#',2),'#',-1) hostname,
                        substring_index(substring_index(subject,'#',4),'#',-1) itemvalue,
                        substring_index(subject,'#',-1) eventtime,
                        a.STATUS 
                    FROM
                        tmp_alerts a) dd
        group by sendto,triggername,status 
        order by eventtime) aa ) bb;


    # 归档已发送短信数据
    insert into all_alerts_history
    select * from all_alerts;

    commit;

END $$

主机上通过python脚本按每分钟定期调用resize_alerts过程,遍历数据然后写入到短信库内

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2019-12-10
# @Author  : Xiong Bin (i@xbdba.com)
# @Link    : http://www.xbdba.com
# @Name    : sendsms.py

import pymysql
import pymssql

mysql_conn = pymysql.connect("127.0.0.1","zabbix","zabbix","zabbix" )
mycursor = mysql_conn.cursor()

ms_conn = pymssql.connect(IP, USERNAME, PASSWORD, DATABASENAME)
mscursor = ms_conn.cursor()

mycursor.callproc('resize_alerts')
QurySql ="""select sendto,subject from all_alerts order by sendto,eventtime"""
mycursor.execute(QurySql)
data = mycursor.fetchall()

sql = "insert into 短信TABLE values (%s, %s, 'Zabbix', 0)"
for row in data:
        mscursor.execute(sql, (row[0], unicode(row[1], 'utf-8')))
ms_conn.commit()

定时任务

*/1  *  *  *  *  /usr/lib/zabbix/alertscripts/sendsms.py 2>&1