cacti源碼分析-獲取數據流程

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、感謝開源。

相關文章
相關標籤/搜索