Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能

在 Kubernetes 中實施 CI/CD 時的典型條件:應用程式必須能夠在完全停止之前不接受新的客戶端請求,最重要的是,成功完成現有請求。

Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能

滿足此條件可以讓您在部署期間實現零停機。 然而,即使使用非常流行的捆綁包(例如 NGINX 和 PHP-FPM),您也可能會遇到困難,導致每次部署時出現大量錯誤...

理論。 Pod 如何生活

我們已經發布了有關 pod 生命週期的詳細信息 本文。 在所考慮的主題背景下,我們感興趣的是以下內容:pod 進入狀態的那一刻 終止,新請求停止發送給它(pod 已刪除 來自服務的端點清單)。 因此,為了避免部署過程中出現停機,我們只要解決正確停止應用程式的問題就足夠了。

您還應該記住,預設寬限期是 30秒:在此之後,pod 將被終止,應用程式必須有時間處理在此期間之前的所有請求。 注意:雖然任何需要超過 5-10 秒的請求已經是有問題的,並且優雅的關閉將不再有幫助...

為了更好地理解 pod 終止時會發生什麼,只需查看下圖:

Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能

A1、B1 - 接收有關爐床狀態的變化
A2 - 出發 SIGTERM
B2 - 從端點刪除 Pod
B3 - 接收變更(端點清單已變更)
B4 - 更新 iptables 規則

請注意:刪除端點 pod 和發送 SIGTERM 不是按順序發生的,而是並行發生的。 並且由於 Ingress 不會立即收到更新的 Endpoints 列表,來自客戶端的新請求將被發送到 pod,這將在 pod 終止期間導致 500 錯誤 (有關此問題的更詳細資料,我們 翻譯的)。 這個問題需要透過以下方式解決:

  • 發送連線:在回應標頭中關閉(如果這涉及 HTTP 應用程式)。
  • 如果無法變更程式碼,則以下文章介紹了一種解決方案,可讓您在寬限期結束之前處理請求。

理論。 NGINX 和 PHP-FPM 如何終止其進程

NGINX

讓我們從 NGINX 開始,因為一切或多或少都是顯而易見的。 深入研究這個理論,我們了解到 NGINX 有一個主進程和多個「工作進程」——這些是處理客戶端請求的子進程。 提供了一個方便的選項:使用指令 nginx -s <SIGNAL> 以快速關閉或正常關閉模式終止進程。 顯然,我們感興趣的是後一種選擇。

那麼一切都很簡單:你需要添加到 前停止鉤子 將發送正常關閉訊號的命令。 這可以在部署中的容器區塊中完成:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

現在,當 Pod 關閉時,我們將在 NGINX 容器日誌中看到以下內容:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

這將意味著我們需要的:NGINX 等待請求完成,然後終止進程。 然而,下面我們還將考慮一個常見問題,由於該問題,即使使用命令 nginx -s quit 該進程錯誤終止。

到了這個階段,我們已經完成了 NGINX:至少從日誌中你可以了解到一切都正常運作。

PHP-FPM 有什麼關係? 它如何處理優雅關閉? 讓我們弄清楚一下。

PHP-FPM

對於 PHP-FPM,資訊有點少。 如果你專注於 官方手冊 根據 PHP-FPM,它會說接受以下 POSIX 訊號:

  1. SIGINT, SIGTERM — 快速關閉;
  2. SIGQUIT - 優雅的關閉(我們需要的)。

其餘訊號在此任務中不需要,因此我們將省略它們的分析。 要正確終止進程,您需要編寫以下 preStop 掛鉤:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

乍一看,這就是在兩個容器中執行正常關閉所需的全部操作。 然而,這項任務比看起來更困難。 以下是兩種情況,在部署過程中,正常關閉不起作用並導致專案短期不可用。

實踐。 正常關閉可能出現的問題

NGINX

首先,記住:除了執行命令之外,這是有用的 nginx -s quit 還有一個階段值得關注。 我們遇到了一個問題,NGINX 仍然會發送 SIGTERM 而不是 SIGQUIT 訊號,導致請求無法正確完成。 類似的案例還可以找到,例如 這裡。 不幸的是,我們無法確定此行為的具體原因:對 NGINX 版本有懷疑,但尚未得到證實。 症狀是在 NGINX 容器日誌中觀察到訊息: “打開連接 10 中剩餘的套接字 #5”,之後吊艙停止了。

我們可以觀察到這樣一個問題,例如,從我們需要的Ingress上的回應:

Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能
部署時的狀態碼指示

在這種情況下,我們只收到來自 Ingress 本身的 503 錯誤代碼:它無法存取 NGINX 容器,因為它不再可存取。 如果您使用 NGINX 檢視容器日誌,它們包含以下內容:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

更改停止訊號後,容器開始正確停止:不再觀察到 503 錯誤這一事實證實了這一點。

如果您遇到類似的問題,那麼弄清楚容器中使用的停止訊號以及 preStop 鉤子到底是什麼樣子是有意義的。 原因很可能正是在這裡。

PHP-FPM...以及更多

PHP-FPM 的問題可以用一個簡單的方式來描述:它不會等待子進程完成,而是終止子進程,這就是為什麼在部署和其他操作期間會出現 502 錯誤。 自 2005 年以來,bugs.php.net 上有多個錯誤報告(例如 這裡 и 這裡),描述了這個問題。 但您很可能不會在日誌中看到任何內容:PHP-FPM 將宣布其過程完成,沒有任何錯誤或第三方通知。

值得澄清的是,問題本身可能或多或少取決於應用程式本身,並且可能不會表現出來,例如在監控中。 如果您確實遇到它,首先想到一個簡單的解決方法:添加一個 preStop 鉤子 sleep(30)。 它將允許您完成之前的所有請求(並且我們不接受新的請求,因為 pod 已經 能夠 終止),30 秒後,pod 本身將結束並發出訊號 SIGTERM.

事實證明, lifecycle 容器將如下所示:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

但由於30秒 sleep 我們 非常 我們將增加部署時間,因為每個 Pod 都會終止 最低限度 30秒,這很糟糕。 關於這個還能做什麼?

讓我們轉向負責直接執行應用程式的一方。 在我們的例子中是 PHP-FPM預設不監控其子進程的執行:master進程立即終止。 您可以使用指令來變更此行為 process_control_timeout,它指定子進程等待來自主進程的訊號的時間限制。 如果將該值設為 20 秒,這將覆蓋容器中執行的大部分查詢,並在完成後停止主進程。

有了這些知識,讓我們回到上一個問題。 如前所述,Kubernetes 不是一個整體平台:不同元件之間的通訊需要一些時間。 當我們考慮 Ingress 和其他相關元件的操作時尤其如此,因為由於部署時的延遲,很容易出現 500 個錯誤激增的情況。 例如,在向上游發送請求的階段可能會發生錯誤,但組件之間互動的「時滯」非常短——不到一秒。

因此, 總共 與已經提到的指令 process_control_timeout 您可以使用以下結構 lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

在這種情況下,我們將使用命令補償延遲 sleep 並且不要大幅增加部署時間:30秒和XNUMX秒之間有明顯的區別嗎?..事實上,這是 process_control_timeoutlifecycle 僅用作滯後情況下的「安全網」。

一般來說 所描述的行為和相應的解決方法不僅適用於 PHP-FPM。 使用其他語言/框架時可能會以某種方式出現類似的情況。 如果您無法透過其他方式修復正常關閉(例如,透過重寫程式碼以便應用程式正確處理終止訊號),您可以使用所描述的方法。 它可能不是最美麗的,但它確實有效。

實踐。 負載測試以檢查 pod 的運作情況

負載測試是檢查容器如何運作的方法之一,因為此過程使其更接近使用者造訪網站時的真實戰鬥條件。 要測試上述建議,您可以使用 Yandex.Tankom:它完美地滿足了我們所有的需求。 以下是透過 Grafana 和 Yandex.Tank 本身的圖表根據我們的經驗進行測試的清晰範例的提示和建議。

這裡最重要的是 逐步檢查更改。 新增修復後,執行測試並查看結果與上次運行相比是否發生了變化。 否則,將很難識別無效的解決方案,從長遠來看,它只會造成損害(例如,增加部署時間)。

另一個細微差別是在終止期間查看容器日誌。 那裡有記錄正常關機的資訊嗎? 存取其他資源(例如,存取相鄰的 PHP-FPM 容器)時日誌中是否有任何錯誤? 應用程式本身存在錯誤(如上述 NGINX 的情況)? 我希望本文的介紹資訊能夠幫助您更好地了解容器在終止期間會發生什麼。

因此,第一次測試運行是在沒有 lifecycle 並且無需應用程式伺服器的附加指令(process_control_timeout 在 PHP-FPM 中)。 此測試的目的是確定錯誤的大致數量(以及是否存在錯誤)。 此外,從其他資訊中,您應該知道每個 Pod 的平均部署時間約為 5-10 秒,直到完全準備就緒。 結果是:

Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能

Yandex.Tank 資訊面板顯示 502 個錯誤激增,這些錯誤發生在部署時,平均持續長達 5 秒。 大概這是因為舊 pod 終止時現有的請求也被終止。 此後,出現了 503 錯誤,這是 NGINX 容器停止的結果,容器也因後端而斷開了連接(這阻止了 Ingress 連接到它)。

讓我們看看如何 process_control_timeout PHP-FPM中的將幫助我們等待子進程的完成,即糾正此類錯誤。 使用此指令重新部署:

Kubernetes 提示與技巧:NGINX 和 PHP-FPM 中優雅關閉的功能

第 500 次部署期間沒有再出現錯誤! 部署成功,正常關閉。

然而,值得記住 Ingress 容器的問題,由於時間滯後,我們可能會收到一小部分錯誤。 為了避免它們,剩下的就是添加一個結構 sleep 並重複部署。 然而,在我們的特定情況下,沒有看到任何變化(同樣,沒有錯誤)。

結論

為了優雅地終止進程,我們期望應用程式出現以下行為:

  1. 等待幾秒鐘,然後停止接受新連線。
  2. 等待所有請求完成並關閉所有未執行請求的保持活動連線。
  3. 結束你的進程。

然而,並非所有應用程式都能以這種方式運作。 Kubernetes 現實中的問題的一種解決方案是:

  • 添加一個將等待幾秒鐘的預停止鉤子;
  • 研究我們後端的設定檔以取得適當的參數。

NGINX 的範例清楚地表明,即使應用程式最初應該正確處理終止訊號,也可能無法這樣做,因此在應用程式部署期間檢查 500 錯誤至關重要。 這也使您能夠更廣泛地看待問題,而不是專注於單一 Pod 或容器,而是將整個基礎架構視為整體。

作為測試工具,您可以將 Yandex.Tank 與任何監控系統結合使用(在我們的範例中,資料是從具有 Prometheus 後端的 Grafana 取得用於測試)。 在基準測試可能產生的重負載下,正常關閉的問題顯而易見,並且監控有助於在測試期間或測試後更詳細地分析情況。

針對文章的回饋:值得一提的是,這裡描述的問題和解決方案與 NGINX Ingress 相關。 對於其他情況,還有其他解決方案,我們可以在該系列的以下材料中考慮。

聚苯乙烯

K8s 提示和技巧系列中的其他內容:

來源: www.habr.com

添加評論