用 StatefulSet 實(shí)現(xiàn)同步服務(wù)和異步服務(wù)的管理

一個(gè)服務(wù)里既有同步邏輯又有異步邏輯是非常常見的事,比如可以通過 Http 接口調(diào)用服務(wù)或者通過消息隊(duì)列傳遞消息來實(shí)現(xiàn)同樣的服務(wù)邏輯,同一套代碼邏輯區(qū)別只是入口不一樣。在 Golang 服務(wù)里我們用 goroutine 非常容易的起兩個(gè)入口;在 Python 服務(wù)里我們可以用多進(jìn)程也容易實(shí)現(xiàn)。
現(xiàn)在我們看另外一種場(chǎng)景,服務(wù)同樣需要對(duì)外提供同步和異步入口,但是服務(wù)本身是計(jì)算密集型的且非常占用資源,典型的例子比如使用 GPU 的推理服務(wù)。首先推理服務(wù)啟動(dòng)時(shí)需要加載模型,而這一般需要很大的顯存可能達(dá)到數(shù)個(gè) G 甚至超過十個(gè) G。但是我們的單張顯卡顯存當(dāng)然是有上限的,就拿 T4 來說顯存只有 15 個(gè) G 可用,所以一個(gè)推理服務(wù)同時(shí)啟動(dòng)同步和異步可能顯存就不夠;影響更嚴(yán)重的一個(gè)問題是即使顯存足夠同時(shí)啟動(dòng)同步和異步服務(wù),但是當(dāng)同步和異步同時(shí)進(jìn)行計(jì)算是,對(duì)顯卡的計(jì)算資源會(huì)存在爭(zhēng)奪的情況,那么這樣計(jì)算速度可能會(huì)大大降低,嚴(yán)重降低了推理服務(wù)的性能。為了解決上述問題一個(gè)很自然的辦法就是同步和異步分別部署一套服務(wù)。對(duì)于一個(gè)這樣的服務(wù)說是沒問題的我們多加一個(gè)服務(wù)節(jié)點(diǎn)、一套 CI/CD 配置、一套服務(wù)配置、一套應(yīng)用配置等就完成了。但是有這樣需求的服務(wù)有多個(gè),并且數(shù)目會(huì)持續(xù)增加,那么分別部署同步和異步的成本和服務(wù)維護(hù)的成本就急劇增加。
在這篇文章里我就分享下我們的解決辦法,如何在一個(gè)服務(wù)里(一個(gè)節(jié)點(diǎn),一套配置)獨(dú)立運(yùn)行同步和異步實(shí)例。(這里的實(shí)例指的是在 K8s 中一個(gè) deployment 里的一個(gè) Pod)。
精確實(shí)例數(shù)量
一個(gè)服務(wù)的總實(shí)例數(shù)目是通過配置定義的。假如我們配置的服務(wù)實(shí)例數(shù)是 10,然后在這 10 個(gè)實(shí)例當(dāng)中一部分是同步實(shí)例,一部分是異步實(shí)例。如何保證這些實(shí)例數(shù)量是按照我們要求分布的呢?
按比例隨機(jī)
假如同步和異步的實(shí)例數(shù)比例是 4:6, 在服務(wù)啟動(dòng)腳本里我們可以生成一個(gè) 1-10 的隨機(jī)整數(shù)然后跟同步實(shí)例數(shù)比例比較,如果小于等于 4 則以同步的方式啟動(dòng)實(shí)例,否則以異步的方式啟動(dòng)。示例腳本如下
??#!/bin/bash
??#?同步實(shí)例數(shù)比例(通過環(huán)境變量注入)
??SYNCS=$SYNC_RATE
??#?默認(rèn)啟動(dòng)同步實(shí)例
??if?[?-z?"$SYNCS"?];?then
????SYNCS=10
??fi
??Rand=$((?(?RANDOM?%?10?)??+?1?))
??if?[?"$Rand"?-le?"$SYNCS"?];?then
????#?啟動(dòng)grpc?server(同步)
????python?/data/app/grpc_server/server.py
??else
????#?啟動(dòng)任務(wù)進(jìn)程(異步)
????python?/data/app/start/__init__.py
??fi
很明顯隨機(jī)啟動(dòng)無法準(zhǔn)確的控制實(shí)例數(shù)量,尤其是總實(shí)例數(shù)目較少的時(shí)候?qū)嵗植计钶^大。
StatefulSet
我們知道在 K8s 中除了常用的服務(wù)形式 Deployment 還有一種比較常用的服務(wù)形式是 StatefulSet, 后者相比前者來說最顯著的一個(gè)區(qū)別是 StatefulSet 下的 Pod 名字是也有序號(hào)的。例如一個(gè) StatefulSet 形式服務(wù)名是 web-service 且有 3 個(gè)實(shí)例,那么 3 個(gè)實(shí)例的名字分別是 web-service-0、web-service-1、web-service-2。我們可以正好利用 StatefulSet 這種 Pod 按編號(hào)順序啟動(dòng)的方式來控制同步和異步實(shí)例啟動(dòng)的數(shù)量。思路就是比如我們?cè)O(shè)置好了同步實(shí)例的數(shù)量,那么在每次實(shí)例啟動(dòng)的時(shí)候我們可以根據(jù)實(shí)例的編號(hào)和同步實(shí)例數(shù)做比較,如果編號(hào)比同步數(shù)小那么就以同步的方式啟動(dòng)實(shí)例,反之則以異步的方式啟動(dòng)。示例腳本如下
??#!/bin/bash
??#?pod?編號(hào)
??POD_NUM=`echo?${POD_NAME}?|?awk?-F'-'?'{print?$NF}'`
??#?同步實(shí)例數(shù)(通過環(huán)境變量注入)
??SYNCS=$SYNC_INSTANCE
??#?默認(rèn)同步方式(INSTANCE是期望的總實(shí)例數(shù))
??if?[?-z?"$SYNCS"?];?then
????SYNCS=$INSTANCE
??fi
??if?[?"$POD_NUM"?-lt?"$SYNCS"?];?then
????#?啟動(dòng)grpc?server(同步)
????python?/data/app/grpc_server/server.py
??else
????#?啟動(dòng)任務(wù)進(jìn)程(異步)
????python?/data/app/start/__init__.py
??fi
所以通過上面的描述我們看到可以利用 StatefulSet 來精確控制同步和異步實(shí)例數(shù)量。
容器多進(jìn)程管理
在我們上面的例子中因?yàn)榉?wù)同時(shí)包含了同步實(shí)例和異步實(shí)例,同步實(shí)例有暴露端口的需求而異步實(shí)例是沒有對(duì)外暴露端口的,這樣帶來的矛盾就是如果服務(wù)配置了端口,則我們的基礎(chǔ)平臺(tái)會(huì)對(duì)異步服務(wù)配置的端口健康檢查和服務(wù)注冊(cè),結(jié)果就是異步服務(wù)必然健康檢查失敗而啟動(dòng)失敗。為了繞過基礎(chǔ)平臺(tái)的功能我們決定對(duì)服務(wù)不配置暴露端口,同步服務(wù)自己實(shí)現(xiàn)服務(wù)注冊(cè)的功能。即我們?cè)谕椒?wù)中除了啟動(dòng)服務(wù)進(jìn)程之外,再啟動(dòng)一個(gè)服務(wù)注冊(cè)管理進(jìn)程,實(shí)現(xiàn)服務(wù)注冊(cè)、服務(wù)心跳和服務(wù)注銷功能。上面這個(gè)情況比較特殊,可能其他同學(xué)并不會(huì)遇到,我們拿這個(gè)例子是為了說明在 K8s 容器中有多個(gè)進(jìn)程該怎么管理。上面說到除了服務(wù)進(jìn)程之外,容器中還存在一個(gè)服務(wù)注冊(cè)進(jìn)程,這個(gè)進(jìn)程必須實(shí)現(xiàn)在容器銷毀的時(shí)候必須向注冊(cè)中心注銷服務(wù)實(shí)例。在 K8s 中,Pod 停止時(shí) kubelet 會(huì)先給容器中的主進(jìn)程發(fā) SIGTERM 信號(hào)來通知進(jìn)程進(jìn)行 shutdown 以實(shí)現(xiàn)優(yōu)雅停止,如果超時(shí)進(jìn)程還未完全停止則會(huì)使用 SIGKILL 來強(qiáng)行終止。但是在我們的場(chǎng)景中我們的業(yè)務(wù)進(jìn)程是在腳本中啟動(dòng)的,容器的啟動(dòng)入口使用了腳本,所以容器中的主進(jìn)程并不是我們所希望的業(yè)務(wù)進(jìn)程而是 shell 進(jìn)程,導(dǎo)致業(yè)務(wù)進(jìn)程收不到 SIGTERM 信號(hào),自然就無法實(shí)現(xiàn)主動(dòng)注銷服務(wù)。我們利用 trap 來實(shí)現(xiàn)
trap 捕捉信號(hào)
通常 trap 都在腳本中使用,主要有 2 種功能:
忽略信號(hào)。當(dāng)運(yùn)行中的腳本進(jìn)程接收到某信號(hào)時(shí)(例如誤按了 CTRL+C),可以將其忽略,免得腳本執(zhí)行到一半就被終止 捕捉到信號(hào)后做相應(yīng)處理,比如清理創(chuàng)建的臨時(shí)文件
常用信號(hào)
Signal?????Value???Comment
─────────────────────────────
SIGHUP??????1??????終止進(jìn)程,特別是終端退出時(shí),此終端內(nèi)的進(jìn)程都將被終止
SIGINT??????2??????中斷進(jìn)程,幾乎等同于sigterm,會(huì)盡可能的釋放執(zhí)行clean-up,
???????????????????釋放資源,保存狀態(tài)等(CTRL+C)
SIGQUIT?????3??????從鍵盤發(fā)出殺死(終止)進(jìn)程的信號(hào)
SIGKILL?????9??????強(qiáng)制殺死進(jìn)程,該信號(hào)不可被捕捉和忽略,進(jìn)程收到該信號(hào)后不會(huì)
???????????????????執(zhí)行任何clean-up行為,所以資源不會(huì)釋放,狀態(tài)不會(huì)保存
SIGTERM????15??????殺死(終止)進(jìn)程,幾乎等同于sigint信號(hào),會(huì)盡可能的釋放執(zhí)行
???????????????????clean-up,釋放資源,保存狀態(tài)等
SIGSTOP????19??????該信號(hào)是不可被捕捉和忽略的進(jìn)程停止信息,收到信號(hào)后會(huì)進(jìn)入stopped狀態(tài)
SIGTSTP????20??????該信號(hào)是可被忽略的進(jìn)程停止信號(hào)(CTRL+Z)
trap 使用
trap?[-lp]?[[arg]?signal_spec?...]
-l ???打印信號(hào)名稱以及信號(hào)名稱對(duì)應(yīng)的數(shù)字。
-p????顯示與每個(gè)信號(hào)關(guān)聯(lián)的trap命令。
參數(shù)
arg:接收到信號(hào)時(shí)執(zhí)行的命令或函數(shù)
signal_spec:信號(hào)名稱或信號(hào)名稱對(duì)應(yīng)的數(shù)字
通過上述介紹之后我們知道給容器多進(jìn)程傳遞信號(hào)方式為:可以在 shell 中使用 trap 來捕獲信號(hào),當(dāng)收到信號(hào)后觸發(fā)回調(diào)函數(shù)來將信號(hào)通過 kill 傳遞給業(yè)務(wù)進(jìn)程。示例腳本如下
#!/bin/bash
#?pod?編號(hào)
POD_NUM=`echo?${POD_NAME}?|?awk?-F'-'?'{print?$NF}'`
#?同步實(shí)例數(shù)
SYNCS=$SYNC_INSTANCE
if?[?-z?"$SYNCS"?];?then
??SYNCS=$INSTANCE
fi
if?[?"$POD_NUM"?-lt?"$SYNCS"?];?then
??#?啟動(dòng)grpc?server
??python?/data/app/grpc_server/server.py?&?pid1="$!"
??python?/data/app/grpc_server/register.py?&?pid2="$!"
??handle_sigterm()?{
????echo?"[INFO]?Received?SIGTERM"
????kill?-SIGTERM?$pid1?$pid2?#?傳遞?SIGTERM?給業(yè)務(wù)進(jìn)程
????wait?$pid1?$pid2?#?等待所有業(yè)務(wù)進(jìn)程完全終止
??}
??trap?handle_sigterm?TERM?#?捕獲?SIGTERM?信號(hào)并回調(diào)?handle_sigterm?函數(shù)
??wait?#?等待回調(diào)執(zhí)行完,主進(jìn)程再退出
else
??#?啟動(dòng)任務(wù)進(jìn)程
??python?/data/app/start/__init__.py
fi
下圖是實(shí)現(xiàn)的效果

原文鏈接:https://lxkaka.wang/service-manage/


你可能還喜歡
點(diǎn)擊下方圖片即可閱讀

云原生是一種信仰???
關(guān)注公眾號(hào)
后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點(diǎn)擊?"閱讀原文"?獲取更好的閱讀體驗(yàn)!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


