本文原创作者鲍光亚,京东商城基础平台部软件开发工程师,经作者同意发表于本人博客,如需转载需经本人同意。
一、 前言
我部门对数据库的监控使用的是开源的Zabbix系统,目前监控了上万台主机。本文旨在通过分析Zabbix系统server端的数据结构和并行计算的实现方法,尝试探寻Zabbix系统server端的潜在扩展能力,同时希望有助于在实际应用过程中进一步优化运行效率和稳定性。
Zabbix系统采用server-proxy-agent架构,其server端的主要功能是收集监控数据并基于所收集的数据触发报警动作。在实际应用中,zabbix有可能会监控10000台主机(host,由hostid唯一标识),如果每台主机设置50个监控指标(item,由itemid唯一标识),并且每分钟收集一次数据,则一共有50万个item,每秒钟需要接收并处理8333项数据(value),即vps(values per second)为8333。如果有三分之一的item设置了报警触发器(trigger,由triggerid唯一标识),则共有17万个trigger。
在以上情境中,为了保证监控的有效性和及时性,zabbix接收到每个value后需要立即在50万个item中找到正确的item,并获取该item的前一个值(previous value,last(),以便计算增量),或者计算前5分钟内的平均值(avg(5m)),以便根据触发器表达式(trigger expression,由functionid唯一标识)判断是否应该触发报警事件(event,由eventid唯一标识)。同时如果item返回值类型为数字型,还需要计算该item在一个小时内的平均值(value_avg)、最大值(value_max)、最小值(value_min)。按照上面的vps数据,zabbix至少需要每秒钟搜索8333*500000次。此外,item和trigger并不是静态数据,用户随时可能会增加、修改、删除、禁用(disable)、启用(enable)某些item和trigger,zabbix需要在处理value时查询该item和trigger最新的状态。如何在一秒时间内完成如此大量的操作,zabbix给出的方案是: 哈希表。
在并行计算的软件方面,由于Zabbix系统监控的各个主机之间是相对独立的,无论在任务还是在数据方面都非常便于计算的并行化。服务器硬件方面,我们实际使用的服务器结构是2*8处理器+三级缓存+16G*8内存+SSD硬盘+10Gbps网卡(数据库、zabbix server和web服务共用)。Zabbix的并行计算主要采用的是共享内存模式。
二、 Zabbix中的哈希表种类
Zabbix使用的哈希表是链式哈希表,主要有以下五类(都是在共享内存中分配空间):
1. Valuecache
Valuecache中包含两个哈希表vc_cache->items(itemid作为键值进行哈希)和vc_cache->strpool(字符串作为键值),用于存储收集到的values(包括数字型和字符串型),每个item占用一个slot,每个槽位都是一个链表,链表节点存储实际需要的信息。
Valuecache的哈希表在服务启动时创建,服务退出时销毁,初始槽数为1009(1000之后的第一个素数),随着表中元素数量的增加,槽数也会按照一定的规则增多。Valuecache可使用的最大空间由配置文件中的ValueCacheSize参数控制,允许的范围是128K-64G。
2. Dbcache
Dbcache中的cache->trends哈希表,用于缓存trends表(每个item的小时平均值、最大值、最小值)的数据。Zabbix server的history_syncer进程会持续接收来自agent或者proxy的数据后会将其加载到缓存中,同时更新cache->trends哈希表。该哈希表中的元素是ZBX_DC_TREND结构体。
Cache->trends表中的数据时间超过整点时会被flush到数据库中,例如10点之后会将9-10点之间的数据flush到数据库中。
Cache->trends哈希表在服务启动时创建,初始槽数与vc_cache->items相同,为1009(1000之后的第一个素数)。Cache->trends哈希表的最大可用空间由配置文件中的TrendCacheSize参数控制,允许的范围是128K-2G。
3. Dbconfig
Dbconfig缓存中存储了多个与监控有关的配置信息的哈希表,包括config->hosts、config->items、config->functions、config->triggers等等。配置信息哈希表的键值包括hostid、itemid、functionid、triggerid、triggerdepid、expressionid、globalmacroid、hostmacroid、hosttemplateid、interfaceid、host_inventory等,其中数量最多的往往是itemid、functionid和triggerid,会在数十万级别(以10000个host计)。
以config->items为例,该哈希表的元素是ZBX_DC_ITEM结构体。Config->items中的数据是从数据库中查询获得的,zabbix server的configuration syncer进程会周期性地从数据库同步数据到缓存中。
Dbconfig缓存中的其他哈希表与config->items表类似,都是从数据库同步数据,都是在服务启动时创建,初始槽数都是1009,并随着数据量的增加动态扩展。整个dbconfig缓存可用空间大小由CacheSize参数决定,取值范围为128K-8G。
4. Strpool
此处的strpool与vc_cache->strpool是相互独立的两个哈希表。此Strpool缓存用于存储配置信息相关的字符串值,它与dbconfig共同分享CacheSize的空间(strpool占15%)。Strpool存储的字符串包括host name、item key、item delay_flex、snmp community、snmp securityname、snmp passphrase、logitem format等数据。Zabbix需要使用host name等字符串时,会首先在strpool中查找。
Strpool的哈希表初始槽数为1009。键值是字符串本身,哈希值是对字符串调用哈希函数的返回值。
5. 其他
除了以上哈希表,还有snmpidx、vmware service等哈希表。
三、 哈希表的实现
下面以config->items哈希表为例,说明zabbix中哈希表的实现方法。
1. 数据结构定义
Zabbix采用的是链式哈希表,哈希表中的每个slot都是一个链表。具体的数据结构定义如下:
2.
槽数取值及负载因子
Zabbix的哈希过程是先调用哈希函数计算键值对应的哈希值,然后用取余法确定槽位号。因此,取余计算时的除数就是槽位数,该数值取素数(因为素数可以做到最大程度上均匀散列)。在config->items哈希表中,槽位数的初始值是1009,随着数据量的增加,当负载因子(元素数/槽数)达到0.8时,会扩充槽数量(扩充为当前数量的1.5倍以上,并取素数)。因此,负载因子总是保持在0.8和0.533之间。
按照以上规则,每次扩展哈希表,其槽数如下表示。当item数量为50万时,槽数应为670849。
序号 | 理论值 | 素数(槽数) | 允许的元素数 |
0 | 1000 | 1009 | 806 |
1 | 1513 | 1523 | 1217 |
2 | 2284 | 2287 | 1828 |
3 | 3430 | 3433 | 2745 |
4 | 5149 | 5153 | 4121 |
5 | 7729 | 7741 | 6191 |
6 | 11611 | 11617 | 9292 |
7 | 17425 | 17431 | 13943 |
8 | 26146 | 26153 | 20921 |
9 | 39229 | 39229 | 31382 |
10 | 58843 | 58889 | 47110 |
11 | 88333 | 88337 | 70668 |
12 | 132505 | 132511 | 106007 |
13 | 198766 | 198769 | 159014 |
14 | 298153 | 298153 | 238521 |
15 | 447229 | 447233 | 357785 |
16 | 670849 | 670849 | 536678 |
17 | 1006273 | 1006279 | 805022 |
18 | 1509418 | 1509427 | 1207540 |
19 | 2264140 | 2264149 | 1811318 |
3. 哈希函数
Zabbix使用的哈希函数是在fnv-1a函数(http://www.isthe.com/chongo/tech/comp/fnv/index.html)的基础上稍微进行了改进。该函数采用乘积和位操作达到快速哈希的目的。具体实现如下:
按照以上函数,模拟620000个itemid的哈希过程(槽数取1006279),哈希效率如下:
总桶数 | 1006279 |
空桶数量 | 543405 |
深度大于1的桶数 | 127940 |
载荷因子 | 0.616131311 |
最大桶深 | 7 |
深桶占有值桶比例 | 0.276403514 |
深桶占总桶数比例 | 0.127141677 |
空桶占总数比例 | 0.540014251 |
四、 任务和数据的并行化
1. 任务的并行
Zabbix系统的任务基本上都是基于所监控的host和item,各个host和item之间有较强的独立性。为了并行化,Zabbix将任务拆分为相对独立的子任务,各个子任务由一个或者多个进程来执行。Zabbix server端的进程划分如下表所示:
启动 顺序 | process title | 允许 进程数 | 默认值 | 任务 |
1 | configuration syncer | 1-1 | 1 | 从数据库同步数据到Dbconfig缓存 |
2 | db watchdog | 1-1 | 1 | 周期性地检查server端数据库是否可用,如果不可用则发送报警信息 |
3 | poller #n | 0-1000 | 5 | 根据dbconfig中的数据,从passive agent和snmp设备采集数据,并flush到共享内存cache->history中 |
4 | unreachable poller #n | 0-1000 | 1 | 当设备处于unreachable状态时,周期性地polling设备 |
5 | trapper #n | 0-1000 | 5 | 从socket接收并处理active agent和active proxy发来的数据(json格式,zabbix通讯协议),并flush到共享内存cache->history中 |
6 | icmp pinger #n | 0-1000 | 1 | 根据dbconfig中的数据,批量采集icmpping相关的item数据,并flush到共享内存cache->history中 |
7 | alerter | 1-1 | 1 | 发送各种报警通知 |
8 | housekeeper | 1-1 | 1 | 周期性地删除过期的历史数据 |
9 | timer #n | 1-1000 | 1 | 计算与时间相关的trigger表达式等 |
10 | node watcher | 1-1 | 1 | 处理与node之间的交互 |
11 | http poller #n | 0-1000 | 1 | 收集web监控相关的数据,并flush到共享内存cache->history中 |
12 | discoverer #n | 0-250 | 1 | 按照指定规则扫描网络,自动发现host、interface等 |
13 | history syncer #n | 1-100 | 4 | 将共享内存cache->history中的数据批量更新到数据库中,并flush到共享内存vc_cache、cache->trends、config->items等中 |
14 | escalator | 1-1 | 1 | 当报警操作需要分步连续执行时,控制各步骤之间的escalations |
15 | ipmi poller #n | 0-1000 | 0 | 与poller进程类似,处理ipmi items |
16 | java poller #n | 0-1000 | 0 | 与poller进程类似,处理JMX items |
17 | snmp trapper #n | 0-1 | 0 | 与trapper进程类似,处理snmp items |
18 | proxy poller #n | 0-250 | 1 | 与passive proxy交互,以设定的频率获取所需要的json格式数据并将数据flush到共享内存cache->history中 |
19 | self-monitoring | 1-1 | 1 | 处理与zabbix自身运行状态相关的item信息,访问共享内存中的collector变量 |
20 | vmware collector #n | 0-250 | 0 | 采集vmware虚拟机相关的数据,并flush到共享内存中 |
所有进程中比较关键的进程有两类:poller/trapper类进程,用于采集数据并加载到共享内存中;history syncer进程,用于更新数据库及触发events和报警。逻辑上这两类任务是先后执行的,首先要采集到数据然后才能触发报警。而每类任务的各个进程之间是独立的,多个poller/trapper进程可以同时执行,多个history syncer进程也可以同时执行。
2. Socket multiplexing对多进程的支持
Zabbix监控系统的数据最终来源是被监控的主机,数据通过socket监听端口接收(监听端口允许的最大连接数由操作系统决定)。Zabbix通过fork多个子进程来共享同一个socket,在读socket时则通过基于select()函数的multiplexing实现多进程同时读取。
按照10000个host,每分钟采集一次数据(假设每个host上的所有item同时采集数据,事实可能并非如此),平均每秒钟有167个连接请求。
3. Mysql数据库的读写
Zabbix支持多种数据库,包括Mysql、Oracle、IBM DB2、PostgreSQL、SQLite,我们实际使用的是Mysql。为了保证数据的持续性,zabbix在触发报警前会先将数据插入到数据库中。History syncer进程数允许最多100个,每个进程可以与数据库建立独立的连接,进行数据更新。
五、 共享内存与进程间通信
1. 共享内存的创建
共享内存是进程间通信中最简单并且速度最快的一种机制。Zabbix的进程间通信主要采用共享内存的方式,主进程在fork出所有子进程之前调用shmget创建共享内存,并attach到地址空间中。
Zabbix调用shmget创建的共享内存segment共有8个,为config_mem、trend_mem、history_mem、history_text_mem、vc_mem、vmware_mem、strpool.mem_info、collector,分别用于dbconfig缓存、cache->trends数据、cache->history(数字和string)、vc_cache、vmware数据、strpool、监控zabbix自身状态的collector结构。如果实际应用中没有启用vmware,则只有7个共享内存被attach到各子进程的地址空间中,如下图所示,这些共享内存段将一直保持attach状态,直到服务停止。
从上图可以看出,每个共享内存段都attach到了553个进程中,即zabbix server的每个进程都可以访问所有七个共享内存。
2. 信号量机制
Zabbix使用二进制信号量机制来协调多个进程对共享内存的同时访问,避免资源争用。系统在创建共享内存之前会调用semget函数,创建一个包含13个信号量的信号量集,并将每个信号量的值初始化为1。各个信号量用于对不同的共享内存进行访问控制,具体如下所示:
# define ZBX_MUTEX_LOG 0
# define ZBX_MUTEX_NODE_SYNC 1
# define ZBX_MUTEX_CACHE 2
# define ZBX_MUTEX_TRENDS 3
# define ZBX_MUTEX_CACHE_IDS 4
# define ZBX_MUTEX_CONFIG 5
# define ZBX_MUTEX_SELFMON 6
# define ZBX_MUTEX_CPUSTATS 7
# define ZBX_MUTEX_DISKSTATS 8
# define ZBX_MUTEX_ITSERVICES 9
# define ZBX_MUTEX_VALUECACHE 10
# define ZBX_MUTEX_VMWARE 11
# define ZBX_MUTEX_SQLITE3 12
当进程需要对某个共享内存进行写操作时,会首先lock(调用semop函数将信号量-1),执行写操作完毕后将再unlock(将信号量+1)。如果执行lock时信号量为0,则等待,直到信号量非0。Zabbix的信号量在释放共享内存时销毁。
六、 声明与结论
本文创作基于zabbix 2.2.10版本的源码分析,欢迎批评指正。
Zabbix所采用的哈希函数效果比较理想。但在实际应用中,仍然可以根据需要和资源情况对负载因子、槽数扩展速度、槽数初值、哈希函数定义进行改进。
在Zabbix的并行计算方面,由于监控系统的特点,数据和任务之间有较强的独立性,非常便于并行化。Zabbix通过多进程+共享内存实现并行,资源争用问题通过信号量进行控制。从实际应用效果来看,并行的性能非常理想。