Skip to main content

6. 服务发现与网络

"怎么找到 Pod"是一个比看起来难得多的问题​

你创建了 3 个 nginx Pod,每个都有 IP。单机上 curl 172.17.0.3:80 直接通。集群里呢?

Pod IP 会变——重启就换 IP。Deployment 滚动更新时新旧 IP 共存一小段时间。

你得有个稳定的访问入口,不管背后 Pod 怎么变、有几个,这个名字永远指向应该走的那个 Pod。

这个入口在 K8S 里叫 Service。

思考题:Pods 已经有 IP 了,为什么还要套一层 Service?直接写 Pod IP 不行吗?Service 说到底解决的是两个什么问题——提示:一个是"怎么找到",另一个是"怎么分发"。

Service:VIP 背后的 iptables​

假设 Deployment 有 3 个 Pod:

apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
selector:
app: nginx # 带这个 Label 的 Pod 自动加入
ports:
- port: 80 # Service 对外端口
targetPort: 80 # Pod 内部端口

创建后你会拿到一个 ClusterIP(虚拟 IP,比如 10.96.0.10)。别的 Pod 只要 curl nginx-svc:80 就能访问到背后的 Pod——这个 DNS 已被 CoreDNS 自动注册。

但 10.96.0.10 是一个虚拟 IP,它不挂在任何网卡上。流量怎么走到具体 Pod 的?

答案是 kube-proxy。kube-proxy 这个组件跑在每个 Node 上,Watch API Server 的 Service 和 Endpoints 变化,然后在宿主机的 iptables 里写入规则。简化来看,它做的事情等于:

# iptables 规则链简写(实际更复杂)
-A KUBE-SVC-XGLPY -m statistic --mode random --probability 0.33 -j KUBE-SEP-POD1
-A KUBE-SVC-XGLPY -m statistic --mode random --probability 0.50 -j KUBE-SEP-POD2
-A KUBE-SVC-XGLPY -j KUBE-SEP-POD3

-A KUBE-SEP-POD1 -j DNAT --to-destination 10.244.1.5:80
-A KUBE-SEP-POD2 -j DNAT --to-destination 10.244.2.3:80
-A KUBE-SEP-POD3 -j DNAT --to-destination 10.244.3.7:80

流量路径:Pod-A 发到 10.96.0.10:80 → 命中了 iptables 规则 → DNAT 到 10.244.2.3:80 真实 Pod。整个过程是在内核 netfilter 层转发的,应用层完全不知情。

kube-proxy 的源码核心在 k8s.io/kubernetes/pkg/proxy/iptables/proxier.go:

// 核心同步函数简化
func (proxier *Proxier) syncProxyRules() {
for _, svc := range proxier.serviceMap {
svcChain := proxier.servicePortChainName(svc)
for _, ep := range svc.endpoints {
// 为每个 endpoint 写 DNAT 规则
proxier.natRules.Write("-A", svcChain, "-m statistic --mode random",
"--probability", prob, "-j", "DNAT", "--to-destination", ep)
}
}
}

iptables 模式的问题是:规则数 = Service × Endpoints。500 个 Service 每个背后 10 个 Pod,5000 条规则,每次 Service 变更都要全部重建,CPU 抖一下。

所以后来多了 IPVS 模式——基于内核 IPVS 模块做四层负载均衡。IPVS 是 Linux 内核内置的传输层负载均衡器(LVS 的核心),它用 hash 表做后端查找,不是 iptables 那种链表匹配。kube-proxy 启动时加 --proxy-mode=ipvs 即可切换。

两者的底层差异决定了一切行为:

维度iptablesIPVS
查找结构链表顺序匹配——规则越多越慢hash 表 O(1)——100 个后端跟 1 个后端一样快
规则数量N 个 Service × M 个 Pod 条规则,变更时全量重建只建 Service → 后端映射,O(N+M)
负载均衡算法随机(--probability 分段模拟)8 种算法:rr、wrr、lc、wlc、sh、dh、sed、nq
连接追踪依赖 conntrack 表——高并发下表溢出独立连接表,不占 conntrack
健康检查无——只靠 Endpoints 对象的增删支持 TCP/HTTP 主动健康检查,自动剔除故障后端
吞吐衰减点~1000 个 Service 开始明显退化~10000 个 Service 依然线性
调试难度iptables -t nat -L 几千行输出ipvsadm -L -n 格式清爽

流量路径对比:

iptables 模式下一个包经过的链路:

PREROUTING → KUBE-SERVICES → KUBE-SVC-XXX → KUBE-SEP-YYY → POSTROUTING

每跳都是一段线性匹配的 iptables 规则。Service 多了之后一个包要匹配几十条规则。

IPVS 模式下:

PREROUTING → IPVS(hash 查到后端)→ POSTROUTING

到 KUBE-SERVICES 链就停了——IPVS 接管了后续的 DNAT,不再走 iptables 规则链路。

那为什么不是所有人都用 IPVS?

  1. 内核模块依赖。IPVS 需要 ip_vs、ip_vs_rr 等内核模块加载——CoreOS、容器优化 OS 可能默认不载入。iptables 是所有 Linux 都有的。
  2. iptables 生态更成熟。很多现有的网络策略、安全规则基于 iptables。IPVS 切进去之后,原有的 iptables 自定义规则需要重新适配。
  3. 小集群不需要。如果你只有 10 个 Service,iptables 和 IPVS 差几微秒——完全无感。

kube-proxy 源码中的切换逻辑:

// k8s.io/kubernetes/cmd/kube-proxy/app/server.go
switch proxyMode {
case proxyModeIPTables:
proxier = iptables.NewProxier(...)
case proxyModeIPVS:
proxier = ipvs.NewProxier(...)
}

对 kube-proxy 的上游来说,两种模式实现了同一个 Provider 接口——本质上只是把 syncProxyRules() 的实现从"写 iptables 规则"换成了"写 IPVS 虚拟服务"。

怎么确认当前用的是哪种?

# 方法一:看 kube-proxy 的 ConfigMap
kubectl -n kube-system get cm kube-proxy -o yaml | grep mode
# mode: "" → iptables(默认)
# mode: "ipvs" → IPVS

# 方法二:直接看 Node 上有没有 IPVS 规则
ipvsadm -L -n
# 如果输出一堆虚拟服务和后端列表 → IPVS 模式
# 如果报错 "can't initialize ipvs" → iptables 模式

# 方法三:看 kube-proxy 的日志
kubectl -n kube-system logs ds/kube-proxy | head -5
# Using iptables Proxier. / Using ipvs Proxier.

思考题:你以前可能见过 spec.clusterIP: None 这样的 Service——这叫什么模式?跟普通 Service 的 VIP 模式有什么区别?再往下想一步:为什么 StatefulSet 通常需要这种"无头" Service?

思考题:云上创一个 type: LoadBalancer 的 Service,K8S 会"自动"给你一个外部 IP。这个 IP 是谁创建的——是 K8S 自己?还是有人替它调的云 API?Service 和它背后的 LB 之间是什么关系——LB 是 K8S 的资源还是云厂商的资源?

思考题:10.96.0.10 这个 ClusterIP 是谁分配的?kube-proxy 在 iptables 里写 DNAT 规则——是 DNAT 到了 Pod IP,那这个规则只对"集群里的 Pod 发出的请求"有效吗?集群外面的东西——比如你本机 curl 10.96.0.10——能通吗?如果不行,外部要怎么访问 Service?

K3S 的网络差异​

K8S 的 CNI 需要你手动部署——Flannel、Calico、Cilium,随便选。你在 bare metal 上装 K8S 经常要额外 kubectl apply 一个网络插件。

K3S 的默认选择是内置 Flannel。启动时自动配好,不需要你手动装 CNI 插件。

// k3s/pkg/agent/flannel/flannel.go 简化
func Run(ctx context.Context, nodeConfig *config.Node) error {
// K3S 直接在代码里启动 flannel 子网管理
return flannel.Run(ctx, nodeConfig)
}

对外的网络模型——所有的 Pod 可以通过 Pod IP 直接通信、每个 Pod 有独立 IP——K3S 和 K8S 完全一样。区别在于 K3S 默认帮你选好了实现(Flannel),K8S 让你自己选。

K3S 也内置了 Traefik 作为默认 Ingress Controller(v1.31 及之前为 Traefik v2,v1.32 起升级为 Traefik v3)。跟 Flannel 同理——你不用额外装 Ingress Controller,集群起来就能用 Ingress 资源。

Pod 级的 DNS 配置​

默认情况下 Pod 的 /etc/resolv.conf 由 kubelet 自动生成:

nameserver 10.96.0.10 ← CoreDNS 的 ClusterIP
search default.svc.cluster.local
options ndots:5

但有些场景需要覆盖——比如 Pod 要解析公司内网的 DNS、测试环境要把 NDS 指向不同的解析器。

通过 dnsPolicy + dnsConfig 可以精确控制每个 Pod 的 DNS:

spec:
dnsPolicy: "None" # 不用 kubelet 默认生成的 /etc/resolv.conf
dnsConfig:
nameservers:
- 10.0.0.10 # 公司内网 DNS
- 8.8.8.8 # 公网 fallback
searches:
- mycorp.internal # 搜索域——短名 x 会尝试 x.mycorp.internal
options:
- name: ndots
value: "2" # 点号少于 2 的域名先进搜索域
- name: timeout
value: "1" # DNS 超时 1 秒

dnsPolicy 四种选择:

值行为场景
ClusterFirst(默认)先查 CoreDNS,查不到才往外走99% 的 Pod
Default使用 Node 的 /etc/resolv.conf跟宿主机 DNS 一致
ClusterFirstWithHostNethostNetwork Pod 默认是宿主机 DNS,这个强制用 CoreDNShostNetwork Pod 需要解析 Service
None完全自定义——用 dnsConfig 字段连公司内网 DNS

ndots 的作用很多新人踩坑——默认 ndots:5,意味着域名单点号少于 5 个先走搜索域。你请求 redis.default.svc 的时候 CoreDNS 会先拼出 redis.default.svc.default.svc.cluster.local 再查一遍。大部分时候这是想要的行为,但如果你的 Pod 大量请求外部短域名(比如 api.internal),搜索域拼接反而慢了——改 ndots:2 跳过不必要的搜索。

Ingress:七层暴露服务的旧方式​

Service 处理了内部通信,但外部流量怎么进来?

Ingress 就是"七层反向代理"的配置接口。你描述路由规则(/api 走到哪个 Service、/ 走到哪个),具体的反代逻辑由 Ingress Controller 实现。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2
port:
number: 80

这个 YAML 被 Ingress Controller(比如 nginx-ingress)Watch 到之后,生成 nginx 配置:

server {
listen 80;
server_name api.example.com;
location /v1 { proxy_pass http://api-v1:80; }
location /v2 { proxy_pass http://api-v2:80; }
}

Ingress 的问题是:它太简单了。只能做基本的 host/path 路由。而且各家 Ingress Controller 用 annotation 扩展,A 家的 annotation B 家不认,完全不互通。跨 namespace 共享网关需要额外约定。

思考题:Ingress 从 extensions/v1beta1 到 networking.k8s.io/v1beta1 再到 networking.k8s.io/v1——这几个版本的 API 到底改了哪些东西?为什么 pathType 从可选变成了必填,为什么 ingressClassName 要单独拆出来?想想集群里装了两个 Ingress Controller 会发生什么。

Gateway API:Ingress 的继任者​

Gateway API 是 K8S 社区对 Ingress 的重新思考。它把网关分成三个角色:

# 1. GatewayClass:声明"什么网关"(比如用 Contour 还是 Istio)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: contour
spec:
controllerName: projectcontour.io/gateway-controller
---
# 2. Gateway:分配具体的一个网关实例,监听某个端口
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: infra # Gateway 由基础设施团队管理
spec:
gatewayClassName: contour
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
expose: "true" # 只有打标了 expose=true 的 namespace 能挂路由
---
# 3. HTTPRoute:应用团队自己写路由规则
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-routes
namespace: app-team # 应用团队在自己的 namespace 里定义
spec:
parentRefs:
- name: prod-gateway
namespace: infra
rules:
- matches:
- path:
type: PathPrefix
value: /v1
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: x-version
value: "v1"
backendRefs:
- name: api-v1
port: 80

Gateway API 比 Ingress 好在哪里:

IngressGateway API
一个对象管所有三个对象分工明确:基础设施、网关、路由
annotation 扩展,无统一标准内建 header 修改、weight-based 分流、镜像、重定向
跨 namespace 共享网关靠约定原生支持引用别的 namespace 的 Gateway
所有 Controller 自己扩展标准字段,跨实现移植

Ingress 还在被大量使用,所以这章两头都讲。你看到旧的 Ingress YAML 知道在看什么,写新的就上 Gateway API。

思考题:HTTPS 证书在 Ingress 里用一个 kubernetes.io/tls 类型的 Secret 挂上去——证书过期了怎么续?Gateway API 有没有内置的自动续期方案?cert-manager 在这两套体系里分别扮演什么角色?

Service Mesh:服务间通信的升级版​

Ingress/Gateway API 解决的是南北向流量——外部怎么进集群。Service Mesh(Istio、Linkerd)解决的是东西向流量——Pod 之间怎么通信。

Mesh 的核心是给每个 Pod 注入一个 sidecar proxy(Envoy/Linkerd-proxy),劫持 Pod 的所有进出流量,在 proxy 层做:

Pod A → Envoy sidecar ──(mTLS)──→ Envoy sidecar → Pod B
↑ 流量控制 ↑ 自动加密 ↑ 可观测性
retry/circuit/ 双向TLS L7 metrics
traffic split tracing

跟 Service + kube-proxy 的区别:

维度kube-proxyService Mesh
层级L4(iptables 只看到 IP:Port)L7(HTTP / gRPC header、path)
加密无自动 mTLS——Pod 之间通信透明加密
流量控制随机负载均衡超时、重试、熔断、金丝雀发布(基于权重或 header)
可观测性看不到请求细节自动 HTTP/gRPC 指标、分布式 tracing

Service Mesh 不是替代 Ingress/Gateway API——它跟 Gateway 配合:Gateway 管外部入口,Mesh 管服务内部的调用链。很多生产架构把 Gateway 当成 Mesh 的边缘入口(Istio Gateway 就是 Envoy 跑在集群边界)。

思考题:Service Mesh 给每个 Pod 注入 sidecar——这等于每个 Pod 多了一个容器。如果集群有 1000 个 Pod,就等于多了 1000 个 sidecar。这些 sidecar 的内存和 CPU 开销是多少?有没有"无 sidecar"的 Service Mesh 方案?

小结​

这一章的核心还是接口:

  • Service 是对 Pod 的访问收敛——你不需要知道 Pod IP,只需要知道 Service 域名
  • kube-proxy 实现了 Service 接口——通过 iptables/IPVS 规则把虚拟 IP 映射到 Pod IP
  • K3S 内置了 Flannel 作为 CNI 实现,省去部署步骤,但网络模型一样
  • Ingress 是七层路由的配置接口,但因缺乏标准扩展被 Gateway API 接替
  • Gateway API 把网关拆成 GatewayClass/Gateway/HTTPRoute,角色更清晰

解答​

1. Service 存在的意义是什么?​

Pod 已经有 IP 了,但 Pod IP 是临时的——重启就变,滚动更新新旧共存。直接用 Pod IP 有三个致命问题:

  1. 谁来找:前端不知道后端 Pod 的 IP。你不可能把 3 个 nginx Pod 的 IP 全配到前端的 nginx.conf 里——Pod 死了又活了,IP 全换了。
  2. 怎么分:3 个 Pod 都在跑,请求打到哪个?任意一个 Pod 都能处理,你需要负载均衡。
  3. 怎么扩:HPA 扩了 5 个 Pod,前端怎么知道多出来了?

Service 解决了其中两个——服务发现(稳的 DNS 名替代变化的 Pod IP)和负载均衡(iptables/IPVS 随机选后端)。第三个"扩"由 Deployment + HPA 管。

Service 本质上是一个"逻辑分组"的概念——所有带 app: nginx Label 的 Pod 被划进一组,这组有一个固定的 DNS 和一个虚拟 IP。Pod 在组内进出,但入口不变。

2. 无头 Service(Headless)是什么意思?​

spec:
clusterIP: None # ← 这就是"无头"

普通 Service 创建时会分配一个 ClusterIP,DNS 解析到的是这个虚拟 IP。kube-proxy 再用 iptables/IPVS 做 DNAT 到具体 Pod。

无头 Service 没有 ClusterIP,没有 DNAT。DNS 直接解析到所有后端 Pod 的 IP:

# 普通 Service
$ nslookup nginx-svc.default.svc.cluster.local
Name: nginx-svc
Address: 10.96.0.10 ← 一个虚拟 IP

# 无头 Service
$ nslookup nginx-headless.default.svc.cluster.local
Name: nginx-headless
Address: 10.244.1.5 ← Pod-1 的真实 IP
Address: 10.244.2.3 ← Pod-2 的真实 IP
Address: 10.244.3.7 ← Pod-3 的真实 IP

什么时候需要无头? 当你的应用需要感知每个后端的独立 IP,而不是一个统一的入口 IP。

StatefulSet 是最大的场景——Kafka Broker、ES Node 需要互相通过 hostname 发现对方并选举 Leader。如果你给 StatefulSet 配了一个普通 Service,Pod 就永远只知道一个虚拟 IP,不知道三个 Broker 的真实地址。但配了无头 Service + serviceName 后,每个 Pod 有自己的 DNS:mysql-0.mysql-headless.default.svc、mysql-1.mysql-headless.default.svc……各 Broker 通过 DNS 直接找到彼此,不需要中间代理。

无头 Service 并不是"去掉了负载均衡"——而是"负载均衡的逻辑留给了应用自己"。DNS 给了你一 Pod IP 列表,你怎么选、怎么重试,应用层决定。

3. Service 和 LoadBalancer 是什么关系?​

type: LoadBalancer 的 Service 创建时,你会看到:

kubectl get svc nginx-lb
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# nginx-lb LoadBalancer 10.96.0.20 xxx.elb.amazonaws.com 80:30080/TCP

EXTERNAL-IP 是 "xxx.elb.amazonaws.com"——这是一个 AWS ELB。K8S 自己没有创建这个 LB。是 Cloud Controller Manager(CCM)调云厂商 API 创建的。

底层两层关系:

Internet
│
▼
AWS ELB (xxx.elb.amazonaws.com) ← CCM 调的云 API 创建的 LB
│
▼
NodePort 30080 (每个 Node 上都有) ← kube-proxy 在 iptables 里写的规则
│
▼
ClusterIP 10.96.0.20 → Pods ← 跟普通 Service 完全一样

你在 kubectl get svc 看到的 LoadBalancer 实际上是一个组合——K8S 的 Service + 云厂商的 LB。LB 是云厂商管理的资源,不在 etcd 里,kubectl get 看不到它。你能看到的是 Service 对象上写回的 status.loadBalancer 字段(云厂商的 LB DNS 地址)。

LB 是云厂商资源还是 K8S 资源? 云厂商资源。你删掉 LoadBalancer 类型的 Service 时,如果 Service 上的 service.kubernetes.io/loadbalancer-cleanup finalizer 存在,CCM 会调云 API 删掉对应的 LB。但这个 LB 不是 K8S 对象——它是云控制台上的一个独立资源。

如果集群不在云上呢? 装 MetalLB。MetalLB 模拟 CCM 的行为——它不是调云 API,而是用 ARP/NDP(Layer2 模式)或 BGP 宣告一个虚拟 IP 给外部访问。Service 的 YAML 不用变——仍然是 type: LoadBalancer——MetalLB 会 Watch Service 变化,给它分一个外部 IP,对外宣告这个 IP 可达。


4. Service 的 ClusterIP 是谁分配的,谁能访问它?​

分配者:kube-controller-manager 里的 ServiceController。每个 Service 创建时,Controller 从 --service-cluster-ip-range(默认 10.96.0.0/12)里挑一个未占用的 IP 写入 spec.clusterIP。这个 IP 不挂在任何网卡上——它只存在于 iptables/IPVS 规则和 CoreDNS 的 A 记录里。

谁能访问:只有集群内的 Pod。你本机 curl 10.96.0.10:80 是不通的——因为 10.96.0.0/12 是私网地址,你的物理网络和路由器不认识这个段。

流量之所以能通,是因为 Pod 发出的包经过了 kube-proxy 写在 Node 上的 iptables 规则——包的目标 IP 在 PREROUTING 链被 DNAT 改成了真实 Pod IP。外部请求(比如你的笔记本)发出的包到达 Node 时,Node 的内核看到目标 IP=10.96.0.10,不会走本地路由表——这个段的路由在 Node 上没有对应接口。只有 Pod 内部发出的包(源地址在 CNI 子网内),内核才会走 NAT 表做 DNAT。

这也是为什么需要 NodePort、LoadBalancer 和 Ingress/Gateway——它们负责把外部流量"引入"到集群的 netfilter 规则里。

5. Service 如何支持外部请求?​

三种方式,从简单到完整:

NodePort——在每个 Node 上开一个端口(30000-32767),直接 DNAT 到 Service 背后的 Pod:

spec:
type: NodePort
ports:
- port: 80
nodePort: 30080

任何能访问 Node IP 的人(包括公网)curl <node-ip>:30080 都能通。缺点:端口范围受限、没有负载均衡、Node IP 变化要改配置。

LoadBalancer——调云厂商 API 创建一个外部 LB(AWS NLB、GCP LB),LB 把流量转到 NodePort:

spec:
type: LoadBalancer

K8S 自己不创建 LB——cloud-controller-manager 调云 API 去创建。如果你的集群不在云上(bare metal),可以装 MetalLB 来模拟 LB 行为。

ExternalName / ExternalIP——把 Service 映射到一个外部 DNS 或 IP,本质上是让集群内的 Pod 通过 Service DNS 访问集群外的服务:

spec:
type: ExternalName
externalName: api.external-service.com # 集群内 curl my-svc 解析到这个外部域名

6. Ingress 的版本演进​

Ingress API 经历了三次大版本迭代:

版本API Group主要变化
v1beta1extensions/v1beta1最早的版本,功能最原始
v1beta1networking.k8s.io/v1beta1从 extensions 迁移到 networking,放对位置了
v1networking.k8s.io/v1GA,K8S 1.19 稳定,1.22 移除旧版

v1beta1 → v1 的关键改动:

1. pathType 从可选变成必填。

以前不写 pathType 默认走 ImplementationSpecific——意思是"具体行为看 Controller 心情"。不同的 Ingress Controller 对同一个 /foo 的处理可能不一样:nginx-ingress 用前缀匹配,Traefik 用精确匹配。这导致了大量跨 Controller 的移植故障。v1 要求必须显式指定:Prefix(前缀匹配)、Exact(精确匹配)或 ImplementationSpecific(你明确知道要用手动行为)。

2. ingressClassName 拆出来解决多 Controller 共存问题。

旧版用 kubernetes.io/ingress.class annotation(比如 nginx、traefik),但 annotation 是字符串,没有类型校验。你写了个 ingressClass: ngix(拼错)K8S 不报错,silently 没有 Controller 接。

v1 把 IngressClass 变成了独立的 API 资源:

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
spec:
controller: k8s.io/ingress-nginx

Ingress 用 spec.ingressClassName: nginx 引用。K8S 会校验这个 IngressClass 是否存在——拼错直接报错。

这也解决了"两个 Ingress Controller 打架"的问题。旧版两个 Controller 同时 Watch 带 kubernetes.io/ingress.class: nginx 的 Ingress(如果没人指定)——两个 Controller 都生成配置,冲突。v1 要求显式指定 ingressClassName,没有默认值。

3. 后端定义从 string 变成 typed object。

旧版 back end 是 serviceName + servicePort(字符串),v1 变成了 service.name + service.port.number(结构化字段)。API Server 可以检查 Service 和端口是否存在。

7. Ingress 和 Gateway API 如何做证书和续期?​

K8S 本身不做证书管理——它只负责把 TLS 私钥和证书挂到网关里。自动续期靠外部工具。

Ingress 的证书配置:

spec:
tls:
- hosts:
- api.example.com
secretName: api-tls # 引用一个 TLS Secret

# Secret 里存证书和私钥
apiVersion: v1
kind: Secret
metadata:
name: api-tls
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUd... # base64 编码的证书链
tls.key: LS0tLS1CRUd... # base64 编码的私钥

Ingress Controller Watch 到这个 Secret 后,把证书内容写进 nginx/Envoy 配置,reload。

Gateway API 的证书配置:

Gateway 用 listeners 里的 tls 字段引用一个 Secret:

spec:
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: api-tls
kind: Secret

跟 Ingress 的区别:证书挂在 Gateway 上,不是路由上。所有绑定到这个 Gateway 的 HTTPRoute 共享同一份证书。

自动续期靠 cert-manager:

cert-manager 是 K8S 生态中事实上的证书管理标准。它对 Ingress 和 Gateway API 都支持,但方式不同:

Ingress 方式——annotation 驱动:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
name: my-ingress
spec:
tls:
- hosts:
- api.example.com
secretName: api-tls

cert-manager Watch 到带这个 annotation 的 Ingress → 用 Let's Encrypt 的 ACME 发起 challenge → 证书签发后自动写入 api-tls Secret → Ingress Controller 看到 Secret 更新 → reload 新证书。

Gateway API 方式——Certificate 资源:

cert-manager 不再靠 annotation,而是为 Gateway 创建独立的 Certificate CRD:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: infra
spec:
secretName: api-tls
dnsNames:
- api.example.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer

Gateway 引用同一个 api-tls Secret。到期前 cert-manager 自动发起 ACME 续期,更新 Secret。Gateway Controller 看到 Secret 变了就 reload 证书。

两种方式的续期都是自动的——cert-manager 在证书过期前 30 天开始尝试续期,成功后更新 Secret,网关自动加载。不用你手动操作。

为什么不能只靠 K8S 自己? K8S 的 Secret 是静态存储——你放进去什么它就存什么,没有一个内置的控制器去"检查 Secret 里的证书是否快要过期并自动更新"。这是故意的——证书签发的逻辑(ACME、DNS challenge、HTTP challenge)高度依赖外部服务(Let's Encrypt、Vault、AWS ACM)和域名验证,K8S 核心不承担这个责任。

8. Service Mesh 的 sidecar 开销和无 sidecar 方案​

Istio 的 Envoy sidecar 默认请求 100m CPU + 128Mi memory,但实际用量取决于流量:高并发场景下 Envoy 动辄吃掉 500m+ 的 CPU。1000 个 Pod = 1000 个 Envoy = 最低 128Gi 内存被 sidecar 吃光。

这也是为什么 "无 sidecar"的 Service Mesh(Ambient Mesh / Istio Ambient)近年火起来的原因——把 L4 处理从 sidecar 移到每个 Node 上的共享代理(ztunnel),只有需要 L7 处理(HTTP header routing / tracing)的才走 Envoy waypoint proxy:

传统 sidecar:每个 Pod 一个 Envoy → 1000 Pod = 1000 Envoy
Istio Ambient:每个 Node 一个 ztunnel → 100 Node = 100 ztunnel
+ 需要 L7 的 Service 才部署 waypoint proxy

开销从 O(Pod数) 降到 O(Node数),翻了几个数量级。

Cilium(eBPF 方案)走得更彻底——用 eBPF 直连内核网络栈,sidecar 都不要了。eBPF 程序直接在内核的 socket / traffic control 层做加密、负载均衡、可观测——连额外容器都不需要。这可能是 Service Mesh 的终极形态。

下一章,看看 Pod 的数据怎么持久化——存储。