PHPer 肯定收到過這樣的投訴:小菊花一直在轉!你們網站怎麼這麼卡!當我們線上業務遇到這種卡住 (阻塞) 的情況,大部分 PHPer 會兩眼一抹黑,隨後想起那句名言:性能瓶頸都在數據庫
然後把鍋甩給 DBA,趕緊找找慢 sql,但這是非常錯誤的做法,因爲有太多因素能導致業務卡住,下面列舉幾種常見的卡住問題。
1. 死循環
最常見的就是寫出了死循環代碼
<?php while(1){ //do something if($condition){ //滿足條件後退出循環 break; } } <?php while(1){ //do something if($condition){ //滿足條件後退出循環 break; } }
上述代碼通過$condition
控制循環退出,如果程序驗證不嚴格,某些情況$condition
永遠爲真就會導致請求卡死。
2.sesstion_start 函數導致卡死
PHP 的 session 鎖等待 (ps: 很多地方叫做 session 死鎖,這不太符合死鎖定義),這個相信大部分 PHPer 都遇到過,PHP 默認會把 session 信息存儲在/tmp/sess_
下面的 session 文件裏面,調用session_start()
函數的時候會調用flock
系統調用給 session 文件加鎖,如果前一個請求沒有結束或者手動釋放 session 就會導致後面的請求無法獲得鎖,卡死在session_start()
這個地方。下面舉個例子,比如這種代碼:
setInterval(function () { $.post("/ajax/doSomething", {}, function (result) {//1s進行一次ajax }); }, 1000)//1000ms == 1s setInterval(function () { $.post("/ajax/doSomething", {}, function (result) {//1s進行一次ajax }); }, 1000)//1000ms == 1s
前端 js 定時通過 ajax 請求一下後端 PHP 的接口 (/ajax/doSomething
) 做一些比較耗時的事情,寫代碼的人可能想當然的認爲第一次的請求即使沒有處理完,也不會影響第二次的請求,因爲有很多的 FPM 進程每次請求會分發到不通的進程,但殊不知第二次請求會卡死在session_start()
。
3.flock 函數導致卡死
最常見的場景就是寫日誌,在 PHP 代碼中確保每次fwrite
寫的日誌內容小於 8k 的情況下我們可以利用 append 原子追加方式寫日誌,但是如果保證不了小於 8k 我們就需要在每次寫日誌前給文件加文件鎖來避免兩次日誌間產生穿插的情況,代碼如下:
<?php $fp = fopen("/home/guoxinhua/php.log", "a+"); if (flock($fp, LOCK_EX)) { //給日誌文件加鎖 //do something fwrite($fp, "the huge string\n"); flock($fp, LOCK_UN); // 釋放鎖定 } <?php $fp = fopen("/home/guoxinhua/php.log", "a+"); if (flock($fp, LOCK_EX)) { //給日誌文件加鎖 //do something fwrite($fp, "the huge string\n"); flock($fp, LOCK_UN); // 釋放鎖定 }
如果在 A 進程獲得鎖後由於某種問題阻塞了那麼 B 進程就會卡死在第三行flock
的位置,除非 A 進程被 kill 掉,系統會自動釋放這個文件鎖
注意還有很多其他類型的鎖即使進程被 kill 也不會自動被釋放。
這個 8k 是可以改的,和 glibc 中的 fwrite 很多細節也不一樣. 你是否有這樣的煩惱,想學習高級技術,缺乏好的高級學習資料,11 年架構師授課的 TP5、laravel、swoole、swoft、高併發、,官方羣:677079770 ,大牛帶你飛 ,PHP/web 從入門到架構 722584796
4. 網絡客戶端未設置超時時間
MySQL、CURL、Swoole\Client 等網絡客戶端未設置超時可能會導致進程阻塞。Swoole\Client 建立 TCP 連接的時候connect
方法的最後一個參數是超時時間,-1
即爲永不超時,注意這裏設置不是單指這次connect
方法,而是後面所有的send
,recv
都永不超時,在同步阻塞的編程模式下,如果此時對端機器直接宕機等原因導致網絡不通,那麼本端業務的表現就是卡死狀態,所有的send
,recv
方法都將被阻塞,代碼如下:
<?php $cli = new Swoole\Client(SWOOLE_SOCK_TCP); if ($cli->connect('127.0.0.1', 9501,-1)) { $cli->send("data"); $cli->recv(); } else { echo "connect failed."; } <?php $cli = new Swoole\Client(SWOOLE_SOCK_TCP); if ($cli->connect('127.0.0.1', 9501,-1)) { $cli->send("data"); $cli->recv(); } else { echo "connect failed."; }
5. Swoole 協程的 lock
在 Swoole 協程模式下,不正確的使用 lock 也會導致所有協程大面積卡死,如下代碼,通過go
方法創建 2 個協程 (不理解協程的同學可以理解爲創建了 2 個線程),第一個協程 lock 獲得鎖後在co::sleep
位置讓出了 cpu 此時開始執行第二個協程,第二個協程會卡死在第 6 行獲得鎖的位置,同時第一個協程也永遠無法恢復繼續執行。
<?php $lock = new Swoole\Lock(); $c = 2;//創建2個協程 while ($c--) { go(function () use ($lock) {//創建協程 $lock->lock();//獲得鎖 Co::sleep(1);//讓出cpu $lock->unlock();//釋放鎖 }); } <?php $lock = new Swoole\Lock(); $c = 2;//創建2個協程 while ($c--) { go(function () use ($lock) {//創建協程 $lock->lock();//獲得鎖 Co::sleep(1);//讓出cpu $lock->unlock();//釋放鎖 }); }
如何發現卡死
上述只是舉了一些例子,真實業務中還有各種姿勢的卡死,遇到這種問題有經驗的 PHPer 會用strace -p
命令查看當前 PHP 進程到底阻塞在哪個系統調用上面來定位問題,但這種方式有幾個問題:
- 定位問題不清晰
比如死鎖這種問題 strace 的時候只能看到類似futex(0x7f4c8d567128, FUTEX_WAIT, 2, NULL)
這種信息,非常的不直觀,很多人根本不知道哪些 PHP 代碼會觸發futex
系統調用,還有前文提到session_start
那個問題,很多人根本不知道這裏會觸發flock
,也就說很難根據一個系統調用定位到具體問題。 - 不知道 - p 哪一個進程
我們線上環境通常會啓動幾十個甚至上百個 PHP 進程,在有些請求卡死,有些請求正常的情況下,你到底該strace -p
哪個進程呢?貌似只能碰碰運氣了。 - 發現不了死循環的問題
由於strace
命令的原理是追蹤所有的系統調用,如果是前文提到的第一種情況,也就是死循環的卡死,strace
根本無法獲得任何有用的信息。此時我們只能用gdb
工具來獲取當前死循環在哪裏具體,具體做法如下:首先:gdb attach
後面接個進程 id。
然後:p (char *)executor_globals.current_execute_data.func.op_array.filename.val
打印當前執行的 PHP 文件。p (char *)executor_globals.current_execute_data.func.op_array.function_name.val
打印當前執行的函數名。p executor_globals.current_execute_data.opline.lineno
打印當前執行的行數。
進一步也可以獲取調用堆棧這裏就不展開了。
但這明顯太底層了,很多細節要注意,不精通 PHP 內核的人很難這樣找問題(ps: 通過.gdbinit
能稍微減少點難度,但是也有很多其他問題)。
使用 Swoole Tracker 發現卡死問題
針對上述問題,Swoole 官方出了一個解決方案 Swoole Tracker 的堆棧工具,同時支持 FPM 和 Swoole。
使用方法很簡單:
- 首先點擊上面的連接註冊個賬戶。
- 然後裝上
swoole_tracker
擴展。 - 最後登陸後臺,在
調試器
=>進程列表
中點擊堆棧
按鈕就能獲得當前卡在哪了,如圖:
結尾
除了上面的卡死問題,還有一種情況是調用變慢,比如原來一個系統調用 5ms,但是由於網絡等等原因,這個調用 100ms 才返回,業務的表現是變慢了而不是卡死在那裏,這種情況通過 tracker 的抓堆棧工具是無法定位問題的,因爲卡住時間很短,很難抓到調用堆棧,此時需要 Swoole 工具鏈中的另外一個工具阻塞IO檢測工具
我們會在後面給大家介紹。