喜馬拉雅容器化實踐

喜馬的容器化歷程伴隨着公司的發展,並帶有很深刻的喜馬的烙印:

在這個過程中,我們堅持了一些原則:

如何讓開發發佈代碼到容器環境

2016 年底,我在 Jenkins 機器上安裝了 docker,製作一個基於 docker 的項目模板。開發克隆 docker  項目模板,更改源碼地址、CPU、內存等配置,由 Jenkins 觸發 shell 腳本:

當時大概用了 5 天時間搭建了組件,跑通了所有步驟,我們的容器化事業就是從這個最 low 的版本開始發展起來的,後來逐步做了以下演進:

  1. 從 marathon 逐步遷移到 Kubernetes。在過渡時期,我們自己實現了一個 docker  發佈系統,對外提供發佈接口,屏蔽 marathon 和 Kubernetes 的差異;

  2. 實現了一個命令行工具 barge。開發在項目文件中附加一個 barge.yaml 文件,裏面設定了項目名等基本配置:

  1. 與公司的發佈系統對接(類似阿里雲),屏蔽物理機與容器環境的使用差異。

一個容器要不要多進程

容器世界盛行的理念是:one process per container。但容器在最開始落地時,爲了降低推廣成本,減少使用差異過大(相比物理機)給開發帶來的不適應,需要在容器內運行 ssh,實質上要求在容器內運行多進程,這需要一個多進程管理工具(entrypoint 不能是業務進程),最終在 runit/systemd/supervisor 中選擇了 runit。

此外,Web 服務每次發佈時 IP 會變化,需要讓 Nginx 感知到 Web 服務 IP 的變化。我們在每個容器內啓動了一個 nile 進程,負責將項目信息註冊到 ZooKeeper 上。使用微博一位小夥伴開源的 upsync 插件,並改寫了部分源碼使其支持了 ZooKeeper(upsync 只支持 etcd 和 Consul),進而使得 Nginx 的 upstream 可以感知到項目實例 ip 的變化。後來在另一個場景中,Nginx 改用了其它插件,我們將實例信息寫入到 Consul 上即可。

隨着 Kubernetes 的鋪開,我們使用 gotty + kubectl exec 逐步推廣了在瀏覽器上通過 Web console 訪問容器。專門的網關係統也投入使用,HTTP 訪問由 nginx => Web 服務變成了 nginx => 網關 => Web 服務,網關提供了 Web 接口同步 Web 實例數據。ssh 及 nile 進程逐步退出歷史舞臺。目前,我們的 entrypoint 仍是 runit, 並對其做了微調,當業務進程啓動失敗時,不會重啓(如果 entrypoint 是業務進程時則會頻繁重啓),以保留現場方便開發排查問題。

健康檢查的三心二意

Kubernetes 有一個 readiness probe,用來檢測項目的健康狀態,上下游組件可以根據項目的 健康狀態採取 相關措施。在 readiness probe 的設置上,經過多次改變:

  1. 每一個 Web 項目會提供一個 /healthcheck 接口,/healthcheck 通過即表示項目啓動成功;

  2. 後來發現,對於 RPC 服務,/healthcheck 有時不能表示項目啓動成功,需要檢測 RPC 服務啓動時監聽的端口是否開啓;

  3. readiness probe 配置(HTTP 和 TCP 方式任選)加大了業務開發的配置負擔,經常出現配置不準確或中途改變導致 readiness probe 探測失敗,進而發出報警,業務開發不勝其煩,我們也非常困擾;

  4. 於是將 readiness probe 配置爲 exec 方式,由 nile 根據項目情況自動執行 HTTP 和 TCP 探測,但仍然依賴項目配置信息(比如 RPC 服務端口)的準確性;

  5. 基於 “RPC 服務場景下,/healthcheck 接口成功,但 RPC 服務啓動失敗 的場景非常的少” 的判斷,我們將 readiness probe 改回了 HTTP /healthcheck 方式。

liveness 起初是 /healthcheck

  1. 有一次運維改動機房網絡,導致 liveness probe 探測失敗(kubelet 無法訪問本機所在容器 IP)。kubelet 認爲 liveness probe 不通過 => livenessProbe 涉及到 RestartPolicy,大量項目重啓 => 因爲項目之間存在依賴關係,依賴服務失敗,項目本身啓動失敗 => 頻繁重啓;

  2. 後來我們不再配置 liveness probe ,僅配置 readiness probe:對於物理機損壞等場景,K8S 可以正常恢復;如果容器內存不足 導致實例掛掉,K8S 無法自動重啓,這個可以通過內存報警來預防。

與發佈平臺對接

喜馬拉雅在發佈平臺的實踐中,爲了保障線上服務的穩定,沉澱了一套自己的經驗和規則。其中一條硬性規定就是:服務端發佈任何項目,最開始只能先發佈一個實例,觀察效果,確認無誤後,再發布剩餘實例。在實際使用時,驗證時間可能會非常長,有時持續一週。這時,Kubernetes deployment 自帶的滾動發佈機制便有些弱。

因此,在容器發佈系統的設計上,我們讓一個項目對應兩個 deployment。新代碼的發佈就是舊 deployment replicas 逐步縮小,新 deployment replicas 逐步擴大的過程。業界有兩種方案:

阿里的 openkruise 實現了一個 crd 名爲 cloneset,可以實現上述類似的功能,後續計劃逐步替換掉兩個 deployment 方案 以精簡發佈系統代碼。全新設計一個 crd 也是一個很不錯的方案。

此外,容器發佈系統對下屏蔽 Kubernetes ,對上提供項目發佈、回滾、擴縮容等接口,在接口定義上也多次反覆。kubernetes client-java 庫也較爲臃腫,爲此對發佈代碼也進行了多次優化。

針對開發日常的各種問題,比如項目 IP 是多少,項目爲什麼啓動失敗等,我們專門開發了一個後臺,起了一個很唬人的名字:容器雲平臺。試圖將我們日常碰到的客服問題通過技術問題來解決,通過一個平臺來歸攏,哪怕不能實現開發完全自助式的操作,也可以儘量減少容器開發的排查時間。比如我們開發了一個 wrench 檢查組件,用來掃描 Java 項目的 classpath、日誌等,分析項目是否有 jar 衝突、類衝突、Tomcat 日誌報錯、業務日誌報錯等,開發在 Web 頁面 上觸發 wrench 執行即可。

與已有中間件對接

喜馬有自己自研的網關及 RPC 框架,最初很少考慮容器的使用場景(比如每次發佈 IP 會變化)。此外,服務實例需要先調用網關和 RPC 框架提供的上線接口,纔可以對外提供服務。服務銷燬時,也必須先調用下線接口,待服務實例將已有流量處理完畢後再銷燬。

爲此我們提供了一個 k8s-sync 組件,監聽 pod 狀態變化,在適當時機調用實例的上下線接口。後續計劃自定義 crd 比如 WebService/RpcService,由自定義 controller 完成實例的上下線及其它工作。

如何做到 Web/RPC 服務的無損上線下線?

上線

pod 配置 readiness probe 對項目的 healthcheck 進行健康檢查。健康檢查通過後,pod 進入 ready 狀態, k8s-sync 監聽到 pod ready 事件,調用 Web/RPC 上線接口。readiness 由 K8S 確保執行,可保證健康檢查不成功不會上線。風險是如果 k8s-sync  掛了則健康檢查通過的 pod 無法上線。解決辦法:

下線

K8S 先執行 pod preStop 邏輯,preStop 先調用 Web/RPC 下線接口,執行 xdcs 提供的零流量檢查接口,檢查通過,則 preStop 執行完畢,銷燬。若項目未接入 xdcs,則等待 preStop 等待 10s 後,執行完畢。銷燬 => preStop 由 K8S 負責確保執行, 正常發佈 / 刪除 / 節點移除 / 節點驅逐 都可以確保被執行,以確保服務無損。如果物理機節點直接掛了,則無法保證無損,因爲一系列機制都來不及執行。

k8s-sync 同時也將 pod 信息同步到 MySQL 中,方便開發根據項目名、IP 等在後臺查詢並訪問項目容器。隨着 k8s-sync 功能的豐富,我們對 k8s-sync 也進行了多次重構。

體會

從 2016 年到現在,一路走來,容器的各種上下游組件、公司的中間件並不是一開始就有或成熟的,落地過程真的可以用 “逢山開路遇水搭橋” 來形容。

1、我們花了很大的精力在容器與公司已有環境的融合上,比學習容器技術本身花的精力都多。爲此我經常跟小夥伴講,我們的工作是容器化,其實質上容器技術 “喜馬拉雅化”。

2、實踐的過程彷彿在走夜路,很多方案心裏都喫不準。爲此看了很多文章,加了很多微信,很感謝一些大牛無私地交流。對於一些方案,走了彎路,兜兜轉轉又回到了原點。但這個過程逼得我們去想本質,去反思哪些因素是真正重要的,哪些因素其實沒那麼重要,哪些問題必須嚴防死守,哪些問題事後處理即可。

3、成長經常不是以你計劃的方式得到的。

比如最開始搞 Kubernetes 落地的時候,想着成爲一個 Kubernetes 高手,但最後發現大量的時間在寫發佈系統、k8s-sync,但再回過頭,發現增進了對 K8S 的理解。

筆者 Java 開發出身,容器相關的組件都是 GO 語言實現的,在最開始給了筆者很大的學習負擔。但兩相交融,有幸對很多事情理解得更深刻,比如親身接觸 Java 的共享內存模型與 GO 的 CSP 模型,最終促進了對併發模型的理解。

爲了大家多用容器,我們做了很多本不歸屬於容器開發的工作。尤其是在容器化初期,很多開發項目啓動一有問題便認爲是 docker 的問題, 我們經常要幫小夥伴排查到問題原因,以證明不是 docker 的問題。這是一個很煩人的事情,但又逼着我們把事情做得更好,更有利於追求卓越。比如 wrench 的開發,真的用技術解決了一個看似不是技術的問題。

4、一定要共同成長,多溝通,容器技術改變了基礎設施、開發模式、習慣,儘量推動跟上級、同事同步,單靠一個人寸步難行。

最後想想,感慨很多,還是那句:凡是過往,皆爲序章。

轉自:李乾坤,

鏈接:qiankunli.github.io/2021/08/02/kubernetes_xima_practice.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RZBp5ctvZ1PT_bLmhmROlw