在 Kubernetes 中,Service
是一種抽象資源,它定義了一組提供相同服務的 Pod 的邏輯集合,並且能夠確保這些 Pod 能夠被持續地訪問。Service
提供了一個穩定的網絡端點,無論後端的 Pod 如何變動,使用者都能通過這個穩定的端點訪問服務。
在 Kubernetes 中,Pod 是有生命周期的,可能會被刪除或重新創建,這會導致它們的 IP 地址發生變化。如果外部應用或其他 Pod 需要與這些 Pod 通信,直接依賴 Pod 的 IP 地址是不可行的。Service
解決了這個問題,它提供了一個固定的訪問點,通過負載均衡來管理後端 Pod 的流量,確保服務的穩定性和可靠性。
Kubernetes 提供了多種 Service 類型,以適應不同的使用場景:
ClusterIP:該 Service 只在集群內部可達,為一組 Pod 提供一個內部訪問點。適用於集群內部的服務通信。ClusterIP 是 Service 的默認類型。
NodePort:該 Service 將服務暴露在每個節點的固定端口上,這個端口範圍通常是 30000-32767。NodePort 允許外部流量通過這些節點端口訪問服務。
LoadBalancer:在雲提供商(如 AWS、GCP)中使用時,該 Service 會創建一個外部負載均衡器,並將流量分發到後端的 Pod。這適用於需要暴露服務給外部客戶端的場景。
ExternalName:該 Service 將服務映射到一個 DNS 名稱,而不直接映射到 Kubernetes 集群內的 Pod。適用於需要指向集群外部服務的情況。
內部服務通信:使用 ClusterIP Service 來管理和暴露集群內部的微服務,使得不同的應用服務可以通過固定的服務名稱來相互通信。
外部訪問:使用 NodePort 或 LoadBalancer 來將集群內的服務暴露給集群外部的用戶或系統,特別是在需要對外提供 API、Web 服務等時。
跨區域或跨集群的服務:使用 ExternalName 將集群內的流量指向外部 DNS 名稱,從而與外部系統或不同區域的服務進行通信。
組態檔案: pod.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
app: foo
group: hello
name: foo
spec:
containers:
- image: lofairy/foo
name: foo
resources: {}
ports:
- containerPort: 8080
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
---
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
app: bar
group: hello
name: bar
spec:
containers:
- image: lofairy/bar
name: bar
resources: {}
ports:
- containerPort: 8080
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
kubectl apply -f pod.yaml
foo
Pod,試著訪問 foo
和 bar
的應用:kubectl exec -it foo -- bin/sh
/ # wget -q -O - foo:8080
---
foo
----------
/ # wget -q -O - bar:8080
---
wget: bad address 'bar:8080'
可以看到 Pod 之間無法互相訪問
組態檔案: clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
name: bar-service
spec:
selector:
app: bar
ports:
- protocol: TCP
port: 9527
targetPort: 8080
建立該 bar-service
會被指派並暴露 Cluster-IP(內部IP) 和 9527 端口,訪問這個 Cluster-IP:PORT
流量將會轉發到目標 Pod 的 8080
端口
bar-service
kubectl apply -f clusterip-service.yaml
bar-service
的 ClusterIPkubectl get svc
---
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bar-service ClusterIP 10.96.123.76 <none> 9527/TCP 12s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h
foo
Pod,並訪問 Cluster-IP:PORT
kubecl exec -it foo -- bin/sh
/ # wget -q -O - 10.96.123.76:9527
---
bar
透過 clusterIP:<port>
,可以訪問到其他 Pod 的應用
也可以使用 [[DNS name]] 訪問
foo
Pod 中,使用 nslookup 指令進行 DNS 解析/ # nslookup bar-service
---
Server: 10.96.0.10
Address: 10.96.0.10:53
** server can't find bar-service.cluster.local: NXDOMAIN
** server can't find bar-service.svc.cluster.local: NXDOMAIN
** server can't find bar-service.cluster.local: NXDOMAIN
Name: bar-service.default.svc.cluster.local
Address: 10.96.123.76
** server can't find bar-service.svc.cluster.local: NXDOMAIN
問題排除
如果遇到 Cluster-IP 可以訪問,但 DNS name 連線卻被拒絕的情況,可以刪除
kube-dns
Pod 來重啟 coredns:
## 刪除 dns pod
kubectl delete pod -n kube-system -l k8s-app=kube-dns
## 查看 dns pod
kubectl get pod -n kube-system -l k8s-app=kube-dns
kubectl delete -f clusterip-service.yaml
組態檔案: nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
name: bar-service
spec:
type: NodePort
selector:
app: bar
ports:
- port: 9527
targetPort: 8080
nodePort: 30000
kubectl apply -f nodeport-service.yaml
bar
Pod 所在的節點kubectl get pod -o wide
---
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
bar 1/1 Running 0 25m 10.244.2.2 wslkind-worker2 <none> <none>
foo 1/1 Running 0 25m 10.244.1.2 wslkind-worker <none> <none>
可以得知,bar
Pod 位於 wslkind-worker2
節點
wslkind-worker2
節點kubectl get node -o wide
---
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
wslkind-control-plane Ready control-plane 8m24s v1.30.0 172.18.0.4 <none> Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 containerd://1.7.15
wslkind-worker Ready <none> 8m4s v1.30.0 172.18.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 containerd://1.7.15
wslkind-worker2 Ready <none> 8m3s v1.30.0 172.18.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 containerd://1.7.15
可以得知,wslkind-worker2
節點的內部 IP 為 172.18.0.3
我們可以使用 docker inspect
命令來檢查 wslkind-worker2
節點容器,得知 Docker 容器分配給該節點的 IP
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 4aa30c0afd06
---
172.18.0.3
可以看到,docker inspect
查到的容器 IP 與 Kubernetes 節點的 internal IP 是相同的
由於使用 KinD 建立了集群,節點並不對容器外暴露 IP。不過我們可以使用 Docker 指令進入任意節點容器,繼續進行測試。
docker exec -it wslkind-control-plane /bin/sh
# curl 172.18.0.3:30000
---
bar
LoadBalancer 服務旨在使用雲端供應商提供的負載平衡器基礎架構。我們不在雲端運行,但我們需要平衡負載器為我們分配外部 IP。作為代替,我們使用第三方的 MetalLB
負載平衡器實作。
metallb
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.7/config/manifests/metallb-native.yaml
組態檔案: IP-Pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
# 可分配的 IP 地址,可以指定多個,包括 ipv4、ipv6
- 172.20.175.140-172.20.175.150
IP-Pool
kubectl apply -f IP-Pool.yaml
kubectl delete -f nodeport-service.yaml
組態檔案: lb-service.yaml
apiVersion: v1
kind: Service
metadata:
name: lb-service
spec:
type: LoadBalancer
selector:
group: hello
ports:
- protocol: TCP
port: 9527
targetPort: 8080
kubectl apply -f lb-service.yaml
kubectl get svc
---
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 148m
lb-service LoadBalancer 10.96.82.109 172.20.175.140 9527:32216/TCP 5s
由於使用 KinD 建立了集群,儘管 MetalLB
分配了外部 IP 給我們,整個叢集對容器外還是不暴露 IP。不過我們可以使用 Docker 指令進入任一節點容器,繼續進行測試。
docker exec -it wslkind-control-plane /bin/sh
# curl 172.20.175.140:9527
bar# curl 172.20.175.140:9527
foo# curl 172.20.175.140:9527
foo# curl 172.20.175.140:9527
foo# curl 172.20.175.140:9527
bar#
kubectl delete -f lb-service.yaml
組態檔案: externalname-service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ExternalName
externalName: www.google.com
kubectl apply -f externalname-service.yaml
kubectl get svc
---
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28d
my-service ExternalName <none> www.google.com <none> 9s
kubectl run -it --image busybox:1.36 dns-test --restart=Never --rm
nslookup my-service.default.svc.cluster.local
---
Server: 10.96.0.10
Address: 10.96.0.10:53
my-service.default.svc.cluster.local canonical name = www.google.com
Name: www.google.com
Address: 2404:6800:4012:3::2004
my-service.default.svc.cluster.local canonical name = www.google.com
Name: www.google.com
Address: 142.251.43.4
在 Kubernetes 中,Headless Service
是一種特別的 Service,它與普通 Service 的主要區別在於它不會分配一個 Cluster IP。當創建 Headless Service 時,Kubernetes 不會配置負載均衡或代理,只是簡單地根據服務的 selector
創建 DNS 條目,將 Pod 的 IP 地址直接暴露給客戶端。這樣,客戶端可以直接與後端的 Pod 通信,而不需要通過 Service 代理。
Headless Service 的主要用途是為那些需要直接處理每個 Pod 的應用程序提供支持,而不是通過一個統一的 IP 和負載均衡器。例如,在某些分佈式系統中,如資料庫分片或狀態一致性協議,每個節點(Pod)可能需要直接被訪問,以便進行節點之間的通信或數據分布。
分布式數據庫集群:在分布式數據庫系統(如 Cassandra、Elasticsearch)中,每個節點(Pod)都是獨立的,並且需要直接與其他節點通信。Headless Service 允許客戶端直接獲取每個 Pod 的 IP,從而進行點對點通信。
有狀態應用:一些有狀態的應用程序(如 StatefulSet 管理的應用)可能需要直接訪問特定的 Pod,Headless Service 提供了這種靈活性。
自定義負載均衡:如果應用程序需要自定義的負載均衡策略,而不是依賴 Kubernetes 的內建負載均衡,則可以使用 Headless Service 來實現。
以下是一個簡單的 Headless Service 組態檔範例:
apiVersion: v1
kind: Service
metadata:
name: my-headless-service
spec:
clusterIP: None
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 8080
只要設定 spec.clusterIP: None
就可以建立 Headless Service。
Headless Service
只有在與 StatefulSet
搭配時,才能為每個 Pod 副本提供獨立的 DNS 名稱。不過,由於我們尚未介紹 StatefulSet
,接下來我們將使用 Deployment
來展示 Headless Service
與一般 ClusterIP Service
的差異。
組態檔案: headless-service-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo-deployment
spec:
replicas: 3
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- name: foo
image: lofairy/foo
---
apiVersion: v1
kind: Service
metadata:
name: foo-headless-svc
spec:
clusterIP: None
selector:
app: foo
ports:
- protocol: TCP
port: 8080
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: foo-clusterip-svc
spec:
selector:
app: foo
ports:
- protocol: TCP
port: 8080
targetPort: 8080
這個組態檔案包含了一個有 3 個副本的 Deployment 以及兩個類型的 Service:
foo-clusterip-svc
是上面展示過的 ClusterIP 類型
foo-clusterip-svc
是 Headless 類型。
創建資源
kubectl apply -f headless-service-demo.yaml
kubectl get pod -o wide
---
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
foo-deployment-66587db9f-285gj 1/1 Running 0 5h50m 10.244.2.31 wslkind-worker2 <none> <none>
foo-deployment-66587db9f-89s7m 1/1 Running 0 5h50m 10.244.2.32 wslkind-worker2 <none> <none>
foo-deployment-66587db9f-kfl85 1/1 Running 0 5h50m 10.244.1.13 wslkind-worker <none> <none>
kubectl get svc
---
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
foo-clusterip-svc ClusterIP 10.96.109.160 <none> 8080/TCP 56m
foo-headless-svc ClusterIP None <none> 8080/TCP 56m
可以看到 foo-headless-svc
沒有被分配叢集 IP。
kubectl get svc -n kube-system
---
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
foo-clusterip-svc ClusterIP 10.96.109.160 <none> 80/TCP 62m
foo-headless-svc ClusterIP None <none> 80/TCP 62m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28d
CoreDNS Service IP 是 10.96.0.10
。
kubectl run -it --image busybox:1.36 dns-test --restart=Never --rm
foo-clusterip-svc
DNS Namenslookup foo-clusterip-svc.default.svc.cluster.local 10.96.0.10
---
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: foo-clusterip-svc.default.svc.cluster.local
Address: 10.96.109.160
foo-headless-svc
DNS Namenslookup foo-headless-svc.default.svc.cluster.local 10.96.0.10
---
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: foo-headless-svc.default.svc.cluster.local
Address: 10.244.2.32
Name: foo-headless-svc.default.svc.cluster.local
Address: 10.244.1.13
Name: foo-headless-svc.default.svc.cluster.local
Address: 10.244.2.31
可以看到 foo-headless-svc
服務找到了三個 Endpoint,正好對應了副本數量,也對上了 Pod 副本分配的 IP。我們試著訪問這三個 Endpoint。
/ # wget -q -O - 10.244.2.32:8080/hostname
{"hostname":"foo-deployment-66587db9f-89s7m"}/ #
---
/ # wget -q -O - 10.244.1.13:8080/hostname
{"hostname":"foo-deployment-66587db9f-kfl85"}/ #
---
/ # wget -q -O - 10.244.2.31:8080/hostname
{"hostname":"foo-deployment-66587db9f-285gj"}/ #
當我們使用 Deployment 搭配 Headless Service 時,無法為 Pod 提供個別的 DNS 名稱。通常,如果需要這種功能,會選擇使用 StatefulSet 搭配 Headless Service。在之後的 StatefulSet 章節中,我們會展示這一特性。