WordPress作為一個老牌的CMS系統,有著龐大插件與豐富的社群資源。然而,它自身也是變得相當臃腫。優化WordPress網站的速度已經成為一個老生常談的話題,其解決方法無非就是以下幾種:
- 使用快取插件,如W3 Total Cache、WP Super Cashe 等。
- 優化圖片大小,如TinyPNG壓縮圖片等。
- 啟用瀏覽器快取。
- 啟用Gzip壓縮。
- 始終保持wordpress核心、主題和插件都是最新版本。
- 使用內容傳遞網絡(CDN)服務。
- 停用不需要的插件。
- 優化數據庫。
- 使用輕量級主題。
- 將網站託管在高品質的主機服務上。
1. 修復 PHP-FPM 佔用 RAM 過多
Check的那幾條Y Cheung 平時都有做,但是呢網站速度依然不能秒開。這不最近偶然去host商看了下服務器監控,發現php-fpm的內存佔用十分大,明顯超出Y Cheung的預期。說起來這台服務器已經用了很多年了,升級過一次硬盤空間和一次RAM,現在是2 CPU Cores + 4 GB RAM,host了3個WordPress站點和一些小的靜態網站,勉勉強強吧,也很久沒有維護過服務器了(捂臉),於是就有了這個超出預期的發現。
登上服務器一看,php-fpm process manager 的設置是這樣,難怪RAM cost那麼大……隨便查看一下children process status都是idle的。Y Chueng 猜測這大概就是網站速度總也上不來的原因了。
1pm = static
2pm.max_children = 40
於是將它改回dynamic,如下,max_children沒有變它,因為之前那麼久網站也只是慢而已,不會崩:
1pm = dynamic
2pm.max_children = 40
3pm.start_servers = 8
4pm.min_spare_servers = 5
5pm.max_spare_servers = 15
6pm.max_requests = 500
然後原本W3 Total Cache 的緩存是設置存到RAM上的,現在看來RAM明顯資源不夠,就將其改成緩存到硬盤。插件多了免費的新功能Y Cheung也給用上,一個是Google PageSpeed Insight,一個是插件綁定Cloudflare,可以使每次更新完網站內容後自動通知cloudflare清緩存。
如此,一切看起來都很美麗,很順利,用匿名瀏覽器訪問站點也確實有感的快了。撒花~~
2. 改進網站保存操作的請求比瀏覽操作慢10倍的問題
Y Chueng 很順手的來調整一下內容標籤什麼的,然後就發現問題了。
瀏覽頁面的行為,不管是前台還是網站後台都很快,但是一旦做保存的動作就超慢的,有感的慢。
用工具測試了一下時間,慢了10倍。超誇張的!以前不管什麼操作都慢就沒什麼感覺,現在突然有些操作變快了,一對比就察覺出來了。
2.1. 利用監控工具觀察CPU、RAM、I/O、Database的運行情況
Y Cheung 重頭開始調查,分別確認了CPU、RAM、I/O在兩種情況下表現一致。考慮到可能瓶頸在資料庫的insert/update操作上,又去觀察database的運行情況,也很好,沒問題呀。Hmm…事情變得有趣奇怪起來了。
2.2. 排查網站異常日誌
這次 Y Cheung 從網站日誌開始。首先確認了沒有異常或者錯誤的log,排除是代碼有bug導致的可能性。
2.3. 增加日誌參數,查看http請求過程中的時間指標
友人KIKI建議加下log,看下具體是哪裡慢。雖然不方便動網站代碼無法加log標記,但是這個思路給Y Cheung提了個醒。
網站由用戶瀏覽器發起請求傳輸給服務器,服務器上的Nginx監聽對應的端口接收到了請求,Nginx將請求轉發給php-fpm執行,php-fpm執行完畢後將結果返回給Nginx,再由Nginx返回到用戶的瀏覽器進行渲染。
這台服務器上的WEBSERVER架構是php-fpm + Nginx,因此大致上就是這麼個流程,Y Cheung 要分別確認哪一段或者哪幾段過程慢了。
- 修改php-fpm配置
先看核心執行的php-fpm部分的吧。默認的LOG中沒有所需要的信息,所以要先修改php-fpm的配置文件
/etc/php-fpm/www.conf
,啟用access log,自定義log format用以記錄相應的時間,再啟用slow request的log,設置3秒為指標。
1access.log = /var/log/php-fpm/$pool.access.log
2
3; The access log format.
4; The following syntax is allowed
5; %%: the '%' character
6; %C: %CPU used by the request
7; it can accept the following format:
8; - %{user}C for user CPU only
9; - %{system}C for system CPU only
10; - %{total}C for user + system CPU (default)
11; %d: time taken to serve the request
12; it can accept the following format:
13; - %{seconds}d (default)
14; - %{miliseconds}d
15; - %{mili}d
16; - %{microseconds}d
17; - %{micro}d
18; %e: an environment variable (same as $_ENV or $_SERVER)
19; it must be associated with embraces to specify the name of the env
20; variable. Some exemples:
21; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
22; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
23; %f: script filename
24; %l: content-length of the request (for POST request only)
25; %m: request method
26; %M: peak of memory allocated by PHP
27; it can accept the following format:
28; - %{bytes}M (default)
29; - %{kilobytes}M
30; - %{kilo}M
31; - %{megabytes}M
32; - %{mega}M
33; %n: pool name
34; %o: output header
35; it must be associated with embraces to specify the name of the header:
36; - %{Content-Type}o
37; - %{X-Powered-By}o
38; - %{Transfert-Encoding}o
39; - ....
40; %p: PID of the child that serviced the request
41; %P: PID of the parent of the child that serviced the request
42; %q: the query string
43; %Q: the '?' character if query string exists
44; %r: the request URI (without the query string, see %q and %Q)
45; %R: remote IP address
46; %s: status (response code)
47; %t: server time the request was received
48; it can accept a strftime(3) format:
49; %d/%b/%Y:%H:%M:%S %z (default)
50; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
51; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
52; %T: time the log has been written (the request has finished)
53; it can accept a strftime(3) format:
54; %d/%b/%Y:%H:%M:%S %z (default)
55; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
56; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
57; %u: remote user
58; Default: "%R - %u %t \"%m %r\" %s"
59;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
60access.format = "%t \"%m %r%Q%q\" %s %f ct=%l dur=%{mili}d m=%{kilo}M uc=%{user}C%% sc=%{system}C%%"
61
62; The log file for slow requests
63; Default Value: not set
64; Note: slowlog is mandatory if request_slowlog_timeout is set
65slowlog = /var/log/php-fpm/$pool-slow.log
66
67; The timeout for serving a single request after which a PHP backtrace will be
68; dumped to the 'slowlog' file. A value of '0s' means 'off'.
69; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
70; Default Value: 0
71request_slowlog_timeout = 3
- 修改php配置
在 php.ini
中設置:
1error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
2display_errors = Off
3display_startup_errors = Off
4log_errors = On
5error_log = /var/log/php-fpm/error.log
- 修改nginx配置
在 /etc/nginx/nginx.conf
中設置:
1http {
2
3 limit_req_zone $binary_remote_addr zone=one:20m rate=5r/s;
4
5 log_format upstream_time '$http_cf_connecting_ip - $remote_user [$time_local] "$request" '
6 '$status $body_bytes_sent "$http_referer" '
7 '"$http_user_agent" "$http_x_forwarded_for" '
8 'to:$upstream_addr rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';
9}
10
11server {
12 access_log /var/log/nginx/site-access.log upstream_time if=$logging;
13}
然後Y Cheung得到了這樣的結果:
109/May/2024:13:18:55 +0800 "POST /wp-admin/admin-ajax.php" 200 /var/www/wp-admin/admin-ajax.php ct=159 dur=1333.916 m=20480 uc=26.99% sc=6.00%
2
3103.103.244.137 - - [09/May/2024:13:41:56 +0800] "POST /wp-admin/admin-ajax.php HTTP/2.0" 200 978 "/wp-admin/edit-tags.php?taxonomy=post_tag&orderby=slug&order=asc&paged=5" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "103.103.244.137" to:unix:/var/run/php-fpm/php-fpm.sock rt=11.681 uct="0.000" uht="11.681" urt="11.681"
看起來是php-fpm返回慢。
2.4. strace 診斷 nginx
1 watch -n 1 -d 'ss -a | grep php-fpm'
2 ps aux | grep -i nginx
3 strace -cp 28209
得到結果
1% time seconds usecs/call calls errors syscall
2------ ----------- ----------- --------- --------- ----------------
3 27.39 0.001226 16 74 write
4 18.79 0.000841 10 84 epoll_wait
5 11.86 0.000531 10 50 24 read
6 10.97 0.000491 27 18 connect
7 7.02 0.000314 6 45 close
8 5.45 0.000244 13 18 socket
9 3.73 0.000167 9 18 writev
10 3.02 0.000135 7 18 getsockname
11 2.35 0.000105 5 19 recvfrom
12 1.97 0.000088 4 20 epoll_ctl
13 1.83 0.000082 16 5 3 accept4
14 1.27 0.000057 3 18 getsockopt
15 1.18 0.000053 3 17 readv
16 1.14 0.000051 2 18 ioctl
17 1.05 0.000047 5 8 shutdown
18 0.67 0.000030 5 6 3 open
19 0.18 0.000008 4 2 setsockopt
20 0.13 0.000006 2 3 fstat
21------ ----------- ----------- --------- --------- ----------------
看起來問題還是發生在php-fpm執行上。
2.5. 查看 php-fpm 慢請求日誌
過了段時間查看slow request log,都是跟curl有關的。為什麼只有在POST保存的時候才會慢呢,就是因為在保存的時候要么同步照片去圖片cdn服務器上,要么驗證amp等等,一系列需要curl的動作都是save的時候觸發的,這就讓人感覺保存操作的時候特別慢了。
1[pool www] pid 22398
2script_filename = /var/www/index.php
3[0x00007f7d21213330] curl_exec() /var/www/wp-includes/Requests/src/Transport/Curl.php:204
4[0x00007f7d21213250] request() /var/www/wp-includes/Requests/src/Requests.php:469
5[0x00007f7d21213150] request() /var/www/wp-includes/class-wp-http.php:396
6[0x00007f7d21212fc0] request() /var/www/wp-includes/class-wp-http.php:637
7[0x00007f7d21212f20] get() /var/www/wp-includes/http.php:168
8[0x00007f7d21212e90] wp_remote_get() /var/www/wp-content/plugins/amp/includes/validation/class-amp-validation-manager.php:2280
9[0x00007f7d21212d30] validate_url() /var/www/wp-content/plugins/amp/includes/validation/class-amp-validation-manager.php:2403
10[0x00007f7d21212c80] validate_url_and_store() /var/www/wp-content/plugins/amp/src/Validation/URLValidationProvider.php:107
11[0x00007f7d21212bf0] get_url_validation() /var/www/wp-content/plugins/amp/src/Validation/URLValidationRESTController.php:212
12[0x00007f7d21212ae0] validate_post_url() /var/www/wp-includes/rest-api/class-wp-rest-server.php:1230
13[0x00007f7d21212a00] respond_to_request() /var/www/wp-includes/rest-api/class-wp-rest-server.php:1063
14[0x00007f7d21212900] dispatch() /var/www/wp-includes/rest-api/class-wp-rest-server.php:439
15[0x00007f7d21212740] serve_request() /var/www/wp-includes/rest-api.php:428
16[0x00007f7d212126a0] rest_api_loaded() /var/www/wp-includes/class-wp-hook.php:324
17[0x00007f7d212125c0] apply_filters() /var/www/wp-includes/class-wp-hook.php:348
18[0x00007f7d21212550] do_action() /var/www/wp-includes/plugin.php:565
19[0x00007f7d21212490] do_action_ref_array() /var/www/wp-includes/class-wp.php:418
20[0x00007f7d21212230] parse_request() /var/www/wp-includes/class-wp.php:813
21[0x00007f7d212121a0] main() /var/www/wp-includes/functions.php:1336
22[0x00007f7d21212100] wp() /var/www/wp-blog-header.php:16
2.6. 查看curl請求
拿google.com來做驗證,先ping下看看ip是什麼。
1$ ping www.google.com
2
3PING www.google.com (142.250.198.4) 56(84) bytes of data.
464 bytes from nrt12s58-in-f4.1e100.net (142.250.198.4): icmp_seq=1 ttl=115 time=0.561 ms
564 bytes from nrt12s58-in-f4.1e100.net (142.250.198.4): icmp_seq=2 ttl=115 time=0.639 ms
664 bytes from nrt12s58-in-f4.1e100.net (142.250.198.4): icmp_seq=3 ttl=115 time=0.646 ms
764 bytes from nrt12s58-in-f4.1e100.net (142.250.198.4): icmp_seq=4 ttl=115 time=0.718 ms
8^C
9--- www.google.com ping statistics ---
104 packets transmitted, 4 received, 0% packet loss, time 3015ms
11rtt min/avg/max/mdev = 0.561/0.641/0.718/0.055 ms
然後分別curl域名和ip,此時已經在懷疑dns解析問題了。
1$ curl -s -w "%{time_total}\n" -o /dev/null www.google.com
2
3129.795
4$ curl -s -w "%{time_total}\n" -o /dev/null 142.250.198.4
5
60.039
耗時差別巨大。
修改 /etc/resolver
文件,將DNS設為google和Cloudflare的。
1nameserver 8.8.8.8
2nameserver 8.8.4.4
3nameserver 1.1.1.1
4nameserver 1.0.0.1
然後重新再curl,跟改了dns之前一樣。
百思不得其解,找客服客服說他無法重現問題。朱師傅建議用httpstat
看看。
於是下載了 httpstat 測試請求google
所以DNS resolver是沒問題的,TCP CONNECTION為什麼這麼慢呢?
找了一圈,看到網上有人說跟curl版本有關,好吧,那就更新看看好了。
2.7. 更新CURL版本
在curl下載頁面可以看到centos最新版本是7.76.1,但現在curl 8.8.0都出來了,這台server上的OS太…傳統了。yum update都拿不到新的curl版本,只好手動更新。網上很多說更新軟件包源的,但這台服務器就是連結遠程地址有問題,一直timeout。只好找手動更新curl的辦法,然後找到了這篇《Update CURL to Latest version in CentOS》,根據上面說的操作就好啦。過程中執行./configure
命令的時候,看錯誤提示,缺什麼lib就再裝就好了。
更新完curl後竟然真的就……解決了。這次httpstat訪問的的結果就很正常了。
1 DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer
2[ 4ms | 201ms | 43ms | 46ms | 0ms ]
3 | | | | |
4 namelookup:4ms | | | |
5 connect:205ms | | |
6 pretransfer:248ms | |
7 starttransfer:294ms |
8 total:294ms
撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。
2.8. 写在最后
不過基友老馬指出,可能是TLS1.3 握手沒成功超時 fallback 到 1.2 了 或者 cacert 裡沒有一個證書鏈導致現場 OCSP 去 fetch 證書鏈中的惡一環?hmm… Y Cheung再觀察觀察XD