【Kubernetes】K8s筆記(十):Service 解決服務發現的關鍵問題
在云原生時代,微服務無疑是應用的主流形態。為了更好地支持微服務以及服務網格這樣的應用架構,Kubernetes 又專門定義了一個新的對象:Service,它是集群內部的負載均衡機制,用來解決服務發現的關鍵問題。在 Kubernetes Service 文檔中,Service 被定義為將運行在一組 Pods 上的應用程序公開為網絡服務的抽象方法。
0. 打造 Service 對象的動機
有了 Deployment 和 DaemonSet,我們在集群里發布應用程序的工作輕松了很多。借助 Kubernetes 強大的自動化運維能力,我們可以把應用的更新上線頻率由以前的月、周級別提升到天、小時級別,讓服務質量更上一層樓。
在 Kubernetes 集群里 Pod 的生命周期是比較“短暫”的,雖然 Deployment 和 DaemonSet 可以維持 Pod 總體數量的穩定,但在運行過程中,難免會有 Pod 銷毀又重建,這就會導致 Pod 集合處于動態的變化之中。
這導致了一個問題: 如果一組 Pod(稱為“后端”)為集群內的其他 Pod(稱為“前端”)提供功能, 那么前端如何找出并跟蹤要連接的 IP 地址,以便前端可以使用提供工作負載的后端部分?
業內早就有解決方案來針對這樣“不穩定”的后端服務,那就是“負載均衡”,典型的應用有 LVS、Nginx 等等。它們在前端與后端之間加入了一個“中間層”,屏蔽后端的變化,為前端提供一個穩定的服務。
因此 Kubernetes 定義了一個新的 API 對象:Service。
1. Service 的工作原理
Service 的工作原理和 LVS、Nginx 差不多,Kubernetes 會給它分配一個靜態 IP 地址,然后它再去自動管理、維護后面動態變化的 Pod 集合,當客戶端訪問 Service,它就根據某種策略,把流量轉發給后面的某個 Pod。
這張圖展示的是 Service 的 iptables
代理模式。每個節點上的 kube-proxy 組件自動維護 iptables 規則,客戶不再關心 Pod 的具體地址,只要訪問 Service 的固定 IP 地址,Service 就會根據 iptables 規則轉發請求給它管理的多個 Pod,這是典型的負載均衡架構。此外,使用 iptables 處理流量具有較低的系統開銷,因為流量由 Linux netfilter 處理, 而無需在用戶空間和內核空間之間切換。 這種方法也可能更可靠。
Service 并不是只能使用 iptables 來實現負載均衡,它還有另外兩種實現技術:性能更差的 userspace 和性能更好的 ipvs,但這些都屬于底層細節,我們不需要刻意關注。
2. 使用 YAML 描述 Service
我們還是可以用命令 kubectl api-resources
查看它的基本信息,可以知道它的簡稱是 svc
,apiVersion 是 v1。注意,這說明它與 Pod 一樣,屬于 Kubernetes 的核心對象,不關聯業務應用,與 Job、Deployment 是不同的。
這里 Kubernetes 又表現出了行為上的不一致。雖然它可以自動創建 YAML 樣板,但不是用命令 kubectl create
,而是另外一個命令 kubectl expose
,也許 Kubernetes 認為 expose
能夠更好地表達 Service “暴露”服務地址的意思吧。
使用 kubectl expose
指令時還需要用參數 --port
和 --target-port
分別指定映射端口和容器端口,而 Service 自己的 IP 地址和后端 Pod 的 IP 地址可以自動生成,用法上和 Docker 的命令行參數 -p
很類似,只是略微麻煩一點。
例如,我們對 Deployment 筆記中的 ngx-dep
對象生成 Service ,命令就應該這樣寫:
$ export out="--dry-run=client -o yaml"
$ kubectl expose deploy ngx-dep --port=80 --target-port=80 $out
下面是剔除一些字段后的 YAML:
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
可以發現,Service 的定義非常簡單,spec
中只有兩個關鍵字 selector
和 ports
。
-
selector
和 Deployment/DaemonSet 里的作用是一樣的,用來過濾出要代理的那些 Pod。因為我們指定要代理 Deployment,所以 Kubernetes 就為我們自動填上了ngx-dep
的標簽,會選擇這個 Deployment 對象部署的所有 Pod。 -
ports
就很好理解了,里面的三個字段分別表示外部端口、內部端口和使用的協議,在這里就是內外部都使用 80 端口,協議是 TCP。\
3. 在 Kubernetes 中使用 Service
在 YAML 創建 Service 對象之前,我們要先對 Deployment 筆記中的 ngx-dep
做一點改造:方便觀察 Service 的效果。
首先,創建一個 ConfigMap,定義一個 Nginx 的配置片段,它會相應服務器地址、主機名、請求的 URI 等信息:
# ngx-conf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location / {
default_type text/plain;
return 200
'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
}
}
$ kubectl apply -f ngx-conf.yaml
然后在 Deployment 中的 template.volumes
里定義存儲卷,再用 volumeMounts
將配置文件加載到容器中:
# ngx-dep.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
replicas: 3
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
$ kubectl apply -f ngx-conf.yaml
部署這個 Deployment 之后,我們就可以創建 Service 對象了:
# ngx-svc.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: ngx-dep
name: ngx-dep
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
$ kubectl apply -f ngx-svc.yaml
創建之后,我們就可以查看該 Service 對象的狀態:
$ kubelet get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d <none>
ngx-svc ClusterIP 10.96.21.182 <none> 80/TCP 10s app=ngx-dep
Kubernetes 為 Service 對象自動分配了一個 IP 地址 10.96.21.182
,這個地址段是獨立于 Pod 地址段的(10.10.xx.xx
)。而且 Service 對象的 IP 地址還有一個特點,它是一個“虛地址”,不存在實體,只能用來轉發流量。
想要看 Service 代理了哪些后端的 Pod,可以使用 describe
子命令:
$ kubectl describe svc ngx-svc
Name: ngx-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=ngx-dep
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.21.182
IPs: 10.96.21.182
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.10.1.34:80,10.10.1.35:80,10.10.1.36:80
Session Affinity: None
Events: <none>
顯示 Service 對象管理了 3 個 endpoint
:10.10.1.34:80
, 10.10.1.35:80
, 10.10.1.36:80
。
下面查看一下 Pod 詳情:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ngx-dep-545884c69c-9q6gd 1/1 Running 0 39m 10.10.1.34 worker1 <none> <none>
ngx-dep-545884c69c-rbfpn 1/1 Running 0 39m 10.10.1.35 worker1 <none> <none>
ngx-dep-545884c69c-vzdf2 1/1 Running 0 39m 10.10.1.36 worker1 <none> <none>
redis-ds-8zmdb 1/1 Running 1 (140m ago) 23h 10.10.0.39 k8s-master <none> <none>
redis-ds-tp9sd 1/1 Running 1 (140m ago) 23h 10.10.1.30 worker1 <none> <none>
可以看到 Service 確實用一個靜態 IP 地址代理了 3 個 Pod 的動態 IP 地址。
接下來測試負載均衡的效果,因為 Service、 Pod 的 IP 地址都是 Kubernetes 集群的內部網段,所以我們需要用 kubectl exec
進入到 Pod 內部(或者 ssh 登錄集群節點),再用 curl
等工具來訪問 Service:
$ kubectl exec -it ngx-dep-545884c69c-9q6gd -- sh
/ # curl 10.96.21.182
srv : 10.10.1.34:80
host: ngx-dep-545884c69c-9q6gd
uri : GET 10.96.21.182 /
date: 2022-10-20T03:04:55+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2022-10-20T03:04:59+00:00
/ # curl 10.96.21.182
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 10.96.21.182 /
date: 2022-10-20T03:05:02+00:00
在 Pod 里,用 curl
訪問 Service 的 IP 地址,就會看到它把數據轉發給后端的 Pod,輸出信息會顯示具體是哪個 Pod 響應了請求,就表明 Service 確實完成了對 Pod 的負載均衡任務。
再試著刪除一個 Pod,看看 Service 是否會更新后端 Pod 的信息,實現自動化的服務發現:
$ kubectl delete pod ngx-dep-545884c69c-9q6gd
pod "ngx-dep-545884c69c-9q6gd" deleted
$ kubectl describe svc ngx-svc
Name: ngx-svc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=ngx-dep
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.21.182
IPs: 10.96.21.182
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.10.1.35:80,10.10.1.36:80,10.10.1.37:80
Session Affinity: None
Events: <none>
可以看到一個 IP 地址為 10.10.1.37
的 Pod 被添加進來了。
由于 Pod 被 Deployment 對象管理,刪除后會自動重建,而 Service 又會通過 controller-manager 實時監控 Pod 的變化情況,所以就會立即更新它代理的 IP 地址。
4. 以域名方式使用 Service
Service 還有一些高級特性值得了解。
首先是 DNS 域名。Service 對象的 IP 地址是靜態的,保持穩定,這在微服務里確實很重要,不過數字形式的 IP 地址用起來還是不太方便。這個時候 Kubernetes 的 DNS 插件就派上了用處,它可以為 Service 創建易寫易記的域名,讓 Service 更容易使用。
使用 DNS 域名之前,我們要先了解一個新的概念:名字空間 namespace
,它被用來在集群里實現對 API 對象的隔離和分組。
namespace 的簡寫是 ns
,使用命令 kubectl get ns
來查看當前集群里都有哪些名字空間,也就是說 API 對象有哪些分組:
$ kubectl get ns
NAME STATUS AGE
default Active 2d
kube-flannel Active 2d
kube-node-lease Active 2d
kube-public Active 2d
kube-system Active 2d
Kubernetes 有一個默認的名字空間 default
,如果不顯式指定,API 對象都會在這個 default
名字空間里。而其他的名字空間都有各自的用途,比如 kube-system
就包含了 apiserver、etcd 等核心組件的 Pod。
因為 DNS 是一種層次結構,為了避免太多的域名導致沖突,Kubernetes 就把名字空間作為域名的一部分,減少了重名的可能性。
Service 對象的域名完全形式是:
Object.namespace.svc.cluster.local
但很多時候也可以省略后面的部分,直接寫 object.namesapce
甚至 object
就足夠了,默認會使用對象所在的名字空間(比如這里就是 default)。
現在我們來試驗一下 DNS 域名的用法,還是先 kubectl exec
進入 Pod,然后用 curl
訪問 ngx-svc
、ngx-svc.default
等域名:
$ kubectl exec -it ngx-dep-545884c69c-rbfpn -- sh
/ # curl ngx-svc
srv : 10.10.1.37:80
host: ngx-dep-545884c69c-mmrnm
uri : GET ngx-svc /
date: 2022-10-20T07:04:40+00:00
/ # curl ngx-svc.default
srv : 10.10.1.35:80
host: ngx-dep-545884c69c-rbfpn
uri : GET ngx-svc.default /
date: 2022-10-20T07:04:45+00:00
可以看到,現在我們就不再關心 Service 對象的 IP 地址,只需要知道它的名字,就可以用 DNS 的方式去訪問后端服務。
順便說一下,Kubernetes 也為每個 Pod 分配了域名,形式是 IP Address.namespace.pod.cluster.local
,但需要把 IP 地址里的 .
改成 -
。比如地址 10.10.1.87
,它對應的域名就是 10-10-1-87.default.pod
。
這里是 Kubernetes 文檔 - Service 與 Pod 的 DNS。
5. 讓 Service 對外暴露服務
由于 Service 是一種負載均衡技術,所以它不僅能夠管理 Kubernetes 集群內部的服務,還能夠擔當向集群外部暴露服務的重任。
Service 對象有一個關鍵字段 type
,表示 Service 是哪種類型的負載均衡。
-
ClusterIP
- 對集群內部 Pod 的負載均衡,Service 的靜態 IP 地址只能在集群內訪問 -
ExternalName
- 通過返回CNAME
和對應值,可以將服務映射到externalName
字段的內容(例如foo.bar.example.com
)。 無需創建任何類型代理 查看文檔 -
LoadBalancer
- 一般依賴云服務提供商 查看文檔 -
NodePort
- 通過每個節點上的 IP 和靜態端口(NodePort)暴露服務
在實驗環境里我們使用 NodePort:
如果在使用命令 kubectl expose
的時候加上參數 --type=NodePort
,或者在 YAML 里添加字段 type:NodePort
,那么 Service 除了會對后端的 Pod 做負載均衡之外,還會在集群里的每個節點上創建一個獨立的端口,用這個端口對外提供服務,這也正是 NodePort 這個名字的由來。
下面我們給 Service 的 YAML 文件加上 type
字段:
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep
$ kubectl apply -f ngx-svc.yaml
service/ngx-svc configured
$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d5h <none>
ngx-svc NodePort 10.96.21.182 <none> 80:30952/TCP 5h25m app=ngx-dep
就會看到 TYPE
變成了 NodePort
,而在 PORT
列里的端口信息也不一樣,除了集群內部使用的 80
端口,還多出了一個 30952
端口,這就是 Kubernetes 在節點上為 Service 創建的專用映射端口。
因為這個端口號屬于節點,外部能夠直接訪問,所以現在我們就可以不用登錄集群節點或者進入 Pod 內部,直接在集群外使用任意一個節點的 IP 地址,就能夠訪問 Service 和它代理的后端服務了。
比如我使用宿主機 IP: 172.16.63.1
訪問集群內的 Worker1 節點 IP: 172.16.63.129:30952
就可以得到 Nginx Pod 的響應數據:
$ curl 172.16.63.129:30952
srv : 10.10.1.36:80
host: ngx-dep-545884c69c-vzdf2
uri : GET 172.16.63.129 /
date: 2022-10-20T08:20:21+00:00
NodePort 類型的 Service 有下面幾個缺點:
-
端口數量很有限。Kubernetes 為了避免端口沖突,默認只在“30000~32767”這個范圍內隨機分配,只有 2000 多個,而且都不是標準端口號,這對于具有大量業務應用的系統來說根本不夠用
-
它會在每個節點上都開端口,然后使用 kube-proxy 路由到真正的后端 Service,這對于有很多計算節點的大集群來說就帶來了一些網絡通信成本,不是特別經濟
-
它要求向外界暴露節點的 IP 地址,這在很多時候是不可行的,為了安全還需要在集群外再搭一個反向代理,增加了方案的復雜度