跳到主要内容

Service 与流量管理

上一篇讲了 Deployment、StatefulSet 这些工作负载怎么把 Pod 跑起来,但 Pod 跑起来之后还有一个问题没解决:别人怎么访问它。Pod 的 IP 是动态分配的,重建、扩缩容之后全变,直接拿 Pod IP 通信根本不可靠。这篇讲 K8s 给出的答案:Service 提供固定入口,EndpointSlice 管流量摘挂,Ingress / Gateway API 接外部流量,CoreDNS 负责服务发现。

Service​

为什么需要 Service​

Pod 有几个天然的特点,决定了它不适合被直接访问:

  • IP 会变:Pod 删除重建、节点漂移、滚动更新之后,IP 都会重新分配。
  • 数量会变:扩缩容时 Pod 从 3 个变 5 个再变回 3 个,靠人工维护一份 IP 列表不现实。
  • 多副本要分摊流量:就算拿到了所有 Pod IP,负载均衡也得自己做。

Service 就是为解决这个问题而生的抽象:给一组 Pod 一个固定的访问入口。它有一个稳定的名字和虚拟 IP,客户端只管访问 Service;Service 按标签选择器找到背后的 Pod,把请求分发过去。Pod 怎么变,Service 的入口不变。

虚拟 IP 与 kube-proxy​

Service 创建后会被分配一个 ClusterIP(虚拟 IP,VIP)。这个 IP 不绑在任何网卡上,也 ping 不通——它只存在于每个节点上 kube-proxy 维护的转发规则里。

kube-proxy 运行在每个节点上,watch API Server 中 Service 和 Endpoint 的变化,然后把规则写进本机内核(默认用 iptables,也可以切换成 IPVS)。一次访问的过程是:

  1. 集群内某个客户端请求 Service 的 ClusterIP。
  2. 请求到达本节点的网络栈,命中 kube-proxy 写入的规则。
  3. 规则把目的地址改写(DNAT)成某个后端 Pod 的 IP,转发过去。

所以 Service 的负载均衡是在内核态、每个节点本地完成的,不经过任何中心化的代理进程,转发开销很小。

iptables 与 IPVS

iptables 模式下规则是一条长链表,Service 数量成千上万时匹配开销会变大;IPVS 模式用哈希表查规则,还支持轮询、最少连接等更多调度算法,大规模集群建议启用 IPVS。学习环境用默认的 iptables 即可,二者对使用者完全透明。

四种类型对比​

Service 的 spec.type 决定它对谁暴露、怎么暴露:

类型访问范围典型场景
ClusterIP仅集群内部默认类型,服务间调用
NodePort集群外(节点 IP + 高位端口)测试环境、没有云 LB 时临时对外
LoadBalancer集群外(云厂商负载均衡器)生产环境对外暴露
Headless(clusterIP: None)不分配虚拟 IPStatefulSet 直连 Pod、自定义服务发现

下面逐一展开。

ClusterIP​

ClusterIP 是默认类型,不写 type 时就是它。它会拿到一个集群内唯一的虚拟 IP,只能在集群内部访问,适合绝大多数「服务调服务」的场景。

apiVersion: v1
kind: Service
metadata:
name: web
namespace: default
spec:
type: ClusterIP # 可省略,默认就是 ClusterIP
selector:
app: web # 选中带这个标签的 Pod
ports:
- port: 80 # Service 自己监听的端口
targetPort: 8080 # 转发到 Pod 容器的端口

集群内其他 Pod 访问它,可以直接写 Service 名,也可以用完整的 DNS 名称,格式为:

服务名.命名空间.svc.cluster.local

例如 web.default.svc.cluster.local。同命名空间内只写 web 就够了;跨命名空间建议至少写到 web.default 这一段,少依赖搜索域的自动补全。

kubectl get svc web
NAME   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
web ClusterIP 10.96.12.34 <none> 80/TCP 1m
ClusterIP 分配后不变

Service 的 ClusterIP 一旦分配,在它的生命周期内就不会再变(删掉重建才会换)。所以应用代码里可以放心地把 Service 名或 ClusterIP 写成依赖地址——这正是「Service 是稳定入口」的具体含义。

NodePort​

NodePort 在 ClusterIP 的基础上更进一步:在每个节点上开一个高位端口(默认范围 30000-32767),访问任意节点的 节点IP:NodePort 都会被转发到背后的 Pod,于是集群外也能访问服务。

apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # 不指定则自动从范围里分配一个
kubectl get svc web
NAME   TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
web NodePort 10.96.12.34 <none> 80:30080/TCP 1m

此时 curl http://任意节点IP:30080 就能访问到 web 服务。流量路径是:节点 30080 端口 → kube-proxy 规则 → 随机一个后端 Pod。注意这个 Pod 不一定在本节点上,可能还要再跨节点跳一次。

externalTrafficPolicy 的权衡​

默认策略(externalTrafficPolicy: Cluster)会把流量均匀分发给所有后端 Pod,代价是:跨节点二次跳转时做一次 SNAT,客户端真实源 IP 丢失(后端看到的是节点 IP)。

把策略改成 Local 可以解决这个问题:

spec:
type: NodePort
externalTrafficPolicy: Local

Local 表示节点只把流量转发给本节点上的后端 Pod,不做二次跳转,源 IP 得以保留。但代价同样明显:

  • 某个节点上没有该服务的 Pod 时,发到这个节点的请求会被直接丢弃。
  • 各节点 Pod 数量不均时负载也跟着不均:一个节点有 3 个 Pod、另一个只有 1 个,后者承受的流量是前者的三分之一,却要和前者「平分」到达量,实际压力差三倍。
怎么选

需要透传客户端 IP(做访问控制、限流、审计)就用 Local,并配合反亲和性把 Pod 均匀打散到各节点;不在乎源 IP、只想流量均匀就保持默认的 Cluster。这个字段在 LoadBalancer 类型上同样生效。

LoadBalancer​

LoadBalancer 是在 NodePort 之上再叠一层:自动向云厂商申请一个外部负载均衡器,并把它的流量指到各节点的 NodePort 上。这是生产环境把服务暴露到公网的标准方式。

apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080

在云厂商托管集群(阿里云 ACK、AWS EKS、GKE 等)上创建后,云控制器会自动创建一个负载均衡器实例,并把分配到的公网 IP 回填到 Service 上:

kubectl get svc web
NAME   TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)        AGE
web LoadBalancer 10.96.12.34 47.100.200.30 80:31234/TCP 3m

裸机集群:MetalLB​

在自建裸机集群里创建 LoadBalancer 类型的 Service,EXTERNAL-IP 会永远卡在 pending——因为没有云厂商去实现「申请一个 LB」这件事。这时可以装 MetalLB:它工作在二层模式(ARP/NDP 应答)或 BGP 模式,从管理员预分配的一段 IP 池里挑一个分给 Service,并向网络宣告,让裸机集群也能用上 LoadBalancer 类型。一句话概括思路:MetalLB 在自建环境里扮演「云厂商 LB 控制器」的角色。

# 官方部署清单(版本号以官网为准)
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

装好后用 IPAddressPool 和 L2Advertisement 两个 CRD 配置一段可用的内网 IP,LoadBalancer 类型的 Service 就能拿到 EXTERNAL-IP 了。

注意

LoadBalancer 类型是「一个 Service 一个 LB」,对外服务多的时候 LB 数量和费用会线性增长。HTTP 服务通常改用 Ingress 或 Gateway API 统一入口(见下文),只给入口 Controller 本身配一个 LoadBalancer。

Headless Service​

Headless Service 是一种特殊的 Service:把 clusterIP 显式设为 None,它就不分配虚拟 IP、也不做 kube-proxy 转发。此时 DNS 查询这个名字,直接返回所有后端 Pod 的 IP 列表,客户端自己决定连哪一个。

apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # 关键:Headless
selector:
app: db
ports:
- port: 5432
targetPort: 5432

什么时候需要绕过负载均衡、直连具体的某个 Pod?典型场景就是 StatefulSet:数据库、消息队列这类有状态应用,每个副本身份不同(主/从、分片 0/分片 1),客户端必须能稳定寻址到「第 N 个副本」。StatefulSet 正是配合 Headless Service,给每个 Pod 生成稳定的 DNS 记录:

pod名.服务名.命名空间.svc.cluster.local

例如 StatefulSet db 的 0 号 Pod 就是 db-0.db.default.svc.cluster.local,这个名字在 Pod 重建后依然指向它。

一句话理解

普通 Service 像「打总机,接线员随机转给一个坐席」;Headless Service 像「查号台,把每个坐席的分机号都给你,想打谁自己拨」。需要「自己拨」的场景(有状态应用、客户端自实现负载均衡如 gRPC)就用它。

Endpoint​

Service 和 Pod 之间靠什么关联?靠的是标签选择器,而落地的结果就是 Endpoint 对象。

每创建一个带 selector 的 Service,Endpoints 控制器就会扫描集群里标签匹配的 Pod,把处于 Ready 状态的 Pod 的 IP 和端口收集起来,写进一个与 Service 同名的 Endpoint 对象。kube-proxy 真正参考的其实是 Endpoint 列表,而不是 Service 本身。

kubectl get endpoints web
NAME   ENDPOINTS                                            AGE
web 10.244.1.12:8080,10.244.2.15:8080,10.244.2.16:8080 5m

这个列表是动态维护的,流量的摘和挂都由它完成:

  • Pod 的 readinessProbe 失败 → IP 从 Endpoint 里摘除 → Service 不再给它转发流量;探针恢复后自动加回。
  • Pod 扩容或滚动更新 → 新 Pod Ready 后加入列表;旧 Pod 被删除前先从列表摘除,再配合 terminationGracePeriodSeconds 优雅退出。
为什么滚动更新能「无损」

滚动更新不中断服务的关键就在这个机制里:先等新 Pod Ready(出现在 Endpoint 中),再把旧 Pod 从 Endpoint 摘掉、发 SIGTERM 终止。「摘 Endpoint」和「发 SIGTERM」有先后顺序,保证旧 Pod 不再接收新流量,只把手上的请求处理完。

EndpointSlice​

Endpoint 对象有一个扩展性问题:一个 Service 的所有后端地址都塞在一个对象里。后端有几千个 Pod 时,这个对象会非常大,任何一个 Pod 上下线都要全量更新并推送给所有节点的 kube-proxy,etcd 和 API Server 压力都不小。

EndpointSlice 是它的替代方案,自 K8s 1.21 起成为默认机制:把同一个 Service 的后端分片存成多个 EndpointSlice 对象(默认每片最多 100 个 endpoint)。单个 Pod 变化只影响它所在的那一片,更新和推送量都大幅下降。

它由控制器自动维护,一般不需要手动创建,但排查问题时要会看:

# 查看某个 Service 对应的 EndpointSlice
kubectl get endpointslices -l kubernetes.io/service-name=web
NAME         ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
web-abc123 IPv4 8080 10.244.1.12,10.244.2.15 5m
web-def456 IPv4 8080 10.244.2.16 5m
# 看某一片的详细信息(含每个 endpoint 的 ready 状态)
kubectl describe endpointslice web-abc123
排查思路

「Service 不通」类问题的标准动作之一就是对比两边:EndpointSlice 里的地址列表,和 kubectl get pod -o wide 看到的实际 Pod IP。如果 EndpointSlice 是空的或缺地址,多半是 selector 和 Pod 标签没对上,或者 readinessProbe 没通过。

Ingress​

Service(NodePort/LoadBalancer)解决的是四层(TCP/UDP)的流量入口。但对外提供 HTTP 服务时,常见需求是按域名和路径路由:api.example.com 走后端服务、example.com/blog 走前端服务,还要统一做 HTTPS 卸载、限流等。这些七层能力由 Ingress 提供。

规则模型与 Controller​

Ingress 的规则模型很直白:host + path → Service。一条规则就是「匹配某个域名下某类路径的请求,转发给某个 Service」。

但要注意一个关键点:Ingress 只是规则声明,本身不干活。真正干活的是 Ingress Controller——一个跑在集群里的反向代理(如 ingress-nginx):它 watch Ingress 资源,把规则翻译成自己的配置并热加载。集群里必须先装 Controller,Ingress 规则才会生效。

安装 ingress-nginx​

# 官方部署清单(云厂商环境,自动申请 LoadBalancer;版本号以官网为准)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.5/deploy/static/provider/cloud/deploy.yaml
# 等待 Controller Pod 就绪
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s

# 查看分配给入口的 EXTERNAL-IP
kubectl get svc -n ingress-nginx ingress-nginx-controller

完整示例(含 TLS)​

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx # 指定由哪个 Controller 处理
tls:
- hosts:
- web.example.com
secretName: web-tls # 引用保存证书的 Secret
rules:
- host: web.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api # host + path → Service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80

TLS 部分引用同命名空间下一个 kubernetes.io/tls 类型的 Secret(内含证书和私钥),Controller 拿到后在入口统一做 HTTPS 卸载,集群内部仍走明文 HTTP。生产上一般用 cert-manager 自动签发和续期证书,不用手工维护 Secret。

pathType 的区别

Prefix 按路径前缀匹配(/api 能匹配 /api/v1/users),Exact 是精确匹配。配 Prefix 时注意 / 会匹配一切,通常放在规则最后兜底。

Gateway API​

Ingress 用了多年,缺点也逐渐暴露:规则表达能力弱(复杂路由、流量切分只能靠各 Controller 私有的 annotation,互不通用)、角色边界不清(基础设施管理员和开发者改同一个对象)、只支持 HTTP。Gateway API 是 K8s 官方推出的下一代流量管理标准,目标就是解决这些问题。

三个核心资源的关系​

资源由谁管理职责
GatewayClass平台/基础设施方声明「这套网关用什么实现」(nginx、envoy、云厂商 LB 等),类似 StorageClass
Gateway平台/基础设施方实例化一个网关入口:监听端口、协议、TLS 证书
HTTPRoute应用开发者路由规则:host/path 匹配、转发到哪个 Service、权重、重写

分工是 Gateway API 设计的精髓:管理员建 Gateway(管入口),开发者写 HTTPRoute(管路由),路由通过 parentRefs 挂到网关上,权限边界天然清晰,配合 RBAC 可以做到「开发者自助发布路由,但碰不到入口配置」。

最小示例​

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
namespace: default
spec:
gatewayClassName: nginx # 使用哪种实现
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: web-route
namespace: default
spec:
parentRefs:
- name: main-gateway # 挂到哪个 Gateway 上
hostnames:
- web.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: web # 转发到 Service
port: 80

相对 Ingress 的优势​

  • 标准化:路由匹配、按 weight 流量切分、请求重写、超时等都是规范的一部分,不再依赖各家私有 annotation,换实现时配置不用重写。
  • 角色分离:Gateway 和 HTTPRoute 是不同资源,入口与路由的管理权限可以分开。
  • 多协议:除 HTTPRoute 外还有 TCPRoute、UDPRoute、TLSRoute、GRPCRoute,Ingress 只有 HTTP。
  • 官方扩展机制:跨命名空间引用(ReferenceGrant)、策略挂载(Policy Attachment)等都有标准做法。
现在学哪个

Ingress 生态最成熟、资料最多,生产上仍是主流,必须会;Gateway API 的核心资源已 GA,ingress-nginx、Envoy Gateway、Cilium 等主流实现都在跟进,是明确的方向,建议一并掌握。两者不冲突,可以共存于同一个集群。

DNS​

K8s 集群内部有一套自己的 DNS 服务(现在统一是 CoreDNS),Service 和 Pod 创建后自动获得域名,应用之间靠域名而不是 IP 互相访问。

集群内域名结构​

完整域名形如:

名字.命名空间.svc.cluster.local

对象域名示例解析结果
普通 Serviceweb.default.svc.cluster.localService 的 ClusterIP
Headless Servicedb.default.svc.cluster.local所有后端 Pod 的 IP
StatefulSet 的 Poddb-0.db.default.svc.cluster.local该 Pod 的 IP

其中 cluster.local 是默认的集群域(cluster domain),安装集群时可以自定义。

Pod 的 dnsPolicy​

Pod 的 spec.dnsPolicy 决定它使用什么 DNS 配置:

策略含义
ClusterFirst默认。集群内域名交给 CoreDNS,外部域名转发到上游 DNS
Default继承节点(宿主机)的 DNS 配置,不认识集群内域名
None完全自定义,配合 dnsConfig 手写 nameserver 和搜索域

普通业务 Pod 保持默认的 ClusterFirst 即可;只有使用 hostNetwork 等特殊场景的 Pod 才可能需要调整。

ndots:5 的坑​

Pod 的 /etc/resolv.conf 里有一行不起眼的配置:ndots:5。含义是:查询名字里的点号(.)少于 5 个时,先依次拼上搜索域尝试解析(default.svc.cluster.local、svc.cluster.local、cluster.local……),全部失败后才去查原始域名。

这对集群内短名字(如 web)很方便,但对外部域名就是个坑:访问 www.baidu.com 只有 2 个点,小于 5,于是 CoreDNS 会先查 www.baidu.com.default.svc.cluster.local 等一串组合,每次都失败,一次外部请求变成好几次 DNS 查询。高并发下会明显拖慢解析,甚至压垮 CoreDNS。

规避方法

应用访问外部服务时,把域名写成末尾带点的 FQDN 形式(如 www.baidu.com.),ndots 判定直接跳过搜索域;或者在 dnsConfig 里把 ndots 调小(比如 2)。遇到「DNS 解析慢」的问题,先怀疑 ndots。

CoreDNS​

CoreDNS 是集群默认的 DNS 服务器,以 Deployment 形式跑在 kube-system 命名空间,通过名为 kube-dns 的 Service 对外提供服务——每个 Pod 的 /etc/resolv.conf 里 nameserver 指向的就是它的 ClusterIP。

Corefile 配置要点​

CoreDNS 的配置文件叫 Corefile,存放在 kube-system 命名空间下名为 coredns 的 ConfigMap 里。一份典型的 Corefile 长这样:

.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}

几个关键插件:

  • kubernetes:解析集群内 Service 和 Pod 的域名,数据源来自 API Server。
  • forward:集群外的域名转发给上游 DNS,默认用节点的 /etc/resolv.conf。想统一走公司内网 DNS 或公共 DNS 就改这里,例如 forward . 223.5.5.5。
  • cache:解析结果缓存,数字是缓存秒数。
  • hosts:像改 /etc/hosts 一样自定义解析记录,常用于把某个域名固定指向内网地址:
.:53 {
hosts {
192.168.1.100 internal.example.com
fallthrough
}
...
}

改完 ConfigMap 后,reload 插件会自动热加载;不放心的话也可以 kubectl rollout restart deployment/coredns -n kube-system 强制重启。

验证解析

排查 DNS 问题时,起一个带解析工具的临时 Pod 实测最直接:

kubectl run -it --rm --image=busybox:1.36 dns-test -- nslookup web.default.svc.cluster.local

服务发现​

「服务发现」回答的是:一个服务怎么找到另一个服务的地址。K8s 提供了两种方式。

环境变量​

Pod 启动时,kubelet 会把同命名空间内已存在的 Service 信息注入为环境变量,格式如:

WEB_SERVICE_HOST=10.96.12.34
WEB_SERVICE_PORT=80

方式简单,但有两个硬伤:只有先于 Pod 创建的 Service 才会被注入(存在顺序依赖,后建的 Service 要重启 Pod 才能拿到);Service 一多,环境变量就泛滥成灾。所以实际几乎没人用它做服务发现。

DNS(主流)​

DNS 是事实标准:任何时刻创建的 Service 立刻就有域名记录,客户端按名字访问即可,和创建顺序无关;配合 Headless Service 还能拿到全部 Pod IP,由客户端自己做负载均衡(gRPC 常这么用)。

为什么 DNS 是主流

对比一下答案很自然:环境变量是「启动时刻的快照」,DNS 是「随时可查的实时目录」。微服务环境下服务数量多、变化快,只有实时目录跟得上节奏。这也是本篇的落脚点——Service 提供稳定入口,CoreDNS 给入口一个名字,Ingress / Gateway API 把外部流量接进来,合起来就是 K8s 完整的流量管理体系。

到这里,从「集群内服务互访」到「外部流量进集群」的整条链路就完整了:Service 的四种类型解决入口稳定性,Endpoint / EndpointSlice 负责流量的动态摘挂,Ingress 和 Gateway API 处理七层路由,CoreDNS 把一切串成名字。下一篇讲存储系统。