筆者在上一篇文章中介紹了如何在POD中部署多個容器實例,特別是我們通過部署邊車容器的這種方式饱普,在不修改原始容器代碼的情況下呻引,對容器的功能進(jìn)行了擴(kuò)充上忍,比如提供HTTPS訪問支持的能力等庐完。坦白講爪模,我們上篇文章中有很多細(xì)節(jié)信息未給大家做展開介紹址遇,比如容器的狀態(tài)队寇,Ready具體代表什么意思等,今天我們就來針對容器的生命周期進(jìn)行一次深入的探索跪另。
當(dāng)我們將應(yīng)用部署到Kubernetes平臺上之后拧抖,我們可以通過kubectl describe命令來返回完整的POD對象。返回的POD對象有個status節(jié)點免绿,包含了對象的狀態(tài)信息唧席,具體來講,這部分會包含如下的POD對象狀態(tài)信息:
1,POD對象的IP地址和POD被調(diào)度到的工作節(jié)點袱吆。
2,POD是什么時候啟動的距淫。
3绞绒,POD的QOS(Quality of service)類型。
4榕暇,POD當(dāng)前處于什么階段(phase)蓬衡。
5,POD的當(dāng)前狀態(tài)(condition)以及POD中每個容器實例的狀態(tài)(state)彤枢。
我們本篇文章主要分析容器的生命周期狰晚,因此上邊的這些狀態(tài)中,我們會討論phase缴啡,condition和state壁晒,因為這些狀態(tài)是構(gòu)成POD和里邊運行容器狀態(tài)的指示燈。首先我們從POD的生命周期來介紹业栅,如下圖所示:
如上圖所示秒咐,POD的整個生命周期中總共有5種phase階段,接下來我們來詳細(xì)介紹一下每種phase:
- Pending階段碘裕,是新建POD的初始情況携取,直到POD被調(diào)度到某個工作節(jié)點,并且POD中的容器實例鏡像被下載到工作節(jié)點的緩存并啟動起來帮孔,那么POD將會一直處于Pending階段雷滋。
- Running階段,POD中至少有一個容器實例處于Running狀態(tài)文兢,那么POD就處于這個階段晤斩。
- Succeeded階段,當(dāng)POD中所有的容器實例都成功運行并退出姆坚,那么POD就處于這個階段尸昧。
- Failed階段,當(dāng)POD中至少有一個容器實例因為某種原因運行失敗而出錯旷偿,那么POD就會被標(biāo)記為Failed階段烹俗。
- Unkown階段,當(dāng)Kublet停止向API Server匯報POD的狀態(tài)信息時萍程,POD就處于這個階段幢妄,造成這種情況的可能原因包括但不限于:工作節(jié)點故障,或者工作節(jié)點的網(wǎng)絡(luò)連接斷開等茫负。
通過分析POD所處的階段能快速獲知POD的實際運行情況蕉鸳,接下來讓我們把yunpan.yaml這個POD重新啟動起來,來實戰(zhàn)一下上邊介紹的五種階段。首先我們在自己的集群上重新部署yunpan.yaml這個對象潮尝,部署命令是:kubectl apply -f yunpan.yaml榕吼,接著我們來通過返回的POD對象信息找到status部分并輸出到控制臺,通過命令:kubectl get po yunpan -o yaml | grep phase勉失,在筆者的本地集群上羹蚣,輸出如下:
?? kubernetes kubectl get po yunpan -o yaml | grep phase
? phase: Running
由于這個POD中只有一個容器,并且這個容器實例正在健康運行乱凿,因此POD所處的階段是Running顽素,和我們上邊分析的結(jié)果一致。我們也可以通過kubectl get pod yunpan來返回POD的運行狀態(tài)信息徒蟆,但是大家要注意的是胁出,如果POD的狀態(tài)是健康的,那么返回的Status列就是POD的階段段审,但是對于出現(xiàn)運行問題的POD全蝶,這個Status列將顯示具體發(fā)生了什么類型的錯誤,我們會在后面詳細(xì)介紹寺枉。
【POD的Conditions】
筆者在過往的容器化項目上裸诽,發(fā)現(xiàn)大家對POD的phase,status和conditions這三個屬性會出現(xiàn)混淆型凳,本質(zhì)上這三個屬性描述的都是POD的狀態(tài)信息丈冬。phase可以看成是POD在經(jīng)歷某些調(diào)度和操作之后的結(jié)果,因此從phase上我們是看不出來POD經(jīng)歷了哪些操作或者異常甘畅,而condition字段向我們展示了這個細(xì)節(jié)信息埂蕊。POD的conditions告訴我們POD從創(chuàng)建之初,具體經(jīng)歷過哪些階段疏唾,以及如果出現(xiàn)異常蓄氧,具體是什么原因造成的。
因此conditions通常會有多個值槐脏,不像phase喉童,每個POD在特定的時間點,只會有一個phase值顿天。我們先來看看Kubernetes的POD對象提供的四種類型的conditions:
- PodScheduled堂氯,表示POD是否被成功的調(diào)度到工作節(jié)點上。
- Initialized牌废,POD中所有的初始化容器都成功的完成執(zhí)行咽白。
- ContainersReady,POD中所有容器都已經(jīng)Ready鸟缕。
- Ready晶框,POD準(zhǔn)備好對外提供服務(wù)排抬,所有容器實例都報告狀態(tài)為Ready。
每個condition類型在POD的聲明周期中有滿足或者不滿足兩個狀態(tài)授段,如下圖所示蹲蒲,在POD剛開始啟動的時候,PodScheduled和Initialized這兩個condition沒有滿足侵贵,但是很快就會滿足届搁,并且會保持到整個POD的生命周期。而Ready和ContainerReady這兩個condition會在POD的整個生命周期中發(fā)生多次變化模燥。
如上圖所示,POD對象有四個condition類型掩宜,并且在生命周期會發(fā)生變化蔫骂。其實對于Kubernetes中的node對象(工作節(jié)點)來說,它也有自己一套condition類型牺汤,MemoryPressure辽旋,DiskPressure,PIDPressure和Ready檐迟。這兩個不同類型對象condition的共性是都有Ready類型补胚,其實大部分Kubernetes對象的condition類型都有Ready,表示事情按預(yù)期的在進(jìn)行這個意思追迟。
接下來我們看看condition在POD對象上的真實輸出溶其,筆者在自己本地集群上運行kubectl describe pod yunpan,從輸出的超長對象信息中敦间,我們找到了condition部分瓶逃,如下圖所示:
從kubectl describe我們只能看到每個condition是否被滿足(true或者false),如果你想知道為什么一個狀態(tài)是false廓块,可以在返回的對象信息中找.status.conditions字段厢绝,如下圖所示:
從上圖可以看出,每個condition類型都有status字段带猴,表示這個condition具體是true還是false昔汉,以及unknow。對于筆者本地的yunpan這個pod來說拴清,所有的condition類型都是true靶病,這就說明pod正在健康運行,對外提供服務(wù)口予。每個condition類型也包含了一個reason字段嫡秕,提供了更多關(guān)于狀態(tài)變化的信息。
我們從POD對象中也可以看到容器實例的狀態(tài)苹威,具體來說昆咽,state字段反映了容器當(dāng)前的狀態(tài),而lastState字段表示上一個容器的啟動實例運行結(jié)束時的狀態(tài)。容器狀態(tài)部分還包括一個ID字段containerID和鏡像imageID字段掷酗,這兩個字段不言自明调违。除了這些字段,容器狀態(tài)部分也有ready和restartCount字段泻轰,分別表示容器是否ready技肩,以及重啟的次數(shù)。對于容器實例的狀態(tài)來說浮声,最最重要的是state字段虚婿,一個容器實例的狀態(tài)機如下圖所示:
如上圖所示,容器的狀態(tài)機中有4中狀態(tài)泳挥,對于每種狀態(tài)的詳細(xì)介紹如下:
- Waiting狀態(tài)然痊,容器實例在等待啟動,從reason和message字段可以得到處于這個狀態(tài)容器實例的更多信息屉符。
- Running狀態(tài)剧浸,容器實例已經(jīng)被創(chuàng)建,并且里邊的進(jìn)程正在運行矗钟。startedAt字段記錄了容器被成功啟動的時間唆香。
- Terminated狀態(tài),容器中的進(jìn)程已經(jīng)運行結(jié)束吨艇,startAt字段和finishedAt字段記錄了容器啟動的時間和運行結(jié)束的時間躬它。exitCode字段記錄了主進(jìn)程退出的狀態(tài)碼。
- Unknow狀態(tài)东涡,容器的狀態(tài)無法判斷虑凛。
我們前邊通過kubectl get pods這個命令只能看到POD中有多少容器處于Ready狀態(tài),要獲取更加詳細(xì)的信息软啼,我們可以使用kubectl describe桑谍,如下圖所示,在筆者的本地Kubernetes環(huán)境的輸出:
到現(xiàn)在為止祸挪,我們創(chuàng)建的所有POD都很健康锣披,運行過程中沒有出現(xiàn)任何問題,但是這并不是常態(tài)贿条,代碼會有缺陷雹仿,網(wǎng)絡(luò)會抖動等,都可能造成POD中運行的所有容器實例出現(xiàn)故障整以,對于生產(chǎn)級別的部署方案胧辽,我們必須有機制確保POD以及運行在POD中的容器健康運行,接下來我們來聊聊這個話題公黑。
【保障容器實例健康運行】
當(dāng)POD被調(diào)度到某個工作節(jié)點后邑商,運行在工作節(jié)點上的Kubelet會負(fù)責(zé)驅(qū)動容器運行時來啟動容器實例摄咆,并盡自己最大的努力來讓容器長時間健康運行,直到運行結(jié)束退出人断。如果容器中的主進(jìn)程因為某種原因退出結(jié)束吭从,那么Kublets會負(fù)責(zé)重啟容器。如果應(yīng)用在運行過程中出現(xiàn)了錯誤恶迈,Kubernetes會自動重新啟動POD涩金,大大減輕了我們?yōu)榱舜_保應(yīng)用健康運行所需要的運維工作。我們來通過一個具體的例子暇仲,看看這一切是如何發(fā)生的步做。
在前邊的文章中,筆者介紹過如何在一個POD中通過邊車模式運行多個容器奈附,我們創(chuàng)建了yunpan-ssl這個POD全度,這個POD中包含兩個容器,我們的SpringCloud應(yīng)用程序容器負(fù)責(zé)提供業(yè)務(wù)服務(wù)桅狠,而Envoy容器實例主要負(fù)責(zé)處理HTTPS流量請求讼载。首先在自己的環(huán)境中將這個POD啟動起來轿秧,并通過port-forward創(chuàng)建本地訪問服務(wù)的代理:kubectl port-forward yunpan-ssl 8085 8443 9901中跌。
接下來,我們要做的是讓Envoy容器crash異常退出菇篡,同時我們觀測Kubernetes是如何處理這種場景漩符。新打開一個終端運行命令kubectl get pods -w,我們就能看到POD狀態(tài)變化的輸出信息驱还,當(dāng)然我們也需要觀察產(chǎn)生的時間嗜暴,請打開第二個終端,運行命令kubectl get events -w议蟆,好了闷沥,監(jiān)控窗口準(zhǔn)備好了,接下來我們想辦法讓Envoy容器退出咐容。
如果你熟悉操作系統(tǒng)原理舆逃,應(yīng)該知道我們可以給進(jìn)程發(fā)送KILL信號,來讓進(jìn)程退出(在macOS上或者Linux上執(zhí)行kill命令戳粒,背后的原理就是給進(jìn)程發(fā)送KILL信號)路狮,但是對于Envoy容器進(jìn)程,因為它是PID為1的進(jìn)程蔚约,而Linux操作系統(tǒng)不允許殺掉PID為1的進(jìn)程奄妨,我們得換個方法。你可能已經(jīng)想到了苹祟,我們可以登錄到宿主機上(對于minikube實例砸抛,宿主機就是這臺叫做minikube的虛擬機评雌,筆者有時候也稱之為工作節(jié)點,因為minikube是一個單機環(huán)境锰悼,管理節(jié)點和工作節(jié)點都運行在這臺叫minikube的虛擬機上)柳骄。
不過我們有更好的方法,而不需要登錄到虛擬機上箕般,就是通過Envoy提供的管理接口耐薯,來遠(yuǎn)程通過API接口停止某個進(jìn)程的運行。我們可以在命令行窗口執(zhí)行curl -X POST http://localhost:9901/quitquitquit 丝里,接著我們就可以從POD的狀態(tài)監(jiān)控接口看到如下的輸出信息:
?? init-demo-image kubectl get pods -w
NAME? ? ? ? READY? STATUS? ? RESTARTS? AGE
yunpan-ssl? 2/2? ? Running? 0? ? ? ? ? 12m
yunpan-ssl? 1/2? ? NotReady? 0? ? ? ? ? 13m
yunpan-ssl? 2/2? ? Running? ? 1? ? ? ? ? 13m
從上邊的輸出可以看到曲初,當(dāng)我們執(zhí)行了殺死Envoy容器進(jìn)程之后,POD的狀態(tài)馬上從Running就變成了NotReady杯聚,從輸出的Ready列也可以看到臼婆,2個容器實例中只有1個處于ready狀態(tài)。Kubernetes接下來會馬上重新啟動Envoy容器實例幌绍,隨著容器實例的狀態(tài)ready颁褂,POD的狀態(tài)也從NotReady變成Running,并且RESTARTS列的計數(shù)加1傀广,記錄了容器被重啟啟動的次數(shù)颁独。
注意:如果POD中的多個容器實例中,有任何一個運行失敗伪冰,其他的容器實例不受影響誓酒,會繼續(xù)運行。
接下來我們看看監(jiān)控時間窗口的輸出信息贮聂,如下所示:
?? kubernetes git:(master) kubectl get events -w
LAST SEEN? TYPE? ? ? REASON? ? ? ? ? ? ? ? ? ? OBJECT? ? ? ? ? ? MESSAGE
0s? ? ? ? ? Normal? ? Pulled? ? ? ? ? ? ? ? ? ? pod/yunpan-ssl? ? Container image "qigaopan/yunpan-ssl-proxy:v1.1" already present on machine
0s? ? ? ? ? Normal? ? Created? ? ? ? ? ? ? ? ? pod/yunpan-ssl? ? Created container envoy
0s? ? ? ? ? Normal? ? Started? ? ? ? ? ? ? ? ? pod/yunpan-ssl? ? Started container envoy
從輸出的事件信息中可以看到靠柑,envoy容器實例被重新啟動了,你可以通過HTTPS來驗證Envoy容器能夠正常接收請求并處理吓懈。從這個輸出中希望大家能看到非常重要的一點歼冰,Kubernetes本質(zhì)上從來都沒有重啟的概念,而是刪除老的容器實例耻警,并且重新創(chuàng)建一個容器實例來取代老的實例隔嫡,但是為了符合大家的直覺,我們還是叫restarting榕栏。
注:容器重新創(chuàng)建之后畔勤,進(jìn)程在運行過程中寫到容器文件系統(tǒng)的所有信息都會丟失,為了讓這些數(shù)據(jù)能夠在容器被重新創(chuàng)建后可用扒磁,我們需要給容器掛載存儲卷庆揪,筆者會在后續(xù)的文章中詳細(xì)介紹。另外需要注意的是妨托,初始化容器不會因為應(yīng)用實例容器實例的重啟而重新執(zhí)行缸榛,從這點可以看到吝羞,初始化容器的生命周期和POD一致。
Kubernetes也并不是在所有情況下都會默認(rèn)的重啟容器實例内颗,我們可以通過restartPolicy這個字段來聲明POD的重啟策略钧排,因為如果不配置,默認(rèn)情況下均澳,Kubernetes不管三七二十一恨溜,無論是POD正常運行結(jié)束還是異常情況,都會重啟容器實例找前,這在某些情況下可能和我們的預(yù)期不符糟袁。Kubernetes提供了三種重啟策略,如下圖所示:
如上圖所示躺盛,Kubernetes提供了三種類型的重啟策略项戴,詳細(xì)介紹如下:
- Always策略,容器任何情況下都會重啟槽惫,無論exitCode返回碼表示運行成功還是失敗周叮。如果不在POD的YAML文件中顯式的設(shè)置,默認(rèn)的重啟策略是Always界斜。
- OnFailure策略仿耽,容器只會在返回碼未非0的時候重啟(對于大部分系統(tǒng)來說,非0都表示運行出錯)锄蹂。
- Never策略氓仲,從不重啟容器實例水慨,即便是運行出現(xiàn)錯誤退出得糜。
注:Kubernetes的重啟策略是配置在POD級別,這就意味著POD中所有的容器使用相同的重啟策略晰洒,我們無法為每個容器單獨配置重啟策略朝抖。
如果我們多次調(diào)用Envoy管理節(jié)點提供的/quitquitquit接口,你會發(fā)現(xiàn)每次調(diào)用后谍珊,容器的重啟所需要的時間都會變長治宣,POD的狀態(tài)處于NotReady或者CrashLoopBackOff狀態(tài),背后的原理如下圖所示:
如上圖所示砌滞,第一次容器異常退出后侮邀,Kubernetes會立即進(jìn)行重啟,如果容器容器由于某種原因贝润,需要再次重啟绊茧,那么Kubernetes會等待10秒鐘再重啟容器實例,而這個等待時間會隨著后續(xù)的重啟被加倍打掘,20s华畏,40s鹏秋,80s,160s亡笑,從160s以后侣夷,延遲時間會變成5分鐘,我們把每次重啟需要等待的時間間隔加倍這種策略叫指數(shù)級回退策略(exponential back-off)仑乌。在極端情況下百拓,容器的確會被block高達(dá)5分鐘才能重啟。而這個指數(shù)級回退策略會在容器成功運行10分鐘后被清零晰甚。筆者在目前負(fù)責(zé)的項目上耐版,多次看到容器處于CrashLoopBackOff的狀態(tài),這是很多調(diào)試階段應(yīng)用經(jīng)常遇到的場景压汪,希望大家通過這里的介紹粪牲,能夠掌握這個原理。
我們在前邊介紹的內(nèi)容都是Kubernetes在我們的容器異常退出的時候止剖,幫助我們重啟容器來提供可用性腺阳,但是很多時候容器并沒有退出,但是提供的服務(wù)卻無法訪問穿香。舉個例子亭引,Java應(yīng)用程序如果有內(nèi)存泄漏,會返回內(nèi)存不足的異常皮获,但是這并不妨礙JVM進(jìn)程繼續(xù)運行焙蚓,理論上Kubernetes應(yīng)用能檢測到這樣的異常,并且重啟容器實例洒宝,但是很顯然Kubernetes把這個問題交給了運維和開發(fā)人員來解決购公。
如果我們從程序本身來考慮處理這問題,很明顯會是個悖論雁歌,因為應(yīng)用自己都沒有內(nèi)存了宏浩,如何檢測自己出問題。因此這個問題必須要從外部來解決靠瞎,而Kubernetes提供了叫Liveness probes的功能比庄,來從外部對應(yīng)用判活。
【應(yīng)用程序的健康探測】
我們可以給POD配置liveness probe來定期的檢測服務(wù)是否正常對外提供訪問能力乏盐,我們可以在容器級別來設(shè)定這個liveness probe佳窑,而Kubernetes要做的就是,基于配置的信息父能,周期性的來訪問應(yīng)用程序神凑,判斷應(yīng)用程序的狀態(tài)。如果應(yīng)用程序沒有影響法竞,或者返回錯誤信息耙厚,容器實例就會被認(rèn)為不健康强挫,Kubernetes就會終止容器,并基于配置的重啟策略來重啟容器實例薛躬。這里需要注意的是俯渤,Liveness probe只能用在正常應(yīng)用容器實例上,不能配置在初始化容器實例上型宝。
Kubernets為了提供對大部分應(yīng)用程序關(guān)于判活場景的支持八匠,提供如下三種liveness probe機制:
- HTTP GET probe通過發(fā)送發(fā)送請求給我們制定的資源地址,包括IP地址和端口號趴酣,如果probe收到正常的http響應(yīng)(比如2xx和3xx)梨树,那么probe就認(rèn)為應(yīng)用健康;如果probe收到除2xx和3xx之外的http狀態(tài)碼岖寞,那么就認(rèn)為此次probe探測失敗抡四。
- TCP Socket probe通過在目標(biāo)容器制定的端口上建立TCP連接來判斷容器的狀態(tài)。如果TCP連接可以被成功的建立仗谆,那么就認(rèn)為probe成功指巡,如果無法成功建立TCP連接,就認(rèn)為失敗隶垮。
- Exec probe通過在目標(biāo)容器實例上執(zhí)行命令來判斷容器的健康狀態(tài)藻雪。執(zhí)行如果返回非0返回碼,就認(rèn)為執(zhí)行失敗狸吞,如果命令執(zhí)行超時勉耀,也會認(rèn)為probe執(zhí)行失敗。
Kubernetes除了提供這種liveness probe的機制蹋偏,還提供了一種叫startup probe的機制便斥,我們會在后續(xù)的文章中介紹這種機制。好了暖侨,今天的內(nèi)容就這么多了椭住,筆者會在下篇文章中詳細(xì)介紹如何配置liveness probe崇渗,敬請期待字逗!