“六天”戀上Kubernetes
譯文在過去相當長一段時間內(nèi),我自認為都是 Kubernetes 的強烈懷疑者。無論是做項目還是做初創(chuàng)公司,裸機始終都是我的首選,包括運行這篇博客的堆棧也同樣用的是裸機(https://freeman.vc/notes/architecting-a-blog)。
堆棧是一個持續(xù)集成(CI)的工具鏈,主機上有 Nginx 配置。它可以處理驚人的并發(fā)負載,而且托管成本很低,每月只需 10 美元,完全可以與托管成本高出兩個數(shù)量級的企業(yè)級博客平臺和諧共處。
我相信,一家公司如果過早地使其架構(gòu)太復雜,不僅給工程師出了難題,還會給用戶帶來不穩(wěn)定性。
所以我倒覺得帶有簡單服務(wù)器的單一代碼庫(Monorepo)或許就夠了。這時候只需要運行基本的 Docker 實例以盡量減小依賴地獄(Dependency Hell),并確保遠程配置可在本地開發(fā)的機器上重現(xiàn)。
如果需要增加流量,可以租用功能更強大的服務(wù)器;或者部署簡單的負載均衡系統(tǒng),路由到幾個后端設(shè)備。
雖然我正是這么做的,但最近負載均衡系統(tǒng)后面用于幾個副業(yè)項目的一臺物理服務(wù)器已滿足不了我的要求——大部分時間里它是空閑的,但突發(fā)狀態(tài)下則需要幾十個帶有 GPU 的虛擬機。
這迫使我只好采用更復雜的服務(wù)器管理解決方案。在用 Kubernetes 構(gòu)建了幾個月之后,我必須承認:我一天比一天更喜歡它。
第一天:努力爭取
我已經(jīng)在裸機上有一個完全正常運行的后端應(yīng)用程序。我真要用不同的產(chǎn)品重新設(shè)計我堆棧的架構(gòu)嗎?尤其是像 Kubernetes 這樣的超強解決方案?
我嘗試使用實例模板和托管組來設(shè)置集群,這是相當于亞馬遜 ECS 的谷歌產(chǎn)品。
從控制臺啟動基本集群相對簡單,但我需要一些自定義邏輯來處理更新 SHAS 時的啟動和停止。
我將其切換到一個編寫成 Python 庫的 GitHub Actions 管道。在研讀 GCP 說明文檔之后,我想出了可行的解決方案,但與此同時也發(fā)現(xiàn)了一些弊端。
① 大部分必要的部署代碼可以用標準的谷歌客戶端庫函數(shù)來實現(xiàn),這對于參數(shù)類型檢查和面向?qū)ο蠓浅S杏谩?/p>
然而,pypi 庫中有一些命令未得到支持。為此,我通過代理連接到 GCP 公開的 REST Web 服務(wù)。
為什么存在這種不匹配?我不確定。在開發(fā)過程中處理兩種相似但不同的 API 格式真的很容易讓人混淆。
② 該部署需要在啟動時提供 Docker 鏡像,而實例模板支持這些鏡像。但是它們并不通過 API 支持它們,而是僅在 Web 控制臺或 CLI 中支持。
通過 REST 調(diào)用實現(xiàn)容器服務(wù)需要檢查 gcloud 網(wǎng)絡(luò)流量,并拷貝包含在實例創(chuàng)建負載中的 yaml 文件。
它附帶一條注釋,警告不得拷貝該 yaml 文件,因為變更不是語義版本控制的,可能隨時有變。
③ 我開始覺得我在重新實現(xiàn)服務(wù)器的核心邏輯。這個問題以前肯定有人解決過成百上千次了,考慮到范圍相對簡單,我可以對此不屑一顧,但它多少對我還是構(gòu)成了困擾。
這里邊關(guān)鍵的阻礙因素是價格。我需要用不同的容器部署其中兩個集群,這些容器需要由內(nèi)部負載均衡系統(tǒng)作為前端。這允許主后端服務(wù)器與靜態(tài) API 端點通信并分配負載。
這兩個負載均衡系統(tǒng)是對路由后端流量的現(xiàn)有負載均衡系統(tǒng)的補充。這意味著存儲成本也開始成倍增加,因為我需要在默認配置的硬驅(qū)上留出一些緩沖區(qū)。
即使在零數(shù)據(jù)處理的情況下,測試該基礎(chǔ)架構(gòu)的費率也飆升至每天 28 美元,這對于一家有錢燒的公司來說微不足道,但對本人來說吃不消。
第二天:Kubernetes,你好嗎?
很顯然,谷歌裸機方法看起來并不樂觀。它需要太多的手動結(jié)合,這導致成本飆升,于是我開始尋找其他的托管產(chǎn)品。
如果我重構(gòu)這個單獨的數(shù)據(jù)管理集群中的計算密集型部分,可能會將數(shù)據(jù)處理與機器學習分開來。
然而,我查看的所有產(chǎn)品都只提供預留的 GPU。如果我們選擇硬件配置,它們會生成虛擬機或?qū)S梅?wù)器。
如果我們有請求有待處理時,卻沒有動態(tài)擴展一說。雖然每美元的芯片比谷歌更好,但除此之外,它是專用計算的相同結(jié)構(gòu)。對于該部署而言,空閑時間將多于使用時間,因此這行不通。
我不情愿地投入到 Kubernetes 堆棧中,從文檔開始,然后用擴展邏輯的核心元素構(gòu)建了一個 POC 集群。
我甚至沒有專注于部署實際的應(yīng)用程序。我只是優(yōu)先考慮基礎(chǔ)架構(gòu)配置,這是讓我親身參與 Kubernetes 做出的設(shè)計的好方法。
第三天:晚餐費用
GCP 和 AWS 每月都為 Kubernetes 控制面板收取 70 美元左右的費用。每個托管的 Kubernetes 集群都捆綁了控制面板,因此它甚至在使用計算資源之前就為業(yè)務(wù)經(jīng)營成本設(shè)定了底線。
當 GCP 增加這項費用時,引起了軒然大波,因為這導致使用單獨集群的用戶成本成倍增加。
雖然 Azure 是唯一的仍然擁有免費控制面板的主要提供商,但我發(fā)現(xiàn)其托管產(chǎn)品的功能比另外兩家要少。所以,我就不指望供應(yīng)商的控制面板永遠免費了。
雖然 GCP 仍然在單個區(qū)域提供免費控制面板,但是它主要用于 beta 測試。
另外,這種單區(qū)域支持與專用服務(wù)器一樣,使用裸機,雖然用戶可以在任何托管設(shè)備的區(qū)域進行托管,但是如果該區(qū)域有問題(或者如果底層服務(wù)器有問題),那就不走運了。
如果要在裸機上進行多區(qū)域負載均衡,那么硬件成本就會增加一倍。
不過即使有管理費用,設(shè)置托管集群仍然比使用云特定產(chǎn)品自行搭建集群要便宜得多。
GCP 上的內(nèi)部負載均衡系統(tǒng)每月花費 18 美元,推出 4 個服務(wù)來創(chuàng)建內(nèi)部 DNS 網(wǎng)格將超出控制面板的成本。而我從頭開始的原型只是使用了谷歌產(chǎn)品的簡單組合,成本就接近了 18 美元。
目前我在做的是,開始在這個免費的區(qū)域集群中托管所有項目。到目前為止,它完全可靠,沒有停機。
如果沒有機器學習相關(guān)計算資源,平均每天的成本約為 6 美元。其中大部分(85%)用于 3 個 CPU、11.25GB 內(nèi)存集群的計算、內(nèi)存和存儲資源。剩余的費用用于存儲和 Docker 鏡像托管。
第四天:了解術(shù)語的時候
我發(fā)現(xiàn) Kubernetes 一個有用的心智模型是將容器編排視為針對不同原語(primitive)的操作。
一些原語包括如下:
- pod 是 Docker 容器或容器化的應(yīng)用程序邏輯;
- 服務(wù)控制同一個 pod 的重復副本,因此如果一個 pod 在托管期間崩潰,可以確保冗余;
- 入站控制器定義外部連接(比如常規(guī) Web 請求)如何流入集群、流向正確的服務(wù);
- 節(jié)點是物理硬件,是運行 1 個以上 pod 的實際服務(wù)器;
- 集群是一組有相似特征的一個或多個節(jié)點,表明了哪些 pod 放置在哪里等信息。
還有一些操作:
- 獲?。╣et)一個原語下可用的實例列表;
- 描述(describe)這些元素的定義,并查看當前狀態(tài);
- 查看活躍或失敗對象的日志(log);
一切都圍繞這些邏輯對象,這些實體中大多數(shù)都有與之關(guān)聯(lián)的相同 CLI 行為:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
backend-deployment-6d985b8854-45wfr 1/1 Running 0 18h
backend-deployment-6d985b8854-g7cph 1/1 Running 0 18h
backend-deployment-6d985b8854-mqtdc 1/1 Running 0 18h
frontend-deployment-5576fb487-7xj5r 1/1 Running 0 27h
frontend-deployment-5576fb487-8dkvx 1/1 Running 0 27h
frontend-deployment-5576fb487-q6b2s 1/1 Running 0 27h
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
backend ClusterIP 10.171.351.3 <none> 80/TCP 34h
frontend ClusterIP 10.171.334.28 <none> 80/TCP 34h
kubernetes ClusterIP 10.171.310.1 <none> 443/TCP 4d23h
正因如此,所以每個命令將根據(jù)邏輯對象來顯示不同的元數(shù)據(jù)。
如果檢查 pod 的話,將會顯示運行中 Docker 容器的版本和運行狀況;部署動作將顯示鏡像的當前冗余和集群的健康狀況;所謂服務(wù),將為其他服務(wù)提供訪問內(nèi)部負載均衡系統(tǒng)的 DNS 地址。
這種邏輯對象方法的優(yōu)點在于,它使 API 發(fā)現(xiàn)變得可訪問。我們只要記住操作(get、describe)和對象(pods、服務(wù)),就可以避免組合爆炸。
Kubernetes 提供的原語對服務(wù)器如何運行有自己的觀點。比如說,pod 和服務(wù)有單獨的概念,因為服務(wù)提供了跨多個 pod 的某種內(nèi)置負載均衡。
我們可以使用單獨的 pod 輕松編寫此邏輯,并探測本地 Kubernetes API,尋找同一組中 pod 的 IP 地址。但是因為服務(wù)被廣泛使用,因此 Kubernetes 將其重構(gòu)為單獨的對象類型。
一般來說,抽象是最常見的服務(wù)器功能上的小插件(shim),它可以:運行進程、接受入站連接、運行守護進程。
這使我們可以輕松地在新工具中執(zhí)行已經(jīng)在執(zhí)行的操作。雖然這些術(shù)語學起來有點費勁,但核心概念學起來并不費勁。
第五天:結(jié)識朋友
我最欣賞 Kubernetes 的地方是,它抽象了跨云主機服務(wù)器的概念。無論我們使用 GCP、AWS 還是 Azure 進行托管,都將擁有想要運行容器(pod)和偶爾一次性腳本(作業(yè))的原始計算(節(jié)點)。
尤其是對托管 Kubernetes 配置而言,云提供商負責編寫 Kubernetes 邏輯對象和物理硬件之間的轉(zhuǎn)換層。
如果我們想啟動新設(shè)備,只需要為集群中的新節(jié)點推送 helm 配置,而云提供商將負責余下的工作。
雖然這不會使云遷移完全順暢無阻,但確實可以將云托管視為一種更大眾化的實用服務(wù),避免因為編寫自定義云集成代碼而被某個供應(yīng)商鎖定。
一切都歸結(jié)為 Kubernetes API。根據(jù)我的經(jīng)驗,無論我們的系統(tǒng)是什么規(guī)模,這個接口層都處于完美的抽象級別。
我們不必擔心本身引導磁盤或機器的底層管理實用程序。API 遵循明確的棄用計劃,這將使我們可以可靠地將業(yè)務(wù)邏輯集成到更大的管道中,比如在 CI 中推送部署變更,通過計劃任務(wù)啟動節(jié)點等。
一切都是程序化的。連 kubectl(本地集群管理實用程序)都是集群內(nèi)托管 API 上的抽象層。
這意味著我們可以編寫任何可以手動執(zhí)行的操作,只要可以對其進行編程,就可以將其自動化。
我之前管理的服務(wù)器已經(jīng)有大約 95% 實現(xiàn)了自動化。有一個主要的 bash 腳本可以完成大部分環(huán)境設(shè)置,不過盡管如此,我仍然需要做一些手動工作,比如更新 Nginx 文件系統(tǒng)配置、配置 iptables 等。
由于每個 Linux 版本都略有不同,因此定期升級底層操作系統(tǒng)時,需要更改該腳本。
API 讓我們的操作手冊(runbook)變得完全程序化。無需編寫說明文檔對已知問題進行分類或?qū)哼\行命令,就可以讓工程師/SRE 程師隨叫隨到并遵循程序指南。
一個常見的需求是檢查 git 分支是否已成功部署,如果我們要用 sha 標記 Docker 鏡像,簡單的方法是檢查 sha 是否一樣。
這通常需要一番遠程 ssh/Docker 處理,或者在某個 API 端點中公開托管的 sha??梢宰詣踊瘑幔慨斎?。很簡單?不是很簡單。
相反,Kubernetes API 使得編寫這樣的邏輯、并將它們捆綁到可安裝的軟包中變得極其容易。這使得 on-call 常見分類操作可以被發(fā)現(xiàn),并引導用戶自行完成命令。
這是我的實用程序中的一個例子:
$ on-call
Usage: on-call [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
check-health Check service health
bootstrap-database Run initial database setup
...
$ on-call check-health --app frontend
1. Checking uptime (`yes` to confirm):
> yes
The application has been stable for 465 minutes.
2. Checking sha matches (`yes` to confirm):
> yes
Latest github sha: 86597fa
Latest deployed sha: 8d3f42e
Mismatching shas...
第六天:勞動專業(yè)化
從架構(gòu)上來說,我仍然認為單體架構(gòu)是可行的方法,但那是另一個話題。
就連微服務(wù)擁躉也會承認,有時我們需要將服務(wù)綁定到底層計算,這在 GPU 上進行硬件加速的機器學習發(fā)行版中尤其如此。
我們通常只能為每個硬件設(shè)備安裝一個 GPU 集群,因此約束存在于節(jié)點層面,而不是針對 pod。
結(jié)合污點(taint)和容差,很容易將一個 pod 分配給獨立的硬件實例。默認情況下,pod 將放置在任何有適當內(nèi)存和 CPU 空間的地方。
我將污點視為油和水,因為它們與默認的 pod 分配不相容。而容差更像油和油,它們可以允許 pod 在帶有污點的那些節(jié)點上啟動。以正確的方式配置污點和容差可以將 1 個 pod 鎖定到 1 個節(jié)點。
這讓我們完全回到了裸機托管一項服務(wù)的范式中。在極端情況下,它讓我們可以只使用 Kubernetes 作為連接服務(wù)的負載均衡層和管理 API 的抽象層。我們可以更輕松地保留單體架構(gòu),并委派其他任務(wù)。
結(jié)語
我仍然看好裸機和功能強大的單體架構(gòu),但是在初步了解 Kubernetes 之后,我很想說,當我們需要多個服務(wù)器協(xié)同工作時,就應(yīng)該使用 Kubernetes。
我們始終都要對 Kubernetes 進行管理,因為這不可避免地帶來可靠性問題。但擔心 Kubernetes 大材小用的想法似乎是多余的,因為編寫微服務(wù)架構(gòu)方面的更底層問題才是值得擔憂的關(guān)鍵所在。
不過如果將兩者分開來,或許我們可以獲得兩全其美的效果。一個是簡單的代碼庫,優(yōu)先考慮功能交付;另一個是靈活但可編程控制的硬件架構(gòu)。
Kubernetes 可以比我預期的更輕松地融入后臺,這讓我們可以專注于交付主要的價值:我們的應(yīng)用程序。
作者:Pierce Freeman,譯者 | 布加迪