Nginx+php-fpm高併發優化

wwwxgb66com17176934555 發表於 2021-07-20
PHP

PHP-php-fpm配置優化
前言:

  1.少安裝PHP模組, 費記憶體

  2.調高linux核心開啟檔案數量,可以使用這些命令(必須是root帳號)(我是修改/etc/rc.local,加入ulimit -SHn 51200的)

echo ulimit -HSn 65536 >> /etc/profile
echo ulimit -HSn 65536 >> /etc/rc.local
source /etc/profile
  如果ulimit -n數量依舊不多(即上面配置沒生效)的話, 可以在 /etc/security/limits.conf 檔案最後加上

  • soft nofile 51200
  • hard nofile 51200

1.與Nginx使用Unix域Socket通訊(Nginx和php-fpm在同一臺伺服器)

  Unix域Socket因為不走網路,的確可以提高Nginx和php-fpm通訊的效能,但在高併發時會不穩定。

  Nginx會頻繁報錯:connect() to unix:/dev/shm/php-fcgi.sock failed (11: Resource temporarily unavailable) while connecting to upstream

  可以通過下面兩種方式提高穩定性:
  1)調高nginx和php-fpm中的backlog
   配置方法為:在nginx配置檔案中這個域名的server下,在listen 80後面新增default backlog=1024。
  同時配置php-fpm.conf中的listen.backlog為1024,預設為128。
  2)增加sock檔案和php-fpm例項數
  再新建一個sock檔案,在Nginx中通過upstream模組將請求負載均衡到兩個sock檔案背後的兩套php-fpm例項上。

2.php-fpm引數調優

  pm = dynamic; 表示使用哪種程式數量管理方式

    dynamic表示php-fpm程式數是動態的,最開始是pm.start_servers指定的數量,如果請求較多,則會自動增加,保證空閒的程式數不小於pm.min_spare_servers,如果程式數較多,也會進行相應清理,保證多餘的程式數不多於pm.max_spare_servers

    static表示php-fpm程式數是靜態的, 程式數自始至終都是pm.max_children指定的數量,不再增加或減少

  pm.max_children = 300; 靜態方式下開啟的php-fpm程式數量
  pm.start_servers = 20; 動態方式下的起始php-fpm程式數量
  pm.min_spare_servers = 5; 動態方式下的最小php-fpm程式數量
  pm.max_spare_servers = 35; 動態方式下的最大php-fpm程式數量

    如果pm為static, 那麼其實只有pm.max_children這個引數生效。系統會開啟設定數量的php-fpm程式

    如果pm為dynamic, 那麼pm.max_children引數失效,後面3個引數生效。系統會在php-fpm執行開始的時候啟動pm.start_servers個php-fpm程式,然後根據系統的需求動態在pm.min_spare_servers和pm.max_spare_servers之間調整php-fpm程式數

    那麼,對於我們的伺服器,選擇哪種pm方式比較好呢?事實上,跟Apache一樣,執行的PHP程式在執行完成後,或多或少會有記憶體洩露的問題。這也是為什麼開始的時候一個php-fpm程式只佔用3M左右記憶體,執行一段時間後就會上升到20-30M的原因了。

    對於記憶體大的伺服器(比如8G以上)來說,指定靜態的max_children實際上更為妥當,因為這樣不需要進行額外的程式數目控制,會提高效率。因為頻繁開關php-fpm程式也會有時滯,所以記憶體夠大的情況下開靜態效果會更好。數量也可以根據 記憶體/30M 得到,比如8GB記憶體可以設定為100,那麼php-fpm耗費的記憶體就能控制在 2G-3G的樣子。如果記憶體稍微小點,比如1G,那麼指定靜態的程式數量更加有利於伺服器的穩定。這樣可以保證php-fpm只獲取夠用的記憶體,將不多的記憶體分配給其他應用去使用,會使系統的執行更加暢通。

    對於小記憶體的伺服器來說,比如256M記憶體的VPS,即使按照一個20M的記憶體量來算,10個php-cgi程式就將耗掉200M記憶體,那系統的崩潰就應該很正常了。因此應該儘量地控制php-fpm程式的數量,大體明確其他應用佔用的記憶體後,給它指定一個靜態的小數量,會讓系統更加平穩一些。或者使用動態方式,因為動態方式會結束掉多餘的程式,可以回收釋放一些記憶體,所以推薦在記憶體較少的伺服器或VPS上使用。具體最大數量根據 記憶體/20M 得到。比如說512M的VPS,建議pm.max_spare_servers設定為20。至於pm.min_spare_servers,則建議根據伺服器的負載情況來設定,比較合適的值在5~10之間。

    在4G記憶體的伺服器上200就可以(我的1G測試機,開64個是最好的,建議使用壓力測試獲取最佳值)

  pm.max_requests = 10240;

    nginx php-fpm配置過程中最大問題是內洩漏出問題:伺服器的負載不大,但是記憶體佔用迅速增加,很快吃掉記憶體接著開始吃交換分割槽,系統很快掛掉!其實根據官方的介紹,php-cgi不存在記憶體洩漏,每個請求完成後php-cgi會回收記憶體,但是不會釋放給作業系統,這樣就會導致大量記憶體被php-cgi佔用。     

    官方的解決辦法是降低PHP_FCGI_MAX_REQUESTS的值,如果用的是php-fpm,對應的php-fpm.conf中的就是max_requests,該值的意思是傳送多少個請求後會重啟該執行緒,我們需要適當降低這個值,用以讓php-fpm自動的釋放記憶體,不是大部分網上說的51200等等,實際上還有另一個跟它有關聯的值max_children,這個是每次php-fpm會建立多少個程式,這樣實際上的記憶體消耗是max_childrenmax_requests每個請求使用記憶體,根據這個我們可以預估一下記憶體的使用情況,就不用再寫指令碼去kill了。

  request_terminate_timeout = 30;

    最大執行時間, 在php.ini中也可以進行配置(max_execution_time)

  request_slowlog_timeout = 2; 開啟慢日誌
  slowlog = log/$pool.log.slow; 慢日誌路徑

  rlimit_files = 1024; 增加php-fpm開啟檔案描述符的限制

3.php-fpm的高CPU使用率排查方法

  1)使用top命令, 直接執行top命令後,輸入1就可以看到各個核心的CPU使用率。而且通過top -d 0.1可以縮短取樣時間

  2)查詢php-fpm慢日誌

grep -v “^$” www.log.slow.tmp | cut -d “ “ -f 3,2 | sort | uniq -c | sort -k1,1nr | head -n 50

5181 run() /www/test.net/framework/web/filters/CFilter.php:41

5156 filter() /www/test.net/framework/web/filters/CFilterChain.php:131

2670 = /www/test.net/index.php

2636 run() /www/test.net/application/controllers/survey/index.php:665

2630 action() /www/test.net/application/controllers/survey/index.php:18

2625 run() /www/test.net/framework/web/actions/CAction.php:75

2605 runWithParams() /www/test.net/framework/web/CController.php:309

2604 runAction() /www/test.net/framework/web/filters/CFilterChain.php:134

2538 run() /www/test.net/framework/web/CController.php:292

2484 runActionWithFilters() /www/test.net/framework/web/CController.php:266

2251 run() /www/test.net/framework/web/CWebApplication.php:276

1799 translate() /www/test.net/application/libraries/Limesurvey_lang.php:118

1786 load_tables() /www/test.net/application/third_party/php-gettext/gettext.php:254

1447 runController() /www/test.net/framework/web/CWebApplication.php:135

    引數解釋:

            sort:  對單詞進行排序
            uniq -c:  顯示唯一的行,並在每行行首加上本行在檔案中出現的次數
            sort -k1,1nr:  按照第一個欄位,數值排序,且為逆序
            head -10:  取前10行資料

  3)用strace跟蹤程式

    a)利用nohup將strace轉為後臺執行,直到attach上的php-fpm程式死掉為止:

nohup strace -T -p 13167 > 13167-strace.log &
    引數說明:

      -c 統計每一系統呼叫的所執行的時間,次數和出錯的次數等.
-d 輸出strace關於標準錯誤的除錯資訊.
-f 跟蹤由fork呼叫所產生的子程式.
-o filename,則所有程式的跟蹤結果輸出到相應的filename
-F 嘗試跟蹤vfork呼叫.在-f時,vfork不被跟蹤.
-h 輸出簡要的幫助資訊.
-i 輸出系統呼叫的入口指標.
-q 禁止輸出關於脫離的訊息.
-r 列印出相對時間關於,,每一個系統呼叫.
-t 在輸出中的每一行前加上時間資訊.
-tt 在輸出中的每一行前加上時間資訊,微秒級.
-ttt 微秒級輸出,以秒了表示時間.
-T 顯示每一呼叫所耗的時間.
-v 輸出所有的系統呼叫.一些呼叫關於環境變數,狀態,輸入輸出等呼叫由於使用頻繁,預設不輸出.
-V 輸出strace的版本資訊.
-x 以十六進位制形式輸出非標準字串
-xx 所有字串以十六進位制形式輸出.
-a column
設定返回值的輸出位置.預設為40.
-e execve 只記錄 execve 這類系統呼叫
-p 主程式號

    b)用利用-c引數讓strace幫助彙總,非常方便非常強大!

[[email protected] log]# strace -cp 9907

Process 9907 attached - interrupt to quit

Process 9907 detached

% time seconds usecs/call calls errors syscall


56.61 0.016612 5 3121 read

11.11 0.003259 1 2517 715 stat

8.04 0.002358 7 349 brk

6.02 0.001767 1 1315 poll

4.28 0.001255 6 228 recvfrom

2.71 0.000796 1 671 open

2.54 0.000745 0 2453 fcntl

2.37 0.000696 1 1141 write

1.69 0.000497 1 593 13 access

1.37 0.000403 0 1816 lseek

0.89 0.000262 1 451 22 sendto

0.56 0.000163 1 276 208 lstat

0.49 0.000145 0 384 getcwd

0.31 0.000090 0 1222 fstat

0.28 0.000082 0 173 munmap

0.26 0.000077 0 174 mmap

0.24 0.000069 2 41 socket

0.23 0.000068 0 725 close

0.00 0.000000 0 13 rt_sigaction

0.00 0.000000 0 13 rt_sigprocmask

0.00 0.000000 0 1 rt_sigreturn

0.00 0.000000 0 78 setitimer

0.00 0.000000 0 26 26 connect

0.00 0.000000 0 15 2 accept

0.00 0.000000 0 39 recvmsg

0.00 0.000000 0 26 shutdown

0.00 0.000000 0 13 bind

0.00 0.000000 0 13 getsockname

0.00 0.000000 0 65 setsockopt

0.00 0.000000 0 13 getsockopt

0.00 0.000000 0 8 getdents

0.00 0.000000 0 26 chdir

0.00 0.000000 0 1 futex


100.00 0.029344 18000 986 total

4.使用Opcode快取(www.cnblogs.com/JohnABC/p/4531038.h...)

5.對PHP效能進行監控

  常用的方法就是開啟xdebug的效能監控功能,將xdebug輸出結果通過WinCacheGrind軟體分析。

  xdebug的安裝和配合IDE除錯的方法參見:Vim+XDebug除錯PHP

  php.ini中配置的這幾項是輸出效能資訊的:

xdebug.auto_trace = on
xdebug.auto_profile = on
xdebug.collect_params = on
xdebug.collect_return = on
xdebug.profiler_enable = on
xdebug.trace_output_dir = “/tmp”
xdebug.profiler_output_dir =”/tmp”

  這樣XDebug會輸出所有執行php函式的效能資料,但產生的檔案也會比較大。可以關閉一些選項如collect_params、collect_return,

  來減少輸出的資料量。或者關閉自動輸出,通過在想要監控的函式首尾呼叫xdebug函式來監控指定的函式。

  輸出的檔名類似cachegrind.out.1277560600和trace.3495983249.txt,可以拿到Windows平臺下用WinCacheGrind進行圖形化分析。

6.監測php-fpm執行緒狀態

  nginx配置

location ~ ^/status$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
}
  php-fpm配置

pm.status_path = /status
  這樣的話通過http://域名/status就可以看到當前的php情況

  下面介紹每個引數的作用:
  pool:php-fpm池的名稱,一般都是應該是www
  process manage:程式的管理方法,php-fpm支援三種管理方法,分別是static,dynamic和ondemand,一般情況下都是dynamic
  start time:php-fpm啟動時候的時間,不管是restart或者reload都會更新這裡的時間
  start since:php-fpm自啟動起來經過的時間,預設為秒
  accepted conn:當前接收的連線數
  listen queue:在佇列中等待連線的請求個數,如果這個數字為非0,那麼最好增加程式的fpm個數
  max listen queue:從fpm啟動以來,在佇列中等待連線請求的最大值
  listen queue len:等待連線的套接字佇列大小
  idle processes:空閒的程式個數
  active processes:活動的程式個數
  total processes:總共的程式個數
  max active processes:從fpm啟動以來,活動程式的最大個數,如果這個值小於當前的max_children,可以調小此值
  max children reached:當pm嘗試啟動更多的程式,卻因為max_children的限制,沒有啟動更多程式的次數。如果這個值非0,那麼可以適當增加fpm的程式數
  slow requests:慢請求的次數,一般如果這個值未非0,那麼可能會有慢的php程式,一般一個不好的mysql查詢是最大的禍首。

7.開啟php-fpm慢日誌

  slowlog = /usr/local/php/log/php-fpm.log.slow

  request_slowlog_timeout = 5s

8.設定php-fpm單次請求最大執行時間,今天碰到一個問題,測試伺服器php-fpm一直是被佔滿狀態,後來發現是set_time_limit(0),file_get_content(),原因如下:

  比如file_get_contents(url)等函式,如果網站反應慢,會一直等在那兒不超時,php-fpm一直被佔用。有一個引數 max_execution_time 可以設定 PHP 指令碼的最大執行時間,但是,在 php-cgi(php-fpm) 中,該引數不會起效。真正能夠控制 PHP 指令碼最大執行時間的是 php-fpm.conf 配置檔案中的以下引數。

  request_terminate_timeout = 10s

  預設值為 0 秒,也就是說,PHP 指令碼會一直執行下去。這樣,當所有的 php-cgi 程式都卡在 file_get_contents() 函式時,這臺 Nginx+PHP 的 WebServer 已經無法再處理新的 PHP 請求了,Nginx 將給使用者返回“502 Bad Gateway”。可以使用 request_terminate_timeout = 30s,但是如果發生 file_get_contents() 獲取網頁內容較慢的情況,這就意味著 150 個 php-cgi 程式,每秒鐘只能處理 5 個請求,WebServer 同樣很難避免“502 Bad Gateway”。php-cgi程式數不夠用、php執行時間長、或者是php-cgi程式死掉,都會出現502錯誤。

  要做到徹底解決,只能讓 PHP 程式設計師們改掉直接使用 file_get_contents(“http://example.com/") 的習慣,而是稍微修改一下,加個超時時間,用以下方式來實現 HTTP GET 請求。要是覺得麻煩,可以自行將以下程式碼封裝成一個函式。

<?php
$ctx = stream_context_create(array(
‘http’ => array(
‘timeout’ => 1 //設定一個超時時間,單位為秒
)
)
);
file_get_contents(“http://example.com/", 0, $ctx);

  當然,導致 php-cgi 程式 CPU 100% 的原因不只有這一種,那麼,怎麼確定是 file_get_contents() 函式導致的呢?

  首先,使用 top 命令檢視 CPU 使用率較高的 php-cgi 程式。

top - 10:34:18 up 724 days, 21:01, 3 users, load average: 17.86, 11.16, 7.69
Tasks: 561 total, 15 running, 546 sleeping, 0 stopped, 0 zombie
Cpu(s): 5.9%us, 4.2%sy, 0.0%ni, 89.4%id, 0.2%wa, 0.0%hi, 0.2%si, 0.0%st
Mem: 8100996k total, 4320108k used, 3780888k free, 772572k buffers
Swap: 8193108k total, 50776k used, 8142332k free, 412088k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10747 www 18 0 360m 22m 12m R 100.6 0.3 0:02.60 php-cgi
10709 www 16 0 359m 28m 17m R 96.8 0.4 0:11.34 php-cgi
10745 www 18 0 360m 24m 14m R 94.8 0.3 0:39.51 php-cgi
10707 www 18 0 360m 25m 14m S 77.4 0.3 0:33.48 php-cgi
10782 www 20 0 360m 26m 15m R 75.5 0.3 0:10.93 php-cgi
10708 www 25 0 360m 22m 12m R 69.7 0.3 0:45.16 php-cgi
10683 www 25 0 362m 28m 15m R 54.2 0.4 0:32.65 php-cgi
10711 www 25 0 360m 25m 15m R 52.2 0.3 0:44.25 php-cgi
10688 www 25 0 359m 25m 15m R 38.7 0.3 0:10.44 php-cgi
10719 www 25 0 360m 26m 16m R 7.7 0.3 0:40.59 php-cgi

  找其中一個 CPU 100% 的 php-cgi 程式的 PID,用以下命令跟蹤一下:

strace -p 10747
  如果螢幕顯示:

select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)
select(7, [6], [6], [], {15, 0}) = 1 (out [6], left {15, 0})
poll([{fd=6, events=POLLIN}], 1, 0) = 0 (Timeout)

  那麼,就可以確定是 file_get_contents() 導致的問題了

本作品採用《CC 協議》,轉載必須註明作者和本文連結