一文詳解 Kubernetes 中的服務發現,運維請收藏

語言: CN / TW / HK

K8S 服務發現之旅

Kubernetes 服務發現是一個經常讓我產生困惑的主題之一。本文分為兩個部分:

  • 網路方面的背景知識

  • 深入瞭解 Kubernetes 服務發現

要了解服務發現,首先要了解背後的網路知識。這部分內容相對淺顯,如果讀者熟知這一部分,完全可以跳過,直接閱讀服務發現部分。

開始之前還有一個需要提醒的事情就是,為了詳細描述這一過程,本文略長。

K8S 網路基礎

要開始服務發現的探索之前,需要理解以下內容:

  1. Kubernetes 應用執行在容器之中,容器處於 Pod 之內。

  2. 每個 Pod 都會附著在同一個大的扁平的 IP 網路之中,被稱為 Pod 網路(通常是 VXLAN 疊加網路)。

  3. 每個 Pod 都有自己的唯一的 IP 地址,這個 IP 地址在 Pod 網路中是可路由的。

上述三個因素結合起來,讓每個應用(應用的元件和服務)無需通過 NAT 之類的網路過程,就能夠直接通訊。

動態網路

在對應用進行橫向擴容時,會在 Pod 網路中加入新的 Pod,新 Pod 自然也伴隨著新的 IP 地址;如果對應用進行縮容,舊的 Pod 及其 IP 會被刪除。這個過程看起來很是混亂。

應用的滾動更新和撤回也存在同樣的情形——加入新版本的新 Pod,或者移除舊版本的舊 Pod。新 Pod 會加入新 IP 到 Pod 網路中,被終結的舊 Pod 會刪除其現存 IP。

如果沒有其它因素,每個應用服務都需要對網路進行監控,並管理一個健康 Pod 的列表。這個過程會非常痛苦,另外在每個應用中編寫這個邏輯也是很低效的。幸運的是,Kubernetes 用一個物件完成了這個過程——Service。

把這個物件叫做 Service 是個壞主意,我們已經用這個單詞來形容應用的程序或元件了。

還有一個值得注意的事情:Kubernetes 執行 IP 地址管理(IPAM)職責,對 Pod 網路上已使用和可用的 IP 地址進行跟蹤。

Service 帶來穩定性

Kubernetes Service 物件在一組提供服務的 Pod 之前建立一個穩定的網路端點,併為這些 Pod 進行負載分配。

一般會在一組完成同樣工作的 Pod 之前放置一個 Service 物件。例如可以在你的 Web 前端 Pod 前方提供一個 Service,在認證服務 Pod 之前提供另一個。行使不同職責的 Pod 之前就不應該用單一的 Service 了。

客戶端和 Service 通訊,Service 負責把流量負載均衡給 Pod。

在上圖中,底部的 Pod 會因為伸縮、更新、故障等情況發生變化,而 Service 會對這些變化進行跟蹤。同時 Service 的名字、IP 和埠都不會發生變化。

K8S Service 解析

可以把 Kubernetes Service 理解為前端和後端兩部分:

  • 前端: 名稱、IP 和埠等不變的部分。

  • 後端: 符合特定標籤選擇條件的 Pod 集合。

前端是穩定可靠的,它的名稱、IP 和埠在 Service 的整個生命週期中都不會改變。前端的穩定性意味著無需擔心客戶端 DNS 快取超時等問題。

後端是高度動態的,其中包括一組符合標籤選擇條件的 Pod,會通過負載均衡的方式進行訪問。

這裡的負載均衡是一個簡單的 4 層輪詢。它工作在連線層面,所以同一個連線裡發起的所有請求都會進入同一個 Pod。因為在 4 層工作,所以對於 7 層的 HTTP 頭或者 Cookie 之類的東西是無法感知的。

小結

應用在容器中執行,在 Kubernetes 中體現為 Pod 的形式。Kubernetes 叢集中的所有 Pod 都處於同一個平面的 Pod 網路,有自己的 IP 地址。這意味著所有的 Pod 之間都能直接連線。然而 Pod 是不穩定的,可能因為各種因素建立和銷燬。

Kubernetes 提供了穩定的網路端點,稱為 Service,這個物件處於一組相似的 Pod 前方,提供了穩定的名稱、IP 和埠。客戶端連線到 Service,Service 把流量負載均衡給 Pod。

接下來聊聊服務發現。

深入 K8S 服務發現

深入瞭解 Kubernetes 服務發現

服務發現實際上包含兩個功能點:

  1. 服務註冊

  2. 服務發現

服務註冊

服務註冊過程指的是在服務登錄檔中登記一個服務,以便讓其它服務發現。

Kubernetes 使用 DNS 作為服務登錄檔。

為了滿足這一需要,每個 Kubernetes 叢集都會在 kube-system 名稱空間中用 Pod 的形式執行一個 DNS 服務,通常稱之為叢集 DNS。

每個 Kubernetes 服務都會自動註冊到叢集 DNS 之中。

註冊過程大致如下:

  1. 向 API Server 用 POST 方式提交一個新的 Service 定義;

  2. 這個請求需要經過認證、鑑權以及其它的准入策略檢查過程之後才會放行;

  3. Service 得到一個 ClusterIP(虛擬 IP 地址),並儲存到叢集資料倉庫;

  4. 在叢集範圍內傳播 Service 配置;

  5. 叢集 DNS 服務得知該 Service 的建立,據此建立必要的 DNS A 記錄。

上面過程中,第 5 個步驟是關鍵環節。叢集 DNS 使用的是 CoreDNS,以 Kubernetes 原生應用的形式執行。

CoreDNS 實現了一個控制器,會對 API Server 進行監聽,一旦發現有新建的 Service 物件,就建立一個從 Service 名稱對映到 ClusterIP 的域名記錄。這樣 Service 就不必自行向 DNS 進行註冊,CoreDNS 控制器會關注新建立的 Service 物件,並實現後續的 DNS 過程。

DNS 中註冊的名稱就是 metadata.name,而 ClusterIP 則由 Kubernetes 自行分配。

Service 物件註冊到叢集 DNS 之中後,就能夠被執行在叢集中的其它 Pod 發現了。

Endpoint 物件

Service 的前端建立成功並註冊到服務登錄檔(DNS)之後,剩下的就是後端的工作了。後端包含一個 Pod 列表,Service 物件會把流量分發給這些 Pod。

毫無疑問,這個 Pod 列表需要是最新的。

Service 物件有一個 Label Selector 欄位,這個欄位是一個標籤列表,符合列表條件的 Pod 就會被服務納入到服務的負載均衡範圍之中。參見下圖:

Kubernetes 自動為每個 Service 建立 Endpoints 物件。Endpoints 物件的職責就是儲存一個符合 Service 標籤選擇器標準的 Pod 列表,這些 Pod 將接收來自 Service 的流量。

下面的圖中,Service 會選擇兩個 Pod,並且還展示了 Service 的 Endpoints 物件,這個物件裡包含了兩個符合 Service 選擇標準的 Pod 的 IP。

在後面我們將解釋網路如何把 ClusterIP 流量轉發給 Pod IP 的過程,還會引用到 Endpoints 物件。

服務發現

假設我們在一個 Kubernetes 叢集中有兩個應用,my-app 和 your-app,my-app 的 Pod 的前端是一個 名為 my-app-svc 的 Service 物件;your-app Pod 之前的 Service 就是 your-app-svc。

這兩個 Service 物件對應的 DNS 記錄是:

  • my-app-svc: 10.0.0.10

  • your-app-svc: 10.0.0.20

要使用服務發現功能,每個 Pod 都需要知道叢集 DNS 的位置才能使用它。因此每個 Pod 中的每個容器的 /etc/resolv.conf 檔案都被配置為使用叢集 DNS 進行解析。

如果 my-app 中的 Pod 想要連線到 your-app 中的 Pod,就得向 DNS 伺服器發起對域名 your-app-svc 的查詢。假設它們本地的 DNS 解析快取中沒有這個記錄,則需要把查詢提交到叢集 DNS 伺服器。會得到 you-app-svc 的 ClusterIP(VIP)。

這裡有個前提就是 my-app 需要知道目標服務的名稱。

至此,my-app 中的 Pod 得到了一個目標 IP 地址,然而這只是個虛擬 IP,在轉入目標 Pod 之前,還有些網路工作要做。

網路

一個 Pod 得到了 Service 的 ClusterIP 之後,就嘗試向這個 IP 傳送流量。然而 ClusterIP 所在的網路被稱為 Service Network,這個網路有點特別——沒有路由指向它。

因為沒有路由,所有容器把發現這種地址的流量都發送到了預設閘道器(名為 CBR0 的網橋)。這些流量會被轉發給 Pod 所在節點的網絡卡上。節點的網路棧也同樣沒有路由能到達 Service Network,所以只能傳送到自己的預設閘道器。路由到節點預設閘道器的資料包會通過 Node 核心——這裡有了變化。

回顧一下前面的內容。首先 Service 物件的配置是全叢集範圍有效的,另外還會再次說到 Endpoints 物件。我們要在回顧中發現他們各自在這一過程中的職責。

每個 Kubernetes 節點上都會執行一個叫做 kube-proxy 的系統服務。這是一個基於 Pod 執行的 Kubernetes 原生應用,它所實現的控制器會監控 API Server 上 Service 的變化,並據此建立 iptables 或者 IPVS 規則,這些規則告知節點,捕獲目標為 Service 網路的報文,並轉發給 Pod IP。

有趣的是,kube-proxy 並不是一個普遍意義上的代理。它的工作不過是建立和管理 iptables/IPVS 規則。這個命名的原因是它過去使用 unserspace 模式的代理。

每個新 Service 物件的配置,其中包含它的 ClusterIP 以及 Endpoints 物件(其中包含健康 Pod 的列表),都會被髮送給 每個節點上的 kube-proxy 程序。

kube-proxy 會建立 iptables 或者 IPVS 規則,告知節點捕獲目標為 Service ClusterIP 的流量,並根據 Endpoints 物件的內容轉發給對應的 Pod。

也就是說每次節點核心處理到目標為 Service 網路的資料包時,都會對資料包的 Header 進行改寫,把目標 IP 改為 Service Endpoints 物件中的健康 Pod 的 IP。

原本使用的 iptables 正在被 IPVS 取代(Kubernetes 1.11 進入穩定期)。長話短說,iptables 是一個包過濾器,並非為負載均衡設計的。IPVS 是一個 4 層的負載均衡器,其效能和實現方式都比 iptables 更適合這種使用場景。

總結

需要消化的內容很多,簡單回顧一下。

建立新的 Service 物件時,會得到一個虛擬 IP,被稱為 ClusterIP。服務名及其 ClusterIP 被自動註冊到叢集 DNS 中,並且會建立相關的 Endpoints 物件用於儲存符合標籤條件的健康 Pod 的列表,Service 物件會向列表中的 Pod 轉發流量。

與此同時叢集中所有節點都會配置相應的 iptables/IPVS 規則,監聽目標為 ClusterIP 的流量並轉發給真實的 Pod IP。這個過程如下圖所示:

一個 Pod 需要用 Service 連線其它 Pod。首先向叢集 DNS 發出查詢,把 Service 名稱解析為 ClusterIP,然後把流量傳送給位於 Service 網路的 ClusterIP 上。

然而沒有到 Service 網路的路由,所以 Pod 把流量傳送給它的預設閘道器。這一行為導致流量被轉發給 Pod 所在節點的網絡卡,然後是節點的預設閘道器。這個操作中,節點的核心修改了資料包 Header 中的目標 IP,使其轉向健康的 Pod。

譯者:偽架構師

來源:https://nigelpoulton.com/blog/f/demystifying-kubernetes-service-discovery

GOPS 全球運維大會 2021 · 上海站,80位運維專家為您分享BATJ、銀行、證券、通訊、網際網路大廠的 DevOps、AIOps、質量保障體系、混沌工程等精彩議題,11月18日-19日,精彩呈 現~

近期好文:

大牛精通各種技術體系,因已45歲求職難?阿里996造成嚴重交通阻塞,遭朝陽群眾舉報;雙 11 資料新鮮出爐 | 一週 IT資訊

“高效運維”公眾號誠邀廣大技術人員投稿,

投稿郵箱:[email protected],或新增聯絡人微信:greatops1118.

點個“在看”,一年不宕機