引言
前面我們已經介紹了 Kubernetes 的常用資源以及 Kubernetes 的實作原理,本文我們将介紹通過 Kubernetes 開發應用的最佳實踐,更多關于 Kubernetes 的介紹均收錄于
<Kubernetes系列文章>中。
開發應用最佳實踐
首先,我們看一看一個實際的應用都應該使用哪些 Kubernetes 資源。

一般應用 manifest 包含了一個或者多個 Deployment 和 StatefulSet 對象。這些對象中包含了一個或者多個容器的 pod 模闆,每個容器都有一個存活探針,并且為容器提供的服務(如果有的話)提供就緒探針。提供服務的 pod 是通過一個或者多個服務來暴露自己的。當需要從叢集外通路這些服務的時候,要麼将這些服務配置為 LoadBalancer 或者 NodePort 類型的服務,要麼通過 Ingress 資源來開放服務。
pod模闆(從中建立 pod 的配置檔案)通常會引用兩種類型的私密憑據(Secret)。一種是從私有鏡像倉庫拉取鏡像時使用的;另一種是 pod 中運作的程序直接使用的。私密憑據本身通常不是應用 manifest 的一部分,因為它們不是由應用開發者來配置,而是由運維團隊來配置的。私密憑據通常會被配置設定給 ServiceAccount,然後 ServiceAccount 會被配置設定給每個單獨的 pod。
一個應用還包含一個或者多個 ConfigMap 對象,可以用它們來初始化環境變量,或者在 pod 中以 configMap 卷來挂載。有一些 pod 會使用額外的卷,例如 emptyDir 或 qitRepo 卷,而需要持久化存儲的 pod 則需要 persistentVolumeClaim 卷。PersistentVolumeClaim 也是一個應用 manifest 的一部分,而被 PersistentVolumeClaim 所引用的 StorageClass 則是由系統管理者事先建立的。
在某些情況下,一個應用還需要使用任務 Jobs 和定時任務 CronJobs。守護程序集(DaemonSet)通常不是應用部署的一部分,但是通常由系統管理者建立,以在全部或者部分節點上運作系統服務。水準 pod 擴容器(HorizontalPodAutoscaler)可以由開發者包含在應用 manifest 中或者後續由運維團隊添加到系統中。叢集管理者還會建立 LimitRange 和 ResourceQuota 對象,以控制每個 pod 和所有的 pod (作為一個整體)的計算資源使用情況。
注意 Pod 的生命周期
我們之前說過,可以将 pod 比作隻運作單個應用的虛拟機。盡管在 pod 中運作的應用和虛拟機中運作的應用沒什麼不同,但是還是存在顯著的差異。其中一個例子就是 pod 中運作的應用随時可能會被殺死,因為 Kubernetes 需要将這個 pod 排程到另外一個節點,或者是請求縮容。這就需要我們的應用能夠做到如下幾點:
- 預料到本地 IP 和主機名會發生變化
- 預料到寫入磁盤的資料會消失
Kubernetes 最佳實踐 - 使用存儲卷來跨容器持久化資料
Kubernetes 最佳實踐 - 對于 MYSQL 那類存儲類應用,你可能還需要 StatefulSet 來保證 pod 重新排程後,仍與之前的存儲卷綁定,進而不丢失資料
pod 中運作的應用和手動運作的應用之間的一個不同就是運維人員在手動部署應用的時候知道應用之間的依賴關系,這樣他們就可以按照順序來啟動應用。當你使用 Kubernetes 來運作多個 pod 的應用的時候,Kubernetes 沒有内置的方法來先運作某些 pod 然後等這些 pod 運作成功後再運作其他 pod。當然你也可以先釋出第一個應用的配置,然後等待 pod 啟動完畢再釋出第二個應用的配置。
Kubernetes API 伺服器确實是按照 YAML/JSON 檔案中定義的對象的順序來進行處理的,但是僅僅意味着它們在被寫入到 etcd 的時候是有順序的。無法確定 pod 會按照那個順序啟動。但是你可以阻止主容器的啟動,直到它的預置條件被滿足,這是通過在 pod 中包含一個叫 init 的容器來實作的。
一個 pod 可以擁有任意數量的 init 容器。init 容器是順序執行的,并且僅當最後一個 init 容器執行完畢才會去啟動主容器。換句話說,init 容器也可以用來延遲 pod 的主容器的啟動。例如,直到滿足某一個條件的時候。init 容器可以一直等待直到主容器所依賴的服務啟動完成并可以提供服務。當這個服務啟動并且可以提供服務之後,init 容器就執行結束了,然後主容器就可以啟動了。這樣主容器就不會發生在所依賴服務準備好之前使用它的情況了。
除此之外,不要忘了 Readiness 探針。如果一個應用在其中一個依賴缺失的情況下無法工作,那麼它需要通過它的 Readiness 探針來通知這個情況,這樣 Kubernetes 也會知道這個應用沒有準備好。
我們已經讨論了如果使用 init 容器來介入pod的啟動過程,另外 pod 還允許你定義兩種類型的生命周期鈎子:
- 啟動後(Post-start)鈎子
- 這個鈎子和主程序是并行執行的,并不是完全啟動結束後。
- 停止前(Pre-stop)鈎子
- 停止前鈎子是在容器被終止之前立即執行的。當一個容器需要終止運作的時候,Kubelet 在配置了停止前鈎子的時候就會執行這個停止前鈎子,并且僅在執行完鈎子程式後才會向容器程序發送 SIGTERM 信号。
這些生命周期的鈎子是基于每個容器來指定的,和 init 容器不同的是,init 容器是應用到整個 pod。這些鈎子,如它們的名字所示,是在容器啟動後和停止前執行的。生命周期鈎子與存活探針和就緒探針相似的是它們都可以:
- 在容器内部執行一個指令
- 向一個URL發送 HTTP GET 請求
很多開發者在定義停止前鈎子的時候會犯錯誤,他們在鈎子中隻向應用發送了 SIGTERM 信号。他們這樣做是因為他們沒有看到他們的應用接收到 Kubelet 發送的 SIGTERM 信号。應用沒有接收到信号的原因并不是 Kubelet 沒有發送信号,而是因為在容器内部信号沒有被傳遞給應用的程序。如果你的容器鏡像配置是通過執行一個 shell 程序,然後在shell程序内部執行應用程序,那麼這個信号就被這個 shell 程序吞沒了,這樣就不會傳遞給子程序。
在這種情況下,合理的做法是讓shell程序傳遞這個信号給應用程序,而不是添加一個停止前鈎子來發送信号給應用程序。可以通過在作為主程序執行的 shell 程序内處理信号并把它傳遞給應用程序的方式來實作。或者如果你無法配置容器鏡像執行 shell 程序,而是通過直接運作應用的二進制檔案,可以通過在 DockerFile 中使用 ENTRYPOINT 或者 CMD 的 exec 方式來實作,即
ENTRYPOINT ["/mybinary"]
而不是
ENTRYPOINT /mybinary
。在通過第一種方式運作二進制檔案 mybinary 的容器中,這個程序就是容器的主程序,而在第二種方式中,是先運作一個shell作為主程序,然後 mybinary 程序作為shell程序的子程序運作。
當 Kubelet 意識到需要終止 pod 的時候, ]它開始終止 pod 中的每個容器。Kubelet 會給每個容器一定的時間來優雅地停止。這個時間叫作終止寬限期(Termination GracePeriod), 每個 pod 可以單獨配置。在終止程序開始之後,計時器就開始計時,接着按照順序執行以下事件:
- 執行停止前鈎子(如果配置了的話),然後等待它執行完畢
- 向容器的主程序發送 SIGTERM 信号
- 等待容器優雅地關閉或者等待終止寬限期逾時,預設 30 秒
- 如果容器主程序沒有優雅地關閉,使用 SIGKILL 信号強制終止程序
應用應該通過啟動關閉流程來響應 SIGTERM 信号,并且在流程結束後終止運作。除了處理 SIGTERM 信号,應用還可以通過停止前鈎子來收到關閉通知。在這兩種情況下,應用隻有固定的時間來幹淨地終止運作。但是如果你無法預測應用需要多長時間來幹淨地終止運作,假設你的應用是一個分布式資料存儲。在縮容的時候,其中一個 pod 的執行個體會被删除然後關閉。在這個關閉的過程中,這個 pod 需要将它的資料遷移到其他存活的 pod 上面以確定資料不會丢失。這時候,我推薦你建立一個專注于善後工作的 Job 資源,這個 Job 資源會運作一個新的 pod, 這個 pod 唯一的工作就是把被删除的 pod 的資料遷移到仍然存活的 pod。
但是你可能注意了,我們無法保證應用每次都能夠成功建立這個 Job 對象。萬一當應用要去建立Job的時候節點出現故障呢? 我們可以用一個專門的持續運作中的 pod 來持續檢查是否存在孤立的資料。當這個 pod 發現孤立的資料的時候,它就可以把它們遷移到仍存活的 pod。當然不一定是一個持續運作的 pod, 也可以使用 CronJob 資源來周期性地運作這個 pod。
妥善處理用戶端請求
毋庸贅言,你希望所有的用戶端請求都能夠得到妥善的處理。你顯然不希望 pod 在啟動或者關閉過程中出現斷開連接配接的情況。Kubernetes 本身并沒有避免這種事情的發生。你的應用需要遵循一些規則來避免遇到連接配接斷開的情況。
首先我們要清楚,當一個 pod 啟動的時候,它以服務端點的方式提供給所有的服務,這些服務的标簽選擇器和 pod 的标簽比對,我們前面說過 pod 需要發送新号給 Kubernetes 通知它自己準備好了之後,它才能變成一個服務端點,否則它不會接受到任何用戶端連接配接。
你需要做的第一點就是當且僅當你的應用準備好處理進來的請求的時候,才去讓就緒探針傳回成功。
現在我們來看一下在 pod 生命周期的另一端,當 pod 被删除,如何妥善的處理用戶端的連接配接。我們知道當要删除 Pod 時,會同時觸發兩條工作線,一條是關閉容器,一條是 kube-proxy 修改 iptables。
那麼怎麼才能讓,修改 iptables 的工作先進行然後再删除 pod 容器呢?最簡單有效的辦法是在進行 pod 的關閉時,等待幾秒鐘再開始停止接受新的連接配接。你無法完美地解決這個問題,但是即使增加 5 秒或者 10 秒延遲也會極大提升使用者體驗,它能保證之後隻有少量的連接配接會流到這個即将關閉的 pod,除此之外,我們還要關閉這個 pod 中不活躍的長連接配接,然後對于那些活躍的長連接配接,等處理完最後一個請求後,再開始關閉應用。
管理容器
為了讓 Kubernetes 的容器更好管理,我們應該合理地給鏡像打标簽,使用多元度而不是單次元的标簽,比如:
- 資源所屬的應用(或者微服務) 的名稱
- 應用層級(前端、後端,等等)
- 運作環境(開發、測試、預釋出、生産,等等)
- 版本号
- 釋出類型(穩定版、金絲雀、藍綠開發中的綠色或者藍色,等等)
- 分片(帶分片的系統)
可以使用注解來給你的資源添加額外的資訊。資源至少應該包括一個描述資源的注解和一個描述資源負責人的注解。在微服務架構中,pod 應該包含一個注解來描述該 pod 依賴的其他服務的名稱。這樣就很容易展現 pod 之間的依賴關系了。
在一個生産環境系統中,你希望使用一個集中式的面向叢集的日志解決方案,是以你所有的日志都會被收集并且(永久地)存儲在一個中心化的位置。這樣你可以檢視曆史日志,分析趨勢。你或許已經聽說過由ElasticSearch、FluentD 和 Kibana 組成的 EFK 棧,它能很好地幫你處理各個 pod 中的日志并整合在一起。
當使用 EFK 作為集中式日志記錄的時候,每個 Kubernetes 叢集節點都會運作一個 FluentD 的代理(通過使用 DaemonSet 作為pod來部署),這個代理負責從容器搜集日志,給日志打上和 pod 相關的資訊,然後把它們發送給 ElasticSearch, 然後由 ElasticSearch 來永久地存儲它們。ElasticSearch 在叢集中也是作為 pod 部署的。這些日志可以通過 Kibana 在Web浏覽器中檢視和分析,Kibana 是一個可視化 ElasticSearch 資料的工具。它經常也是作為 pod 來運作的,并且通過一個服務暴露出來。EFK的三個元件如下圖所示。
FluentD 代理将日志檔案的每一行當作一個條目存儲在 ElasticSearch 資料存儲中。這裡就會有一個問題。當日志輸出跨越多行的時候,例如Java 的異常堆棧,就會以不同條目存儲在集中式的日志記錄系統中。
為了解決這個問題,可以讓應用日志輸出 JSON 格式的内容而不是純文字。這樣的話,一個多行的日志輸出就可以作為一個條目進行存儲了,也可以在 Kbiana 中以一個條目的方式顯示出來。但是這種做法會讓通過 kubectl logs 指令檢視日志變得不太人性化了。
為了解決這個問題,可以讓輸出到标準輸出終端的日志仍然是使用者可讀的日志,但是寫入日志檔案供 FluentD 處理的日志是JSON格式。這就要求在節點級别合理地配置 FluentD 代理或者給每一個 pod 增加一個輕量級的日志記錄容器。
文章說明
更多有價值的文章均收錄于
貝貝貓的文章目錄版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!
創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。
參考内容
[1]
kubernetes GitHub 倉庫[2]
Kubernetes 官方首頁[3]
Kubernetes 官方 Demo[4] 《Kubernetes in Action》
[5]
了解Kubernetes網絡之Flannel網絡[6]
Kubernetes Handbook[7]
iptables概念介紹及相關操作[8]
iptables超全詳解[9]
了解Docker容器網絡之Linux Network Namespace[10]
A Guide to the Kubernetes Networking Model[11]
Kubernetes with Flannel — Understanding the Networking[12]
四層、七層負載均衡的差別