zaoqiden 发表于 2013-5-27 09:40:58

cacti获取数据流程分析

cacti用于监控系统的各项运行指标,提示了操作界面和图表,是一个整合工具集,它完成两个核心任务:1,指标数据的获取。2,将数据通过数图进行展示。
在cacti中,图表的绘制、图表数据的存储是通过rrdtool工具实现的,《RRDtool简体中文教程》对rrdtool工具进行了介绍,是很好的资料。
获取数据的途径,视目标数据暴露的方式而定,如:网卡数据流量、系统负载等数据已通过SNMP标准化,使用SNMP方式获取。如果要监控nginx(stub_status)数据,该项数据是通过http方式暴露,需使用http获取数据。如果要监控mysql-server(show status)数据,需要可以连接到mysql服务器,并有权限打印数据。
作为参照比较,nagios提供了check_by_ssh nrpe NSCA几种标准化、易于扩展的机制。如果要整合nagios和cacti的话,在获取数据层面,应该可以实施。如果有net-snmp开发能力,也可以把nginx、mysql-server的数据通过SNMP的方式暴露。
下面开始分析cacti“获取数据”部分的功能实现细节。通过观察日志,可以对分析提供帮助。打开日志方式:在cacti的"console" - "General" - "Poller Logging Level",需设置 >= POLLER_VERBOSITY_DEBUG
在我们配置cacti的过程中,会提到要求配置以下的计划任务,我们就从这个脚本开始。

1
*/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粒度。

1
$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 2host2 3host3 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.phpspine。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"],$_SERVER["argv"] 获取,取出给定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::ifDescrRFC1213-MIB::ifInOctetsRFC1213-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对应到一个管道的写端,描述符2、3都对应到一个文件。$pipes(参数3)是一个数组,$pipes对应描述符0(stdin)的管道的写端,$pipes对应描述符1(stdout)的管道的读端。文件描述符处理完成后,ss进程运行(exe函数簇)$cmd(第一个参数)程序的上下文,这里是(script_server.php)。进程上下文的替换不影响描述符表。
cmd进程生成ss进程后,在$pipes(对应ss进程的stdout)读取子进程的输出,以判定子进程是否已经成功启动。


$output = fgets($pipes, 1024);
substr_count($output, "Started") != 0fgets会阻塞,直到从$pipes中读取足够数据、或者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, $command . "\r\n");
    $output = fgets($pipes, 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_filefunction      名字为include_file文件中定义的函数名。parameters      为function的参数列表。
通过这个机制,我们可以扩展新的获取数据的方法。
至此,获取数据流程实现就分析完了。平常用php写网页比较多一些,基本是与数据库打交道,不会涉及到进程层面的处理,cacti的实现恰好在把php用作shell方面给我一个学习的样例。学习过程中,涉及到了操作系统的一些知识,如系统调用、进程间通信、IO阻塞等;有时候会感叹:“哦,原来php是这样来实现这个功能的”。感谢cacti、感谢开源。

xingyu655 发表于 2013-5-27 10:10:43

一时的冲动,子孙的危机!

小乔 发表于 2013-5-27 12:48:38

生活***好玩,因为生活老***玩我!

hyzqb 发表于 2013-5-27 12:52:05

内练一口气,外练一口屁。

xsmscb 发表于 2013-5-27 13:16:50

所有刻骨铭心的爱都灵魂游离于床上的瞬间!

xuanxi 发表于 2013-5-27 15:38:51

看尽天下A片,心中自然无码~

hongblue 发表于 2013-5-27 18:11:04

有竞争才有进步嘛
页: [1]
查看完整版本: cacti获取数据流程分析