[譯] 在 Kubernetes 之上架構應用

piglei發表於2018-08-05

原文:architecting-applications-for-kubernetes

作者:Justin Ellingwood

譯者:朱雷(piglei)

簡介

設計並執行一個兼顧可擴充套件性、可移植性和健壯性的應用是一件很有挑戰的事情,尤其是當系統複雜度在不斷增長時。應用或系統本身的架構極大的影響著其執行方式、對環境的依賴性,以及與相關元件的耦合強弱。當應用在一個高度分散式的環境中執行時,如果能在設計階段遵循特定模式,在運維階段恪守特定實踐,就可以幫助我們更好的應對那些最常出現的問題。

儘管軟體設計模式和開發方法論可以幫助我們生產出滿足恰當擴充套件性指標的應用,基礎設施與執行環境也在影響著已部署系統的運維操作。像 DockerKubernetes 這些技術可以幫助團隊打包、分發、部署以及在分散式環境中擴充套件應用。學習如何最好的駕馭這些工具,可以幫助你在管理應用時擁有更好的機動性、控制性和響應能力。

在這份指南里,我們將探討一些你可能想採用的準則和模式,它們可以幫助你在 Kubernetes 上更好的擴充套件和管理你的工作集(workloads)。儘管在 Kubernetes 上可以執行各種各樣的工作集,但是你的不同選擇會影響運維難度和部署時的可選項。你如何架構和構建應用、如何將服務用容器打包、如何配置生命週期管理以及在 kubernetes 上如何操作,每一個點都會影響你的體驗。

為可擴充套件性做應用設計

當開發軟體時,你所選用的模式與架構會被很多需求所影響。對於 Kubernetes 來說,它最重要的特徵之一就是要求應用擁有水平擴充套件能力 - 通過調整應用副本數來分擔負載以及提升可用性。這與垂直擴充套件不同,垂直擴充套件嘗試使用同樣的引數將應用部署到效能更強或更弱的伺服器上。

比如,微服務架構是一種適合在叢集中執行多個可擴充套件應用的軟體設計模式。開發者們建立一些可組合的簡單應用,它們通過良好定義的 REST 介面進行網路通訊,而不是像更復雜的單體式應用那樣通過程式內部機制通訊。將單體式應用拆分為多個獨立的單一功能元件後,我們可以獨立的擴充套件每個功能元件。很多之前通常存在於應用層的組合與複雜度被轉移到了運維領域,而它們剛好可以被像 Kubernetes 這樣的平臺搞定。

位元定的軟體模式更進一步,雲原生(cloud native)應用在設計之初就有一些額外的考量。雲原生應用是遵循了微服務架構模式的程式,擁有內建的可恢復性、可觀測性和可管理性,專門用於適應雲叢集平臺提供的環境。

舉例來說,雲原生應用在被創造出時都帶有健康度指標資料,當某個例項變得不健康時,平臺可以根據指標資料來管理例項的生命週期。這些指標產生(也可以被匯出)穩定的遙控資料來給運維人員告警,讓他們可以依據這些資料做決策。應用被設計成可以應付常規的重啟、失敗、後端可用性變化以及高負載等各種情況,而不會損壞資料或者變得無法響應。

遵循 “12 法則應用”應用理論

在建立準備跑在雲上的 web 應用時,有一個流行的方法論可以幫你關注到那些最重要的特徵:“12 法則應用理論”( Twelve-Factor App)。它最初被編寫出來,是為了幫助開發者和運維團隊瞭解所有被設計成在雲環境執行的 web 服務的共有核心特徵,而對於那些將在 Kubernetes 這種叢集環境中執行的應用,這個理論也非常適用。儘管單體式應用可以從這些建議中獲益,圍繞這些原則設計的微服務架構應用也會工作的非常好。

“12 法則”的一份簡單摘要:

  1. 基準程式碼(Codebase): 將你的所有程式碼都放在版本控制系統中(比如 Git 或者 Mercurial)。被部署的內容完全由基準程式碼決定。
  2. 依賴(Dependencies):應用依賴應該由基準程式碼全部顯式管理起來,無論是用 vendor(指依賴程式碼和應用程式碼儲存在一起),還是通過可由包管理軟體解析安裝的依賴配置檔案的方式。
  3. 配置(Config):把應用配置引數與應用本身分開來,配置應該在部署環境中定義,而不是被嵌入到應用本身。
  4. 後端服務(Backing Services):本地或遠端的依賴服務都應該被抽象為可通過網路訪問的資源,連線細節應該在配置中定義。
  5. 構建、釋出、執行(Build, release, run):應用的構建階段應該完全與釋出、運維階段區分開來。構建階段從應用原始碼建立出一個可執行包,釋出階段負責把可執行包和配置組合起來,然後在執行階段執行這個釋出版本。
  6. 程式(Processes):應用應該由不依賴任何本地狀態儲存的程式實現。狀態應該被儲存在第 4 個法則描述的後端服務中。
  7. 埠繫結(Port binding):應用應該原生繫結埠和監聽連線。所有的路由和請求轉發工作應該由外部處理。
  8. 併發(Concurrency):應用應該依賴於程式模型擴充套件。只需同時執行多份應用(可能分佈在不同伺服器上),就能實現不調整應用程式碼擴充套件的目的。
  9. 易處理(Disposability):程式應該可以被快速啟動、優雅停止,而不產生任何嚴重的副作用。
  10. 開發環境與線上環境等價(Dev/prod parity):你的測試、預釋出以及線上環境應該儘可能一致而且保持同步。環境間的差異有可能會導致相容性問題和未經測試的配置突然出現。
  11. 日誌(Logs):應用應該將日誌輸出到標準輸出(stdout),然後由外部服務來決定最佳的處理方式。
  12. 管理程式(Admin processes):一次性管理程式應該和主程式程式碼一起釋出,基於某個特定的釋出版本執行。

依照“12 法則”所提供的指南,你可以使用完全適用於 Kubernetes 執行環境的模型來建立和執行應用。“12 法則”鼓勵開發者們專注於他們應用的首要職責,考慮運維條件以及元件間的介面設計,並使用輸入、輸出和標準程式管理功能,最終以可被預見的方式將應用在 Kubernetes 中跑起來。

容器化應用元件

Kubernetes 使用容器在叢集節點上執行隔離的打包應用程式。要在 Kubernetes 上執行,你的應用必須被封裝在一個或者多個容器映象中,並使用 Docker 這樣的容器執行時執行。儘管容器化你的元件是 Kubernetes 的要求之一,但其實這個過程也幫助強化了剛剛談到的“12法則應用”裡的很多準則,從而讓我們可以簡單的擴充套件和管理應用。

舉例來說,容器提供了應用環境與外部宿主機環境之間的隔離,提供了一個基於網路、面向服務的應用間通訊方式,並且通常都是從環境變數讀取配置、將日誌寫到標準輸出與標準錯誤輸出中。容器本身鼓勵基於程式的併發策略,並且可以通過保持獨立擴充套件性和捆綁執行時環境來幫助保持開發/線上環境一致性*(#10 Dev/prod parity)*。這些特性讓你可以順利打包應用,從而順利的在 Kubernetes 上執行起來。

容器優化準則

因為容器技術的靈活性,我們有很多不同種封裝應用的方式。但是在 Kubernetes 環境中,其中一些方式比其他方式工作的更好。

映象構建*(image building)*,是指你定義應用將如何在容器裡被設定與執行的過程,絕大多數關於“如何容器化應用”的最佳實踐都與映象構建過程有關。通常來說,保持映象尺寸小以及可組合會帶來很多好處。在映象升級時,通過保持構建步驟可管理以及複用現有映象層,被優化過尺寸的的映象可以減少在叢集中啟動一個新容器所需要的時間與資源。

當構建容器映象時,盡最大努力將構建步驟與最終在生產環境執行的映象區分開來是一個好的開始。構建軟體通常需要額外的工具、花費更多時間,並且會生產出在不同容器裡表現不同、或是在最終執行時環境里根本不需要的內容。將構建過程與執行時環境清晰分開的辦法之一是使用 Docker 的“多階段構建(multi-stage builds)” 特性。多階段構建配置允許你為構建階段和執行階段設定不同的基礎映象。也就是說,你可以使用一個安裝了所有構建工具的映象來構建軟體,然後將結果可執行軟體包複製到一個精簡過的、之後每次都會用到的映象中。

有了這類功能後,基於最小化的父映象來構建生產環境映象通常會是個好主意。如果你想完全避免由 ubuntu:16.04(該映象包含了一個完整的 Ubuntu 16.04 環境)這類 “Linux 發行版” 風格父映象帶來的臃腫,你可以嘗試用 scratch - Docker 的最簡基礎映象 - 來構建你的映象。不過 scratch 基礎映象缺了一些核心工具,所以部分軟體可能會因為環境問題而無法執行。另外一個方案是使用 Alpine Linuxalpine 映象,該映象提供了一個輕量但是擁有完整特性的 Linux 發行版。它作為一個穩定的最小基礎環境獲得了廣泛的使用。

對於像 Python 或 Ruby 這種解釋型程式語言來說,上面的例子會稍有變化。因為它們不存在“編譯”階段,而且在生產環境執行程式碼時一定需要有直譯器。不過因為大家仍然追求精簡的映象,所以 Docker Hub 上還是有很多基於 Alpine Linux 構建的各語言優化版映象。對於解釋型語言來說,使用更小映象帶來的好處和編譯型語言差不多:在開始正式工作前,Kubernetes 能夠在新節點上快速拉取到所有必須的容器映象。

在 Pod 和“容器”之間做選擇

雖然你的應用必須被“容器”化後才能在 Kubernetes 上跑起來,但 pods*(譯註:因為 pod、service、ingress 這類資源名稱不適合翻譯為中文,此處及後面均使用英文原文)* 才是 Kubernetes 能直接管理的最小抽象單位。pod 是由一個或更多緊密關聯的容器組合在一起的 Kubernetes 物件。同一個 pod 裡的所有容器共享同一生命週期且作為一個獨立單位被管理。比如,這些容器總是被排程到同一個節點上、一起被啟動或停止,同時共享 IP 和檔案系統這類資源。

一開始,找到將應用拆分為 pods 和容器的最佳方式會比較困難。所以,瞭解 Kubernetes 是如何處理這些物件,以及每個抽象層為你的系統帶來了什麼變得非常重要。下面這些事項可以幫助你在使用這些抽象概念封裝應用時,找到一些自然的邊界點。

尋找自然開發邊界是為你的容器決定有效範圍的手段之一。如果你的系統採用了微服務架構,所有容器都經過良好設計、被頻繁構建,各自負責不同的獨立功能,並且可以被經常用到不同場景中。這個程度的抽象可以讓你的團隊通過容器映象來發布變更,然後將這個新功能釋出到所有使用了這個映象的環境中去。應用可以通過組合很多容器來構建,這些容器裡的每一個都實現了特定的功能,但是又不能獨立成事。

與上面相反,當考慮的是系統中的哪些部分可以從獨立管理中獲益最多時,我們常常會用 pods。Kubernetes 使用 pods 作為它面向使用者的最小抽象,因此它們是 Kubernetes API 和工具可以直接互動與控制的最原生單位。你可以啟動、停止或者重啟 pods,或者使用基於 pods 建立的更高階別抽象來引入副本集和生命週期管理這些特性。Kubernetes 不允許你單獨管理一個 Pod 裡的不同容器,所以如果某些容器可以從獨立管理中獲得好處,那麼你就不應該把它們分到到一個組裡。

因為 Kubernetes 的很多特性和抽象概念都直接和 pods 打交道,所以把那些應該被一起擴縮容的東西捆綁到一個 pod 裡、應該被分開擴縮容的分到不同 pod 中是很有道理的。舉例來說,將前端 web 伺服器和應用服務放到不同 pods 裡讓你可以根據需求單獨對每一層進行擴縮容。不過,有時候把 web 伺服器和資料庫適配層放在同一個 pod 裡也說得過去,如果那個介面卡為 web 伺服器提供了它正常執行所需的基本功能的話。

通過和支撐性容器捆綁到一起來增強 Pod 功能

瞭解了上面這點後,到底什麼型別的容器應該被捆綁到同一個 pod 裡呢?通常來說,pod 裡的主容器負責提供 pod 的核心功能,但是我們可以定義附加容器來修改或者擴充套件那個主容器,或者幫助它適配到某個特定的部署環境中。

比如,在一個 web 伺服器 pod 中,可能會存在一個 Nginx 容器來監聽請求和託管靜態內容,而這些靜態內容則是由另外一個容器來監聽專案變動並更新的。雖然把這兩個元件打包到同一個容器裡的主意聽上去不錯,但是把它們作為獨立的容器來實現是有很多好處的。nginx 容器和內容拉取容器都可以獨立的在不同情景中使用。它們可以由不同的團隊維護並分別開發,達到將行為通用化來與不同的容器協同工作的目的。

Brendan Burns 和 David Oppenheimer 在他們關於“基於容器的分散式系統設計模式”的論文中定義了三種打包支撐性容器的主要模式。它們代表了一些最常見的將容器打包到 pod 裡的用例:

  • Sidecar(邊車模式):在這個模式中,次要容器擴充套件和增強了主容器的核心功能。這個模式涉及在一個獨立容器裡執行非標準或工具功能。舉例來說,某個轉發日誌或者監聽配置值改動的容器可以擴充套件某個 pod 的功能,而不會改動它的主要關注點。

  • Ambassador(大使模式):Ambassador 模式使用一個支援性容器來為主容器完成遠端資源的抽象。主容器直接連線到 Ambassador 容器,而 Ambassador 容器反過來連線到可能很複雜的外部資源池 - 比如說一個分散式 Redis 叢集 - 並完成資源抽象。主容器可以完成連線外部服務,而不必知道或者關心它們實際的部署環境。

  • Adaptor(介面卡模式):Adaptor 模式被用來翻譯主容器的資料、協議或是所使用的介面,來與外部使用者的期望標準對齊。Adaptor 容器也可以統一化中心服務的訪問入口,即便它們服務的使用者原本只支援互不相容的介面規範。

使用 Configmaps 和 Secrets 來儲存配置

儘管應用配置可以被一起打包進容器映象裡,但是讓你的元件在執行時保持可被配置能更好支援多環境部署以及提供更多管理靈活性。為了管理執行時的配置引數,Kubernetes 提供了兩個物件:ConfigMapsSecrets

ConfigMaps 是一種用於儲存可在執行時暴露給 pods 和其他物件的資料的機制。儲存在 ConfigMaps 裡的資料可以通過環境變數使用,或是作為檔案掛載到 pod 中。通過將應用設計成從這些位置讀取配置後,你可以在應用執行時使用 ConfigMaps 注入配置,並以此來修改元件行為而不用重新構建整個容器映象。

Secrets 是一種類似的 Kubernetes 物件型別,它主要被用來安全的儲存敏感資料,並根據需要選擇性的的允許 pods 或是其他元件訪問。Secrets 是一種方便的往應用傳遞敏感內容的方式,它不必像普通配置一樣將這些內容用純文字儲存在可以被輕易訪問到的地方。從功能性上講,它們的工作方式和 ConfigMaps 幾乎完全一致,所以應用可以用完全一樣的方式從二者中獲取資料。

ConfigMaps 和 Secrets 可以幫你避免將配置內容直接放在 Kubernetes 物件定義中。你可以只對映配置的鍵名而不是值,這樣可以允許你通過修改 CongfigMap 或 Secret 來動態更新配置。這使你可以修改線上 pod 和其他 kubernetes 物件的執行時行為,而不用修改這些資源本身的定義。

實現“就緒檢測(Readiness)”與“存活檢測(Liveness)”探針

Kubernetes 包含了非常多用來管理元件生命週期的開箱即用功能,它們可以確保你的應用始終保持健康和可用狀態。不過,為了利用好這些特性,Kubernetes 必須要理解它應該如何監控和解釋你的應用健康情況。為此,Kubernetes 允許你定義“就緒檢測探針(Readiness Probe)”與“存活檢測探針(Liveness Probe)”。

“存活檢測探針”允許 Kubernetes 來確定某個容器裡的應用是否處於存活與執行狀態。Kubernetes 可以在容器內週期性的執行一些命令來檢查基本的應用行為,或者可以往特定地址傳送 HTTP / TCP 網路請求來判斷程式是否可用、響應是否符合預期。如果某個“存活探測指標”失敗了,Kubernetes 將會重啟容器來嘗試恢復整個 pod 的功能。

“就緒檢測探針”是一個類似的工具,它主要用來判斷某個 Pod 是否已經準備好接受請求流量了。在容器應用完全就緒,可以接受客戶端請求前,它們可能需要執行一些初始化過程,或者當接到新配置時需要重新載入程式。當一個“就緒檢測探針”失敗後,Kubernetes 會暫停往這個 Pod 傳送請求,而不是重啟它。這使得 Pod 可以完成自身的初始化或者維護任務,而不會影響到整個組的整體健康狀況。

通過結合使用“存活檢測探針”與“就緒檢測探針”,你可以控制 Kubernetes 自動重啟 pods 或是將它們從後端服務組裡剔除。通過配置基礎設施來利用好這些特性,你可以讓 Kubernetes 來管理應用的可用性和健康狀況,而無需執行額外的運維工作。

使用 Deployments 來管理擴充套件性與可用性

在早些時候討論 Pod 設計基礎時,我們提到其他 Kubernetes 物件會建立在 Pod 的基礎上來提供更高階的功能。而 deployment 這個複合物件,可能是被定義和操作的最多次的 Kubernetes 物件。

Deployments 是一種複合物件,它通過建立在其他 Kubernetes 基礎物件之上來提供額外功能。它們為一類名為 replicasets 的中間物件新增了生命週期管理功能,比如可以實施“滾動升級(Rolling updates)”、回滾到舊版本、以及在不同狀態間轉換的能力。這些 replicasets 允許你定義 pod 模板並根據它快速拉起和管理多份基於這個模板的副本。這可以幫助你方便的擴充套件基礎設施、管理可用性要求,並在故障發生時自動重啟 Pods。

這些額外特性為相對簡單的 pod 抽象提供了一個管理框架和自我修復能力。儘管你定義的工作集最終還是由 pods 單元來承載,但是它們卻不是你通常應該最多配置和管理的單位。相反,當 pods 由 deployments 這種更高階物件配置時,應該把它們當做可以穩定執行應用的基礎構建塊來考慮。

建立 Services 與 Ingress 規則來管理到應用層的訪問

Deployment 允許你配置和管理可互換的 Pod 集合,以擴充套件應用以及滿足使用者需求。但是,如何將請求流量路由到這些 pods 則是例外一碼事了。鑑於 pods 會在滾動升級的過程中被換出、重啟,或者因為機器故障發生轉移,之前被分配給這個執行組的網路地址也會發生變化。Kubernetes services 通過維護動態 pods 資源池以及管理各基礎設施層的訪問許可權,來幫助你管理這部分複雜性。

在 Kuberntes 裡,services 是控制流量如何被路由到多批 pods 的機制。無論是為外部客戶轉發流量,還是管理多個內部元件之間的連線,services 允許你來控制流量該如何流動。然後,Kubernetes 將更新和維護將連線轉發到相關 pods 的所有必需資訊,即使環境或網路條件發生變化也一樣。

從內部訪問 Services

為了有效的使用 services,你首先應該確定每組 pods 服務的目標使用者是誰。如果你的 service 只會被部署在同一個 Kubernetes 叢集的其他應用所使用,那麼 ClusterIP 型別允許你使用一個僅在叢集內部可路由的固定 IP 地址來訪問一組 pods。所有部署在叢集上的物件都可以通過直接往這個 service IP 地址傳送請求來與這組 pod 副本通訊。這是最簡單的 service 型別,很適合在內部應用層使用。

Kubernetes 提供了可選的 DNS 外掛來為 services 提供名字解析服務。這允許 pods 和其他物件可以使用域名來代替 IP 地址進行通訊。這套機制不會顯著改動 service 的用法,但基於域名的識別符號可以使連線元件和定義服務間互動變得更簡單,而不需要提前知道 service IP 地址。

將 Services 向公網開放

如果你的應用需要被公網訪問,那麼 “負載均衡器(load balancer)”型別的 service 通常會是你的最佳選擇。它會使用應用所在的特定雲提供商 API 來配置一個負載均衡器,由這個負載均衡器通過一個公網 IP 來服務所有到 service pods 的流量。這種方式提供了一個到叢集內部網路的可控網路通道,從而將外部流量引入到你的 service pods 中。

由於“負載均衡器”型別會為每一個 service 都建立一個負載均衡器,因此用這種方式來暴露 Kubernetes 服務可能會有些昂貴。為了幫助緩解這個問題,我們可以使用 Kubernetes ingress 物件來描述如何基於預定規則集來將不同型別的請求路由到不同 services。例如,發往 “example.com” 的請求可能會被指向到 service A,而往 “sammytheshark.com” 的請求可能會被路由到 service B。Ingress 物件提供了一種描述如何基於預定義模式將混合請求流分別路由到它們的目標 services 的方式。

Ingress 規則必須由一個 ingress controller 來解析,它通常是某種負載均衡器(比如 Nginx),以 pod 的方式部署在叢集中,它實現了 ingress 規則並基於規則將流量分發到 Kubernetes serices 上。目前,ingress 資源物件定義仍然處於 beta 階段,但是市面上已經有好幾個能工作的具體實現了,它們可以幫助叢集所有者最小化需要執行的外部負載均衡器數量。

使用宣告式語法來管理 Kubernetes 狀態

Kubernetes 在定義和管理部署到叢集的資源方面提供了很大靈活性。使用 kubectl 這樣的工具,你可以命令式的定義一次性資源並將其快速部署到叢集中。雖然在學習 Kubernetes 階段,這個方法對於快速部署資源可能很有用,但這種方式也存在很多缺點,不適合長週期的生產環境管理。

命令式管理方式的最大問題之一就是它不儲存你往叢集部署過的變更記錄。這使得故障時恢復和跟蹤系統內運維變更操作變得非常困難,甚至不可能。

幸運的是,Kubernetes 提供了另外一種宣告式的語法,它允許你使用文字檔案來完整定義資源,並隨後使用 kubectl 命令應用這些配置或更改。將這些配置檔案儲存在版本控制系統裡,是監控變更以及與你的公司內其他部分的審閱過程整合的一種簡單方式。基於檔案的管理方式也讓將已有模式適配到新資源時變得簡單,只需要複製然後修改現有資源定義即可。將 Kubernetes 物件定義儲存在版本化目錄裡允許你維護叢集在每個時間節點的期望叢集狀態快照。當你需要進行故障恢復、遷移,或是追蹤系統裡某些意料之外的變更時,這些內容的價值是不可估量的。

總結

管理執行應用的基礎設施,並學習如何最好的利用這些現代化編排系統提供的特性,這些事情可能會令人望而生畏。但是,只有當你的開發與運維過程與這些工具的構建概念一致時,Kubernetes 系統、容器技術提供的優勢才能更好的體現出來。遵循 Kubernetes 最擅長的那些模式來架構你的系統,以及瞭解特定功能如何能緩解由高度複雜的部署帶來的挑戰,可以幫助改善你執行平臺時的體驗。

相關文章