Kubernetes Service 完全指南:从基础概念到金丝雀发布实战
Kubernetes Service
摘要:本文全面介绍了 Kubernetes Service 的核心概念与实践应用。首先概述了 Service 的四种类型:ClusterIP(集群内部访问)、NodePort(节点端口暴露)、LoadBalancer(负载均衡器)以及 ExternalName 和 Headless Services。随后详细讲解了 Service 会话保持(Session Affinity)的配置原理及其与 kube-proxy IPVS 的关系。最后通过金丝雀发布(Canary Deployment)的完整示例,演示了如何实现渐进式应用更新。文章包含丰富的命令行操作、YAML 配置及 Mermaid 流程图,适合作为 Kubernetes 网络与服务发现的实践指南。
学习参考:Service
环境准备
root@worker30 ~ 10:03:42# kubectl create ns services
root@master30 ~ 09:49:39# kubectl config set-context --current --namespace services
Context "kubernetes-admin@kubernetes" modified.
Service 类型
Kubernetes Service 支持以下四种类型:
-
ClusterIP:只能在集群内部访问。
-
NodePort:通过物理节点的端口来访问,每个物理节点都提供相同的端口。
-
LoadBalancer:负载均衡,来源于物理网段中一个独立的IP。
-
ExternalName:配置集群内部的CName。
-
Headless:只有服务名,不分配IP地址。
我们的应用可能希望将 Service 暴露在一个外部 IP 地址上。 Kubernetes 支持两种实现方式:NodePort 和 LoadBalancer。
环境准备
创建 deployment
root@master30 ~ 10:13:26# kubectl create deployment web --image=hub.laoma.cloud/library/httpd --replicas=2
ClusterIP
ClusterIP,是通过集群的内部 IP 公开 Service,选择该值时 Service 只能够在集群内部访问。 这也是服务类型的默认值。
-
ClusterIP 从集群中预留的 IP 地址池中分配一个 IP 地址。其他几种 Service 类型在
ClusterIP类型的基础上进行构建。 -
在创建
Service的请求中,可以通过设置.spec.clusterIP字段来指定自己的集群 IP 地址。如果将 Service 的
.spec.clusterIP设置为"None",则 Kubernetes 不会为其分配 IP 地址。 -
所选择的 IP 地址必须是合法的 IPv4 或者 IPv6 地址,并且这个 IP 地址在 API 服务器上所配置的
service-cluster-ip-rangeCIDR 范围内。 如果你尝试创建一个带有非法clusterIP地址值的 Service,API 服务器会返回 HTTP 状态码 422, 表示值不合法。
其他信息参考上面的 发现 Service 章节。
NodePort
-
如果将Service 的
type字段设置为NodePort,则 Kubernetes 控制平面将在--service-node-port-range标志所指定的范围内分配端口(默认值:30000-32767)。 Service 在其.spec.ports[*].nodePort字段中报告已分配的端口。 -
NodePort类型的 Service 通过每个节点上的 IP 和分配的端口(NodePort)公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 NodePort 类型的Service 配置 clusterIP 地址。每个节点将该端口(每个节点上的相同端口号)上的流量代理到 Service。
示例:
root@master30 ~ 10:13:28# kubectl expose deployment web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-NodePort.yaml
root@master30:~# vim service-NodePort.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 80
selector:
app: web
type: NodePort
status:
loadBalancer: {}
# 创建NodePort类型Service
root@master30:~# kubectl apply -f service-NodePort.yaml
# 查看node节点对应端口为31917
root@master30 ~ 10:14:41# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.104.229.35 <none> 8080:31667/TCP 4s
# 说明:
# 1-EXTERNAL-IP为nodes, 表示可通过Cluster每个节点自身的IP访问Service。
# 2-PORT(S)为8080:31917。 8080是ClusterIP监听的端口,31917则是节点上监听的端口。
# Kubernetes会从30000~32767中分配一个可用的端口,每个节点都会监听此端口并将请求转发给Service
# 访问集群中任一节点测试
root@client:~# curl http://10.1.8.30:31667
root@client:~# curl http://10.1.8.31:31667
root@client:~# curl http://10.1.8.32:31667
分析防火墙规则:
# 与ClusterIP对比,每个节点的iptables中额外增加了下面两条规则:
root@master30 ~ 10:20:04# iptables-save | grep 31667
-A KUBE-NODEPORTS -p tcp -m comment --comment "services/web" -m tcp --dport 31917 -j KUBE-SVC-WE5D4GWSX3MMPCHH
# 规则的含义是:访问当前节点31917端口的请求会应用规则KUBE-SVC-WE5D4GWSX3MMPCHH
# 进一步分析,其作用就是负载均衡到每一个Pod。
root@master30:~# iptables-save |grep KUBE-SVC-WE5D4GWSX3MMPCHH
-A KUBE-SERVICES -d 10.110.40.0/32 -p tcp -m comment --comment "laoma/web:http cluster IP" -m tcp --dport 8080 -j KUBE-SVC-WE5D4GWSX3MMPCHH
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "services/web" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LUIIA2GDKT4VDBA2
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "services/web" -j KUBE-SEP-Y6B6PNSXFIBR4D2K
NodePort默认的是随机选择, 我们可以使用nodePort指定为特定端口。
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 8080
protocol: TCP
# 指定节点固定端口
nodePort: 30080
targetPort: 80
selector:
app: web
type: NodePort
status:
loadBalancer: {}
端口说明:
- nodePort是节点上监听的端口。
- port是ClusterIP上监听的端口。
- targetPort是Pod监听的端口。
清理环境
root@master30 ~ 10:20:32# kubectl delete svc web
LoadBalancer
kubernetes并没有真正实现 LoadBalancer,需要借助第三方工具实现,例如metallb。
每个LoadBalancer类型的service需要关联一个公网IP。创建LoadBalancer类型的service只需要将Service 的 Type 改成 LoadBalancer。
这里我们使用 metallb。
官方地址: https://metallb.universe.tf/
github地址:https://github.com/metallb/metallb
部署
部署 metallb
root@master30 ~ 11:14:48# wget http://192.168.46.200/class/course-materials/softwares/stage03/metallb-0.14.8.tar.gz
root@master30 ~ 11:16:12# tar -xf metallb-0.14.8.tar.gz
# 查看镜像
root@master30:~# grep image metallb-0.14.8/config/manifests/metallb-native.yaml
image: quay.io/metallb/controller:v0.14.8
image: quay.io/metallb/speaker:v0.14.8
# 按需修改镜像
root@master30:~# sed -i 's/quay.io/hub.laoma.cloud/g' metallb-0.14.8/config/manifests/metallb-native.yaml
root@master30 ~ 11:16:59# kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
# 等待着所有pod正常运行再进行下一步
root@master30 ~ 11:17:45# kubectl get all -n metallb-system
NAME READY STATUS RESTARTS AGE
pod/controller-d6499775f-kdmvt 1/1 Running 0 40s
pod/speaker-gvlm4 1/1 Running 0 40s
pod/speaker-vkws9 1/1 Running 0 40s
pod/speaker-wv6rg 1/1 Running 0 40s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/metallb-webhook-service ClusterIP 10.103.135.65 <none> 443/TCP 40s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 3 3 3 3 3 kubernetes.io/os=linux 40s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 40s
NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-d6499775f 1 1 1 40s
配置地址池
root@master30 ~ 11:17:46# cat << 'EOF' > ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.40-10.1.8.80
EOF
root@master30 ~ 11:17:55# kubectl apply -f ippool.yaml
配置 lay2
root@master30 ~ 11:17:59# cat << 'EOF' > L2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
EOF
root@master30 ~ 11:18:06# kubectl apply -f L2.yaml
测试
root@master30 ~ 11:18:10# kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml
root@master30 ~ 11:18:16# cat service-LoadBalancer.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: web
type: LoadBalancer
status:
loadBalancer: {}
root@master30 ~ 11:18:24# kubectl apply -f service-LoadBalancer.yaml
service/web created
root@master30 ~ 11:18:32# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.107.58.174 10.1.8.40 80:32620/TCP 5s
# 访问测试,端口号使用service的80,非32101端口
root@master30 ~ 11:18:37# curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html>
总结
- 客户端流量到达pod路径:客户端请求 → MetalLB 虚拟 IP(LB IP) → 节点 kube-proxy → Service 转发 → 后端 Pod。
- ✅ MetalLB 只做 ARP 宣告 + IP 占坑,转发全靠 kube-proxy + Service。
- MetalLB + Service 是标准 K8s 负载均衡流程。
详细流程图
- 客户端发起请求:用户通过浏览器 / 应用访问 MetalLB 分配的 LB VIP(如 10.1.8.200)。
- ARP 寻址,流量进入集群节点:MetalLB 使用 Layer2 模式,通过 ARP 广播声明 VIP 归属,流量进入集群任意一个节点。
- **节点内核拦截流量:**节点识别目标地址为 Service LB IP,将流量交给内核网络框架处理。
- **kube-proxy 执行转发规则:**kube-proxy 匹配 iptables/IPVS 规则,确定流量所属 Service。
- **Service 负载均衡选择 Pod:**从 Service 后端 endpoints 中,按策略选择一个健康 Pod。
- **CNI 网络跨节点转发:**若 Pod 不在当前节点,流量通过 Calico/Flannel 等 CNI 网络转发到目标节点。
- **流量进入 Pod:**目标节点通过 veth-pair 设备,将流量送入 Pod 网络命名空间。
- **Pod 响应请求:**业务容器接收请求并返回数据,原路响应给客户端。
ExternalName
ExternalName,将服务映射到 externalName 字段的内容(例如,api.foo.bar.example)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。
例如,将 prod 命名空间中的 my-service 服务映射到 database.example.com。
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: database.example.com
当查找主机 my-service.prod.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 database.example.com。访问 my-service 的方式与访问其他 Service 的方式相同, 主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发来完成。
Headless Services
有时并不需要负载均衡,也不需要单独的 Service IP。遇到这种情况,可以通过显式设置ClusterIP的值为 None 来创建无头服务(Headless Service)。
无头 Service 不会获得集群 IP,kube-proxy 不会处理这类 Service, 而且平台也不会为它们提供负载均衡或路由支持。
取决于 Service 是否定义了选择算符,DNS 会以不同的方式被自动配置。
-
带选择算符的服务,对定义了选择算符的无头 Service,Kubernetes 控制平面在 Kubernetes API 中创建 EndpointSlice 对象,并且修改 DNS 配置返回 A 或 AAAA 记录(IPv4 或 IPv6 地址), 这些记录直接指向 Service 的后端 Pod 集合。
-
无选择算符的服务,对没有定义选择算符的无头 Service,控制平面不会创建 EndpointSlice 对象。 然而 DNS 系统会执行以下操作之一:
-
对于
type: ExternalNameService,查找和配置其 CNAME 记录; -
对所有其他类型的 Service,针对 Service 的就绪端点的所有 IP 地址,查找和配置 DNS A/AAAA 记录:对于 IPv4 端点,DNS 系统创建 A 记录;对于 IPv6 端点,DNS 系统创建 AAAA 记录。
当你定义无选择算符的无头 Service 时,
port必须与targetPort匹配。
-
Service 会话保持
会话保持介绍
如果要确保来自特定客户端的连接每次都传递给同一个 Pod, 你可以通过设置 Service 的 .spec.sessionAffinity 为 ClientIP 来设置基于客户端 IP 地址的会话亲和性(默认为 None)。
你还可以通过设置 Service 的 .spec.sessionAffinityConfig.clientIP.timeoutSeconds 来设置最大会话粘性时间(默认值为 10800,即 3 小时)。
工作原理:
- 客户端首次访问服务,Service将请求转发给某个Pod,Service记录客户端和Pod的对应关系。
- 客户端的后续请求继续转发给同一个Pod。
适合有状态服务(如:Java Web、WebSocket、游戏服务):
- 登录状态不丢失
- 购物车不丢失
- 长连接稳定
准备测试环境
root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/httpd --replicas=2
root@master30:~# kubectl expose deployment web --port 80
root@master30:~# for pod in $(kubectl get pods -o name | awk -F/ '{print $2}'); do kubectl exec -it $pod -- bash -c "echo $pod > htdocs/index.html"; done
root@master30:~# kubectl get svc web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.111.90.183 <none> 80/TCP 9m25s
root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
10 web-6db76cb4fc-5gbx2
10 web-6db76cb4fc-b8f7l
设置会话保持
root@master30:~# kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'
# 再次访问:结果保持一致
root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
20 web-6db76cb4fc-5gbx2
会话保持与 kube-proxy IPVS 的关系
1. service 里的 sessionAffinity 优先级最高
service 配置 ClientIP → kube-proxy 自动使用 IPVS 的 SH 算法
- SH = Source Hashing 源地址哈希
- 保证同一 IP → 同一 Pod
2. 如果 service 不配置 sessionAffinity
kube-proxy 就用ipvs scheduler 里设置的算法:
- rr(轮询)
- lc(最少连接)
- wrr(加权轮询)
金丝雀发布
学习参考:金丝雀部署
环境准备:
root@master30:~# mkdir web root@master30:~# echo hello nginx 1.28 > web/index28.html root@master30:~# echo hello nginx 1.29 > web/index29.html root@master30:~# kubectl create configmap web --from-file=./web root@master30:~# kubectl get configmaps web -o yaml |grep ^data -A4 data: index28.html: | hello nginx 1.28 index29.html: | hello nginx 1.29
使用金丝雀发布部署应用新版本 ,同时保留用旧版本。 这样,新版本在完全发布之前也可以接收实时的生产流量。
例如,你可以使用 track 标签来区分不同的版本。
-
主要稳定的发行版将有一个
track标签,其值为stable:root@master30:~# vim webapp-1.28.yamlapiVersion: apps/v1 kind: Deployment metadata: labels: app: web name: web-28 spec: replicas: 10 selector: matchLabels: app: web tier: frontend track: stable template: metadata: labels: app: web tier: frontend track: stable spec: containers: - image: hub.laoma.cloud/library/nginx:1.28 name: nginx imagePullPolicy: IfNotPresent ports: - containerPort: 80 volumeMounts: - name: webcontent mountPath: "/usr/share/nginx/html" volumes: - name: webcontent configMap: name: web items: - key: index28.html path: index.htmlroot@master30:~# kubectl apply -f webapp-1.28.yaml -
创建 service
root@master30:~# vim webapp-svc.yamlapiVersion: v1 kind: Service metadata: labels: app: web name: web spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: web tier: frontendroot@master30:~# kubectl apply -f webapp-svc.yaml root@master30:~# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web ClusterIP 10.98.235.36 <none> 80/TCP 5m31s -
部署应用新版本。新的发行版将有一个
track标签,其值为canary:root@master30:~# vim webapp-1.29.yamlapiVersion: apps/v1 kind: Deployment metadata: labels: app: web name: web-29 spec: replicas: 1 selector: matchLabels: app: web tier: frontend track: canary template: metadata: labels: app: web tier: frontend track: canary spec: containers: - image: hub.laoma.cloud/library/nginx:1.29 name: nginx imagePullPolicy: IfNotPresent ports: - containerPort: 80 volumeMounts: - name: webcontent mountPath: "/usr/share/nginx/html" volumes: - name: webcontent configMap: name: web items: - key: index29.html path: index.htmlroot@master30:~# kubectl apply -f webapp-1.29.yaml验证访问比例:
root@master30:~# kubectl scale deployment web-28 --replicas 8 root@master30:~# kubectl scale deployment web-29 --replicas 2 root@master30:~# for i in {1..50}; do curl -s 10.98.235.36; done | sort -n|uniq -c 39 hello nginx 1.28 11 hello nginx 1.29 -
总pod数量不变的情况下,逐步减少旧版本和增加新版本副本数量,。
root@master30:~# kubectl scale deployment web-28 --replicas 6 root@master30:~# kubectl scale deployment web-29 --replicas 4 root@master30:~# for i in {1..50}; do curl -s 10.98.235.36; done | sort -n|uniq -c 31 hello nginx 1.28 19 hello nginx 1.29
环境清理
root@master30:~# kubectl delete ns services
更多推荐


所有评论(0)