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

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

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


你可能還喜歡
點擊下方圖片即可閱讀

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


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


