使用nacos作為springcloud的配置和注冊中心,在k8s上設置容器生命周期鈎子preStop的最佳實踐操作


背景

在生產環境中使用springcloud框架,由於服務更新過程中,容器服務會被直接停止,部分請求仍被分發到終止的容器,導致服務出現500錯誤,這部分錯誤請求數據占用比較少,因為Pod滾動更新都是一對一。因為部分用戶會產生服務器錯誤的情況,考慮使用優雅的終止方式,將錯誤請求降到最低,直至滾動更新不影響用戶。這里結合nacos使用來分析。

nacos心跳檢測時間

Nacos 目前支持臨時實例使用心跳上報方式維持活性,發送心跳的周期默認是 5 秒,Nacos 服務端會在 15 秒沒收到心跳后將實例設置為不健康,在 30 秒沒收到心跳時將這個臨時實例摘除。這里要注意30秒這個時間。

正常更新流程

當更新某一個應用時,先給nacos發送這個模塊下線通知,等待30s中后再更新這個應用。
應用啟動時會自動注冊到nacos中。

引出問題

現在把該應用部署到k8s中,需要實現上面說的正常更新流程。這里就牽涉到使用k8s中的容器生命周期鈎子PreStop。

Kubernetes鈎子函數

PostStart: 這個鈎子在容器創建后立即執行。但是,並不能保證鈎子將在容器ENTRYPOINT之前運行,因為沒有參數傳遞給處理程序。 主要用於資源部署、環境准備等。不過需要注意的是如果鈎子花費時間過長以及於不能運行或者掛起,容器將不能達到Running狀態。

PreStop: 鈎子在容器終止前立即被調用。它是阻塞的,意味着它是同步的,所以它必須在刪除容器的調用出發之前完成。主要用於優雅關閉應用程序、通知其他系統等。如果鈎子在執行期間掛起,Pod階段將停留在Running狀態並且不會達到failed狀態

簡單說一下Pod終止的過程:

  • 用戶發送命令刪除Pod,Pod進入Terminating狀態
  • service摘除Pod節點
  • 當kubelet看到Pod已被標記終止,開始執行preStop鈎子,假如preStop hook的運行時間超過了grace period,kubelet會發送SIGTERM並等2秒

k8s Pod Hook回顧

Pod Hook是由kubelet發起的,當容器中的進程啟動前或者容器中的進程終止之前運行,這是包含在容器的生命周期之中。我們可以同時為Pod中的所有容器都配置hook。
在k8s中,理想的狀態是pod優雅釋放,並產生新的Pod。但是並不是每一個Pod都會這么順利

  • Pod卡死,處理不了優雅退出的命令或者操作
  • 優雅退出的邏輯有BUG,陷入死循環
  • 代碼問題,導致執行的命令沒有效果

對於以上問題,k8s的Pod終止流程中還有一個"最多可以容忍的時間",即grace period (在pod的.spec.terminationGracePeriodSeconds字段定義),這個值默認是30秒,當我們執行kubectl delete的時候也可以通過--grace-period參數顯示指定一個優雅退出時間來覆蓋Pod中的配置,如果我們配置的grace period超過時間之后,k8s就只能選擇強制kill Pod。

Kubernetes等待指定的時間稱為優雅終止寬限期。默認情況下,這是30秒。值得注意的是,這與preStop Hook和SIGTERM信號並行發生。Kubernetes不會等待preStop Hook完成。如果你的應用程序完成關閉並在terminationGracePeriod完成之前退出,Kubernetes會立即進入下一步。

如果您的Pod通常需要超過30秒才能關閉,請確保增加優雅終止寬限期(通過terminationGracePeriodSeconds來實現)

簡單的說Kubernetes終止生命周期的每一步

  • Pod 設置為Terminating狀態,並從所有服務的Endpoints列表中刪除
  • 此時,Pod停止停止,但是Pod中運行的容器不受影響
  • PreStop Hook被執行
  • preStop Hook發送容器特殊命令或者Http請求到Pod中,Pod應用程序在接收到SIGTERM(該SIGTERM信號是用於導致程序終止的通用信號。不同於SIGKILL,該信號可以被阻止,處理和忽略。這是禮貌地要求程序終止的正常方法),如果使用第三方代碼或者管理系統無法控制,則preStop Hook是在不修改應用程序的情況下觸發
  • SIGTERM信號發送給Pod
  • 此時,Kubernetes將向Pod中的容器發送SIGTERM信號,這個信號即通知容器他們很快將進行關閉。
  • Kubernetes等待優雅的終止
  • 此時,Kubernetes等待指定的時間稱為優雅終止寬限期。默認情況下,這是30秒(可以修改),值得注意的是,PreStop Hook和SIGTREM信息是屬於並行執行,Kubernetes不會等待PreStop Hook完成。

如果Pod在terminationGracePeriod完成之前推出,Kubernetes將進如釋放階段,如果容器在優雅終止寬限期(terminationGracePeriod限定時間),則會發送SIGKILL信號並強制刪除。與此同時,所有的Kubernetes對象也會被清除

問題分析

通過以上回顧,可以知道pod退出有個優雅終止寬限期(terminationGracePeriod限定時間),假如preStop hook的運行時間超過了grace period,kubelet會發送SIGTERM並等2秒,Kubernetes不會等待preStop Hook完成。

這里主要涉及到的就是preStop hook的運行時間和優雅終止寬限期(terminationGracePeriod限定時間)。

nacos下線應用地址舉例:

http://192.168.0.218:8848/nacos/v1/ns/instance?serviceName=jdd-parking-cloud-admin&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=172.16.246.32&port=8093&ephemeral=true&weight=1&enabled=false&namespaceId=a9076f8c-a1c7-474c-9ea4-1112677d9af7

說明:

  • 192.168.0.218:8848 nacos注冊地址
  • jdd-parking-cloud-admin 注冊的應用名稱
  • 172.16.246.32 注冊的應用名稱所在主機地址
  • 8093 注冊的應用名稱使用的端口號
  • enabled=false 下線,enabled=true 上線
  • namespaceId 命令空間,默認使用public命名空間則不寫這個

通過分析nacos下線應用地址,需要如下參數:nacos注冊地址,應用名稱,應用所在主機ip,應用端口號,命令空間(public不需要)

考慮到應用所在主機ip是pod ip,這個需要從pod容器中獲取,因此,不能在PreStop中使用命令行的形式,也就是如下的形式

curl -x PUT http://192.168.0.218:8848/nacos/v1/ns/instance?serviceName=jdd-parking-cloud-admin&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=172.16.246.32&port=8093&ephemeral=true&weight=1&enabled=false&namespaceId=a9076f8c-a1c7-474c-9ea4-1112677d9af7

原因:nacos地址可以寫死,應用名稱可以寫死,應用端口號可以寫死,但是應用所在主機ip也就是pod ip沒法獲取。
1.PreStop是配置在Daeployment中的,pod的數量和ip都是不固定的。
2.就算把pod ip設置成環境變量的形式,也只能是在pod容器中使用,在PreStop中還是獲取不到pod ip

綜合以上分析,這里采取的辦法是在構造鏡像的時候入手,新增一個preStop.sh腳本,內容寫上nacos下線的那個命令, 然后載PreStop命令行中執行這個腳本文件。

在這個過程中,若是有些參數值無法從環境變量中獲取,則需要增加這些參數的環境變量。

preStop腳本內容

注意腳本中的sleep 45命令,這個是確保應用從nacos中下線使用的,默認是30秒,具體看開頭nacos心跳檢測時間,sleep設置時間大於30秒就可以,這里設置45秒
preStop.sh腳本中使用的變量有些是默認提供的,有些是需要提前設置環境變量的,取值是容器中的值

#!/bin/sh

# shell腳本作用
# 在更新pod時先執行這個腳本,把pod應用從nacos中下線,然后再關閉pod

#echo "輸出必要的環境變量"
#echo "${NACOS_SERVER_ADDR}"
#echo "${PODNAME}"
#echo "${PODIP}"
#echo "${NACOS_NAMESPACE}"

result=$(curl -X PUT "http://${NACOS_SERVER_ADDR}/nacos/v1/ns/instance?serviceName=${PODNAME}&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=${PODIP}&port=8093&enabled=false&namespaceId=${NACOS_NAMESPACE}")

echo "輸出curl執行結果result:${result}"

if [ ${result} == "ok" ]; then
  echo "執行成功"
  sleep 45
  exit 0
else
  echo "執行失敗"
  exit 1
fi

如上腳本中,
NACOS_SERVER_ADDR和NACOS_NAMESPACE從ConfigMap中設置中獲取,
PODIP和PODNAME是在環境變量中手動設置的

然后修改Dockerfile文件,增加這個preStop腳本,設置可執行權限,注意腳本放置的路徑,后面會用到

ADD preStop.sh /tmp/preStop.sh
RUN chmod 777 /tmp/preStop.sh

經過以上操作,項目中新增一個preStop腳本文件,把這個文件給添加到Dockerfile文件中,並放置到指定路徑下,然后提交到gitlab,自動構建docker鏡像,記住鏡像標簽。
然后在k8s中設置PreStop內容如下:

然后調整應用的deployment的yaml文件,把優雅終止寬限期(terminationGracePeriod限定時間)由默認的30秒調整為60秒,確保這個時間大於sleep 45的時間。

同時更新應用使用的docker鏡像,待鏡像啟動后,增加副本數,由1增加到3,同時觀察nacos中注冊的應用數,確認顯示有3個。

然后縮減一個副本數,副本數由3變成2,注意觀察nacos中的應用是否有一個ip狀態的變成"上線"(顯示這個表示應用是下線狀態),等待30秒后就看不到這個ip應用了。
然后觀察k8s中pod的消失,等了60秒后才開始取消一個pod,最后查看事件events,發現並沒有FailedPreStopHook,這是正常的,因為只有報錯的情況下才會出現FailedPreStopHook,正常情況下不會出現這個。

Hook調用的日志沒有暴露給Pod的Event,所以只能到通過describe命令來獲取,如果是正常的操作是不會有event,如果有錯誤可以看到FailedPostStartHook和FailedPreStopHook這種event。並且如果Hook調用出現錯誤,則Pod狀態不會是Running

總結

1.pod滅亡有個優雅終止寬限期(terminationGracePeriod限定時間),默認是30秒,nacos中應用超過30秒則摘除,主要圍繞這倆時間來進行處理
2.項目中新增一個preStop.sh腳本,並添加到Dockerfile文件中,確保構造的鏡像中有這個sh文件
腳本內容是應用從nacos下面的命令,以及sleep時間,這個時間需要超過nacos默認的30秒 (pod鏡像中確保有curl命令)
3.k8s中增加sh腳本中使用到的環境變量,以便pod中sh腳本可以從pod環境中獲取這些變量的值
4.k8s中設置設置PreStop,使用命令行的方式執行如上的sh腳本
5.nacos中驗證,事件events中驗證







進一步升級考慮

1.每個應用使用的端口號不一樣,sh腳本內容在不同的項目中還得手動修改端口號,比較麻煩,可以把這個端口號也給做成變量的形式來使用,k8s中給這個端口設置一個環境變量

2.k8s中設置的環境變量中有個應用名稱,pod本身的環境變量中有個metadata.name,是pod的名稱,跟應用名稱本身相比后面多了一些隨機字符串,另外pod本身存在的環境變量HOSTNAME的值跟metadata.name的值一樣,可以在sh腳本中獲取HOSTNAME的值,然后截取前面的字符串從而獲得應用名稱,這樣就不用在k8s中給應用名稱設置一個新的環境變量了。

# HOSTNAME=jdd-parking-cloud-admin-5456f59db7-hcdd7
# 第一次去掉的是隨機字符串,第二次去掉的是deployment的名稱

tmp=${HOSTNAME%-*}
PODNAME=${tmp%-*}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM