雲原生架構之服務發現與註冊-Kubernetes中服務註冊發現
“我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第2篇文章,點擊查看活動詳情”
一 K8s 網絡基礎
要理解K8s中服務註冊發現,需要先了解K8s中網絡相關概念。
- Kubernetes 應用運行在容器之中,容器處於 Pod 之內。
- 每個 Pod 都會附着在同一個大的扁平的 IP 網絡之中,被稱為 Pod 網絡(通常是 VXLAN 疊加網絡)。
- 每個 Pod 都有自己的唯一的 IP 地址,這個 IP 地址在 Pod 網絡中是可路由的。
上述三個因素結合起來,讓每個應用(應用的組件和服務)無需通過 NAT 之類的網絡過程,就能夠直接通信。 在對應用進行橫向擴容時,會在 Pod 網絡中加入新的 Pod,新 Pod 自然也伴隨着新的 IP 地址;如果對應用進行縮容,舊的 Pod 及其 IP 會被刪除。
應用的滾動更新和撤回也存在同樣的情形——加入新版本的新 Pod,或者移除舊版本的舊 Pod。新 Pod 會加入新 IP 到 Pod 網絡中,被終結的舊 Pod 會刪除其現存 IP。
從上面可以看出Pod網絡是一個扁平的網絡,隨着集羣中Pod的動態變化而變化,此時存在一個問題,如果想要統一提供一個固定的訪問入口,顯然Pod網絡無法實現,此時K8s提供了一個service網絡,
二 K8s service
2.1 K8s service簡介
Kubernetes Service 對象在一組提供服務的 Pod 之前創建一個穩定的網絡端點,併為這些 Pod 進行負載分配,可以理解為service為一個負載均衡,後端關聯一組Pod,這組Pod的IP+Port組成稱為endpoints。
一般會在一組完成同樣工作的 Pod 之前放置一個 Service 對象。例如可以在你的 Web 前端 Pod 前方提供一個 Service,在認證服務 Pod 之前提供另一個。行使不同職責的 Pod 之前就不應該用單一的 Service 了,Service利用Lable來匹配後端的真實Pod。
客户端和 Service 通信,Service 負責把流量負載均衡給 Pod。
在上圖中,底部的 Pod 會因為伸縮、更新、故障等情況發生變化,而 Service 會對這些變化進行跟蹤。同時 Service 的名字、IP 和端口都不會發生變化。 通常K8s service通過Kube-proxy 在宿主機通過Iptables/LVS來進行實施。
2.2 K8S Service 解析
可以把 Kubernetes Service 理解為前端和後端兩部分:
- 前端:名稱、IP 和端口等不變的部分。
- 後端:符合特定標籤選擇條件的 Pod 集合。
前端是穩定可靠的,它的名稱、IP 和端口在 Service 的整個生命週期中都不會改變。前端的穩定性意味着無需擔心客户端 DNS 緩存超時等問題。 後端是高度動態的,其中包括一組符合標籤選擇條件的 Pod,會通過負載均衡的方式進行訪問。
這裏的負載均衡是一個簡單的 4 層輪詢。它工作在連接層面,所以同一個連接裏發起的所有請求都會進入同一個 Pod。因為在 4 層工作,所以對於 7 層的 HTTP 頭或者 Cookie 之類的東西是無法感知的,通常K8s中的service僅具備簡單的輪詢(round robbin),如果需要高級服務治理能力,需要引入其他治理服務例如Istio/Linkerd等。
三 理解K8s服務註冊發現
深入瞭解 Kubernetes 服務發現服務發現實際上包含兩個功能點:
- 服務註冊
- 服務發現
3.1 服務註冊
服務註冊過程指的是在服務註冊表中登記一個服務,以便讓其它服務發現。
Kubernetes 使用 DNS 作為服務註冊表,詳細可參考:https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/dns-debugging-resolution/
為了滿足這一需要,每個 Kubernetes 集羣都會在 kube-system 命名空間中用 Pod 的形式運行一個 DNS 服務,通常稱之為集羣 DNS。
應用到K8s中的service會自動註冊到集羣 DNS 之中,註冊過程大致如下:
- 向 API Server 用 POST 方式提交一個新的 Service 定義;
- 這個請求需要經過認證、鑑權以及其它的准入策略檢查過程之後才會放行;
- Service 得到一個 ClusterIP(虛擬 IP 地址),並保存到集羣數據倉庫;
- 在集羣範圍內傳播 Service 配置;
- 集羣 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 發現了。
在此我們可以瞭解到,在K8s中的服務註冊,其實是通過CoreDNS的控制器自動實現,通過List/Watch K8s api自己關注的資源實現,底層使用到informer又是通過etcd的List/Watch原理來實現。
3.2 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。
3.3 服務發現
Kubernetes 支持兩種基本的服務發現模式,環境變量和 DNS。
3.3.1 環境變量
當 Pod 運行在 Node
上,kubelet 會為每個活躍的 Service 添加一組環境變量。 kubelet 為 Pod 添加環境變量 {SVCNAME}_SERVICE_HOST
和 {SVCNAME}_SERVICE_PORT
。 這裏 Service 的名稱需大寫,橫線被轉換成下劃線。 它還支持與 Docker Engine 的 "legacy container links" 特性兼容的變量 (參閲 makeLinkVariables) 。
舉個例子,一個名稱為 redis-master
的 Service 暴露了 TCP 端口 6379, 同時給它分配了 Cluster IP 地址 10.0.0.11,這個 Service 生成了如下環境變量:
javascript
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
説明:當你具有需要訪問服務的 Pod 時,並且你正在使用環境變量方法將端口和集羣 IP 發佈到客户端 Pod 時,必須在客户端 Pod 出現 之前 創建服務。 否則,這些客户端 Pod 將不會設定其環境變量。
3.3.2 DNS
假設我們在一個 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。
總體來説K8s的服務註冊與發現通過Etcd+CordDNS來實現,其中又包含了endpoints/kube-proxy等組件的協同。
- 雲原生架構之配置中心-總述
- 雲原生架構之Spring Cloud Kubernetes配置中心方案
- 雲原生架構之SpringCloudKubernetes 服務註冊發現方案(東西流量)
- 雲原生架構之服務發現與註冊-Kubernetes中服務註冊發現
- 雲原生架構之Springboot Gateway K8s 服務註冊發現方案(南北流量)
- 雲原生架構之SpringBoot K8s service服務註冊發現方案(東西流量)
- 雲原生架構之服務發現與註冊-總述
- 雲原生備份恢復工具Velero二開實戰
- Kubernetes安全之KubeEye
- Terraform 集成Ansible 實戰(remote)
- Terraform Gitlab CI簡單集成方案
- 實戰Kubernetes Gitlab CI
- Kubernetes集羣中流量暴露的幾種方案
- Kubectl插件開發及開源發佈分享
- Client-go源碼分析之SharedInformer及實戰
- TAPD 集成 GitLab
- Gin框架優雅關機和重啟
- Gin Swagger快速生成API文檔
- Golang 可視化工具之go-callvis
- 跨平台數據備份工具之restic詳解