PHP-FPM reload 真的有優雅嗎?

# 前言

其實這篇文章跟 OPcache 沒什麼太大的關係, 但卻是做這個實驗的根本原因!




# OPcache

OPcache 可以對於 PHP 效能優化有著非常顯著的效果, 而有 cache 就有 clear 的問題。 在每一次更新程式碼後, 都必須 clear 舊的 cache。
然而 clear cache 有以下幾種方法:

  • 特別製作一支 API, 在裡面執行 opcache_reset()
  • 使用套件, 這樣可以透過 CLI 清除 cache, 不過套件具體原理也是跟第一個一樣, 只不過有實作了 CLI 出來
  • reload PHP-FPM




# PHP-FPM

PHP-FPM 為 FastCGI Process Manager, 簡單來說, 這個 Manager 也是一個 Process, 而它會 fork 複數的 child FastCGI process 當收到 request 時, 便分派任務給底下的 child process 處理。
故事的起源, 都來自於 這篇 Stack Overflow

文中提到, 如果使用 reload 參數, 便可以 gracefully restart PHP-FPM。 那這又跟今天的故事有什麼關係呢?
上面提到三個方法, 毫無疑問 reload PHP-FPM 是最簡單的, 但同時它也有著一個致命的缺點, 那就是當 reload 時, 所有 processed 中的 request 無可避免的都會中斷。 但根據這篇文章描述, 如果可以不中斷的 restart PHP-FPM 的話似乎是個 zero downtime 的最佳解啊!

高能大大先別噴, 我知道還有 rolling update 這東西, 不過這不在今天的討論範圍內, 乾蝦!

然而未經過自己實驗的解法, 真要拿到 production 上面用, 我心裡會毛毛的。 於是我 Google 了一下, 果然發現一些不一樣的聲音:

於是就開始了今天的實驗…




# Docker 環境測試

首先我使用了 Docker container 做測試, 我當然不是個空口說白話的人, 附上測試 docker-compose.yaml 檔

version: "3"
services:
nginx:
restart: unless-stopped
image: nginx:latest
container_name: nginx
networks:
- app-network
ports:
- 8888:80
- 9999:443
php74:
image: php:7.4.3-fpm
container_name: php74
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge

由於測試過程比較簡單, 我就沒 mount volume 出來了

  • 首先先進到 php-fpm container

    docker exec -it phpFpmContainerId /bin/bash
  • 緊接著修改 php-fpm 的 config

    vim /usr/local/etc/php-fpm.conf.default

哦哦, 對了如果你跟著照做發現 vim command not found 的話, 記得自己安裝啊哈哈

  • 然後 reload PHP-FPM
    kill -usr2 1

因為 kill -usr2 是源碼中, 當使用 reload 時所使用的 command, 可以參考 源碼
當然, 也因為在 container 中無法直接使用 systemctl reload serviceName

  • 接著將 process_control_timeout 改成 30s, 會修改這行是因為預設是 0s, 我修改是為了確認到底有沒有作用
  • 然後進到 nginx 的 container, 同上, 換個 container ID 而已, 我就不多加贅述
  • 然後改一下 default 的 config, 這不是個好的做法, 我一般習慣在 sites-available 寫好 config, 然後在 soft link 到 site-enabled, 不過只是為了測試就將就啦~

    vim /etc/nginx/conf.d/default.conf
  • 然後使用以下設定

    server {
    listen 80;

    index index.php index.html;

    error_log /var/log/nginx/error.log;

    access_log /var/log/nginx/access.log;

    root /var/www/;

    location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php74:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
    try_files $uri $uri/ /index.php?$query_string;
    gzip_static on;
    }
    }
  • 再來重啟 nginx

    nginx -s reload
  • 最後, 把以下的 index.php file 複製到上面兩個 container 的 /var/www 目錄下

    <?php
    sleep(60);
    echo 'Hello World';
  • 測試的時候到了! 靠, 我都已經測過了, 還興奮個屁啊!

    curl localhost:8888

恩恩, 有進入 sleep 的階段

  • 接著 reload PHP-FPM
    docker exec -it containerId kill -USR2 1

結果你會發現, connection 斷開了!斷開了!斷開了! 因為很震驚, 所以打了三次!




# GCP VM 測試

不信邪的我, 立馬在 GCP 上面開了一台小 VM 做測試, 方式跟 Docker 大同小異我就不多加贅述了, 結果是
還是斷開了!
還是斷開了!
還是斷開了!

因為很失望所以要說三次…




# 暴風雨後的曙光?

原本傷心欲絕, 悲憤交加的我, 在前往自我了斷的路上… 想說發個文在 FB 警醒一下世人好了, 以防有跟我一樣想不開的人… 唉, 沒有希望就不會失望啊!

誰知有個大大給個提醒, 會不會是 PHP 的 sleep() function 的問題? 或許可以試試 select sleep(30) ?

此話當如久旱後的甘霖, 讓我放下了手中的菜刀… 想說不然試一試好了, 反正要了斷也不差這些時間, 然後冒險繼續 ing

  • 更新 index.php 內容

    <?php
    $hostname="localhost";
    $username="test";
    $password="test";
    $dbname="test";

    $connection = mysqli_connect($hostname,$username, $password) or die ("html>script language='JavaScript'>alert('無法連線至資料庫!請稍後再重試一次。'),history.go(-1)/script>/html>");

    $query = "SELECT sleep(10)";
    $result = mysqli_query($connection, $query);
    ?>
  • 接著在 reload PHP-FPM, 然後抱著破罐子破摔的心情, 再給他發了一次 request

It worked!! It worked!!

  • 我一邊擦著我喜極而泣的淚水, 嗯…, 還有鼻水, 一邊接著測試, 因為我想要知道 reload 到底有多優雅?




# 窺探 reload 的優雅程度

  • 首先, 在發 request 之前, 我先觀察 process 狀態

    ps -ax | grep php-fpm
  • 然後在 request 處理完畢後, 我再觀察一次

  • 反覆觀察幾次之後, 終於探得 reload 的優雅程度
  • 蛤? 你還在這啊? 那直接看結論吧!




# 結論

  • 文中提到的 timeout 如果比 request 的時間還短, 那 timeout 時間到了, FPM 會強制的殺掉所有 process, 立馬跳 502 給你看
  • 在下達 reload 之後, PHP-FPM 會逐步的殺掉 process, 如果還沒完成的, PHP-FPM 會等待他們完成, 當然, 最多等待 timeout 的時間
  • 在所有 reload 之前的 request 都處理完之前, PHP-FPM 不會開啟新的 child process, 那你問我與此同時新的 request 怎麼辦? 好問題! PHP-FPM 會 queue 他們, 但不處理
  • 直到所有的 request 都處理完了, 才會啟動新的 reload 過的 process 開始處理之前 queue 裡的 request

這種行為算是有某種程度上的優雅, 但也不算很優雅。 雖然可以避免 request 被強制斷開, 但在新的 process 啟動之前, 所有的 request 都是被 queue 住的, 這同時也衍伸一些問題…

  • Server 能支援 queue 的 request 最大數量?
  • queue 住 request, 這表示在使用者端, 畫面是整個卡住的, 雖不強制關閉, 但算是採用延遲處理

至此, 本次實驗也告一段落, 也算是得知 PHP-FPM reload 採用得像是 k8s 中的 recreate policy, 而不是 rolling update, 實在不能說沒有 downtime (卡住算不算 downtime?)
好啦, 希望本篇文章對你有幫助! 我們下次見!

Insertion Sort (插入排序法) In PHP Laravel - Documentation - 目錄 (官方文件原子化翻譯)

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×