歡迎光臨
每天分享高質量文章

如何在 Kubernetes 之上架構應用?

簡介

設計並運行一個兼顧可擴展性、可移植性和健壯性的應用是一件很有挑戰的事情,尤其是當系統複雜度在不斷增長時。應用或系統本身的架構極大的影響著其運行方式、對環境的依賴性,以及與相關組件的耦合強弱。當應用在一個高度分佈式的環境中運行時,如果能在設計階段遵循特定樣式,在運維階段恪守特定實踐,就可以幫助我們更好的應對那些最常出現的問題。
儘管軟體設計樣式和開發方法論可以幫助我們生產出滿足恰當擴展性指標的應用,基礎設施與運行環境也在影響著已部署系統的運維操作。像 Docker、Kubernetes 這些技術可以幫助團隊打包、分發、部署以及在分佈式環境中擴展應用。學習如何最好的駕馭這些工具,可以幫助你在管理應用時擁有更好的機動性、控制性和響應能力。
在這份指南里,我們將探討一些你可能想採用的準則和樣式,它們可以幫助你在 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 Linux 的 alpine 鏡像,該鏡像提供了一個輕量但是擁有完整特性的 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 提供了兩個物件:ConfigMaps 與 Secrets。
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 最擅長的那些樣式來架構你的系統,以及瞭解特定功能如何能緩解由高度複雜的部署帶來的挑戰,可以幫助改善你運行平臺時的體驗。
原文鏈接:https://www.digitalocean.com/community/tutorials/architecting-applications-for-kubernetes


Kubernetes專案實戰訓練營

Kubernetes專案實戰訓練將於2018年8月17日在深圳開課,3天時間帶你系統掌握Kubernetes本次培訓包括:Docker介紹、Docker鏡像、網絡、儲存、容器安全;Kubernetes架構、設計理念、常用物件、網絡、儲存、網絡隔離、服務發現與負載均衡;Kubernetes核心組件、Pod、插件、微服務、雲原生、Kubernetes Operator、集群災備、Helm等,點擊下方圖片查看詳情。

赞(0)

分享創造快樂