本文原创作者鲍光亚,京东商城基础平台部软件开发工程师,经作者同意发表于本人博客,如需转载需经本人同意。

一、 前言

我部门对数据库的监控使用的是开源的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. clip_image002    
槽数取值及负载因子

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)的基础上稍微进行了改进。该函数采用乘积和位操作达到快速哈希的目的。具体实现如下:

clip_image004

按照以上函数,模拟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状态,直到服务停止。

clip_image006

从上图可以看出,每个共享内存段都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通过多进程+共享内存实现并行,资源争用问题通过信号量进行控制。从实际应用效果来看,并行的性能非常理想。

image