Kubernetes 基础体系 · 第 33/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes Service Mesh:Sidecar、mTLS、流量治理、遥测和边界

Service Mesh(服务网格)是一组为服务间通信提供统一能力的基础设施。它通常不改变业务代码,而是在服务旁边运行代理,由代理处理连接建立、TLS、重试、超时、路由、指标和分布式追踪等工作。

这里有一个容易混淆的边界:

  • Kubernetes Service 解决的是服务发现和四层访问入口;
  • Ingress 或 Gateway API 解决的是进入集群的 HTTP、TLS 和路由;
  • Service Mesh 主要解决服务之间的通信治理,也可以延伸到入口和出口;
  • NetworkPolicy 解决的是网络层面的允许与拒绝;
  • Service Mesh 不会自动替代身份系统、应用鉴权、数据库权限或主机安全。

Service Mesh 不是 Kubernetes 内置资源。Kubernetes 提供 Pod、Service、EndpointSlice、ServiceAccount、NetworkPolicy 等基础能力,具体的 Mesh 实现由 Istio、Linkerd 等项目提供。因此,本文把 Kubernetes 的稳定 API 与 Mesh 实现的扩展 API 分开说明。


一、先明确通信模型:服务不是一个 Pod

假设服务 frontend 调用服务 orders

frontend Pod
  └── 应用容器
        └── HTTP 请求 http://orders.default.svc.cluster.local
              ↓
        orders Service 的虚拟地址
              ↓
        某个 orders Pod

Kubernetes 中,Service 通常通过 DNS 名称和虚拟 IP 暴露一组后端 Pod。控制面根据 Pod 的标签维护 EndpointSlice,节点上的 kube-proxy 或其他数据平面组件再把访问转发到具体 Pod。

Service Mesh 加入后,数据路径通常变成:

frontend Pod
  ├── frontend 应用容器
  │     └── 请求 orders
  │
  └── frontend sidecar 代理
        └── mTLS、路由、重试、指标、追踪
              ↓
        orders Pod 的 sidecar 代理
              ↓
        orders 应用容器

这里的关键变化不是“Service 被替换”,而是应用连接被代理截获。应用仍然可能使用 Kubernetes Service 的 DNS 名称,但连接的建立、加密和转发由 sidecar 代理接管。

1. Service Mesh 的三个平面

一个典型实现可以拆成三个部分:

  1. 数据平面(data plane)

    每个业务 Pod 中运行一个代理,例如 Envoy。它真正处理字节流、TLS、HTTP、连接池和统计信息。

  2. 控制平面(control plane)

    负责向代理下发服务发现、证书、路由和策略。例如 Istio 的控制组件会观察 Kubernetes 对象,并向 Envoy 提供配置。

  3. 管理与观测系统

    负责收集代理指标、访问日志和追踪数据,或者把这些数据发送到 Prometheus、OpenTelemetry Collector、日志系统和追踪后端。

控制平面通常不在每个请求的同步路径上。一个常见流程是:

sequenceDiagram
    participant API as Kubernetes API Server
    participant CP as Mesh 控制平面
    participant P1 as frontend sidecar
    participant P2 as orders sidecar
    participant A1 as frontend 应用
    participant A2 as orders 应用

    API->>CP: Pod、Service、EndpointSlice、策略变化
    CP->>P1: 服务发现、路由、证书配置
    CP->>P2: 服务发现、路由、证书配置

    A1->>P1: HTTP 请求
    P1->>P2: mTLS 连接
    P2->>A2: 本地转发
    A2-->>P2: HTTP 响应
    P2-->>P1: mTLS 响应
    P1-->>A1: HTTP 响应

控制平面不可用时,已经下发到代理的数据平面配置通常仍可继续转发,但新的配置、证书轮换或新增后端可能无法及时生效。数据平面代理本身故障则可能直接影响业务请求。


二、Sidecar:为什么要在业务容器旁边放一个代理

1. Sidecar 的定义

Sidecar 是与主应用运行在同一个 Pod 中的辅助容器。因为同一 Pod 内的容器共享:

  • 网络命名空间;
  • Pod IP;
  • 端口空间;
  • 通常也可以共享卷。

所以 sidecar 可以观察和处理应用容器发出的网络连接。

典型 Pod 结构如下:

apiVersion: v1
kind: Pod
metadata:
  name: example
spec:
  containers:
    - name: app
      image: example/app:1.0
    - name: proxy
      image: example/mesh-proxy:1.0

在真实 Mesh 中,不建议手工写入代理容器。通常由 admission webhook 根据命名空间标签或 Pod 注解自动注入。这样做的原因是代理参数、证书卷、启动顺序和版本升级都由 Mesh 控制面统一维护。

2. 请求如何被 Sidecar 截获

常见实现使用以下一种或多种机制:

  • init container 配置 iptables 规则;
  • CNI 插件在 Pod 创建时配置网络规则;
  • eBPF 或其他内核数据路径机制;
  • 代理监听透明代理端口,再把流量转回原目标。

以 iptables 透明代理为例,抽象后的流程是:

应用 connect(Service IP:80)
        ↓
OUTPUT 链匹配
        ↓
重定向到本地 sidecar 监听端口
        ↓
sidecar 判断原始目标地址
        ↓
选择服务集群、路由和后端
        ↓
建立到目标 Pod sidecar 的连接

入站流量也可能在到达应用端口前先经过 sidecar:

远端 Pod sidecar
        ↓
本地 Pod 网络命名空间
        ↓
sidecar inbound listener
        ↓
应用容器端口

具体的 iptables 链名、端口和绕过规则属于实现细节,不应当把某个实现的规则名当成 Kubernetes 标准。

3. 注入不是“只多一个容器”

Sidecar 注入至少涉及以下状态:

  1. admission webhook 看到 Pod 创建请求;
  2. webhook 修改 Pod,加入代理容器、卷和环境变量;
  3. init container 或 CNI 配置流量重定向;
  4. 代理启动并向控制平面获取配置;
  5. Pod 的 Ready 状态是否依赖代理准备完成;
  6. 业务容器是否在代理尚未准备时开始发送请求;
  7. Pod 删除时,代理是否先停止接收新请求并等待已有连接结束。

如果代理未就绪但业务容器已经对外宣告 Ready,可能出现“应用健康检查成功、服务间请求失败”的启动竞态。因此生产配置中常见代理 readiness 检查、启动延迟和优雅终止机制。

4. Sidecar 的典型失效方式

注入失败

Pod 没有注入 sidecar,常见原因包括:

  • 命名空间没有正确标签;
  • admission webhook 不可用;
  • Pod 被显式禁用注入;
  • webhook 的证书或网络访问异常;
  • 使用了不支持注入的工作负载或特殊运行时设置。

表现是 Pod 只有业务容器,Mesh 的 mTLS、流量策略和代理遥测不会覆盖该 Pod。

流量没有被捕获

即使有 sidecar,也不代表所有流量都经过它。例如:

  • Pod 使用 hostNetwork: true
  • 目标端口被配置为排除;
  • 应用使用特殊的网络协议;
  • 规则被手工修改;
  • 使用 Unix domain socket;
  • 代理无法识别某种加密或封装协议;
  • 内核、CNI 或安全策略阻止透明转发。

sidecar 本身成为故障点

应用进程可能正常运行,但 sidecar:

  • 配置未下发;
  • 证书过期;
  • 资源耗尽;
  • 连接池达到上限;
  • 代理进程崩溃;
  • 由于错误的重试策略放大了请求量。

因此,加入 Mesh 后需要同时监控业务容器和代理容器。仅查看应用日志不能证明网络路径正常。


三、mTLS:身份、加密和认证分别解决什么问题

1. TLS 与 mTLS

普通 TLS 通常是服务端向客户端证明身份:

客户端 ──验证服务端证书──> 服务端

mTLS(mutual TLS,双向 TLS)则要求双方都证明身份:

客户端 ──验证服务端证书──> 服务端
客户端 <─验证客户端证书── 服务端

在 Service Mesh 中,应用通常不直接实现 mTLS。客户端 sidecar 和服务端 sidecar 完成握手,业务应用收到的可能仍是本地明文连接:

frontend 应用
   ──本地连接──> frontend sidecar
                     ║
                     ║ mTLS
                     ║
orders sidecar
   ──本地连接──> orders 应用

所以必须准确理解“加密范围”:mTLS 保护的是两个代理之间的网络段,并不自动保护应用到本地 sidecar 的所有数据,也不自动保护代理到外部系统的连接。

2. mTLS 证书中的身份

Mesh 需要给工作负载分配身份。常见身份来源包括 Kubernetes ServiceAccount,经过 Mesh 的身份系统映射后形成 URI SAN(Subject Alternative Name),例如:

spiffe://cluster.local/ns/orders/sa/orders

这里可以抽象出四个变量:

  • trust domain:信任域,例如 cluster.local
  • namespace:工作负载所在命名空间;
  • service account:工作负载使用的 Kubernetes ServiceAccount;
  • workload identity:最终写入证书并用于授权判断的身份。

证书有效并不等于请求被授权。证书验证回答:

对端是否持有受信任 CA 签发的、属于某个身份的证书?

授权策略还要回答:

这个身份是否允许访问当前服务、端口、路径或方法?

因此:

mTLS 身份认证 ≠ 应用授权

即使 frontend 能证明自己是一个受信任工作负载,也不意味着它可以访问 orders 的所有接口。

3. mTLS 握手过程

一个简化的 mTLS 握手如下:

  1. 客户端 sidecar 连接服务端 sidecar;
  2. 双方协商 TLS 版本和密码套件;
  3. 服务端发送证书链;
  4. 客户端验证证书签发者、有效期、SAN 和信任根;
  5. 客户端发送自己的证书链;
  6. 服务端验证客户端证书;
  7. 双方使用密钥交换结果建立会话密钥;
  8. 后续 HTTP 或 TCP 数据使用会话密钥加密。

Mesh 控制平面通常负责:

  • 生成或签发工作负载证书;
  • 分发信任根;
  • 证书轮换;
  • 将身份和服务发现配置发送给代理。

证书轮换时,代理需要在旧证书过期前获取新证书。若控制平面不可用,短期内可能仍可使用缓存证书,但最终会因为证书过期导致新连接失败。

4. 三种常见 mTLS 模式

具体名字依实现而异,但语义通常接近:

  • 禁用或明文模式:代理之间不要求 TLS;
  • 兼容模式:既接受明文,也接受 mTLS;
  • 严格模式:只接受 mTLS。

兼容模式便于迁移,因为网格内和网格外客户端都可能访问服务;但它不能证明所有连接都已加密。严格模式能够发现遗漏注入的客户端,但启用顺序错误会造成大面积故障。

一个安全迁移顺序通常是:

确认服务端和客户端都已注入
        ↓
观察代理握手和错误日志
        ↓
先启用兼容接收模式
        ↓
修复未纳管调用方
        ↓
再切换严格接收模式
        ↓
验证外部、健康检查和特殊端口

5. Kubernetes 与 mTLS 的关系

Kubernetes Service 不会自动提供服务间 mTLS。Kubernetes 的 ServiceAccount 也不等于“已经完成网络双向认证”。

ServiceAccount 主要提供工作负载身份和 API 访问凭据的基础。Mesh 可以使用它构建工作负载身份,但证书签发、代理握手和授权策略仍由 Mesh 实现完成。

此外,Kubernetes Secret 中的 TLS 证书也不自动进入 Mesh 的身份体系。Ingress 的证书、应用自行挂载的证书和 sidecar 的工作负载证书,可能是三套不同的证书生命周期。


四、一个可运行的 mTLS 验证示例

下面使用 Istio 作为具体实现示例。PeerAuthenticationAuthorizationPolicyVirtualService 等不是 Kubernetes 内置 API,而是 Istio CRD。Istio 的安装方式、配置字段和行为应以所使用版本的官方文档为准。

1. 前置条件

需要:

  • 一个可用的 Kubernetes 集群;
  • kubectl
  • 与集群版本兼容的 Istio 发行版;
  • istioctl
  • 能拉取示例镜像的节点;
  • 集群中允许 admission webhook 注入 sidecar。

演示安装可以使用 Istio 的 demo 配置,但它面向学习和验证,不适合直接作为生产配置:

istioctl install --set profile=demo -y

检查控制面:

kubectl get pods -n istio-system

预期可以看到 Istio 控制组件处于 RunningReady。如果控制面未就绪,后续注入和证书下发都可能失败。

2. 创建启用自动注入的命名空间

kubectl create namespace mesh-demo

kubectl label namespace mesh-demo istio-injection=enabled

这是 Istio 传统自动注入方式的示例。部分版本和部署模式也支持 revision 标签,例如 istio.io/rev。两者不要在不了解版本行为的情况下混用。

3. 部署服务端和客户端

apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: mesh-demo
spec:
  selector:
    app: orders
  ports:
    - name: http
      port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
  namespace: mesh-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
    spec:
      serviceAccountName: orders
      containers:
        - name: orders
          image: hashicorp/http-echo:1.0
          args:
            - "-listen=:8080"
            - "-text=orders-v1"
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders
  namespace: mesh-demo
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: frontend
  namespace: mesh-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: mesh-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      serviceAccountName: frontend
      containers:
        - name: curl
          image: curlimages/curl:8.10.1
          command: ["/bin/sh", "-c"]
          args:
            - "sleep 3600"

保存为 mesh-demo.yaml 后应用:

kubectl apply -f mesh-demo.yaml
kubectl -n mesh-demo get pods

预期每个 Pod 都有两个容器,例如:

NAME                        READY   STATUS
orders-...                  2/2     Running
frontend-...                2/2     Running

如果是 1/1,说明 sidecar 没有注入,不能继续把结果解释为 Mesh 行为。此时应先检查命名空间标签、webhook 和 Pod 创建事件。

4. 从客户端访问服务

kubectl -n mesh-demo exec deploy/frontend -c curl -- \
  curl -sS http://orders:8080

预期输出:

orders-v1

这里 curl 容器访问的是 Kubernetes Service DNS 名称;mTLS 是否存在由两个 sidecar 之间的连接决定,而不是由 curl 命令本身决定。

5. 切换服务端为严格 mTLS

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: orders-strict
  namespace: mesh-demo
spec:
  selector:
    matchLabels:
      app: orders
  mtls:
    mode: STRICT

应用:

kubectl apply -f orders-strict.yaml

此策略要求匹配到的 orders 工作负载只接受 mTLS。因为 frontend 也有 sidecar,正常访问应继续成功:

kubectl -n mesh-demo exec deploy/frontend -c curl -- \
  curl -sS http://orders:8080

预期仍为:

orders-v1

这说明客户端代理能够使用受信任身份与服务端代理建立 mTLS。它不能单独证明授权策略正确,也不能证明所有端口都受到同样保护。

6. 限制访问身份

可以进一步使用 Istio 的 AuthorizationPolicy,只允许 frontend ServiceAccount 访问 orders 的 HTTP 端口:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: orders-allow-frontend
  namespace: mesh-demo
spec:
  selector:
    matchLabels:
      app: orders
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/mesh-demo/sa/frontend
      to:
        - operation:
            ports: ["8080"]
kubectl apply -f orders-allow-frontend.yaml

这里的 principals 是 Istio 对工作负载身份的表达方式,具体格式与 trust domain 和版本配置有关。策略生效后:

  • frontend 的 sidecar 携带的身份匹配规则,请求允许;
  • 其他带有不同 ServiceAccount 的工作负载,即使同样使用 mTLS,也可能被拒绝;
  • 没有 sidecar 的客户端通常无法满足该身份条件。

诊断时可以查看:

istioctl proxy-status
istioctl proxy-config clusters deploy/frontend -n mesh-demo
istioctl proxy-config listeners deploy/orders -n mesh-demo

这些命令用于确认代理是否连接控制平面、是否获得目标服务配置、是否监听了预期端口。命令输出格式会随 Istio 版本变化,不应在自动化脚本中依赖未经确认的列位置。


五、流量治理:代理究竟能改变什么

流量治理是通过代理控制“请求应该去哪里、失败后怎么办、如何逐步发布”的能力。它通常包括:

  • 超时;
  • 重试;
  • 熔断和连接池限制;
  • 权重路由;
  • 按请求头、路径或来源路由;
  • 故障注入;
  • 流量镜像;
  • 入口和出口策略。

1. 路由与负载均衡不是一回事

假设 orders 有两个版本:

orders-v1: 3 个 Pod
orders-v2: 1 个 Pod

Kubernetes Service 的选择器只能按标签选出后端集合。它不理解 HTTP 请求头中的用户、版本或租户,也不天然知道“把 5% 请求送到 v2”。

Service Mesh 可以先在 L7(HTTP)层决定版本,再在目标版本内部做负载均衡:

请求
  ↓
VirtualService:选择 v1 或 v2
  ↓
DestinationRule:把目标分成 v1、v2 子集
  ↓
代理在子集内选择某个 Pod

这两个步骤必须区分:

  • 路由:决定请求属于哪个目标子集;
  • 负载均衡:决定该子集中的具体后端。

2. Istio 权重路由示例

下面是 Istio 风格的版本子集与权重路由。它依赖 Istio CRD,不是 Kubernetes 原生对象:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: orders
  namespace: mesh-demo
spec:
  host: orders.mesh-demo.svc.cluster.local
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: orders
  namespace: mesh-demo
spec:
  hosts:
    - orders.mesh-demo.svc.cluster.local
  http:
    - route:
        - destination:
            host: orders.mesh-demo.svc.cluster.local
            subset: v1
          weight: 95
        - destination:
            host: orders.mesh-demo.svc.cluster.local
            subset: v2
          weight: 5

应用前,必须确保 Pod 标签真的有:

labels:
  app: orders
  version: v1

和:

labels:
  app: orders
  version: v2

权重 955 表示代理在符合条件的请求中按配置比例选择路由。它不是严格的全局计数器,因此短时间内可能看到偏离 95:5 的结果,尤其是在请求量很小、连接复用明显或多个代理分别做本地决策时。

3. 超时与重试的因果关系

超时限制请求允许占用资源的最长时间;重试则在失败后再次发送请求。二者组合不当会造成请求放大。

设:

  • 原始请求数为 NN
  • 每个请求最多重试 rr 次;
  • 每次尝试失败概率为 pp

最坏情况下,发送尝试数上界为:

Amax=N(r+1)A_{\max}=N(r+1)

例如 N=1000N=1000、最多重试 r=2r=2,后端完全不可用时:

Amax=1000×3=3000A_{\max}=1000 \times 3=3000

如果每次失败概率近似为 p=0.5p=0.5,并且每次尝试独立,那么一个请求最终成功的概率为:

P(success)=1pr+1P(\text{success})=1-p^{r+1}

r=2r=2 时:

P(success)=10.53=0.875P(\text{success})=1-0.5^3=0.875

表面上成功率提高了,但后端承担的尝试数也增加了。真实系统中失败通常不是独立随机事件:数据库过载、线程池耗尽和下游限流会产生相关失败,重试可能进一步加重过载。

因此重试必须同时考虑:

  • 请求是否幂等;
  • 哪些状态码和网络错误可以重试;
  • 最大重试次数;
  • 每次重试的退避时间;
  • 整体请求截止时间;
  • 下游是否已经返回业务副作用;
  • 是否需要使用重试预算。

对支付、下单、写入数据库等非幂等操作,不能仅因为 HTTP 请求失败就盲目重试。连接在响应返回前断开时,服务端可能已经完成了写操作,客户端无法仅凭网络错误判断“操作没有发生”。

4. 连接池、熔断与隔离

代理可以对单个上游设置:

  • 最大连接数;
  • 最大并发请求数;
  • 每个连接允许的请求数;
  • 等待队列长度;
  • 异常实例驱逐时间。

这些能力常被称为熔断或过载保护,但它们不是万能的服务恢复机制。

例如,某个上游实例响应很慢:

请求进入代理
  ↓
连接池已满
  ↓
新请求排队
  ↓
等待超时
  ↓
客户端重试
  ↓
更多请求进入连接池

如果重试发生在多层代理中,放大效应会更严重。假设三层服务各自最多重试 2 次,最坏尝试数可能达到:

3×3×3=273 \times 3 \times 3=27

这不是所有实现的实际行为,而是说明“每层都独立重试”会产生组合放大。通常应该明确重试责任由哪一层承担,并让内部服务的超时总和小于调用方的截止时间。

5. 故障注入与流量镜像的边界

Mesh 可以在代理层注入延迟或错误,用于验证超时、重试和降级逻辑。但它只覆盖能够被代理识别和截获的流量,不能替代真实的:

  • 节点故障;
  • DNS 故障;
  • CNI 故障;
  • 磁盘耗尽;
  • 内核丢包;
  • 外部依赖不可达;
  • 证书签发系统故障。

流量镜像通常把请求副本发送到另一个后端,但镜像响应不会返回给原调用方。它对读请求或经过隔离的测试服务更安全;对写请求可能产生真实副作用,不能因为“响应被丢弃”就认为没有风险。


六、遥测:代理看到的是什么,应用又缺失什么

遥测(telemetry)是对系统运行状态的可观测记录,通常包括:

  • 指标(metrics);
  • 日志(logs);
  • 分布式追踪(traces);
  • 访问记录和元数据。

Service Mesh 的重要价值是:代理天然处在通信路径上,因此可以在不修改业务代码的情况下记录一部分网络事实。

1. 代理指标

常见指标包括:

  • 请求总数;
  • 响应状态码;
  • 请求延迟;
  • 上游连接数;
  • TLS 握手错误;
  • 重试次数;
  • 连接池溢出;
  • 请求或响应字节数。

但代理通常不知道业务含义。例如:

HTTP 200

只能说明 HTTP 层返回了成功状态,不代表订单已经创建成功。业务响应体可能包含:

{"code":"INSUFFICIENT_BALANCE"}

因此 Mesh 指标适合回答:

哪个服务、哪个版本、哪个路径、哪个调用方的网络请求失败或变慢?

应用指标适合回答:

订单创建失败的业务原因是什么?库存扣减失败了多少次?

两者需要关联,而不是互相替代。

2. 分布式追踪传播

分布式追踪需要一个 trace context 在调用链中传播。现代系统常见 W3C Trace Context,例如:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

可以把它抽象为:

  • trace-id:整条调用链的标识;
  • parent-id:当前调用的父 span;
  • flags:采样等控制信息。

代理可以生成自己的 span,也可以转发已有的追踪头。但“代理能转发”不等于“链路一定完整”。业务应用在发起下游调用时仍需要正确传递上下文;如果应用丢弃或覆盖请求头,调用链会断裂。

一个典型链路是:

入口代理 span
  ↓
frontend 应用 span
  ↓
frontend sidecar span
  ↓
orders sidecar span
  ↓
orders 应用 span

不同 Mesh 和遥测栈对 span 命名、采样和父子关系的处理可能不同。排障时应同时检查请求头是否传播、代理是否采样、Collector 是否丢弃数据,而不能只看追踪后端页面。

3. 遥测成本与数据质量

每个请求都记录完整访问日志和追踪信息会增加:

  • CPU 和内存消耗;
  • 网络发送量;
  • 日志存储成本;
  • 高基数标签带来的指标压力;
  • 敏感数据泄露风险。

尤其不能把用户 ID、完整 URL 查询参数、Authorization 头或响应体直接作为高基数标签。指标标签应保持有限且稳定;敏感字段应在代理或采集器处脱敏。

采样还会改变解释方式。例如采样率为 10% 时,观测到的请求数不是完整请求数。可以估计总量,但小流量场景的方差很大;错误请求往往应该采用尾部采样或按错误保留,而不是简单均匀采样。


七、入口边界:Service Mesh 与 Ingress、Gateway API 如何配合

入口流量是从集群外部进入集群的流量。Ingress 和 Gateway API 都可以描述 HTTP、HTTPS 和路由,但它们与 Service Mesh 的职责不完全相同。

1. Ingress 的基本模型

Kubernetes Ingress 是一个 API 对象,用于描述外部 HTTP/HTTPS 到 Service 的规则。它本身不是数据平面,也不会自动监听端口,必须由 Ingress Controller 实现。

大致路径是:

外部客户端
  ↓
负载均衡器或节点端口
  ↓
Ingress Controller
  ↓
Kubernetes Service
  ↓
应用 Pod

Ingress API 覆盖了常见路径和主机路由,但扩展能力和策略表达能力依赖 Controller。不同厂商对注解、TLS、重写、限流和认证的支持并不一致。

2. Gateway API 的基本模型

Gateway API 将入口配置拆成更明确的角色:

  • GatewayClass:由谁实现 Gateway;
  • Gateway:监听地址、端口和协议;
  • HTTPRoute:HTTP 路由规则;
  • 其他 Route 类型:根据 API 版本和实现支持 TCP、TLS 等场景。

一个简化的 HTTP Gateway API 示例:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public-gateway
  namespace: mesh-demo
spec:
  gatewayClassName: example-gateway-class
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders-route
  namespace: mesh-demo
spec:
  parentRefs:
    - name: public-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /orders
      backendRefs:
        - name: orders
          port: 8080

其中 example-gateway-class 只是说明接口关系的示例名称,必须替换成集群中实际存在的 GatewayClass。Gateway API 资源由具体 Controller 实现,Controller 是否支持某种过滤器、跨命名空间引用和 TLS 模式,需要检查其 status.conditions 和实现文档。

3. 入口 Gateway 与 Mesh Gateway

很多 Mesh 实现提供自己的入口网关工作负载。它可能同时承担:

  • 外部 TLS 终止;
  • HTTP 路由;
  • 外部客户端到网关的身份认证;
  • 网关到内部服务的 mTLS;
  • 入口访问日志和指标。

但不能假设“进入网关后就自动全链路加密”。需要分别验证:

客户端 ──HTTPS──> 入口网关
入口网关 ──mTLS 或明文──> 内部服务代理
内部服务代理 ──本地连接──> 应用

如果网关到内部服务的连接未启用 mTLS,外部 TLS 终止后到内部服务之间仍可能是明文。若内部服务启用了严格 mTLS,而入口网关没有 sidecar 或没有正确配置身份,入口访问会失败。

4. Ingress 到 Gateway API 的迁移边界

迁移不是把 IngressapiVersion 改掉。两者资源模型不同:

  • Ingress 的实现行为常依赖注解;
  • Gateway API 显式分离平台管理员和应用团队的职责;
  • Gateway API 的路由挂载有 allowedRoutes 和引用授权;
  • TLS、重写、认证、限流等能力可能需要实现特有扩展;
  • 原 Controller 的注解不一定有一一对应的 Gateway API 字段。

迁移时应先建立行为对照:

主机/路径匹配
TLS 证书来源
重定向和重写
后端引用
跨命名空间权限
健康检查
限流与认证
访问日志

然后通过 GatewayHTTPRoute 的状态条件、实际请求和回滚配置逐项验证。不能因为资源被 API Server 接受,就认为 Controller 已经实现了全部语义。


八、出站边界:Egress、NAT、代理、DNS 和白名单

出站流量是从集群工作负载访问集群外部系统的流量,例如数据库、支付接口、对象存储和 SaaS API。

1. 出站路径并不固定

一个请求可能经过:

应用
  ↓
sidecar
  ↓
Egress Gateway(可选)
  ↓
节点 SNAT
  ↓
云厂商 NAT Gateway 或出口防火墙
  ↓
外部服务

也可能直接由 Pod 经节点 SNAT 出网:

应用
  ↓
sidecar
  ↓
节点网络
  ↓
SNAT
  ↓
外部服务

因此“Mesh 已经治理了出口”需要明确是哪个层次:

  • sidecar 是否截获;
  • 是否强制经过 Egress Gateway;
  • 节点是否执行 SNAT;
  • 云 NAT 是否记录源地址;
  • 防火墙是否按固定出口 IP 放行;
  • 外部服务是否验证客户端证书。

2. DNS 与出口治理

应用通常先解析:

api.example.com

DNS 解析可能发生在:

  • Pod 的 /etc/resolv.conf 指向 CoreDNS;
  • sidecar 代理负责代理 DNS;
  • 应用自己使用 DoH、DoT 或自带解析器;
  • 外部 DNS 解析服务返回不同地址。

如果白名单按域名,而实际防火墙按 IP,必须处理 DNS 变化、CDN、多 A 记录和 TTL。域名白名单并不自动等于固定网络边界。

此外,若代理根据 SNI 或 HTTP Host 识别外部目标,应用使用 IP 直连、加密 DNS 或非 HTTP 协议,可能无法应用同样的 L7 规则。

3. NAT 的可见身份

出站到公网时,外部服务通常看到的是 NAT 后的地址,而不是 Pod IP:

Pod IP: 10.244.1.10
        ↓ SNAT
节点或 NAT Gateway IP: 203.0.113.10
        ↓
外部 API 看到 203.0.113.10

如果外部系统要求 IP 白名单,白名单应配置实际出口地址。Mesh 的工作负载身份不会自动被普通公网服务识别;除非双方实现了基于证书或其他协议的身份认证。

4. 强制出口网关的失败路径

若设计要求所有外部访问必须经过 Egress Gateway,至少需要验证:

  1. 普通业务 Pod 的出站连接确实被截获;
  2. 代理不能通过排除规则绕过;
  3. Egress Gateway 有可用的 mTLS 或身份认证;
  4. 节点路由不允许业务 Pod 直接 SNAT 出网;
  5. DNS 解析不会把流量导向未预期的路径;
  6. Gateway 故障时,业务是快速失败还是意外直连;
  7. Gateway 的日志能关联到原始工作负载身份。

只配置一个 Egress Gateway Deployment,不会自动形成强制边界。若底层网络仍允许 Pod 直接访问公网,Gateway 只是“常规路径”,不是安全控制点。


九、Mesh 策略与 NetworkPolicy 的分工

Service Mesh 策略通常在代理能够看到的连接或请求上执行;NetworkPolicy 通常在 Pod、命名空间、IP、端口等网络层属性上执行。

例如:

NetworkPolicy:frontend Pod 可以连接 orders Pod 的 TCP/8080
Mesh AuthorizationPolicy:只有 frontend 身份可以访问 GET /orders

两者可以叠加:

  • NetworkPolicy 先拒绝,Mesh 根本看不到请求;
  • NetworkPolicy 放行但 Mesh 拒绝,连接可能建立到代理后由代理返回拒绝;
  • Mesh 放行但 NetworkPolicy 拒绝,请求仍然失败;
  • 未注入的 Pod 可能仍受 NetworkPolicy 约束,但不具备 Mesh 的身份和 L7 策略能力。

因此不能用 Mesh 策略替代 NetworkPolicy,也不能用只允许端口的 NetworkPolicy 表达完整的 HTTP 授权。


十、常见误解与对应的失败表现

误解一:启用 mTLS 后,应用中的所有数据都加密了

实际情况可能是:

应用 → 本地 sidecar:明文
sidecar → 远端 sidecar:mTLS
远端 sidecar → 应用:明文

如果攻击者已经能够读取 Pod 网络命名空间、进程内存或容器间本地流量,mTLS 不是针对这种威胁模型的保护。

误解二:有 sidecar 就能治理所有协议

HTTP 路由、请求头匹配和路径策略要求代理能够解析 HTTP。对任意 TCP 流量,代理通常只能执行连接级治理;对自定义加密协议,代理可能看不到业务语义。

某些协议还需要正确声明端口名称或协议类型,否则代理可能按 TCP 处理,导致 HTTP 路由、指标和追踪缺失。

误解三:代理返回 200 就代表业务成功

代理可能只看到 HTTP 状态码,无法理解响应体中的业务错误。另一方面,TCP 连接成功也不代表 HTTP 请求成功。

排查时应分层观察:

DNS 是否解析成功
TCP 是否建立
TLS/mTLS 是否握手成功
HTTP 是否返回
应用业务码是否成功

误解四:严格 mTLS 可以自动发现所有未纳管客户端

严格模式通常只约束匹配到的服务端代理。若流量没有经过代理、目标不是该策略的选择范围,或者存在外部入口和特殊端口,行为可能不同。

正确做法是从服务端代理日志、配置和连接身份验证结果确认,而不是只看某个 Pod 是否有两个容器。

误解五:重试能提高可靠性,所以越多越好

重试提高的是部分瞬时故障下的成功概率,同时增加后端负载和请求延迟。对非幂等操作,重试还可能造成重复写入。重试配置必须和请求截止时间、幂等性、限流和后端容量共同设计。


十一、生产诊断:从应用层向下定位

1. 先确认 Pod 和注入状态

kubectl -n mesh-demo get pod -o wide
kubectl -n mesh-demo describe pod <pod-name>

关注:

  • 容器数量是否符合预期;
  • sidecar 是否 Ready;
  • init container 是否成功;
  • Pod 事件中是否有 webhook、卷挂载或探针错误;
  • Pod 是否使用 hostNetwork
  • readiness 是否依赖代理。

2. 再确认 Service 和 EndpointSlice

kubectl -n mesh-demo get svc orders -o yaml
kubectl -n mesh-demo get endpointslice \
  -l kubernetes.io/service-name=orders -o yaml

如果 EndpointSlice 为空,Mesh 再正确也找不到后端。此时应检查:

  • Service selector 是否匹配 Pod;
  • Pod 是否 Ready;
  • 端口名称和 targetPort 是否正确;
  • 是否存在错误的地址族或拓扑条件。

3. 确认代理配置与同步状态

以 Istio 为例:

istioctl proxy-status
istioctl proxy-config clusters deploy/frontend -n mesh-demo
istioctl proxy-config routes deploy/frontend -n mesh-demo

这些检查分别回答:

  • 代理是否连接控制平面;
  • 是否知道 orders 服务和后端;
  • HTTP 路由是否按预期下发。

若路由配置存在但请求仍失败,应继续检查证书、授权、端口和底层网络,而不能反复重发配置。

4. 区分 TLS 失败和授权失败

常见错误含义不同:

DNS 解析失败       → 服务名、CoreDNS 或搜索域问题
connection refused → 目标端口没有监听或转发错误
TLS handshake error → 证书、信任根、SNI、mTLS 模式问题
403               → 代理或应用授权拒绝
503               → 无可用后端、路由失败、熔断或上游连接失败
504               → 超时,可能是后端慢、排队或重试耗尽

应结合代理访问日志、应用日志和抓包结果判断。仅依据客户端的 curl 错误文本,通常无法区分代理拒绝和应用拒绝。

5. 更改策略前准备回滚

策略类变更至少应保留:

kubectl get peerauthentication,authorizationpolicy \
  -n mesh-demo -o yaml > policy-backup.yaml

应用新策略后,验证:

kubectl -n mesh-demo exec deploy/frontend -c curl -- \
  curl -i http://orders:8080

若严格 mTLS 或授权策略导致调用失败,应先恢复上一版策略,而不是直接删除所有 Mesh 资源。删除策略可能回退到命名空间或网格级默认行为,结果不一定是“允许全部”或“拒绝全部”。


十二、性能、并发与资源取舍

每个 sidecar 都会增加代理进程、连接池、监听器、配置和遥测开销。影响大小与以下因素有关:

  • Pod 数量;
  • 每个代理管理的服务和路由数量;
  • 并发连接数;
  • HTTP/1.1、HTTP/2 或 TCP 模式;
  • TLS 握手频率;
  • 访问日志和追踪采样率;
  • 重试与连接池配置;
  • 代理 CPU、内存限制。

不要把某个项目或云厂商的单一性能数字当成普遍保证。应在接近生产的工作负载下测量:

  • P50、P95、P99 延迟;
  • CPU 和内存;
  • TLS 握手次数;
  • 连接复用率;
  • 代理重启时的请求失败;
  • 控制平面配置变更时的传播时间;
  • 节点和网络故障下的恢复行为。

代理资源限制过低会导致 OOM 或连接被拒绝;限制过高则可能挤压业务容器。Pod 级 QoS、节点容量和 sidecar 注入后的实际资源需求都需要重新评估。


十三、Sidecar 模式之外:Ambient 等实现变化

Sidecar 不是 Service Mesh 的唯一数据平面形态。部分 Mesh 实现提供 ambient 等模式,通过节点级或共享代理处理部分流量,以减少每个 Pod 都运行一个代理的成本。

这并不意味着:

  • 所有协议都自动获得相同的 L7 能力;
  • 所有 sidecar 的配置、策略和调试命令仍然适用;
  • 可以忽略 Pod、节点和 CNI 的安全边界;
  • 迁移成本为零。

ambient、eBPF、共享代理和 sidecar 的数据路径、故障域、升级方式、身份粒度和流量截获范围都可能不同。它们属于具体 Mesh 实现的能力,不是 Kubernetes 当前稳定 API 的一部分。部署前应验证目标版本的支持范围,特别是协议、入口、出口、NetworkPolicy 交互和调试工具。


十四、正确建立边界模型

一个完整的服务间请求可以按下面的边界拆开:

[应用业务]
  产生 HTTP/TCP 请求
      ↓
[本地代理边界]
  是否被捕获?是否明文?
      ↓
[服务发现边界]
  DNS、Service、EndpointSlice、代理集群
      ↓
[身份边界]
  mTLS 证书、trust domain、ServiceAccount
      ↓
[授权边界]
  NetworkPolicy、Mesh 授权、应用鉴权
      ↓
[路由治理边界]
  版本、权重、超时、重试、熔断
      ↓
[出口或入口边界]
  Gateway、NAT、防火墙、外部 TLS
      ↓
[业务结果]
  应用是否真正完成业务操作

其中任意一层成功,都不能推出其他层成功。例如:

  • DNS 成功,不代表 mTLS 成功;
  • mTLS 成功,不代表授权成功;
  • 授权成功,不代表后端有容量;
  • HTTP 200,不代表业务写入成功;
  • 入口 TLS 成功,不代表出口固定 IP 生效;
  • 有访问日志,不代表日志包含正确的工作负载身份。

Service Mesh 的核心价值,是把许多服务间通信动作从业务代码中抽离到统一的数据平面,并通过控制平面集中配置。但它的边界同样明确:它只能治理实际经过代理、且代理能够理解的通信;它不能自动修复错误的服务发现、错误的业务语义、缺失的网络隔离或不一致的外部身份体系。

因此,验证一个 Mesh 部署是否正确,不应只问“sidecar 是否注入”或“mTLS 是否开启”,而应沿着完整路径验证:

请求是否经过预期代理
→ 代理是否发现正确后端
→ 双方是否使用预期身份完成握手
→ 策略是否允许该调用
→ 路由是否选择正确版本
→ 超时和重试是否符合业务语义
→ 遥测是否能解释失败
→ 入口、出口和 NAT 是否符合真实安全边界

只有这些问题的答案相互一致时,Service Mesh 才真正成为 Kubernetes 网络体系中的治理层,而不是额外增加的一组代理容器。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。