PHP底層和mysql的通信原理

需要清楚的幾個概念:

FPM進程:進程數在php-fpm.ini中設置。沒有設置 max_requests ,那麼進程是不會銷燬的,也就是說當一個進程裡面出現死循環或者內存溢出等導致進程僵死的情況出現的時候,處理的進程就會少一個。

mysql連接數:一個進程連接到mysql的一個庫,算是一個連接。連接數默認100,我們線上是5000,進程數在my.cnf中設置。mysql連接數要大於等於FPM進程數,否則會報錯。

長連接和短連接:FPM短連接MYSQL的時候,無需調用CLOSE函數,因為在RSHUTDOWN的時候,會調用清理。長連接的時候,需要調用close,長連接會一直霸佔資源,直到進程死掉。

首先我們來理解一下 php-fpm 的工作原理,php-fpm 是一個 php-cgi 進程管理器,其實就是一個連接池,它和nginx配合的工作原理如下。

我們先從最簡單的靜態方式入手觀察他的工作原理

vim php-fpm.ini

[www]

pm = static

pm.max_children = 5

pm.max_requests = 2

上面三句話的含義是什麼呢:

1、static 表示靜態以靜態方式生成 php-fpm 進程

2、pm.max_children = 5 表示當 php-fpm 啟動時就啟動 5 個 php-fpm 子進程 等待處理 nginx 發過來的請求

3、pm.max_requests = 2 表示每個 php-fpm 子進程處理 2 個請求就銷燬,當然父進程每次看到有銷燬的自然也就會生成新的子進程

我們來簡單驗證一下這個說法:

首先重啟 php-fpm,讓它復位一下

接下來寫一條簡單的語句輸出當前進程ID

echo "當前 php-fpm 進程ID:".posix_getpid();

不斷刷新瀏覽器觀察輸出變化

當前 php-fpm 進程ID:24548

當前 php-fpm 進程ID:24549

當前 php-fpm 進程ID:24550

當前 php-fpm 進程ID:24547

當前 php-fpm 進程ID:24551

當前 php-fpm 進程ID:24548

當前 php-fpm 進程ID:24549

當前 php-fpm 進程ID:24550

當前 php-fpm 進程ID:24547

當前 php-fpm 進程ID:24551

當前 php-fpm 進程ID:24563

當前 php-fpm 進程ID:24564

當前 php-fpm 進程ID:24565

當前 php-fpm 進程ID:24566

當前 php-fpm 進程ID:24567

當前 php-fpm 進程ID:24563

當前 php-fpm 進程ID:24564

當前 php-fpm 進程ID:24565

當前 php-fpm 進程ID:24566

當前 php-fpm 進程ID:24567

當前 php-fpm 進程ID:24568

當前 php-fpm 進程ID:24569

當前 php-fpm 進程ID:24570

當前 php-fpm 進程ID:24571

當前 php-fpm 進程ID:24572

當前 php-fpm 進程ID:24568

當前 php-fpm 進程ID:24569

當前 php-fpm 進程ID:24570

當前 php-fpm 進程ID:24571

當前 php-fpm 進程ID:24572

可以看得出,第一批id不是按照順序執行的,進程id為24547的進程是在第四位處理的,然後從下面開始,所有id都是順序執行的而且每次生成的一批id都是遞增,是不是有種mysql自增主鍵的趕腳呢?

這裡需要注意的是,無論是靜態還是下面的動態配置方式,只要沒有設置 max_requests ,那麼進程是不會銷燬的,也就是說當一個進程裡面出現死循環或者內存溢出等導致進程僵死的情況出現的時候,處理的進程就會少一個了

好吧理解了靜態的處理方式,我們其實也很容易知道這個方式的弊端了,當然我們平時服務器不可能就開5個進程每個進程處理2個請求,我們來做一個簡單的加減乘除,看看一個服務器應該開多少個 php-fpm 合適

首先我們來看看一個簡單的echo需要多少內存:

$size = memory_get_usage();

$unit = array('b','kb','mb','gb','tb','pb');

$memory = @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];

echo "當前 php-cgi 進程所使用內存:".$memory;

觀察瀏覽器我們可以得到一下數據:

當前 php-cgi 進程所使用內存:227.17 kb

也就是說一個簡單的什麼都不幹的php就已經佔用了200多K的內存,當然這也不算多。

不過進程多了cpu切換進程速度就會變慢,所以這個數還是需要通過ab等測試工具才能測試出具體應該開多少比較合理

我們先從200個cgi進程開始,不斷的增加,架設增加到800的時候,效率和400一樣,那我們就沒必要開800那麼多進程浪費內存了。

那麼問題就來了,如果同一時間請求出超過400呢?有人說會排隊等待,真的會排隊等待嗎?答案明顯是 php-fpm 是沒能力排隊了,因為處理請求的php-fpm子進程都用完了,那麼等待也就只能是在 nginx 等待,通常一個 nginx 也不只是轉發請求給 php-fpm 就完事了,他還要處理靜態文件呢?如果這些php請求導致nginx的請求數過多一直在等待,那麼訪問靜態文件自然也會卡了,這時候我們就需要配置成下面的動態處理方式。

[www]

pm.max_children = 10

pm.start_servers = 5

pm.min_spare_servers = 2

pm.max_spare_servers = 8

;pm.max_requests = 2

上面五句話的含義是什麼呢:

1、dynamic 表示靜態以動態方式生成 php-fpm 進程

2、pm.max_children = 10 同時活動的進程數 10個

3、pm.start_servers = 5 表示當 php-fpm 主進程啟動時就啟動 5 個 php-fpm 子進程

4、pm.min_spare_servers = 2 表示最小備用進程數

5、pm.max_spare_servers = 8 表示最大備用進程數

6、pm.max_requests = 2 上面說過就不說了

當前 php-fpm 進程ID:2270

當前 php-fpm 進程ID:2271

當前 php-fpm 進程ID:2272

當前 php-fpm 進程ID:2273

當前 php-fpm 進程ID:2274

當前 php-fpm 進程ID:2270

當前 php-fpm 進程ID:2271

當前 php-fpm 進程ID:2272

當前 php-fpm 進程ID:2273

當前 php-fpm 進程ID:2274

當前 php-fpm 進程ID:2270

當前 php-fpm 進程ID:2271

當前 php-fpm 進程ID:2272

當前 php-fpm 進程ID:2273

當前 php-fpm 進程ID:2274

為什麼這裡沒有重新生成新的進程?因為pm.max_requests = 2被註釋掉了,這個上面其實已經提及過一次了

我們也可以從 ps 看出這批進程id

ps aux|grep php

root 2269 0.0 0.1 134560 4616 ? Ss 14:27 0:00 php-fpm: master process (/etc/php/php-fpm.ini)

www-data 2270 0.2 0.2 136736 9188 ? S 14:27 0:00 php-fpm: pool www

www-data 2271 0.2 0.2 136740 9192 ? S 14:27 0:00 php-fpm: pool www

www-data 2272 0.2 0.2 134684 7284 ? S 14:27 0:00 php-fpm: pool www

www-data 2273 0.2 0.2 136732 9120 ? S 14:27 0:00 php-fpm: pool www

www-data 2274 0.1 0.2 134684 7244 ? S 14:27 0:00 php-fpm: pool www

從上面我們可以看到一個 id 為 2269 的 php-fpm 主進程 管理著 id 為 2270、2271、2272、2273、2274 的5個php-fpm 子進程

這裡需要注意的是,當併發大過start_servers數的處理能力是,備用進程才會啟動,當併發數小的時候,備用進程也會銷燬掉,所以無論什麼時候,ps 出來的進程都是上面那5個

下面來看看php-fpm+mysql的效果

mysql> show processlist;

+----+------------------+-----------+------+---------+------+----------------+-------------------------+

| Id | User | Host | db | Command | Time | State | Info |

+----+------------------+-----------+------+---------+------+----------------+-------------------------+

| 9 | root | localhost | NULL | Query | 0 | NULL | show processlist |

+----+------------------+-----------+------+---------+------+----------------+-------------------------+

接下來我們看短連接:

$conn = new mysqli("192.168.0.170", "redol", "redol", "test_db");

然後不斷訪問上面的php文件,每次看到的都是

+----+---------+-----------+------+---------+------+----------------+--------------------+

| Id | User | Host | db | Command | Time | State | Info |

+----+---------+-----------+------+---------+------+----------------+--------------------+

| 9 | root | localhost | NULL | Query | 0 | NULL | show processlist |

+----+---------+-----------+------+---------+------+----------------+--------------------+

這也是php神奇的地方,居然不用close的,每次請求完了他就自己給你close掉和mysql的連接了,這點確實也讓很多新手少了不少下面的煩惱啊

Warning: mysqli::mysqli(): (HY000/1040): Too many connections in ...

不要以為語言應該都是這樣那就打錯特錯了,去看看golang吧

下面看看長連接

$conn = new mysqli("p:192.168.0.170", "redol", "redol", "test_db");

你沒看錯,mysqli的長連接和mysql不同,是在host前面加 p:,沒有mysqli_pconnet 的用法,估計很多剛開始用mysqli也是摸不著頭腦吧?

第一次訪問:

+----+-------+-------------------------+-------+---------+------+-------+------------------+

| Id | User | Host | db | Command | Time | State | Info |

+----+-------+-------------------------+-------+---------+------+-------+------------------+

| 9 | root | localhost | NULL | Query | 0 | NULL | show processlist |

| 10 | redol | bbs.demo.kkk5.com:16650 | redol | Sleep | 34 | | NULL |

+----+-------+-------------------------+-------+---------+------+-------+------------------+

刷新一下網頁

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

| Id | User | Host | db | Command | Time | State | Info |

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

| 9 | root | localhost | NULL | Query | 0 | NULL | show processlist |

| 10 | redol | bbs.demo.kkk5.com:16650 | redol | Sleep | 4 | | NULL |

| 727 | redol | bbs.demo.kkk5.com:16657 | redol | Sleep | 1 | | NULL |

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

再刷新一下網頁,效果我就不發了,反正你知道最後無論怎麼刷新,都是如下即可

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

| Id | User | Host | db | Command | Time | State | Info |

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

| 9 | root | localhost | NULL | Query | 0 | NULL | show processlist |

| 10 | redol | bbs.demo.kkk5.com:16650 | redol | Sleep | 4 | | NULL |

| 727 | redol | bbs.demo.kkk5.com:16657 | redol | Sleep | 1 | | NULL |

| 728 | redol | bbs.demo.kkk5.com:16659 | redol | Sleep | 16 | | NULL |

| 729 | redol | bbs.demo.kkk5.com:16661 | redol | Sleep | 12 | | NULL |

| 730 | redol | bbs.demo.kkk5.com:16663 | redol | Sleep | 8 | | NULL |

+-----+-------+-------------------------+-------+---------+------+-------+------------------+

也就是說,長連接是真的會一直霸佔mysql連接的,那麼問題就來了,如果我沒有重啟 php-fpm,只重啟了mysql,會出現什麼問題呢?答案是第一次連接的時候會報下面錯誤

Warning: mysqli::mysqli(): MySQL server has gone away in ###也就是連接已經存在

所以用長連接query前還是先判斷有沒有連接,沒有就close連接,注意一定要close,再連接,否則連接是失效的。

下面我們來試試 Too many connections 的錯誤吧

先調整一下mysql的最大連接數

vim /etc/mysql/my.cnf

max_connections = 3

你沒看錯,我只給了他3個連接,而上面的php-fpm是5個,所以結果不用我說都知道了吧,如期的出現

Warning: mysqli::mysqli(): (HY000/1040): Too many connections in ...

所以線上的mysql,還是注意一下這個max_connections數吧,我只能告訴你他默認是100,如果你覺得100不夠用的話,自己改去吧

從上面可知,短連接是不用close也會自動關閉的,那如果是設置了 pm.max_requests = 2,每個php-fpm處理兩個請求就銷燬,銷燬了會close麼?我就不截圖了,直接告訴答案吧,會的,所以無論如何,看到的mysql都是1、2、3、4、5、4、3、2、1、2、3、4、5這樣的連接數,就是慢慢增加,再慢慢減少,減少是因為php-fpm子進程銷燬了嘛


分享到:


相關文章: