cacti用于监控系统的各项运行指标,提示了操做界面和图表,是一个整合工具集,它完成两个核心任务:php
1,指标数据的获取。html
2,将数据经过数图进行展现。mysql
在cacti中,图表的绘制、图表数据的存储是经过rrdtool工具实现的,《RRDtool简体中文教程》对rrdtool工具进行了介绍,是很好的资料。ios
获取数据的途径,视目标数据暴露的方式而定,如:nginx
网卡数据流量、系统负载等数据已经过SNMP标准化,使用SNMP方式获取。sql
若是要监控nginx(stub_status)数据,该项数据是经过http方式暴露,需使用http获取数据。shell
若是要监控mysql-server(show status)数据,须要能够链接到mysql服务器,并有权限打印数据。数据库
做为参照比较,nagios提供了check_by_ssh nrpe NSCA几种标准化、易于扩展的机制。若是要整合nagios和cacti的话,在获取数据层面,应该能够实施。若是有net-snmp开发能力,也能够把nginx、mysql-server的数据经过SNMP的方式暴露。数组
下面开始分析cacti“获取数据”部分的功能实现细节。bash
经过观察日志,能够对分析提供帮助。
打开日志方式:在cacti的"console" - "General" - "Poller Logging Level",需设置 >= POLLER_VERBOSITY_DEBUG
在咱们配置cacti的过程当中,会提到要求配置如下的计划任务,咱们就从这个脚本开始。
*/5 * * * * /PATH/TO/php /PATH/TO/cacti/poller.php > /dev/null 2>&1
poller.php
---------------
根据cacti界面设置的参数,肯定以何种方式获取数据
1,当计划任务启动poller.php进程后,这个进程的生存周期内执行几回数据轮询。
2,一次数据轮询使用几个进程(cmd进程)同时进行数据获取,如何分配任务给cmd进程。
3,cmd进程使用何种实现方式获取数据。
4,将cmd进程获取的数据解析后存入rrd。
1,当计划任务启动poller.php进程后,这个进程的生存周期内执行几回数据轮询。
涉及 $cron_interval,$poller_interval 两项参数
好比cron的周期是5分钟,poller周期是1分钟,则cron触发的poller.php进程,要负责安排5次数据轮询,以知足poller粒度。
$poller_runs = intval($cron_interval / $poller_interval);
poller.php进程的最大运行时间(比计划任务周期少2秒)
define("MAX_POLLER_RUNTIME", $poller_runs * $poller_interval - 2);
2,一次数据轮询使用几个进程(cmd进程)同时进行数据获取,如何分配任务给cmd进程。
这里先解释一下cmd进程,为了提升轮询数据的效率,cacti容许使用多个进程同时进行获取数据的工做,每一个进程负责必定数量的host,由于这些进程运行cmd.php(默认是cmd.php,也能够是spine)的上下文,所取这个名称以方便描述。
$concurrent_processes = 2;
系统默认值为1,即只会使用一个进程负责全部主机的数据轮询,咱们讨论 >= 2的场景。
cacti设置界面有以下说明,大意是说,当cmd进程使用cmd.php抓取数据时,能够经过增长这个参数,提升性能(多进程模型);当cmd进程使用spine执行数据抓取时,应该经过增长“Maximum Threads per Process”的值,提高性能(多线程模型)。
The number of concurrent processes to execute. Using a higher number when using cmd.php will improve performance. Performance improvements in spine are best resolved with the threads parameter
决定cmd进程数量的另外一个因素是,cacti会查询当前有多少个任务(即cacti管理界面中Data Sources页面显示的条目内容),只有每一个cmd进程均可以分配到至少一个任务时,才会启用。
cmd进程任务分配。
1,分配是在host粒度上进行的。即只分配host编号,该host上的全部子数据项都在一个进程中完成。
2,poller.php遍历全部的host,并累计任务数,当当前累计任务数 >= $items_per_process值(每一个cmd进程平均完成任务数)时,就生成一个cmd进程,负责完成累计范围内的host的数据获取任务。
例若有以下任务
host1 2
host2 3
host3 3
若是$concurrent_processes = 2, $items_per_process = 8 / 2 = 4;
按主机任务进行累计和分配
累计host1 $items_launched = 2;
累计host2 $items_launched = 5; 此时累计任务数 >= $items_per_process; 派生一个cmd.php后台进程,并将开始host(1)和结束host(2)的id传递给这个进程,以下的调用。
exec_background('/PATH/TO/php', "-q /PATH/TO/cacti/cmd.php 1 2");
poller.php的代码以下:
exec_background($command_string, "$extra_args $first_host $last_host");
exec_background的实现,主要由下面这条语句实现。
exec($filename . ' ' . $args . ' > /dev/null &');
关于exec,手册有以下说明,因此' > /dev/null &'很重要。
若是程序使用此函数启动,为了能保持在后台运行,此程序必须将输出重定向到文件或其它输出流。 不然会致使 PHP 挂起,直至程序执行结束。
经过查看php的实现,exec、system、passthru都是基于php_exec的。
流程运行至此,获取数据的任务就交给cmd进程在后台运行了,cmd进程的实现,后面再分析。
3,cmd进程使用何种实现方式获取数据。
cmd进程有两个实现 cmd.php spine。
cmd.php是一个php脚本,经过php解析运行。
spine,不是很了解。猜测它是一个能够访问系统调用、线程模型的可执行文件。它实现与cmd.php相同的功能。
cacti默认使用的是cmd.php,咱们只分析这种方式。
cmd进程将指定host的数据项获取后,保存至数据库中(poller_output表),这里存储的数据是暂时性的,用于绘图的数据最终须要存储在rrd文件中。
4,将cmd进程获取的数据解析后存入rrd。
poller.php分配完后台cmd进程后,自已则开始检测后台cmd进程的处理结果。检测方式为:经过循环轮询数据库(poller_output表),若是没有完成的条目,执行usleep(500);若是有完成的条目,将数据经过rrdtool工具,写入到rdd文件,写入过程实现以下:
1)$rrdtool_pipe = rrd_init();
rrd_init()主要使用下面的代码实现:
$command = "/usr/bin/rrdtool - > /dev/null 2>&1";
popen($command, "w");
2)rrdtool_function_update($command_line, $rrdtool_pipe);
rrdtool_function_update()主要使用下面的代码实现:
fwrite($rrdtool_pipe, escape_command(" $command_line") . "\r\n")
fflush($rrdtool_pipe);
3)rrd_close($rrdtool_pipe);
若是成功完成的进程(end_time)数 >= 启动的后台进程数,则视为处理完成。
/* process poller commands */
exec_background($command_string, "$extra_args");
/PATH/TO/php -q /PATH/TO/cacti/poller_commands.php
cmd.php
---------------
接收两个参数 $first_host $last_host,脚本经过$_SERVER["argv"][1],$_SERVER["argv"][2] 获取,取出给定host的poller_items(cacti管理界面中Data Souries)记录。
若是没有提供参数($_SERVER["argc"] == 1),脚本会从数据库中取最近须要轮询数据的host。
下面是数据轮询流程的伪码。
$ping = new Net_Ping; foreach ($polling_items as $item) { if (($new_host) && (!empty($host_id))) { /* perform the appropriate ping check of the host */ /* 若是是一个新host,cmd进程会先检测host是否可到达。 */ if ($ping->ping(... ...) { /* up or down*/ } } /* 根据目标数据暴露的方式,提供了3种获取数据的方式 */ if (!$host_down) { switch ($item["action"]) { case POLLER_ACTION_SNMP: /* snmp */ case POLLER_ACTION_SCRIPT: /* script (popen) */ case POLLER_ACTION_SCRIPT_PHP: /* script (php script server) */ default: } } }
下面来看3种获取数据的实现
POLLER_ACTION_SNMP
即经过snmp协议获取目标数据,需提供commuinty、OID等参数,若是启用了php_snmp扩展,可以使用扩展提供了snmp函数获取数据;若是没有php_snmp扩展,能够经过php脚本调用net-snmp包提供的命令行工具来获取数据;或者用php的socket本身实现snmp协议也能够,如下是一些OID。
host信息
snmptranslate -Td -Of 1.3.6.1.2.1.25
.iso.org.dod.internet.mgmt.mib-2.host
网卡流量信息
RFC1213-MIB::ifDescr
RFC1213-MIB::ifInOctets
RFC1213-MIB::ifOutOctets
POLLER_ACTION_SCRIPT,
主要由下面的调用语句实现,是基于popen实现的
$output = trim(exec_poll($command));
exec_poll()主要经过下面的调用实现:
$fp = popen($command, "r"); $output = fgets($fp, 8192);
POLLER_ACTION_SCRIPT_PHP
主要由下面语句调用,是基于proc_open实现的。这种方式经过一个可双向通讯的子进程(运行script_server.php上下文,后面均称ss进程),相互配合完成任务。这种方式能够复用子进程,所以会减小一些系统开销。ss进程经过下面的调用初始化。
$cactiphp = proc_open("/PATH/TO/php -q /PATH/TO/cacti/script_server.php cmd", $cactides, $pipes);
关于proc_open的参数,做一些笔记,它表现以下的行为:
它派生(fork)一个子进程,子进程使用$cactides(参数2)指定的方式,初始化本身已打开的文件描述符表。如
cactides = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 2 => array("file", "/tmp/error-output.txt", "a"), // stderr is a file to write to 3 => array("file", "/PATH/TO/file", 'rw'), );
该参数(参数2)是一个索引数组,其中0,1,2三个索引位置已经固定分配(0 is stdin, 1 is stdout, while 2 is stderr),其它索引位置能够是任何有效的文件描述符编号。
索引数组的元素也须要是一个数组,其元素有如下约定。
第一个元素是文件描述符的类型,有两种类型可供选择:pipe和file。第二个元素的语义依赖于第一个元素,若是是pipe,第二个元素是文件描述符对应到管道的哪一端,"r"为读端,"w"为写端;管道的另外一端将经过$pipes(参数3)返回。管道经常使用做进程间通讯,它是半双工的,数据只能向一个方向流动,即会有只能“写”数据的一端和只能“读”数据的一端。若是是file,第二个元素值是打开文件的路径,第三个元素是打开文件的模式;
在上例中,ss进程的文件描述符0,对应到一个管道(pipe)的读端,描述符1对应到一个管道的写端,描述符二、3都对应到一个文件。$pipes(参数3)是一个数组,$pipes[0]对应描述符0(stdin)的管道的写端,$pipes[1]对应描述符1(stdout)的管道的读端。
文件描述符处理完成后,ss进程运行(exe函数簇)$cmd(第一个参数)程序的上下文,这里是(script_server.php)。进程上下文的替换不影响描述符表。
cmd进程生成ss进程后,在$pipes[1](对应ss进程的stdout)读取子进程的输出,以断定子进程是否已经成功启动。
$output = fgets($pipes[1], 1024); substr_count($output, "Started") != 0
fgets会阻塞,直到从$pipes[1]中读取足够数据、或者EOF、或者cmd进程收到一个signal。而后在输出字串中查找"Started"串。
ss进程启动后,会在stdout输出下面的内容(包含“Started”串),以告知cmd进程,本身已经准备好了。
fputs(STDOUT, "PHP Script Server has Started - Parent is " . $environ . "\n");
而后ss进程阻塞在“读取stdin”上,等待cmd进程的指令
/* process waits for input and then calls functions as required */ while (1) { ... ... $input_string = fgets(STDIN, 1024); ... ... }
至此,cmd进程和ss进程,已完成协调工做的准备工做。当cmd进程须要ss进程配合获取数据时,cmd进程就往ss进程的stdin写命令,ss进程完成后,将结果写入stdout以返回给cmd进程。下面是调用代码:
$output = trim(str_replace("\n", "", exec_poll_php($item["arg1"], $using_proc_function, $pipes, $cactiphp)));
其中exec_poll_php的主要实现语句以下:
function exec_poll_php($command, $using_proc_function, $pipes, $proc_fd) { ... ... fwrite($pipes[0], $command . "\r\n"); $output = fgets($pipes[1], 8192); ... ... return $output; }
cmd进程的分析就到这里,下面咱们来看一下ss进程的实现。
script_server.php
---------------
上文提到,ss进程初始完成后,便阻塞于 fgets(STDIN, 1024);以等待cmd进程的命令,下面咱们就来看一下,ss进程从stdin获取命令后的流程实现。
$input_string = fgets(STDIN, 1024); /* 若是命令的前4个字符是“quit”,ss进程执行退出 */ if (substr($input_string,0,4) == "quit") { exit(1); } /* pull off the parameters */ $i = 0; while ( true ) { /* 查找命令中的空格,以切分命令参数 */ $pos = strpos($input_string, " "); if ($pos > 0) { switch ($i) { case 0: /* cut off include file as first part of input string and keep rest for further parsing */ $include_file = trim(substr($input_string,0,$pos)); $input_string = trim(strchr($input_string, " ")) . " "; break; case 1: /* cut off function as second part of input string and keep rest for further parsing */ $function = trim(substr($input_string,0,$pos)); $input_string = trim(strchr($input_string, " ")) . " "; break; case 2: /* take the rest as parameter(s) to the function stripped off previously */ $parameters = trim($input_string); break 2; } }else{ break; } $i++; }
上面的代码显示,传递给ss进程的命令串如下面的格式组织:
include_file<空格>function名<空格>parameters串
include_file 的路径为/PATH/TO/cacti/scripts/include_file
function 名字为include_file文件中定义的函数名。
parameters 为function的参数列表。
经过这个机制,咱们能够扩展新的获取数据的方法。
至此,获取数据流程实现就分析完了。日常用php写网页比较多一些,基本是与数据库打交道,不会涉及到进程层面的处理,cacti的实现刚好在把php用做shell方面给我一个学习的样例。学习过程当中,涉及到了操做系统的一些知识,如系统调用、进程间通讯、IO阻塞等;有时候会感叹:“哦,原来php是这样来实现这个功能的”。
感谢cacti、感谢开源。