最后更新時間 2023-01-24.
背景
筆者所在公司技術棧為 Golang + PHP,目前部分項目已經逐步轉 Go 語言重構,部分 PHP 業(yè)務短時間無法用 Go 重寫。
相比 Go 語言,互聯(lián)網公司常見的 Nginx + PHP-FPM 模式,經常會出現(xiàn)性能問題——
特別是我們的活動業(yè)務,盡管底層用了鳥哥的 Yaf 框架,
但由于業(yè)務邏輯繁重,即使框架層面上完全零損耗,常常支撐不了流量高峰。
一旦某個時間段開啟活動,虛擬機的擴容真的非常痛苦。
SRE、開發(fā)、QA 三方經常需要因為某個運營活動的進行,提前壓測預估容量。
目前活動業(yè)務已經逐步用 Go 語言改造,此處不具體展開,防止跑題哈哈。
正因為 PHP 虛擬機模式,每次擴容需要流量剔除、克隆、操作負載均衡、驗證流量等等,
推進 PHP 容器化就顯得格外重要。
公司在去年年中,已經開始進行 PHP 容器化,不過由于項目優(yōu)先級以及人力原因,進度較為遲緩。
事情經過
- 某項目進行 PHP 容器化改造,切換少許流量到容器中
- 逐步加大灰度流量
- 某一天開發(fā)上線新功能,發(fā)現(xiàn)滾動部署過程中存在 502 錯誤
分析原因
nginx 發(fā)生了 502,很多時候是后端,也就是 php-fpm 不在工作。
我們的 PHP 業(yè)務的 Pod,由以下 5 個容器組成:
- nginx
- php-fpm
- metric(監(jiān)控)
- jaeger(鏈路追蹤)
- log(日志收集)
滾動時存在關閉舊 Pod 啟動新 Pod 的過程,借助 K8s 官方文檔 的描述,我們看看 Pod 結束的一個例子:
- 你使用 kubectl 工具手動刪除某個特定的 Pod,而該 Pod 的體面終止限期是默認值(30 秒)。
- API 服務器中的 Pod 對象被更新,記錄涵蓋體面終止限期在內 Pod 的最終死期,超出所計算時間點則認為 Pod 已死(dead)。 如果你使用 kubectl describe 來查驗你正在刪除的 Pod,該 Pod 會顯示為 "Terminating" (正在終止)。 在 Pod 運行所在的節(jié)點上:kubelet 一旦看到 Pod 被標記為正在終止(已經設置了體面終止限期),kubelet 即開始本地的 Pod 關閉過程。
- 在 kubelet 啟動體面關閉邏輯的同時,控制面會將關閉的 Pod 從對應的 EndpointSlice(和 Endpoints)對象中移除,過濾條件是 Pod 被對應的服務以某 選擇算符選定。 ReplicaSet 和其他工作負載資源不再將關閉進程中的 Pod 視為合法的、能夠提供服務的副本。 關閉動作很慢的 Pod 也無法繼續(xù)處理請求數(shù)據, 因為負載均衡器(例如服務代理)已經在終止寬限期開始的時候將其從端點列表中移除。
- 超出終止寬限期限時,kubelet 會觸發(fā)強制關閉過程。容器運行時會向 Pod 中所有容器內仍在運行的進程發(fā)送 SIGKILL 信號。 kubelet 也會清理隱藏的 pause 容器,如果容器運行時使用了這種容器的話。
- kubelet 觸發(fā)強制從 API 服務器上刪除 Pod 對象的邏輯,并將體面終止限期設置為 0 (這意味著馬上刪除)。
- API 服務器刪除 Pod 的 API 對象,從任何客戶端都無法再看到該對象。
通常情況下,容器運行時會發(fā)送一個 TERM 信號到每個容器中的主進程。很多容器運行時都能夠注意到容器鏡像中 STOPSIGNAL 的值,并發(fā)送該信號而不是 TERM。一旦超出了體面終止限期,容器運行時會向所有剩余進程發(fā)送 KILL 信號,之后 Pod 就會被從 API 服務器上移除。 如果 kubelet 或者容器運行時的管理服務在等待進程終止期間被重啟,集群會從頭開始重試,賦予 Pod 完整的體面終止限期。
所以,我們可以發(fā)現(xiàn):
- nginx、php-fpm 收到 TERM 信號后,不做請求的優(yōu)雅處理,直接強制退出了!強制退出的原因,可以移步這倆文檔:nginx - http://nginx.org/en/docs/control.html、php-fpm - https://linux.die.net/man/8/php-fpm;
- 參考上面的第 3 點,在容器運行時發(fā)送 TERM 信號后,也同時移除 endpoint,此處不是串行的。一旦 endpoint 移除的時間晚了,流量就會剔不干凈,到達了 nginx 后,php-fpm 進程已經退出從而導致 502 的產生。
解決辦法
有了上面的分析,解決起來就方便多了!
查看上面文檔,我們可以了解到,nginx 和 php-fpm 喜歡 QUIT 信號,均可做到 graceful shutdown。
只需要在 Dockerfile 指定 STOPSIGNAL SIGQUIT 即可。
但我記得之前封裝的 php-fpm 鏡像使用的是社區(qū)維護版本,應該加上了才對。
而我看了線上的 Dockerfile,nginx 使用的是社區(qū)維護的,已經配置了 STOPSIGNAL SIGQUIT,沒問題!
但是 PHP 由于之前的 alpine linux 因為監(jiān)控擴展、鏈路追蹤擴展編譯環(huán)境的原因,使用了 CentOS 鏡像。
鏡像的來源都是自己打包的,并沒有指定退出信號!
加上了之后,發(fā)現(xiàn)不會有 502 了!
至此,問題解決。
延伸思考
本來文章到此結束,突然想到線上的 Go 服務會不會有同樣的問題?
想了一下,也好辦!
要么自行處理 TERM 信號,做好優(yōu)雅退出的姿勢!
要么學 nginx,也用 QUIT 信號并做好優(yōu)雅退出處理,Dockerfile 指定 STOPSIGNAL。
至于怎么處理信號,Go 實現(xiàn)起來非常舒服:
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT)
<-ch
// 收到信號了(還可以根據信號類型做不同的處理邏輯),自行處理剩余任務實現(xiàn)優(yōu)雅退出。
嗯,channel 大法好。