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