8.2. Service Mesh:Istio、Linkerd 与 Cilium Mesh

服务网格解决的不是「请求怎么送到」,而是「送到之后怎么加密、怎么重试、怎么切流、怎么证明它按预期工作」。它把这些事从每个服务的代码里挪到一组可统一配置的代理上,代价是多一层数据面、多一套控制面,以及一套必须自己运维的证书体系。

层级:

L4 · 技术栈

难度:

进阶

预计:

70 分钟

前置:

Service 与 DNS、Ingress、NetworkPolicy

你将学到

  • 服务网格的五项核心能力,以及它们分别落在 L4 还是 L7

  • sidecar、ambient、eBPF 三种数据面形态的取舍

  • Istio、Linkerd、Cilium、Kuma、Consul 的定位差异与选型依据

  • mTLS 的证书链路,PeerAuthentication 与 AuthorizationPolicy 如何配合

  • 灰度发布、故障注入,以及与 Ingress / Gateway API 的边界

8.2.1. 为什么需要服务网格

微服务拆开之后,每个服务都得自己实现超时、重试、熔断、指标、链路追踪和传输加密。麻烦不在于写不出来,而在于每个团队、每种语言各写一遍:Go 的 SDK 和 Java 的 SDK 语义不一致,安全团队无法证明「集群内所有流量都已加密」,一次重试策略调整要几十个服务重新发版。

服务网格的做法是把这些能力下沉:控制面统一下发配置和证书,数据面在 Pod 内或节点上拦截流量,应用仍然收到普通 TCP/HTTP 请求。应用不需要改代码,但换来的是「多一跳、多一套要运维的组件」。

能力

应用自己做

网格做

代价与边界

mTLS

引库、管证书、轮换

控制面签发短期证书,代理间 TLS

只保护代理到代理,进程内仍是明文

重试 / 超时

SDK 配置,各语言不同

VirtualService / HTTPRoute 声明

重试会放大故障,非幂等接口必须关

流量分割

发布系统改权重

按 subset 权重、Header、Cookie 切流

依赖版本标签,L4 连接会「粘」在旧后端

可观测性

埋点、上报

代理自动产出指标、访问日志、trace

指标基数与采样成本要提前规划

授权

应用内 RBAC

按工作负载身份(SPIFFE)放行

不等于多租户隔离,也不替代应用鉴权

并不是所有集群都需要网格。服务数量少、语言栈统一、已有成熟 SDK 时,网格的收益可能抵不过它的运维成本;反过来,当「证明东西向流量全部加密」变成合规要求,或者需要按版本精细切流时,自己实现往往更贵。

8.2.2. 核心概念

8.2.2.1. 控制面与数据面

控制面只做三件事:把声明式配置翻译成数据面能懂的协议(Istio 用 xDS)、给每个工作负载签发身份证书、把配置推给正确的代理。数据面才是真正处理包的那一层。

架构图

图:业务流量不经过控制面,控制面只发证书与配置;控制面挂掉时既有连接继续工作,但新配置不再生效。

Istio 的 istiod 合并了 Pilot(xDS)、Citadel(证书)、Galley(配置校验)的职责;Linkerd 拆成 destination(服务发现与路由)、identity(证书)、proxy-injector(注入)三个 Deployment;Cilium 则由 operator 加每节点 agent 组成,控制面更贴近内核。

8.2.2.2. Sidecar、ambient 与无 sidecar

Sidecar 是每 Pod 注入一个代理,L4/L7 全在 Pod 内完成。隔离清晰、L7 能力完整、可以按 Pod 配资源;代价是每个 Pod 多 50–100Mi 内存和启动延迟,注入后必须重建 Pod,Job 还会因为代理不退出而永远不结束。

Ambient(Istio) 把 L4 收到节点级的 ztunnel,需要 L7 时才给命名空间或服务挂 waypoint 代理。它不注入 Pod、不重启工作负载,资源随节点数而不是 Pod 数增长;代价是 L7 能力要显式部署 waypoint,排障路径也更长。

Cilium 用 eBPF 在 socket 层做 L3/L4 策略与负载均衡,需要 L7 时把流量重定向到节点级 Envoy,根本没有 per-Pod 代理。

形态

每 Pod 开销

L7 能力

适用

Sidecar

有,随 Pod 数线性增长

完整(路由、重试、AuthZ、Wasm)

需要细粒度 L7 策略

Ambient

无,L4 在节点级

需 waypoint,覆盖度略小

大集群、只想先拿 mTLS

eBPF(Cilium)

无

L7 策略与 Gateway API,功能少于 Istio

已用 Cilium,不想加代理

8.2.3. mTLS 与授权

Istio 的身份是 SPIFFE 格式的 spiffe://cluster.local/ns/<ns>/sa/<sa>,证书由 istiod 签发、默认 24 小时轮换,sidecar 通过 SDS 取用。默认的 PERMISSIVE 模式同时接受明文与 mTLS,方便迁移;要真正强制加密,必须切到 STRICT。

peer-authn-strict.yaml
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: demo
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: allow-frontend
  namespace: demo
spec:
  selector:
    matchLabels: {app: demoapp}
  action: ALLOW
  rules:
    - from:
        - source:
            principals: ["cluster.local/ns/demo/sa/frontend"]
      to:
        - operation: {methods: ["GET"], paths: ["/api/*"]}

两个对象职责不同:PeerAuthentication 只决定「连接是否必须用 mTLS」,AuthorizationPolicy 决定「这个身份能调哪些方法、哪些路径」。一旦命名空间里出现 ALLOW 策略,未被任何规则匹配的请求就会被拒绝;DENY 的优先级高于 ALLOW,排查「为什么 403」时先看有没有遗留的 DENY。

警告

mTLS 不等于授权 开了 STRICT 只说明链路加密、身份可信,任何持有合法证书的工作负载仍然能调用你的服务。真正的访问控制要落在 AuthorizationPolicy 上,并且用「默认拒绝 + 明确放行」的写法,而不是先放行再补规则。

8.2.3.1. 流量分割与故障注入

灰度靠两层对象:DestinationRule 用标签把后端分成 subset,VirtualService 决定权重和匹配条件。同一个 Service 下两个版本的 Pod 必须带可区分的标签,否则 subset 选不中。

vs-canary.yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: {name: demoapp, namespace: demo}
spec:
  host: demoapp
  subsets:
    - {name: v1, labels: {version: v1}}
    - {name: v2, labels: {version: v2}}
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: {name: demoapp, namespace: demo}
spec:
  hosts: [demoapp]
  http:
    - match:
        - headers: {x-canary: {exact: "true"}}
      route:
        - destination: {host: demoapp, subset: v2}
    - route:
        - destination: {host: demoapp, subset: v1}
          weight: 90
        - destination: {host: demoapp, subset: v2}
          weight: 10
      retries: {attempts: 2, perTryTimeout: 2s, retryOn: "5xx,reset"}
      timeout: 10s

Header 匹配优先于权重,所以测试账号可以稳定命中新版本,其余流量仍按 90/10 分流。retries 要谨慎:对一个已经过载的后端重试三次,等于把压力放大三倍;非幂等的 POST 一般应显式排除。

故障注入是同一套路由规则里的 fault 字段,用来在上线前验证超时、降级和告警是否真的生效:

vs-fault.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: {name: demoapp-fault, namespace: demo}
spec:
  hosts: [demoapp]
  http:
    - fault:
        abort: {percentage: {value: 50}, httpStatus: 503}
      route:
        - destination: {host: demoapp, subset: v2}

8.2.3.2. 与 Ingress、Gateway API 的边界

  • 南北向归 Ingress / Gateway,东西向归网格。入口需要公网 IP、TLS 终止、WAF、全局限流,这些不属于网格的职责。

  • Gateway API 正在统一两者:Gateway + HTTPRoute 既能描述入口,也能描述网格内路由。Istio 支持把 HTTPRoute 的 parentRefs 指向网格 Service,Cilium 也原生实现了 Gateway API。

  • 不要在网格里重造入口:把 ingress-nginx 换成 mesh gateway 之前,先确认 WAF、证书、限流这些能力有对等替代。

  • 注意加密边界:如果入口网关不在网格内,从网关到后端那一段是明文;把网关纳入网格,或者在网关上终止 TLS 后重新以 mTLS 连后端。

  • 端口命名决定协议识别:Service 端口必须叫 http、http2、grpc 之类,网格才能按 L7 处理,否则只做 L4 转发。

8.2.4. 选型:五个项目怎么放

项目

数据面

强项

弱项

适合

Istio

Envoy sidecar / ambient

功能最全:L7 路由、AuthZ、多集群、Wasm、Gateway API

复杂度最高,升级与排障成本大

中大型团队,需要细粒度策略

Linkerd

Rust 编写的 linkerd2-proxy

轻量、默认 mTLS、延迟低、运维简单

L7 策略与路由能力较少,无 ambient

想快速拿到 mTLS 与指标

Cilium

eBPF + 节点级 Envoy

与 CNI 一体、无 sidecar、性能好

依赖内核与 CNI,L7 弱于 Istio

已经用 Cilium,不想再加代理

Kuma

Envoy

单控制面管多网格、支持 VM、多租户

社区与生态较小

混合云、跨集群统一策略

Consul

Envoy

服务发现跨 VM 与 K8s,intentions 模型清晰

需引入 Consul 全家桶,2023 年起改为 BUSL 许可

已有 Consul 的异构环境

选型先问三个问题:集群里是否已经跑着 Cilium?团队有没有人愿意维护控制面?需要的是 L7 授权,还是只要 mTLS 加指标?Linkerd 与 Istio 的差距主要在 L7 策略和生态,不在基础加密——只要 mTLS 和指标,Linkerd 的运维成本明显更低。

8.2.5. 动手实验

实验:Istio 最小闭环(安装 → 注入 → mTLS → 灰度 → 故障注入)

目标:装一个 demo 版 Istio,给命名空间开启注入,用 STRICT 验证 mTLS 真的生效,再做 90/10 灰度与 50% 故障注入。

前置:Kubernetes 1.27+、istioctl 与集群版本匹配、curl 镜像可拉取。

8.2.5.1. 步骤 1 · 安装控制面

istioctl install --set profile=demo -y
kubectl -n istio-system get pod
NAME                      READY   STATUS    RESTARTS   AGE
istiod-7d9f8c6b5d-4xk2p   1/1     Running   0          2m
istio-ingressgateway-...  1/1     Running   0          2m

demo profile 会一并装 ingress/egress gateway,适合实验;生产用 default 或 minimal,并通过 revision 做控制面灰度升级。升级前先跑 istioctl x precheck,它会检查 CRD 版本与集群兼容性。

8.2.5.2. 步骤 2 · 部署两个版本并注入 sidecar

demoapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: demoapp-v1, namespace: demo, labels: {app: demoapp, version: v1}}
spec:
  replicas: 2
  selector: {matchLabels: {app: demoapp, version: v1}}
  template:
    metadata: {labels: {app: demoapp, version: v1}}
    spec:
      containers:
        - {name: app, image: "ikubernetes/demoapp:v1.0", ports: [{containerPort: 80}]}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: demoapp-v2, namespace: demo, labels: {app: demoapp, version: v2}}
spec:
  replicas: 1
  selector: {matchLabels: {app: demoapp, version: v2}}
  template:
    metadata: {labels: {app: demoapp, version: v2}}
    spec:
      containers:
        - {name: app, image: "ikubernetes/demoapp:v1.1", ports: [{containerPort: 80}]}
---
apiVersion: v1
kind: Service
metadata: {name: demoapp, namespace: demo}
spec:
  selector: {app: demoapp}
  ports: [{name: http, port: 80, targetPort: 80}]
kubectl create namespace demo
kubectl label namespace demo istio-injection=enabled
kubectl apply -f demoapp.yaml
kubectl -n demo get pod -L version
NAME                           READY   STATUS    AGE   VERSION
demoapp-v1-6c9d7f8b5-2xj9p     2/2     Running   1m    v1
demoapp-v1-6c9d7f8b5-9k4tq     2/2     Running   1m    v1
demoapp-v2-5b8f6d9c4-h7m2s     2/2     Running   1m    v2

2/2 说明 istio-proxy 注入成功。如果只有 1/1,通常是命名空间标签在 Pod 创建之后才加,kubectl rollout restart 即可。

8.2.5.3. 步骤 3 · 验证 mTLS 真的在工作

先应用前面那份 peer-authn-strict.yaml,再准备两个客户端:default 命名空间里的 plain(不注入),demo 命名空间里的 meshed(注入)。

kubectl run plain --image=curlimages/curl:8.10.1 --restart=Never --command -- sleep 3600
kubectl -n demo run meshed --image=curlimages/curl:8.10.1 --restart=Never --command -- sleep 3600
kubectl wait --for=condition=Ready pod/plain pod/meshed -n default --timeout=60s
kubectl exec plain -- curl -sS -m 3 http://demoapp.demo/
kubectl -n demo exec meshed -- curl -sS -m 3 http://demoapp.demo/ | head -3
curl: (56) Recv failure: Connection reset by peer
<!DOCTYPE html>
<html>...

第一条失败、第二条成功,就是 STRICT 生效的最直接证据:明文连接在握手阶段被代理拒绝。再补一条证书视角的检查:

istioctl proxy-config secret deploy/demoapp-v1 -n demo
istioctl proxy-status

proxy-config secret 里 default 证书的 VALID 应为 true,proxy-status 里所有代理应为 SYNCED。

8.2.5.4. 步骤 4 · 90/10 灰度与故障注入

kubectl apply -f vs-canary.yaml
for i in $(seq 1 20); do
  kubectl -n demo exec meshed -- curl -s http://demoapp/ | grep -o 'v1\.[01]'
done | sort | uniq -c

理想结果是 18 次 v1.0、2 次 v1.1。偏差较大时先看 istioctl proxy-config route deploy/meshed -n demo,确认权重真的下发到了客户端代理——流量分割是在调用方的 sidecar 上执行的,改服务端的规则不会影响别人怎么调它。

把 vs-fault.yaml 应用后,重复请求应看到约一半 503;验证完记得删掉:

kubectl apply -f vs-fault.yaml && sleep 2
for i in $(seq 1 10); do kubectl -n demo exec meshed -- curl -s -o /dev/null -w '%{http_code}\n' http://demoapp/; done | sort | uniq -c
kubectl delete -f vs-fault.yaml
   5 200
   5 503

验证清单

  • kubectl -n demo get pod 全部 2/2,istioctl proxy-status 全部 SYNCED

  • STRICT 后非注入命名空间访问失败,注入命名空间访问成功

  • istioctl proxy-config secret 中证书 VALID,且 AuthorizationPolicy 拒绝未授权身份

  • 20 次请求里约 2 次命中 v1.1

  • 故障注入期间约一半请求返回 503,删除后恢复

8.2.6. 常见问题与排错

现象

可能原因

处理方式

Pod 只有 1/1,没有 istio-proxy

命名空间未打注入标签,或 label 在 Pod 创建后才加

检查 kubectl get ns demo --show-labels,重建 Pod

注入后所有请求 503 no healthy upstream

端口未命名或 targetPort 与容器端口不一致

端口名改成 http,kubectl -n demo get endpointslice 确认后端

STRICT 后探针失败、Pod 反复重启

探针路径被授权策略拦截,或 sidecar 未就绪

用 rewriteAppHTTPProbe,或把探针路径加进 AuthorizationPolicy

Job / CronJob 永远不结束

sidecar 进程不随主容器退出

该 Job 关闭注入,或启用 native sidecar

灰度权重不生效

改的是服务端规则,客户端没收到;或 L4 连接复用

看调用方 proxy-config route;gRPC 场景改用 Header 或每请求连接

istioctl install 报 CRD 版本不兼容

集群里已有旧版 Istio 或残留 CRD

istioctl x precheck,按 revision 安装后迁移

访问集群外服务失败

outboundTrafficPolicy 为 REGISTRY_ONLY

创建 ServiceEntry,或显式改回 ALLOW_ANY

8.2.7. 进阶挑战

  1. 用 Gateway API 的 HTTPRoute 替掉 VirtualService 做同样的 90/10 灰度,对比两者在 Header 匹配和权重表达上的差异。

  2. 给 demoapp 加 RequestAuthentication,要求 JWT 中 groups 含 ops 才允许 DELETE,并验证无 token 时返回 401 而不是 403。

  3. 对整个命名空间做 STRICT 迁移演练:用 istioctl analyze 找出会受影响的工作负载,记录哪些必须排除(例如不兼容的 Job 或第三方组件)。

8.2.8. 小结

  • 网格把 mTLS、重试、切流、指标、授权从应用代码搬到数据面,收益是统一,成本是层级与运维。

  • sidecar 能力最全,ambient 与 eBPF 用节点级代理换掉每 Pod 开销,但要接受 L7 覆盖度的折损。

  • mTLS 由 PeerAuthentication 强制,授权由 AuthorizationPolicy 表达,两者不能互相替代。

  • 灰度在调用方 sidecar 上执行,L4 连接复用会让权重失真;排障顺序是状态 → 配置 → 证书 → 路由。

  • 南北向交给 Ingress / Gateway,东西向交给网格,Gateway API 是两者正在收敛的共同语言。

延伸阅读