Kubernetes 實戰 —— 05. 服務:讓客戶端發現 pod 並與之通訊(下)

滿賦諸機發表於2021-04-04

將服務暴露給外部客戶端 P136

圖 5.5 將服務暴露給外部客戶端

有以下三種方式可以在外部訪問服務:

  • 將服務的型別設定成 NodePort
  • 將服務的型別設定為 LoadBalance
  • 建立一個 Ingress 資源

使用 NodePort 型別的服務 P137

通過建立一個 NodePort 服務,可以讓 Kubernetes 在其所有節點上保留一個埠(所有節點上都使用相同埠號),並將傳入的連線轉發給作為服務部分的 pod 。 P137

建立 NodePort 型別的服務 P137

可以使用如下描述檔案 kubia-svc-nodeport.yaml 建立一個 NodePort 型別的服務。

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 資源型別為 Service
kind: Service
metadata:
  # Service 的名稱
  name: kubia-nodeport
spec:
  # 指定服務型別為 NodePort
  type: NodePort
  # 該服務可用的埠
  ports:
    # 第一個可用埠的名字
    - name: http
      # 可用埠為 80
      port: 80
      # 服務將連線轉發到容器的 8080 埠
      targetPort: 8080
      # 通過叢集節點的 30000 埠可以訪問該服務
      nodePort: 30000
    # 第二個可用埠的名字
    - name: https
      # 可用埠為 443
      port: 443
      # 服務將連線轉發到容器的 8443 埠
      targetPort: 8443
      # 通過叢集節點的 32767 埠可以訪問該服務
      nodePort: 32767
  # 具有 app=kubia 標籤的 pod 都屬於該服務
  selector:
    app: kubia

nodePort 屬性不是強制的,如果忽略就會隨機選擇一個埠。 P137

kubectl get services kubia-nodeport: 檢視該服務的基礎資訊

NAME               TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)                      AGE
kubia-nodeport     NodePort       10.111.59.156    <none>            80:30000/TCP,443:32767/TCP   2s

PORT(S) 列顯示叢集 IP 內部埠 (80, 443) 和節點埠 (30000, 32767) ,可通過 10.111.59.156:80<any-node-ip>:30000 等訪問服務。 P138

圖 5.6 外部客戶端通過節點 1 或者節點 2 連線到 NodePort 服務

使用 JSONPath 輸出需要的資訊:通過指定 kubectl 的 JSONPath ,我們可以只輸出需要的資訊。例如: kubectl get nodes -o jsonpath='{.items[*].status.addresses[0].address}' 將輸出所有節點的 IP 地址。

通過負載均衡器將服務暴露出來 P140

負載均衡器擁有自己獨一無二的可公開訪問的 IP 地址,並將所有連線重定向到服務。 P140

建立 LoadBalance 服務 P140

可以使用如下描述檔案 kubia-svc-loadbalancer.yaml 建立一個 LoadBalancer 型別的服務。

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 資源型別為 Service
kind: Service
metadata:
  # Service 的名稱
  name: kubia-loadbalancer
spec:
  type: LoadBalancer
  # 該服務可用的埠
  ports:
    # 第一個可用埠的名字
    - name: http
      # 可用埠為 80
      port: 80
      # 服務將連線轉發到容器的 8080 埠
      targetPort: 8080
    # 第二個可用埠的名字
    - name: https
      # 可用埠為 443
      port: 443
      # 服務將連線轉發到容器的 8443 埠
      targetPort: 8443
  # 具有 app=kubia 標籤的 pod 都屬於該服務
  selector:
    app: kubia

使用 minikube 會發現服務的 EXTERNAL-IP 一直為 <pending> ,我們可以使用 minikube 自帶的 minikube tunnel 命令可以完成暴露(02. 開始使用 Kubernetes 和 Docker 介紹過相關處理及踩過的坑)。

圖 5.7 外部客戶端連線一個 LoadBalancer 服務.png

LoadBalancer 型別的服務是一個具有額外的基礎設施提供的負載均衡器 NodePort 服務。使用 kubectl describe service kubia-loadbalancer 命令可以發現該服務選擇了一個節點埠。 P141

瞭解外部連線的特性 P142

瞭解並防止不必要的網路跳數:當外部客戶端通過節點埠連線到服務時,隨機選擇的 pod 並不一定在接收連線的同一節點上。可能需要額外的網路跳轉才能到達 pod 。可配置 Service.spec.externalTrafficPolicy 的值為 Local 指定僅將外部通訊重定向到接收連線的節點上執行的 pod 。

  • 如果接收連線的節點上沒有執行對應的 pod ,那麼連線將掛起,所以需要確保負載均衡器將連線轉發給至少具有一個 pod 的節點
  • 假設有一個兩節點三個 pod 的叢集(節點 A 執行一個 pod ,節點 B 執行兩個 pod),如果負載均衡器在兩個節點間均勻分佈連線,那麼 pod 的負載分佈不均衡

圖 5.8 使用 Local 外部流量策略的服務可能會導致 pod 的負載分佈不均衡

客戶端 IP 是不記錄的:當通過節點埠接收到連線時,由於對資料包執行了源網路地址轉換 (SNAT) ,因此資料包對源 IP 將發生更改。如果配置 Service.spec.externalTrafficPolicy 的值為 Local ,那麼將會保留客戶端 IP ,因為在接收連線的節點和託管目標 pod 的節點之間沒有額外的跳躍(不執行 SNAT )。 P143

通過 Ingress 暴露服務 P143

為什麼需要 Ingress P144

每個 LoadBalancer 服務都需要自己的負載均衡器,以及獨有的公有 IP 地址,而 Ingress 只需要一個公網 IP 就能為許多服務提供訪問。當客戶端向 Ingress 傳送 HTTP 請求時, Ingress 會根據請求的主機名和路徑決定請求轉發到的服務。 P144

圖 5.9 通過一個 Ingress 暴露多個服務

Ingress 在網路棧 (HTTP) 的應用層操作,並且可以提供一些服務不能實現的功能,例如基於 cookie 的會話親和性 (session affinity) 等功能。 P144

Ingress 控制器是必不可少的 P144

只有 Ingress 控制器在叢集中執行, Ingress 資源才能正常工作。 P144

在 minikube 上啟動 Ingress 的擴充套件功能 P145

minikube addons list: 可以列出所有的外掛及其啟用狀態

minikube addons enable ingress: 啟用 ingress 外掛

kubectl get pods -n kube-system: 檢視 kube-system 名稱空間下的 pod ,可以發現 Ingress 控制器 pod

建立 Ingress 資源 P145

可以使用如下描述檔案 kubia-ingress.yaml 建立一個 Ingress 資源。

# 遵循 extensions/v1beta1 版本的 Kubernetes API
apiVersion: extensions/v1beta1
# 資源型別為 Ingress
kind: Ingress
metadata:
  # Ingress 的名稱
  name: kubia
spec:
  # Ingress 的規則列表
  rules:
    # 第一條規則匹配的域名為 kubia.example.com
    - host: kubia.example.com
      # 匹配 http
      http:
        # 匹配的路徑列表
        paths:
          # 第一條路徑為 /
          - path: /
            # 該路徑將被轉發到的後端服務
            backend:
              # 將被轉發到 kubia-nodeport 服務
              serviceName: kubia-nodeport
              # 對應服務的埠為 80
              servicePort: 80

minikube 下建立 Ingress 資源時報錯了,提示超時。後來找到一種解決方案:使用 kubectl edit ValidatingWebhookConfiguration/ingress-nginx-admission 進行編輯,找到 failurePolicy: Fail 這行,並將 Fail 改為 Ignore ,然後就能成功建立 Ingress 資源了,等一段時間後就可以看見其分配了一個 IP 地址 (192.168.64.70) 。

為了能將指定的域名 kubia.example.com 指向分配的 IP 地址 (192.168.64.70),可以使用 SwitchHosts 這個軟體進行快速切換。

此時我們在主機上就可以通過 curl kubia.example.com 訪問 kubia-nodeport 服務了。

瞭解 Ingress 的工作原理 P147

  1. 客戶端對 kubia.example.com 執行 DNS 查詢,本地作業系統返回了 Ingress 控制器的 IP
  2. 客戶端向 Ingress 控制器傳送 HTTP 請求,並在 Host 頭中指定 kubia.example.com
  3. 控制器從頭部確定客戶端嘗試訪問哪個服務,通過與該服務關聯的 Endpoint 物件檢視 pod IP ,並將客戶端的請求轉發給其中一個 pod

圖 5.10 通過 Ingress 訪問 pod

Ingress 控制器不會將請求轉發給服務,只用它來選擇一個 pod 。大多數控制器都是這樣工作的。 P147

通過相同的 Ingress 暴露多個服務 P147

Ingerssrulespaths 都是陣列,所以它們可以包含多個條目,因此一個 Ingress 可以將多個域名和路徑對映到多個服務。 P147

配置 Ingress 處理 TLS 傳輸 P149

Ingress 建立 TLS 認證 P149

當客戶端建立到 Ingress 控制器到 TLS 連線時,控制器將終止 TLS 連線。客戶端和控制器之間到通訊是加密的,而控制器和後端 pod 之間的通訊則未加密。執行在 pod 上的應用程式不需要支援 TLS 。 P149

為了讓 Ingress 控制器負責處理與 TLS 相關的所有內容,需要將證書和私鑰附加到 Ingress 。這兩個必須資源儲存在稱為 Secret 的 Kubernetes 資源中(將在第 7 章中詳細介紹 Secret),然後在 Ingress 的描述檔案中引用它。 P149

openssl genrsa -out tls.key 2048: 建立私鑰

openssl req -new -x509 -key tls.key -out tls.cert -days 365 -subj /CN=kubia.example.com: 建立證書

kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key: 建立 Secret 資源

然後我們就可以改寫 kubia-ingress.yaml 得到 kubia-ingress-tls.yaml 描述檔案:

...
spec:
  # 配置 TLS
  tls:
    # 第一條配置的域名列表
    - hosts:
      - kubia.example.com
      # 這些域名使用 tls-secret 獲得私鑰和證書
      secretName: tls-secret
  ...

然後我們就可以使用 curl -k -v https://kubia.example.com 通過 HTTPS 訪問服務了。( minikube 下未進行上述操作前也可以訪問,不過可以發現是 Ingress 控制器使用了假證書) P150

pod 就緒後發出訊號 P150

與存活探測器(04. 副本機制和其他控制器:部署託管的 pod 中介紹過)類似, Kubernetes 還允許為容器定義就緒探測器。就緒探測器會定期呼叫,並確保特定的 pod 是否接收客戶端請求。當容器的就緒探測器返回成功時,表示容器已準備好接收請求。 P151

就緒探測器的型別P151

  • Exec 探測器:在容器內執行任意命令,並檢查命令的退出狀態碼。如果狀態碼是 0 ,則探測成功,認為容器已經就緒,所有其他狀態碼都被認為失敗
  • HTTP GET 探測器:對容器的 IP 地址(指定的埠和路徑)執行 HTTP GET 請求。如果探測器收到響應,並且響應狀態碼不代表錯誤(狀態碼為 2xx 或 3xx ),則認為探測成功,認為容器已經就緒。如果伺服器返回錯誤響應狀態碼或者沒有響應,那麼探測就被認為是失敗的
  • TCP Socket探測器:嘗試與容器指定埠建立 TCP 連線。如果連線成功建立,則探測成功,認為容器已經就緒

瞭解就緒探測器的操作 P151

啟動容器時,可以為 Kubernetes 配置一個等待時間,經過等待時間後才可以執行第一次準備就緒檢查。之後,它會週期性地呼叫探測器,並根據就緒探測器的結果採取行動。如果某個 pod 報告它尚未準備就緒,那麼就會從服務中刪除該 pod ;如果這個 pod 再次準備就緒,那麼就會將給 pod 重新新增到服務中。 P151

圖 5.11 就緒探測失敗的 pod 從服務的 endpoint 中移除

就緒探測器和存活探測器的區別 P151

存活探測器通過殺死異常的容器並用新的正常容器替代它們來保持 pod 正常工作,而就緒探測器確保只有準備好處理請求的 pod 才可以接收請求,並不會終止或重新啟動容器。 P151

就緒探測器的重要性:確保客戶端只與正常的 pod 互動,並且永遠不會知道系統存在的問題。 P152

瞭解就緒探測器的實際作用 P154

務必定義就緒探測器 P155

應該始終定義一個就緒探測器,即使它只是向基準 URL 傳送 HTTP 請求一樣簡單。如果沒有將就緒探測器新增到 pod 中,那麼它們啟動後幾乎立即成為服務端點。 P155

不要將停止 pod 的邏輯納入到就緒探測器中 P155

當一個容器關閉時,執行在其中的應用程式通常會在收到終止訊號後立即停止接收連線。但在啟動關機程式後,沒有必要讓就緒探測器返回失敗以達到從所有服務中移除 pod 目的,因為在該容器刪除後, Kubernetes 就會自動從所有服務中移除該容器。 P155

使用 headless 服務來發現獨立的 pod P155

要讓客戶端連線到所有 pod ,需要找出每個 pod 的 IP 。 Kubernetes 允許客戶通過 DNS 查詢發現 pod IP 。通常,當執行服務的 DNS 查詢時, DNS 伺服器會返回單個 IP —— 服務的叢集 IP 。但是,如果告訴 Kubernetes ,不需要為服務提供叢集 IP (通過在服務 spec 中將 clusterIP 欄位設定為 None 來完成此操作),則 DNS 伺服器將會返回 pod IP 而不是單個服務 IP 。 P155

DNS 伺服器不會返回單個 DNS A 記錄,而是會為該服務返回多個 A 記錄,每個記錄指向當時支援該服務的單個 pod 的 IP 。客戶端因此可以做一個簡單的 DNS A 記錄查詢並獲取屬於該服務的所有 pod 的 IP 。客戶端可以使用該資訊連線到其中的一個、多個或全部。 P155

建立 headless 服務 P156

可以使用如下描述檔案 kubia-svc-headless.yaml 建立一個 headless 的 Service 資源。

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 資源型別為 Service
kind: Service
metadata:
  # Service 的名稱
  name: kubia-headless
spec:
  # 該服務的叢集 IP 為 None ,使其變為 headless 的
  clusterIP: None
  # 該服務可用的埠
  ports:
    # 第一個可用埠的名字
    - name: http
      # 可用埠為 80
      port: 80
      # 服務將連線轉發到容器的 8080 埠
      targetPort: 8080
    # 第二個可用埠的名字
    - name: https
      # 可用埠為 443
      port: 443
      # 服務將連線轉發到容器的 8443 埠
      targetPort: 8443
  # 具有 app=kubia 標籤的 pod 都屬於該服務
  selector:
    app: kubia

通過 DNS 發現 pod P156

kubia 容器映象不包含 nslookup 二進位制檔案,所以需要用一個新的容器映象來執行相應的命令。 P156

kubectl run dnsutils --image=tutum/dnsutils --generator=run-pod/v1 --command -- sleep infinity: 建立一個可以執行 nslookup 命令的 pod

kubectl exec dnsutils nslookup kubia-headless: 在 dnsutils pod 內執行 nslookup kubia-headless 命令,可以發現 DNS 伺服器為 kubia-headless.default.svc.cluster.local FQDN 返回了多個 IP ,且它們都是 pod 的 IP ,可以通過 kubectl get pods -o wide 進行確認

kubectl exec dnsutils nslookup kubia: 在 dnsutils pod 內執行 nslookup kubia 命令,可以發現 DNS 伺服器為 kubia.default.svc.cluster.local FQDN 返回了一個 IP ,該 IP 是服務的叢集 IP

儘管 headless 服務看起來可能與常規服務不同,但是在客戶的視角上它們並無不同。對於 headless 服務,由於 DNS 返回了 pod 的 IP ,客戶端直接連線到該 pod ,而不是通過服務代理(注意這裡是直接訪問的 pod ,所以對應的埠要改成 pod 的埠)。 P157

注意: headless 服務仍然提供跨 pod 的負載均衡,但是通過 DNS 輪循機制不是通過服務代理 P157

發現所有的 pod —— 包括未就緒的 pod P157

可以通過在 Service.metadata.annotations 下面增加一條 service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" 告訴 Kubernetes 無論 pod 的準備狀態如何,希望將所有 pod 新增到服務中 P158

排除服務故障 P158

如果無法通過服務訪問 pod ,應根據下面的列表進行排查: P158

  • 確保從叢集內連線到服務的叢集 IP
  • 不要通過 ping 服務 IP 來判斷服務是否可訪問(服務的叢集 IP 是虛擬 IP ,是無法 ping 通的)
  • 如果已經定義了就緒探測器,請確保它返回成功;否則該 pod 不會成為服務的一部分
  • 要確認某個容器是服務的一部分,請使用 kubectl get endpoints 來檢查相應的端點物件
  • 如果嘗試通過 FQDN 或其中一部分來訪問服務,但並不起作用,請檢視是否可以使用其叢集 IP 而不是 FQDN 來訪問服務
  • 檢查是否連線到服務公開的埠,而不是目標埠
  • 嘗試直接連線到 pod IP 以確認 pod 正在接收正確埠上的連線
  • 如果甚至無法通過 pod 的 IP 訪問應用,請確保應用不是僅繫結到 localhost (127.0.0.1)

本文首發於公眾號:滿賦諸機(點選檢視原文) 開源在 GitHub :reading-notes/kubernetes-in-action

相關文章