3.9 Zabbix-Server对数据的存储
无论监控系统采用何种架构,是否使用代理,对于Zabbix-Server来说,数据存储都是一个非常大的挑战。通常来说,使用分布式架构,并让客户端处于主动模式,会提高Zabbix-Server的吞吐性能。关于构建分布式架构和客户端主动模式的一些细节问题,请读者阅读本书第9章。本节主要讲述如何对Zabbix-Server端数据的存储进行有效优化,这里采用表分区的方式对数据库进行优化。除此之外,还可以采用分布式数据库,以及Zabbix 4.0版本所支持的Elasticsearch搜索引擎数据库,以提高数据库的性能。
如果读者对本节内容不是特别感兴趣,只是测试验证功能,则可略过此节,直接阅读后续的3.10节。
由于整个章节的数据库环境均为MySQL,故表分区的方式仅针对MySQL。
3.9.1 监控数据的存储
通过前面的学习,我们知道Zabbix-Server将采集到的数据存储在数据库中,也了解到数据存储的大小与每秒处理的数据量有关,所以数据存储取决于以下两个因素。
· New values per second(每秒处理的数据值):Zabbix-Server从客户端每秒接收到的数据。
· Housekeeper的设置:对超过存储期限数据的删除策略。
Zabbix-Server将采集到的数据主要存储于历史表和趋势表中,对于不同类型的数据,则分别存储于不同的表中,如表3-7所示。
表3-7 数据存储表
历史表存储在每个采集周期内收集到的全部历史数据;趋势表存储经过聚合后的历史数据,将每个采集周期的数据合并成每小时的最小值、最大值和平均值。比如监控项每秒采集1个数据,1小时则存储历史数据60条记录,存储趋势数据1条记录。
历史表的表结构如图3-30所示,可以看到每个表都有“时间-值”的字段,即监控数据存储的是一个时间序列值(请参考本书第1章探讨的时序数据库相关知识)。
图3-30 历史表的表结构
在趋势表中都包含最小值(value_min)、最大值(value_max)和平均值(value_avg),图3-31展示了其表结构。
图3-31 趋势表的表结构
对于超过存储期限的数据,Zabbix-Server使用Housekeeper进程进行数据清理。从源码中我们可以看到,其删除机制通过housekeeping_history_and_trends函数实现,其核心删除SQL语句为“delete from %s where itemid=" ZBX_FS_UI64 " and clock<%d”,具体代码如下:
shell# cat src/zabbix_server/housekeeper/housekeeper.c static int housekeeping_history_and_trends(int now) { /*省略非关键代码*/ for(i = 0; i < rule->delete_queue.values_num; i++) { /*省略非关键代码*/ /*从表中删除大于clock(指定时间区域)的监控项(item)*/ rc = DBexecute("delete from %s where itemid=" ZBX_FS_UI64 " and clock<%d", rule->table, item_record->itemid, item_record->min_clock); if(ZBX_DB_OK < rc) deleted += rc; } /*省略非关键代码*/ } /*省略非关键代码*/ return deleted; }
通过阅读源码,我们知道了Zabbix-Server对数据的清理是通过SQL语句DELETE来执行删除动作的。随着数据存储的增多,其执行效率会显著下降。有经验的读者都知道,在一个有千万条、亿条记录的表中执行一条DELETE SQL语句,少则数秒,多则数十分钟才能够执行完毕。所以,Housekeeper程序执行的SQL语句会严重影响数据库的性能,从而导致数据库成为整个监控系统的性能瓶颈。
对于记录条数很多的表,在SQL优化方案中最常见的方式有横向扩展和纵向扩展。在这两种方式中,一是用足够好的硬件;二是将数据进行分布式存储。而分表可以看作分布式的一种,即按一定的规则将数据划分区间,从而避免全表扫描所带来的性能损失,这样就可以最大程度地提高性能。在这里我们就采用对表区间进行划分的方式来优化数据库。
下面来看一个在线的Zabbix数据库中历史表数据量的大小(见图3-32)。
图3-32 数据大小和数量统计
mysql> select table_name, (data_length+index_length)/1024/1024 as total_mb, table_rows from information_schema.tables where table_schema='zabbix';
在history_uint表中,数据达到3.8亿条,如果在这个表中执行DELETE的SQL语句,其速度是可想而知的。
在了解了对Zabbix数据库进行分表的必要性之后,下面介绍如何划分表的区间。首先,对history表进行区间划分,这里分区的标准是按天进行划分的,分区后,会提高查询数据的效率。如图3-33所示,基于时间片段,直接找到数据所在的分区,而不用扫描全表进行数据匹配。
图3-33 有无表分区查询数据的方式
有些读者肯定会想到,既然采用了表分区,那么对过期的数据是否可以通过表分区的规则方法直接删除,而不再使用效率低下的Housekeeper?答案是可以的,我们可以直接将Housekeeper关闭,使其不再执行DELETE这种落后的方式来删除数据。在Web页面中,依次找到Administration→General→Housekeeper,去掉History和Trends选项的勾选状态,即可关闭History和Trends的Housekeeper功能,如图3-34所示。
图3-34 历史数据和趋势数据的Housekeeper开关
3.9.2 MySQL表分区实例
1.表分区脚本代码实现
以下脚本实现对数据库zabbix的表分区:
shell# vim /usr/sbin/partitiontables_zabbix.sh #! /bin/bash # author: itnihao # date: 2018-06-06 # funtion: create parition for zabbix MySQL # repo: https://github.com/zabbix-book/partitiontables_zabbix #配置环境变量,读者可根据实际情况修改 ZABBIX USER="zabbix" ZABBIX PWD="zabbix" ZABBIX DB="zabbix" ZABBIX PORT="3306" ZABBIX HOST="127.0.0.1" MYSQL BIN="mysql" #历史数据保留时间,单位是“天”,读者可根据实际情况修改 HISTORY DAYS=30 #趋势数据保留时间,单位是“月”,读者可根据实际情况修改 TREND MONTHS=12 HISTORY_TABLE="history history_log history_str history_text history_uint" TREND_TABLE="trends trends_uint" #MySQL连接命令,如果读者的MySQL为非标准安装路径,可增加--socket=参数 MYSQL_CMD=$(echo ${MYSQL_BIN} -u${ZABBIX_USER} -p${ZABBIX_PWD} -P${ZABBIX_PORT} -h${ZABBIX_HOST} ${ZABBIX_DB}) function create_partitions_history(){ #给历史表创建分区 for PARTITIONS_CREATE_EVERY_DAY in $(date +"%Y%m%d")$(date+"%Y%m%d" --date='1 days')$(date +"%Y%m%d" --date='2 days')$(date+"%Y%m%d" --date='3 days') $(date +"%Y%m%d" --date='4 days')$(date+"%Y%m%d" --date='5 days')$(date +"%Y%m%d" --date='6 days')$(date+"%Y%m%d" --date='7 days') do TIME_PARTITIONS=$(date -d "$(echo ${PARTITIONS_CREATE_EVERY_DAY}23:59:59)" +%s) for TABLE_NAME in ${HISTORY_TABLE} do SQL1=$(echo "show create table ${TABLE_NAME}; ") RET1=$(${MYSQL_CMD} -e "${SQL1}"|grep "PARTITION BY RANGE"|wc -l) #表结构中的表分区不存在,创建将表修改为支持分区的表结构 if [ "${RET1}" == "0" ]; then SQL2=$(echo "ALTER TABLE $TABLE_NAME PARTITION BY RANGE(clock)(PARTITION p${PARTITIONS_CREATE_EVERY_DAY} VALUES LESS THAN(${TIME_PARTITIONS})); ") RET2=$(${MYSQL_CMD} -e "${SQL2}") if [ "${RET2}" ! = "" ]; then echo ${RET2} echo "${SQL2}" else printf "table %-12s create partitions p${PARTITIONS_CREATE_EVERY_DAY}\n" ${TABLE_NAME} fi continue fi #表结构中的表分区已经存在,则可以创建新的分区 if [ "${RET1}" ! = "0" ]; then SQL3=$(echo "show create table ${TABLE_NAME}; ") RET3=$(${MYSQL_CMD} -e "${SQL3}"|grep "p${PARTITIONS_CREATE_EVERY_DAY}"|wc -l) if["${RET3}"=="0"];then TIME_PARTITIONS=$(date -d "$(echo ${PARTITIONS_CREATE_EVERY_DAY} 23:59:59)" +%s) SQL4=$(echo"ALTERTABLE$TABLE_NAMEADDPARTITION(PARTITIONp${PARTITIONS_CREATE_EVERY_DAY}VALUESLESSTHAN(${TIME_PARTITIONS}));") RET4=$(${MYSQL_CMD} -e "${SQL4}") if["${RET4}"!=""];then echo ${RET4} echo "${SQL4}" else printf "table %-12s create partitions p${PARTITIONS_CREATE_EVERY_DAY}\n" ${TABLE_NAME} fi fi fi done done } function drop_partitions_history(){ #删除历史表分区 for PARTITIONS_DELETE_DAYS_AGO in $(date +"%Y%m%d" --date="${HISTORY_DAYS} days ago") do for TABLE_NAME in ${HISTORY_TABLE} do SQL=$(echo-e"showcreatetable${TABLE_NAME};") RET=$(${MYSQL_CMD} -e "${SQL}"|grep "p${PARTITIONS_DELETE_DAYS_AGO}"|wc -l) if["${RET}"=="1"];then SQL=$(echo"ALTERTABLE${TABLE_NAME}DROPPARTITION p${PARTITIONS_DELETE_DAYS_AGO};") RET=$(${MYSQL_CMD} -e "${SQL}") if["${RET}"!=""];then echo ${RET} echo "${SQL}" else printf "table %-12s drop partitions p${PARTITIONS_DELETE_DAYS_AGO}\n" ${TABLE_NAME} fi fi done done } function create_partitions_trend(){ #创建趋势表分区 for PARTITIONS_CREATE_EVERY_MONTHS in $(date +"%Y%m")$(date +"%Y%m"--date='1 months') $(date +"%Y%m" --date='2 months') $(date +"%Y%m" --date='3 months')$(date +"%Y%m" --date='4 months')$(date +"%Y%m" --date='5 months') do TIME_PARTITIONS=$(date -d "$(echo ${PARTITIONS_CREATE_EVERY_MONTHS}01 00:00:00)" +%s) for TABLE_NAME in ${TREND_TABLE} do SQL1=$(echo "show create table ${TABLE_NAME}; ") RET1=$(${MYSQL_CMD}-e"${SQL1}"|grep "PARTITION BY RANGE"|wc -l) #表结构中的表分区不存在,创建将表修改为支持分区的表结构 if [ "${RET1}" == "0" ]; then SQL2=$(echo "ALTER TABLE $TABLE_NAME PARTITION BY RANGE(clock)(PARTITION p${PARTITIONS_CREATE_EVERY_MONTHS} VALUES LESS THAN(${TIME_PARTITIONS})); ") RET2=$(${MYSQL_CMD} -e "${SQL2}") if [ "${RET2}" ! = "" ]; then echo ${RET2} echo "${SQL2}" else printf "table %-12s create partitions p${PARTITIONS_CREATE_EVERY_MONTHS}\n" ${TABLE_NAME} fi continue fi #表结构中的表分区已经存在,则可以创建新的分区 if [ "${RET1}" ! = "0" ]; then SQL3=$(echo "show create table ${TABLE_NAME}; ") RET3=$(${MYSQL_CMD} -e "${SQL3}"|grep "p${PARTITIONS_CREATE_EVERY_MONTHS}"|wc -l) if [ "${RET3}" == "0" ]; then SQL4=$(echo "ALTER TABLE ${TABLE_NAME} ADD PARTITION(PARTITION p${PARTITIONS_CREATE_EVERY_MONTHS} VALUES LESS THAN(${TIME_PARTITIONS})); ") RET4=$(${MYSQL_CMD} -e "${SQL4}") if [ "${RET4}" ! = "" ]; then echo ${RET4} echo "${SQL4}" else printf "table %-12s create partitions p${PARTITIONS_CREATE_EVERY_MONTHS}\n" ${TABLE_NAME} fi fi fi done done } function drop_partitions_trend(){ #删除趋势表分区 for PARTITIONS_DELETE_MONTHS_AGO in $(date +"%Y%m" --date="${TREND_MONTHS} months ago") do for TABLE_NAME in ${TREND_TABLE} do SQL=$(echo "show create table ${TABLE_NAME}; ") RET=$(${MYSQL_CMD} -e "${SQL}"|grep "p${PARTITIONS_DELETE_MONTHS_AGO}"|wc -l) if [ "${RET}" == "1" ]; then SQL=$(echo "ALTER TABLE ${TABLE_NAME} DROP PARTITION p${PARTITIONS_DELETE_MONTHS_AGO}; ") RET=$(${MYSQL_CMD} -e "${SQL}") if [ "${RET}" ! = "" ]; then echo ${RET} echo "${SQL}" else printf "table %-12s drop partitions p${PARTITIONS_DELETE_MONTHS_AGO}\n" ${TABLE_NAME} fi fi done done } function main(){ create_partitions_history create_partitions_trend drop_partitions_history drop_partitions_trend } main
注意:该脚本在Zabbix 3.0和4.0版本中均测试通过,适用于已经安装过Zabbix但未进行表分区的数据库;对于已经在线运行的环境,Zabbix数据库中的表数据量会比较大,执行此脚本的时间会非常长,笔者测试时遇到过长达10多个小时表分区仍未执行完毕的情况,故建议读者先清空history、trends表中数据(注意,历史记录会全部被清空),再执行。清空语句如下:
mysql> use zabbix; mysql> truncate table history; mysql> optimize table history; mysql> truncate table history_str; mysql> optimize table history_str; mysql> truncate table history_uint; mysql> optimize table history_uint; mysql> truncate table history_log; mysql> optimize table history_log; mysql> truncate table history_text; mysql> optimize table history_text; mysql> truncate table trends; mysql> optimize table trends; mysql> truncate table trends_uint; mysql> optimize table trends_uint;
2.运行表分区脚本
为了防止网络中断后引起脚本运行中断而造成数据库故障,我们应该选用screen后台执行的方法。如果没有screen程序,请先安装(运维人员要处处持有谨慎态度)。
shell# screen -R zabbix shell# sh partitiontables_zabbix.sh table history create partitions p20180716 table history_log create partitions p20180716 table history_str create partitions p20180716 table history_text create partitions p20180716 table history_uint create partitions p20180716 table history create partitions p20180717 table history_log create partitions p20180717 table history_str create partitions p20180717 table history_text create partitions p20180717 table history_uint create partitions p20180717 #中间省略部分输出内容 table trends create partitions p201807 table trends_uint create partitions p201807 table trends create partitions p201808 table trends_uint create partitions p201808
退出screen,脚本将在后台执行。方法如下:
按“Ctrl+A”组合键之后,再按“Ctrl+D”组合键。
进入screen,可以查看后台运行的任务:
shell# screen -R zabbix
注意:严禁在脚本运行的过程中中断脚本的运行,否则可能造成表的损坏。脚本运行完毕后,请读者手动添加定时任务(用于每天创建新的表空间),每天凌晨运行。
shell# crontab -e 1 0 * * * /usr/sbin/partitiontables_zabbix.sh Shell# chmod 700 /usr/sbin/partitiontables_zabbix.sh
验证表分区是否成功,可以查看history表结构,输出如下:
MariaDB [zabbix]> show create table history\G; *************************** 1. row *************************** Table: history Create Table: CREATE TABLE `history`( `itemid` bigint(20)unsigned NOT NULL, `clock` int(11)NOT NULL DEFAULT '0', `value` double(16,4)NOT NULL DEFAULT '0.0000', `ns` int(11)NOT NULL DEFAULT '0', KEY`history_1`(`itemid`,`clock`) )ENGINE=InnoDB DEFAULT CHARSET=utf8 /*!50100 PARTITION BY RANGE ( clock) (PARTITION p20180716 VALUES LESS THAN (1531756799) ENGINE = InnoDB, PARTITION p20180717 VALUES LESS THAN (1531843199) ENGINE = InnoDB, PARTITION p20180718 VALUES LESS THAN (1531929599) ENGINE = InnoDB, PARTITION p20180719 VALUES LESS THAN (1532015999) ENGINE = InnoDB, PARTITION p20180720 VALUES LESS THAN (1532102399) ENGINE = InnoDB, PARTITION p20180721 VALUES LESS THAN (1532188799) ENGINE = InnoDB, PARTITION p20180722 VALUES LESS THAN (1532275199) ENGINE = InnoDB, PARTITION p20180723 VALUES LESS THAN (1532361599) ENGINE = InnoDB)*/ /*粗体字部分为表分区*/ 1 row in set(0.00 sec) ERROR: No query specified MariaDB [zabbix]>
在上面的分区中,时间戳使用的是数值,若需将特定日期转换为时间戳,则可以通过Linux下的date命令直接进行转换。
在表分区中,使用的是时间戳格式进行日期区间划分的,如需将时间转换为时间戳数值格式,则可以使用如下命令进行转换:
shell# date -d "2018-07-16" +%s
1531756799
将时间戳数值转换为时间格式,命令如下:
shell# date -d @1531756799 "+%Y-%m-%d"
2018-07-16
下面的SQL查询语句用于查询指定时间段的数据,以验证数据是否写入。
mysql> select count(*) from history where clock > 1531670399 and clock<1531756799; +----------+ | count(*)| +----------+ | 1356 | +----------+ 1 row in set(0.13 sec) mysql> select count(*) from history_uint where clock > 1531670399 and clock <1531756799; +----------+ | count(*)| +----------+ | 6302 | +----------+ 1 row in set(0.05 sec)
至此,表分区已经完成,这对提高数据库的性能具有重要的作用。对MySQL的优化还可以通过调整其本身的配置参数来进行。
若读者想了解更多关于表分区的知识,可以访问以下地址: