加載速度慢如牛?揭秘如何找出Wordpress網站加載緩慢的原因!

Posted by Y Cheung on Fri, May 24, 2024

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