设为首页 收藏本站
查看: 1098|回复: 0

[经验分享] cacti源码分析-获取数据流程

[复制链接]

尚未签到

发表于 2019-1-10 13:23:23 | 显示全部楼层 |阅读模式
  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的过程中,会提到要求配置以下的计划任务,我们就从这个脚本开始。
*/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对应到一个管道的写端,描述符2、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_filefunction名parameters串  

  include_file    的路径为/PATH/TO/cacti/scripts/include_file
  function        名字为include_file文件中定义的函数名。
  parameters      为function的参数列表。
  

  通过这个机制,我们可以扩展新的获取数据的方法。
  

  至此,获取数据流程实现就分析完了。平常用php写网页比较多一些,基本是与数据库打交道,不会涉及到进程层面的处理,cacti的实现恰好在把php用作shell方面给我一个学习的样例。学习过程中,涉及到了操作系统的一些知识,如系统调用、进程间通信、IO阻塞等;有时候会感叹:“哦,原来php是这样来实现这个功能的”。
  感谢cacti、感谢开源。
  





运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-661689-1-1.html 上篇帖子: Cacti 监控windows server 2008图文详解 下篇帖子: cacti优化(面向大规模集群)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表