通过Zabbix监控业务数据

为了事后分析服务质量,我们的云认证系统将商户访问记录保存到了数据库中(MySQL)。在日志中我们记录了商户每次访问的返回结果,耗费时间等信息。通过这些信息我们可以分析发现服务什么时候性能比较低,什么时候处理效果比较差。

之前通过写脚本,每小时对这些信息进行一次统计,并根据情况(如错误比例过高等)发送邮件通知给相关人员。但是这存在两个问题:

  • 响应不及时:因为一小时才统计一次,所以滞后效应太明显,经常问题已经发生很久报警才发送出来;
  • 报警条件更新不方便:报警条件只能通过脚本编写人员实现,修改起来不方便。

鉴于此,一直想对这个报警体系进行改造,最后选择通过Zabbix实现这个功能。

1. 背景介绍

Zabbix这里就不介绍了,具体怎么操作网上很多教程。这里会介绍我在使用中碰到的问题,以及对应的解决办法,不涉及具体操作。

这次改造主要对返回值进行判断。简单介绍一下数据:每条记录都有一个返回值字段ERROR_CODE,其可能的值为:

  • 100 服务处理成功;
  • 441 服务处理失败(服务失败,但是如果比例过高,可能有隐含的问题发生。例如商户提交的信息有问题,编码错误导致问题等);
  • 442 有异常发生;
  • 443 有超时发生。

这次希望达到的目标为:
当成功比例过低(例如低于70%)的时候,触发报警;另外检查频度要提高(例如1分钟检查一次,不再1小时一次),这样可以快速发现问题。但是具体的报警条件还要考虑访问量等要素:例如晚上访问量很少,经常1分钟只有几条,这时很容易出发70%这个警戒线,所以还需要考虑过滤这种情况。

2. 使用Zabbix

定下了目标,最初感觉实现起来比较容易,所以通过修改Zabbix agent,创建了几个Item,分别用来采集每分钟的编码100、441、442、443的记录数量、以及成功率,Zabbix中创建对应的Item。然后创建Trigger,判断成功率是否小于70即可。

但是这样使用起来发现有问题:从数据库中获取上述记录数以及成功率的脚本执行速度较慢,偶尔会出现执行超时的问题,这会导致数据采集中断(Zabbix无法采集到数据),影响判断效果。虽然后来发现脚本本身优化有问题,经过优化速度有所提升,但是这种长时间的操作理应使用Zabbix Agent Active模式来处理。最后决定切换到Zabbix agent(active)

3. 使用Zabbix Agent active

Zabbix agent和Zabbix agent(active)的区别在于后者会主动从Server拉取要监控的项目,成功采集之后提交给Server,相当于两步操作;而前者则是Server直接发命令给Agent,并等待返回,相当于一步操作。Active模式就是为了解决部分采集项耗时时间长而设计的。

切换到Active Agent比较简单,只要在/etc/zabbix/zabbix_agentd.conf中指定配置即可:

1
2
3
4
# 指定Zabbix服务器地址
ServerActive=192.168.251.11
# 配置当前主机的Hostname,需要与Zabbix中配置的主机名对应,否则会无法获取监控项
Hostname=192.168.61.53

然后在Zabbix中把对应Item项改成Zabbix agent(active)即可。

这样配置下来,解决了上面的耗时过长问题。但是实际使用中又发现了新问题。这个问题跟我们采用的报警判断条件有关。所以先介绍一下报警条件的思路:要触发警报,需要满足以下条件:

  1. 最近5分钟内成功率低于70%(不是只判断当前1分钟,因为考虑单分钟可能有采样不够的问题);
  2. 最近5分钟访问总量不低于50(如果过低,因为采样条数有限,所以可能误判)。

要满足上面这种判断,上面的数据采集方式有很大的问题:

  1. 数据不是同时采集的:Zabbix无法驱动Agent同时采集一批Item,Agent只能一个一个的进行采集。这就导致几个Item并不是同一时期的数据。例如有可能这样:错误码100的信息是0点整采集的,而错误码441的信息可能是0点0分10秒采集的,错误码442的信息采集的时间可能是0点0分30秒。拿不同时间点采集的数据去计算成功率是不正确的;
  2. 如果单纯的采用每分钟的成功率进行平均(avg),也是不准确的,因为每分钟的访问量是不一样的。正确的算法是成功率与访问量进行加权平均。但是上述采集方法显然满足不了这种计算要求。

4. 使用Zabbix Trapper

经过阅读官方文档,发现可以使用Zabbix Trapper这种模式来解决问题。

Trapper类型的Item一般翻译为捕捉器,可能理解起来有些困难,其核心的用法是Zabbix创建此种类型的Item,但是不会主动去进行数据采集。而不管是Agent、Agent(active)实际上都是Zabbix在主动收集数据,只是主动的有可能是Zabbix Server(对应Zabbix Agent类型)或者Zabbix Agent(对应Zabbix Agent active类型)。但是对Zabbix Trapper类型,Zabbix只是准备好了数据存放的地方,等待数据的到来。

使用Trapper类型,数据采集的工作由使用者来实现。数据采集完成后,可以通过zabbix_sender发送到Zabbix Server中。然后Server就可以对各种Trigger进行判断了。也就是说,Trapper类型将数据采集功能交给了使用者。

我采用的方法是通过cron定时执行(每分钟)一个脚本,脚本负责采集数据并发送给Zabbix。脚本内容大体如下,可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 获取当前时间
DATE_NOW=`date +'%Y-%m-%d %H:%M'`
# 获取上次执行时间
DATE_LAST=`cat /tmp/last.access.txt`
if [ -z "$DATE_LAST" ]; then
# 如果没有上次执行时间,上次执行时间假定为5分钟前
DATE_LAST=`date -d'-5 minute' +'%Y-%m-%d %H:%M'`
fi
# 保存上次执行时间
echo $DATE_NOW > /tmp/last.access.txt
# 获取统计信息
sql="select count(id) as error_all, count(if(error_code=100,id,null)) as error_100,count(if(error_code=441,id,null)) as error_441,count(if(error_code=442, id, null)) as error_442,coun
t(if(error_code=443,id,null)) as error_443,count(if(error_code=100,id,null))/count(id)*100 as percent from xwf_logs.uid_bankcard_auth_no_otp_log where ACCESS_TIME<'${DATE_NOW}' and AC
CESS_TIME>='${DATE_LAST}'"
result=`${MYSQL_CONN_READ} -e "${sql}"`
error_all=`echo $result | awk '{print $7}'`
error_100=`echo $result | awk '{print $8}'`
error_441=`echo $result | awk '{print $9}'`
error_442=`echo $result | awk '{print $10}'`
error_443=`echo $result | awk '{print $11}'`
error_per=`echo $result | awk '{print $12}'`
# 发送到Zabbix中
echo Data was sent at: $DATE_NOW
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_ALL] -o $error_all
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_100] -o $error_100
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_441] -o $error_441
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_442] -o $error_442
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_443] -o $error_443
zabbix_sender -z $ZABBIX_SERVER -p $ZABBIX_PORT -s $HOST_NAME -k uid.logs[ERROR_CODE_100_PERCENT] -o $error_per
echo Data was sent successfully.
echo ----------------------------------------------

上述代码中,使用一条语句计算所有的指标,这样及减轻了MySQL的压力,也保证了数据都是同一个是简单采集出来的。另外,通过使用临时文件/tmp/last.access.txt保存上次数据采集的时间点,保证了即使定时任务触发有所变化,也不会导致部分数据没有采集到。

Trigger中的条件设置成这样:

也就是最近5分钟内访问量>50并且成功率低于70%就触发报警。

5. 解决误报

5.1 数据提交顺序

配置完成之后,运行一段时间发现经常误报:明明检查数据不符合条件,但是就是会触发一个状态OK的报警通知,持续时间(Duration)为0,没有对应的PROBLEM状态的报警。这真是非常奇怪。

这个问题耗费了很长时间,经过仔细研读Zabbix的官方手册,发现里面有这么一段 Triggers

Trigger status (the expression) is recalculated every time Zabbix server receives a new value that is part of the expression.
也就是说,当Trigger的表达式依赖于多个Item(例如上面的依赖于ALL和100两个Item)的时候,当每一个Item的新值到来的时候,Zabbix都会对Trigger进行重新计算。Zabbix不会等到多个数据都齐全之后再进行判断。

返回头看上面的脚本,先提交了ALL,然后提交了100。这样Zabbix先拿到了新的ALL值,然后就会拿最近的5分钟的ERROR_CODE_100的值(这时实际上只有4个,最新的数据服务器还没有收到),和最近5分钟的ERROR_CODE_ALL的值(5个)一起计算成功率,显然这样很容易触发低于70%这个条件。所以Zabbix就认为这个Trigger处于PROBLEM状态。但是当100的数据也收到之后,Zabbix再次重新计算,又发现状态实际上是OK的。所以就出现上面那种反复出现OK报警的情况。但是为什么没有对应的PROBLEM报警,可能是Zabbix本身进行了限制,不发送这种Duration很短的PRBLEM状态。有时间看看源码确认一下。

最先想到的解决办法是将脚本中的数据提交顺序换一下,将100的数据先提交,然后是ALL。但是实际上这样也是有问题的,它会导致Trigger从PRBLEM状态错误的变成OK状态(5个100数据和4个ALL数据计算成功率)。所以最后的解决办法是这样:

就是判断最近30秒之内ALL和100的数据有没有,没有就认为数据不全,认为状态正常。只有当两者数据都有的情况下,才计算比例是否小于70%。

5.2 Recovery条件

仔细分析一下就会发现,上面的判断条件在Trigger状态为PROBLEM的时候还是有问题的:当ALL或者100的数据没有收到的时候,不进行计算百分比,上面的Express被判断没有问题,Trigger就会被判定为OK。但是当数据齐全之后,计算百分比之后发现还是有问题。这样就导致了状态反复切换,频繁的发送PRBLEM、OK的报警信息。

这个问题的解决办法是增加Recovery Expression到Trigger中。这个Expression的作用是当Trigger处于PROBLEM状态的时候,即使判断Expression状态没有问题(FALSE),还需要判断Recovery Expression装个表达式,只有它判断为TRUE的时候才解除报警状态。

最后增加Recovery Expression,内容如下:

在这个表达式中,会判断成功率高于等于70%才认为状态恢复。

6. 心得

  1. Zabbix提供的基本功能能够满足大部分的需求,同时它还提供了很好的扩展方法。例如这里用到的Trapper,可以让用户自由的采集数据,而Zabbix专心干好它的Trigger功能,各司其职,配合高效。
  2. 官方文档作为权威资料一定要看,很多疑难问题,可以在里面找到权威解答。

附录A. 参考资料

热评文章