1 前言
我部门对数据库的监控使用的是开源的Zabbix系统,目前监控了数万个MySQL实例。本文旨在通过分析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的并行计算主要采用的是共享内存模式。
2 Zabbix中的哈希表种类
Zabbix使用的哈希表是链式哈希表,主要有以下五类(都是在共享内存中分配空间):
Valuecache
Valuecache中包含两个哈希表vc_cache->items(itemid作为键值进行哈希)和vc_cache->strpool(字符串作为键值),用于存储收集到的values(包括数字型和字符串型),每个item占用一个slot,每个槽位都是一个链表,链表节点存储实际需要的信息。
Valuecache的哈希表在服务启动时创建,服务退出时销毁,初始槽数为1009(1000之后的第一个素数),随着表中元素数量的增加,槽数也会按照一定的规则增多。Valuecache可使用的最大空间由配置文件中的ValueCacheSize参数控制,允许的范围是128K-64G。
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。
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。
此处的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。键值是字符串本身,哈希值是对字符串调用哈希函数的返回值。
其他
除了以上哈希表,还有snmpidx、vmware service等哈希表。
3 哈希表的实现
下面以config->items哈希表为例,说明zabbix中哈希表的实现方法。
数据结构定义
Zabbix采用的是链式哈希表,哈希表中的每个slot都是一个链表。具体的数据结构定义如下:
槽数取值及负载因子
Zabbix的哈希过程是先调用哈希函数计算键值对应的哈希值,然后用取余法确定槽位号。因此,取余计算时的除数就是槽位数,该数值取素数(因为素数可以做到最大程度上均匀散列)。在config->items哈希表中,槽位数的初始值是1009,随着数据量的增加,当负载因子(元素数/槽数)达到0.8时,会扩充槽数量(扩充为当前数量的1.5倍以上,并取素数)。因此,负载因子总是保持在0.8和0.533之间。
按照以上规则,每次扩展哈希表,其槽数如下表示。当item数量为50万时,槽数应为670849。
哈希函数
Zabbix使用的哈希函数是在fnv-1a函数(http://www.isthe.com/chongo/tech/comp/fnv/index.html)的基础上稍微进行了改进。该函数采用乘积和位操作达到快速哈希的目的。具体实现如下:
按照以上函数,模拟620000个itemid的哈希过程(槽数取1006279),哈希效率如下:
4 哈希表的实现
任务的并行
Zabbix系统的任务基本上都是基于所监控的host和item,各个host和item之间有较强的独立性。为了并行化,Zabbix将任务拆分为相对独立的子任务,各个子任务由一个或者多个进程来执行。Zabbix server端的进程划分如下表所示:
所有进程中比较关键的进程有两类:poller/trapper类进程,用于采集数据并加载到共享内存中;history syncer进程,用于更新数据库及触发events和报警。逻辑上这两类任务是先后执行的,首先要采集到数据然后才能触发报警。而每类任务的各个进程之间是独立的,多个poller/trapper进程可以同时执行,多个history syncer进程也可以同时执行。
Socket multiplexing对多进程的支持
Zabbix监控系统的数据最终来源是被监控的主机,数据通过socket监听端口接收(监听端口允许的最大连接数由操作系统决定)。Zabbix通过fork多个子进程来共享同一个socket,在读socket时则通过基于select()函数的multiplexing实现多进程同时读取。
按照10000个host,每分钟采集一次数据(假设每个host上的所有item同时采集数据,事实可能并非如此),平均每秒钟有167个连接请求。
Mysql数据库的读写
Zabbix支持多种数据库,包括Mysql、Oracle、IBM DB2、PostgreSQL、SQLite,我们实际使用的是Mysql。为了保证数据的持续性,zabbix在触发报警前会先将数据插入到数据库中。History syncer进程数允许最多100个,每个进程可以与数据库建立独立的连接,进行数据更新。
5 共享内存与进程间通信
共享内存的创建
共享内存是进程间通信中最简单并且速度最快的一种机制。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的每个进程都可以访问所有七个共享内存。
信号量机制
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的信号量在释放共享内存时销毁。
6 声明与结论
本文创作基于zabbix 2.2.10版本的源码分析,欢迎批评指正。
Zabbix所采用的哈希函数效果比较理想。但在实际应用中,仍然可以根据需要和资源情况对负载因子、槽数扩展速度、槽数初值、哈希函数定义进行改进。
在Zabbix的并行计算方面,由于监控系统的特点,数据和任务之间有较强的独立性,非常便于并行化。Zabbix通过多进程+共享内存实现并行,资源争用问题通过信号量进行控制。从实际应用效果来看,并行的性能非常理想。
以上内容为IPD原创,如需转载,请注明出处~
扫描下方二维码关注我们~IPDCHAT专注输出技术干货~!