1. 程式人生 > >一文看懂 Kubernetes 服務發現: Service

一文看懂 Kubernetes 服務發現: Service


Service 簡介

K8s 中提供微服務的實體是 Pod,Pod 在建立時 docker engine 會為 pod 分配 ip,“外部”流量通過訪問該 ip 獲取微服務。但是,Pod 的狀態是不穩定的,它容易被銷燬,重建,一旦重建, Pod 的 ip 將改變,那麼繼續訪問原來 ip 是不現實的。針對這個問題 K8s 引入 services 這一 kind,它提供類似負載均衡的作用。與 Pod 不同 service 在建立時 K8s 會為其分配一固定 ip,叫做 ClusterIP。外部流量訪問 ClusterIP 即可實現對 Pods 的訪問。   進一步的,通過以下示意圖說明 service 的工作原理:   如圖所示,service 定義了微服務的入口地址,它通過標籤選擇器匹配到需要轉發流量的 pod,將外部來的流量這裡是 frontend pod 來的流量引入到 Pod 中。

建立 Service

根據上節分析,這裡我們通過配置 yaml 檔案來建立 service,首先建立 service 需要“引流”的 Pods:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpd-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web_server
  template:
    metadata:
      labels:
        app: web_server
    spec:
      containers:
      - name: httpd-demo
        image: httpd
  分別介紹上面引數:
  • apiVersion: 建立資源的 api 版本,這裡是 apps/v1。
  • kind: 建立的資源型別為 Deployment。
  • metadata.name: 建立的 Deployment 名字。
  • replicas: 資源 Deployment 包括三個 Pods 副本。
  • matchLabels: 匹配到對應的 Pod 標籤。
  • labels: 副本 Pod 的標籤。
  • containers: Pod 內的 container,它是實際提供微服務的單元。
  上面我們建立了三個副本,且標籤為 app:web_server,RC(replicationController) 通過 matchLabels 和建立的三個副本關聯。   繼續建立 service:
apiVersion: v1
kind: Service
metadata:
  name: httpd-svc
spec:
  selector:
    app: web_server
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80
  建立名為 httpd-svc 的 service,標籤選擇器將 service 的標籤 app:web_server 和對應的 pods 關聯。service “對外”(對外實際上還是在叢集內)開放的埠為 8080,它將對映到 Pods 中的 80 埠。   Deployment,service 建立好後,我們構建瞭如下的測試場景:
## 建立 Deployment
$ kubectl apply -f deployment-test.yaml
deployment.apps/bootcamp-deployment created
$ kubectl get deployments.apps
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
bootcamp-deployment   3/3     3            3           2m5s
$ kubectl get replicasets.apps
NAME                            DESIRED   CURRENT   READY   AGE
bootcamp-deployment-f94bcd74c   3         3         3       2m16s
$ kubectl get pods -o wide
NAME                                  READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
bootcamp-deployment-f94bcd74c-k4mrc   1/1     Running   0          3m17s   172.18.0.5   minikube   <none>           <none>
bootcamp-deployment-f94bcd74c-q2x6c   1/1     Running   0          3m17s   172.18.0.6   minikube   <none>           <none>
bootcamp-deployment-f94bcd74c-wwcqx   1/1     Running   0          3m17s   172.18.0.4   minikube   <none>           <none>
 
## 建立 Service
$ kubectl get services
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
httpd-svc    ClusterIP   10.108.52.85   <none>        8080/TCP   41s
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP    10m
 
## frontend pod
$ kubectl run kubernetes-bootcamp --image=docker.io/jocatalin/kubernetes-bootcamp:v1 --port=8080 --labels="app=bootcamp"
deployment.apps/kubernetes-bootcamp created
$ kubectl get pods -o wide
NAME                                  READY   STATUS    RESTARTS   AGE     IP           NODE       NOMINATED NODE   READINESS GATES
bootcamp-deployment-f94bcd74c-k4mrc   1/1     Running   0          6m53s   172.18.0.5   minikube   <none>           <none>
bootcamp-deployment-f94bcd74c-q2x6c   1/1     Running   0          6m53s   172.18.0.6   minikube   <none>           <none>
bootcamp-deployment-f94bcd74c-wwcqx   1/1     Running   0          6m53s   172.18.0.4   minikube   <none>           <none>
kubernetes-bootcamp-9966c6d5-qpr9w    1/1     Running   0          14s     172.18.0.7   minikube   <none>           <none>
  開始訪問 Service:
## cluster node 訪問
$ curl 172.18.0.5:8080
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-k4mrc | v=1
 
$ curl 10.108.52.85:8080
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-k4mrc | v=1
$ curl 10.108.52.85:8080
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-q2x6c | v=1
$ curl 10.108.52.85:8080
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-wwcqx | v=1
## frontend pod 訪問
$ kubectl exec -it kubernetes-bootcamp-9966c6d5-qpr9w /bin/bash root@kubernetes-bootcamp-9966c6d5-qpr9w:/# curl 172.18.0.5:8080 Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-k4mrc | v=1 root@kubernetes-bootcamp-9966c6d5-qpr9w:/# curl 10.108.52.85:8080 Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-k4mrc | v=1 root@kubernetes-bootcamp-9966c6d5-qpr9w:/# curl 10.108.52.85:8080 Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-q2x6c | v=1 root@kubernetes-bootcamp-9966c6d5-qpr9w:/# curl 10.108.52.85:8080 Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-wwcqx | v=1




 
可以看出,訪問 service 即是訪問與 service 關聯的 pod。這裡未指定 service 的負載分發策略,它有兩種策略 RoundRobin 和 SessionAffinity。預設策略為 roundRobin 輪詢,即輪詢將請求轉發到後端各個 Pod。service 的 spec.sessionAffinity 欄位可修改訪問策略,當值為 ClientIP(預設為空) 即表示將同一個客戶端的訪問請求轉發到同一個後端 Pod。

叢集外部訪問 Service

service 是 K8s 中的概念,它分配的 ip 是邏輯的,沒有實體的 ip。所以在 K8s 叢集外無法訪問 service 的 ip,K8s 提供了 NodePort 和 LoadBalancer 兩種方式實現叢集外訪問 Service。這裡因實驗環境限制只介紹 NodePort 方式。   建立型別為 NodePort 的 service:
## 命令列建立 service, 也可通過 yaml 檔案建立
$ kubectl expose deployment/bootcamp-deployment --type="NodePort" --port 8080
service/bootcamp-deployment exposed
 
$ kubectl get service
NAME                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
bootcamp-deployment   NodePort    10.107.172.165   <none>        8080:32150/TCP   5s
httpd-svc             ClusterIP   10.108.52.85     <none>        8080/TCP         36m
kubernetes            ClusterIP   10.96.0.1        <none>        443/TCP          46m
 
$ kubectl describe service bootcamp-deployment
Name:                     bootcamp-deployment
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=web_server
Type:                     NodePort
IP:                       10.107.172.165
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  32150/TCP
Endpoints:                172.18.0.4:8080,172.18.0.5:8080,172.18.0.6:8080
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>
  可以看出,K8s 將 Service 的 8080 埠和 node 上的 32150 埠關聯,並且 node 上的 32150 埠被 (kube-proxy) 監聽:
$ netstat -antp | grep 32150
tcp6       0      0 :::32150                :::*                    LISTEN      4651/kube-proxy
 
$ ps aux | grep 4651 | grep -v grep
root      4651  0.0  1.2 140108 31048 ?  Ssl  07:15   0:01 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=minikube
  外部訪問 NodePort Service 的 ClusterIP + Port,即可訪問到對應的 Pod:
$ curl 172.17.0.72:32150
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-q2x6c | v=1
$ curl 172.17.0.72:32150
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-k4mrc | v=1

Service 的底層實現

探究 service 的底層實現就不得不提到 K8s 的核心元件 kube-proxy,它是一個位於 kube-system namespace 的 Pod,其核心功能是將到 service 的訪問請求轉發到後端 Pods:
$ kubectl get pods --namespace=kube-system | grep kube-proxy
kube-proxy-gk8zm                   1/1     Running   0          37s
  kube-proxy 的工作流程大致為,查詢和監聽 API server 的 services 和 Endpoints 變化,如果有變化則修改本機的 iptables。 kube-proxy 在 iptables 中自定義了 KUBE-SERVICES, KUBE-NODEPORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ 和 KUBE-MARK-DROP 五個鏈,其中 KUBE-SERVICES 鏈用來新增流量路由規則。每個鏈的作用分別為:
  • KUBE-SERVICES:操作跳轉規則的主要鏈。
  • KUBE-NODEPORTS:通過 nodeport 訪問的流量經過的鏈。
  • KUBE-POSTROUTING:post 路由經過的鏈。
  • KUBE-MARK-MASQ:對符合條件的 package set MARK0x4000,有此標記的資料包會在 KUBE-POSTROUTING 鏈中做 MASQUERADE。
  • KUBE-MARK-DROP:對未能匹配到跳轉規則的package set mark 0x8000,有該標記的包會在 filter 表中被 drop 掉。
  檢視路由表,以型別為 ClusterIP 的 service 為例:
-A KUBE-SERVICES -d 10.108.52.85/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-RL3JAE4GN7VOGDGP
  鏈 KUBE-SERVICES 的規則為訪問目的地址 10.108.52.85,埠為 8080 的資料包都被轉發到規則 KUBE-SVC-RL3JAE4GN7VOGDGP:
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-NGIJJXQTL6LQACUG
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-KSASEUT37GIWYDZK
-A KUBE-SVC-RL3JAE4GN7VOGDGP -j KUBE-SEP-TTP6SZ4CLZYRFEZJ
  規則 RL3JAE4GN7VOGDGP 有三條,三條規則分別是資料包隨機 1/3 的概率發到規則 KUBE-SEP-NGIJJXQTL6LQACUG,KSASEUT37GIWYDZK 和 KUBE-SEP-TTP6SZ4CLZYRFEZJ。再看這三條定義的是什麼規則:
-A KUBE-SEP-NGIJJXQTL6LQACUG -s 172.18.0.4/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-NGIJJXQTL6LQACUG -p tcp -m tcp -j DNAT --to-destination172.18.0.4:8080
  三條規則都是類似的,以規則 NGIJJXQTL6LQACUG 為例,其定義了兩條規則。第一條,如果發往 ClusterIP 的源 ip 地址是 172.18.0.4 則進入到鏈 KUBE-MARK-MASQ。第二條,如果是“外部”協議為 tcp 的資料包進入該規則,則做目的 NAT 轉換,將目的地址轉換為 Pod 的地址 172.18.0.4:8080。   繼續,檢視型別為 NodePort 的 service:
-A KUBE-SERVICES -d 10.107.172.165/32 -p tcp -m comment --comment "default/bootcamp-deployment: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-7BU2JDGBFZPRVB5H
  路由表中定義,叢集外部訪問 10.107.172.165 埠為 8080 的地址的流量將跳轉到規則 KUBE-SVC-7BU2JDGBFZPRVB5H:
-A KUBE-SVC-7BU2JDGBFZPRVB5H -m statistic --mode random --probability0.33333333349 -j KUBE-SEP-JRC46L5OEHUJ3JOG
-A KUBE-SVC-7BU2JDGBFZPRVB5H -m statistic --mode random --probability0.50000000000 -j KUBE-SEP-RVACF472KUDH2WI5
-A KUBE-SVC-7BU2JDGBFZPRVB5H -j KUBE-SEP-NHLSGKWGDK2BACCW
  類似的,規則 KUBE-SVC-7BU2JDGBFZPRVB5H 分別是資料包隨機 1/3 的概率發到規則 KUBE-SEP-JRC46L5OEHUJ3JOG,KUBE-SEP-RVACF472KUDH2WI5 和 KUBE-SEP-NHLSGKWGDK2BACCW。檢視規則 KUBE-SEP-JRC46L5OEHUJ3JOG 定義:
-A KUBE-SEP-JRC46L5OEHUJ3JOG -s 172.18.0.4/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-JRC46L5OEHUJ3JOG -p tcp -m tcp -j DNAT --to-destination 172.18.0.4:8080
  它有兩條規則,重點是第二條,當外部協議為 tcp 的資料包到達該規則,則做目的 NAT 將資料包直接發到目的地址 172.18.0.4:8080。

Service 與 Endpoints

細心的讀者會發現在 service 的 describe 內容中有個 Endpoints 引數,它描述的是 service 所對映的後端 pod 的地址,如下所示:
$ kubectl describe service httpd-svc
Name:              httpd-svc
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"httpd-svc","namespace":"default"},"spec":{"ports":[{"port":8080,"...
Selector:          app=web_server
Type:              ClusterIP
IP:                10.108.52.85
Port:              <unset>  8080/TCP
TargetPort:        8080/TCP
Endpoints:         172.18.0.4:8080,172.18.0.5:8080,172.18.0.6:8080
Session Affinity:  None
Events:            <none>
  selector 在匹配到對應的後端 pod 後,service 會更新 Endpoints 為後端 pod 的地址。這裡我們可以構造這樣一種場景,service 不通過 selector 選擇後端 pod,而是直接將它與 Endpoints 做關聯:   建立 Endpoints:
apiVersion: v1
kind: Endpoints
metadata:
  name: httpd-svc-endpoints
subsets:
  - addresses:
    - ip: 1.2.3.4
    ports:
    - port: 80
  建立 service:
apiVersion: v1
kind: Service
metadata:
  name: httpd-svc-endpoints
spec:
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80
 
[centos@k8s-master-node-1 test]$ kubectl describe services httpd-svc-endpoints
Name:              httpd-svc-endpoints
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          <none>
Type:              ClusterIP
IP:                10.102.65.186
Port:              <unset>  8080/TCP
TargetPort:        80/TCP
Endpoints:         1.2.3.4:80
Session Affinity:  None
Events:            <none>
  建立完畢,可以看到 service 的 Endpoints 更新為提前建立好的 Endpoints,且 selector 為 none。相應的,kube-proxy 會在路由表中建立 service 到 Endpoints 的規則。   除了不指定 selector,建立 service 時也可以不指定 ClusterIP (ClusterIP: None),不指定 ClusterIP 的 service 稱為 Headless service,使用該 service 即是去中心化,外部流量直接獲取到後端 pod 的 Endpoints,然後自行選擇該訪問哪個 pod。

Service 與 DNS

除了直接訪問 service ClusterIP 訪問後端 pod 外,還可以通過 service 名,對於 NodePort 型別的 service 也可通過叢集 node 名訪問 pod。K8s 中實現名稱解析和 ip 對應的核心元件是 coredns: 
$ curl minikube:32150
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-q2x6c | v=1
$ curl minikube:32150
Hello Kubernetes bootcamp! | Running on: bootcamp-deployment-f94bcd74c-wwcqx | v=1
 
$ kubectl get pods --namespace=kube-system -o wide | grep dns
coredns-6955765f44-rb828           1/1     Running   0          24m  172.18.0.3    minikube   <none>           <none>
coredns-6955765f44-rm4pv           1/1     Running   0          24m  172.18.0.2    minikube   <none>           <none>
  叢集內部訪問 pod 的流量路徑大致為流量訪問 service,通過 coredns 解析該 service 對應的 ClusterIP,繼續訪問 ClusterIP,當請求到達宿主機網路後 kube-proxy 會對請求做攔截,根據路由表規則將請求轉發到後端 pod 實現服務發現和流量轉發。叢集外部訪問 pod 的流量路徑大致類似。       關於 Kubernetes 服務發現 service 就介紹到這裡,想繼續深入瞭解,推薦看這篇博文。

&n