服务网格实践笔记

很多人一上来就讲服务网格的全景图;我更想先把这次卡住的点说清楚。"

第一次听到这句话的时候,我正在给一个25个服务的微服务集群装Istio。

起因:为什么需要服务网格

我们的场景是:一个电商后端,25个微服务,部署在Kubernetes上,用Java写的服务大约15个,Go写的8个,还有2个Node.js的服务。问题很典型:

  • 服务之间的调用链路越来越乱,出问题根本不知道谁在调用谁
  • 要加个灰度发布,得改代码或者搞一套复杂的路由配置
  • 之前的一个安全审计报告说我们的服务间通信没有mTLS加密
  • 想看每个接口的QPS、延迟、错误率,得在每个服务里埋点

那时候我们用的是Spring Cloud,但不是所有服务都是Spring Cloud,几个Go服务就接不进这套体系。团队决定上服务网格。

Istio:看起来很美,用起来很痛

选Istio是因为它是最火的服务网格,文档多,案例多。2023年底,我们用1.19版本开始折腾。

安装和基础配置

安装Istio挺简单的:

istioctl install --set profile=default -y

然后在命名空间里开启自动注入:

kubectl label namespace default istio-injection=enabled

重启服务,Sidecar注入就完成了。看起来一切正常,直到我们开始搞流量管理。

流量管理的噩梦

先从一个简单的场景开始:给订单服务做灰度发布。10%的流量走新版本,90%走旧版本。

配置文件是这样的:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
  - order-service
  http:
  - match:
    - headers:
        x-user-id:
          regex: "^[0-9]{5,}$"
    route:
    - destination:
        host: order-service
        subset: v2
      weight: 100
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 90
    - destination:
        host: order-service
        subset: v2
      weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

这个配置能工作,但有几个问题:

  1. VirtualService和DestinationRule太容易搞混:新人每次都要看半天文档才能搞明白这两个东西的关系
  2. 配置太容易出错:一个weight写成100而不是10,瞬间就把所有流量切过去了
  3. 调试很痛苦:配置不生效,你不知道是CRD没生效,还是Sidecar没更新,还是路由规则写错了

我们踩过的一个坑:VirtualService的host字段一定要写成服务名(不带namespace),但DestinationRule的host字段可以写完整的服务名。文档里有写,但不仔细看就会搞混。

性能问题的真相

真正让我们头疼的是性能问题。上线一周后,我们发现了几个异常现象:

  1. P99延迟比之前增加了30-50ms
  2. CPU使用率明显上升:每个Pod的CPU使用率上升了15-20%
  3. 内存泄漏:有个服务隔几天就会OOM重启

我们花了两周时间排查,最后发现是Sidecar的问题。Envoy的配置太复杂了,我们25个服务,每个服务平均要和8-10个其他服务通信,每个通信都要配置路由、重试、熔断、超时。

我们尝试过优化配置:

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: order-service-sidecar
  namespace: default
spec:
  egress:
  - hosts:
    - "default/*"
    - "kube-system/*"
    - "istio-system/*"
    istioConfig:
      proxyStatsMatcher:
        inclusionRegexps:
        - ".*_cx_.*"
        - ".*_cluster_.*"
        inclusionPrefixes:
        - "listener."

这个配置限制了Sidecar只处理特定命名空间的流量,确实减少了资源消耗。但问题是,你得为每个服务都写一遍这样的配置,维护成本很高。

证书管理的噩梦

Istio的mTLS证书管理理论上应该自动化的,但实际上我们遇到了一些问题:

  1. 证书轮换失败:有时候证书到期了,但新的证书没有自动签发,服务之间就无法通信
  2. 证书验证失败:有个第三方服务要调用我们的服务,配置了TLS,但总是握手失败

我们最终发现是Istiod的证书签发逻辑和我们的安全策略有冲突。改了几个配置才解决问题:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: default
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: jwt-example
  namespace: default
spec:
  jwtRules:
  - issuer: "https://auth.example.com"
    jwksUri: "https://auth.example.com/.well-known/jwks.json"
    audiences:
    - "order-service"

这个配置确实安全,但调试起来很痛苦。每次验证失败,你都很难知道是证书问题、JWT问题,还是其他问题。

最后的稻草

压死骆驼的最后一根稻草是可观测性。我们想用Prometheus监控Istio的指标,发现:

  1. 指标太多:Envoy暴露的指标有上千个,但大部分都用不上
  2. 指标不清晰:有些指标的名字和标签很难理解,比如istio_requests_totalistio_request_duration_milliseconds_bucket的区别
  3. Jaeger追踪太重:开启全链路追踪后,Jaeger的存储很快就爆了

我们最终只能关闭了一些指标,只保留最关键的几个:

apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-custom-config
  namespace: istio-system
data:
  mesh: |-
    enablePrometheusMerge: true
    defaultConfig:
      proxyStatsMatcher:
        inclusionRegexps:
        - ".*_cx_.*"
        - ".*_cluster_.*"
        - ".*_listener_.*"
        - ".*_upstream_rq_.*"
        inclusionPrefixes:
        - "listener."

但这样一来,很多有用的指标就看不到了。

转向Cilium:一次意外的尝试

2024年中,我们公司的一个新项目用了Cilium。我们的一个同事负责那个项目,回来跟我说:“Cilium比Istio轻量很多,而且配置简单得多。”

我一开始是怀疑的。Cilium不是主打eBPF的网络插件吗?它也能做服务网格?

结果发现,Cilium 1.12版本之后确实有了完整的服务网格能力。我们决定在一个小集群里试用Cilium。

安装Cilium

安装Cilium比Istio简单:

helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.15.0 \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set serviceMonitor.enabled=true \
  --set operator.replicas=1

我们使用的是Kubernetes 1.28,这个配置能直接工作。Cilium会自动替换kube-proxy,用eBPF实现网络路由和服务发现。

Cilium的流量管理

Cilium的流量管理用的是Cilium Network Policy (CNP) 和 CiliumClusterwide Network Policy (CCNP)。配置比Istio的VirtualService和DestinationRule简单很多:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: order-service-policy
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: order-service
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: user-service
    toPorts:
    - ports:
      - port: 8080
        protocol: TCP
      rules:
        http:
          - method: GET
            path: /api/v1/orders/*
  - fromEndpoints:
    - matchLabels:
        app: payment-service
    toPorts:
    - ports:
      - port: 8080
        protocol: TCP
      rules:
        http:
          - method: POST
            path: /api/v1/orders

这个配置说的是:

  • 只有user-service可以GET订单列表
  • 只有payment-service可以POST创建订单

比Istio的VirtualService简单多了,而且很直观。

Cilium的灰度发布

Cilium的灰度发布用的是Ingress和Service的组合,而不是VirtualService。虽然看起来不如Istio那么"服务网格原生",但实际用起来更简单:

apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service-v2
spec:
  selector:
    app: order-service
    version: v2
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-service-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: order.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: order-service-v2
            port:
              number: 80

这个配置用的是Nginx Ingress的Canary功能,10%的流量走v2版本。虽然不如Istio的配置那么"云原生",但很直观,而且Ingress的配置大家都懂。

Cilium的安全性

Cilium的mTLS配置比Istio简单很多:

apiVersion: cilium.io/v2
kind: CiliumIdentity
metadata:
  name: order-service
  namespace: default
spec:
  securityIdentity:
    type: ServiceIdentity
    labels:
      - app=order-service
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: mtls-policy
spec:
  endpointSelector:
    matchLabels:
      k8s:io.kubernetes.pod.namespace: default
  egress:
  - toEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: default
    authentication:
      mode: mTLS
  ingress:
  - fromEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: default
    authentication:
      mode: mTLS

这个配置说的是:在default命名空间里,所有服务之间的通信都要用mTLS。

我们实际用的时候,只对关键服务启用mTLS,因为完全启用mTLS会增加延迟:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: critical-services-mtls
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      security-level: critical
  egress:
  - toEndpoints:
    - matchLabels:
        security-level: critical
    authentication:
      mode: mTLS
  ingress:
  - fromEndpoints:
    - matchLabels:
        security-level: critical
    authentication:
      mode: mTLS

这个配置只对标记为security-level: critical的服务启用mTLS。

Cilium的可观测性

Cilium的可观测性是我们的最爱。它内置了Hubble,一个专门为Cilium设计的可观测性工具:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: order-service-policy
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: order-service
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: user-service
    toPorts:
    - ports:
      - port: 8080
        protocol: TCP
      rules:
        http:
          - method: GET
            path: /api/v1/orders/*
    # 记录流量的详细信息
    http:
      - headers:
        - name: X-Request-ID
          type: Request

配置很简单,但Hubble的界面很强大:

  1. 实时流量监控:可以看到每个请求的详细信息,包括HTTP头、响应码、延迟
  2. 追踪功能:可以追踪一个请求从进入到退出的完整链路
  3. 告警:可以配置告警,比如某个服务的错误率超过阈值就告警

而且Hubble的存储成本比Jaeger低很多,因为它只存储元数据,不存储完整的payload。

性能对比:真实的测试结果

2024年底,我们在一个测试环境里做了性能对比。测试环境:3个node,每个node 4核8G,Kubernetes 1.28。

CPU使用率

  • 不使用服务网格:每个Pod平均CPU使用率 5-8%
  • Istio:每个Pod平均CPU使用率 15-25%,增加了约10-17%
  • Cilium:每个Pod平均CPU使用率 7-12%,增加了约2-5%

P99延迟

  • 不使用服务网格:P99延迟 15-25ms
  • Istio:P99延迟 30-50ms,增加了15-25ms
  • Cilium:P99延迟 18-32ms,增加了3-7ms

内存使用

  • 不使用服务网格:每个Pod平均内存使用 200-300MB
  • Istio:每个Pod平均内存使用 400-600MB,增加了200-300MB
  • Cilium:每个Pod平均内存使用 250-400MB,增加了50-100MB

这些数据不是说Istio不好,而是说Istio太"重"了。如果你需要Istio的高级功能,比如复杂的路由规则、多集群流量管理、高级可观测性,那Istio是值得的。但如果你只需要基本的服务网格功能,Cilium更轻量。

迁移过程:我们是怎么从Istio搬到Cilium的

2025年初,我们决定把生产环境从Istio迁移到Cilium。这个过程花了大约一个月。

第一步:准备

我们先在测试环境搭建了Cilium,然后逐步迁移服务:

  1. 先迁移不重要的服务:比如一些内部工具、后台任务
  2. 再迁移一般的服务:比如用户中心、配置中心
  3. 最后迁移核心服务:比如订单服务、支付服务

第二步:并行运行

我们有两个选择:

  1. 一下子全部迁移,风险太大
  2. 先并行运行Istio和Cilium,逐步切换

我们选择了第二种。但这遇到了一个问题:Istio和Cilium的Network Policy不兼容。

最终我们是这样做的:

  1. 先在测试环境关闭Istio的mTLS,让所有服务之间的通信都是明文的
  2. 然后在生产环境安装Cilium,但只用于网络策略,不用于服务网格
  3. 逐步把服务从Istio的VirtualService和DestinationRule切换到Cilium的CNP和CCNP
  4. 最后移除Istio

第三步:验证

每迁移一个服务,我们都要验证:

  1. 流量正常:所有API都能正常调用
  2. 延迟正常:P99延迟在可接受范围内
  3. 错误率正常:没有出现新的错误
  4. 监控正常:Prometheus和Grafana的数据正常

我们踩过的一个坑:迁移一个服务后,发现它的延迟突然增加了。排查了很久,发现是Cilium的网络策略写错了,流量绕了很多弯。修正后就好了。

第四步:清理

所有服务都迁移完成后,我们开始清理Istio:

istioctl uninstall -y --purge
kubectl delete namespace istio-system

这个命令会删除Istio的所有资源,包括CRD、ConfigMap、Service等。但我们还是手动检查了一遍,确保没有遗留。

后续优化:Cilium的高级功能

迁移完成后,我们开始探索Cilium的高级功能。

eBPF的性能优化

Cilium使用eBPF,这意味着它可以在内核层面处理网络流量,比传统的iptables和ipvs快很多。我们开启了一些eBPF优化:

apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  # 启用eBPF的socket-level负载均衡
  socket-lb: "true"
  # 启用eBPF的host-routing
  host-routing: "true"
  # 启用eBPF的ipv4和ipv6
  enable-ipv4: "true"
  enable-ipv6: "false"

这些配置进一步降低了延迟,P99延迟又降低了2-3ms。

Cilium的Bandwidth Manager

Cilium有一个Bandwidth Manager,可以限制每个Pod的带宽:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: bandwidth-limit
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: order-service
  egress:
  - toEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: default
    egress:
      - bandwidthLimit:
          egress: 1G
          ingress: 1G

这个配置限制了order-service的egress和ingress带宽都是1G。这对一些容易跑满带宽的服务很有用。

Cilium的Observability增强

我们用Hubble的增强功能做了一些监控:

apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: policy-with-observability
spec:
  endpointSelector:
    matchLabels:
      app: order-service
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: user-service
    toPorts:
    - ports:
      - port: 8080
        protocol: TCP
      rules:
        http:
          - method: GET
            path: /api/v1/orders/*
  # 启用详细日志
  description: "Allow user-service to call order-service"
  # 启用度量
  metrics:
    - type: L7
      name: requests_total
      labels:
        - source
        - destination

这个配置会记录详细的HTTP请求信息,包括请求路径、方法、响应码等,并生成Prometheus指标。

一些判断和经验

折腾了一年多的服务网格,我有一些判断:

Istio适合什么场景?

  1. 复杂的微服务架构:如果你的服务超过50个,而且服务之间的调用关系很复杂,Istio的功能会很有用
  2. 多集群流量管理:如果你有多个Kubernetes集群,需要在这些集群之间做流量管理,Istio的多集群功能很强大
  3. 高级可观测性:如果你需要非常详细的追踪和监控,Istio的集成会更好
  4. 企业级功能:如果你需要授权策略、JWT认证、灰度发布等企业级功能,Istio更成熟

Cilium适合什么场景?

  1. 轻量级服务网格:如果你的服务数量在50个以内,而且只需要基本的服务网格功能,Cilium更轻量
  2. 性能要求高:如果你的应用对延迟和CPU使用率很敏感,Cilium的性能会更好
  3. 网络策略需求:如果你主要需要网络策略和安全隔离,Cilium的CNP和CCNP更简单
  4. eBPF经验:如果你的团队有eBPF经验,Cilium的深度优化会更有价值

我们的决策

最后,我们的决策是:

  • 主集群:用Cilium,因为我们的服务数量不大,而且对性能敏感
  • 多集群场景:用Istio,因为我们有跨集群的流量管理需求
  • 特殊服务:比如一些需要高级功能的服务,用Istio的Sidecar模式

这不是一个完美的解决方案,但它是适合我们的解决方案。

还有一些坑

DNS问题

迁移到Cilium后,我们遇到过一个DNS问题。有些服务无法解析其他服务的DNS。排查后发现是Cilium的DNS Proxy配置问题:

apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  # 启用DNS Proxy
  enable-ipv4: "true"
  enable-ipv6: "false"
  # DNS Proxy的配置
  dns-proxy-response-delay: "10ms"
  # 排除某些域名的DNS代理
  dns-proxy-exclusions: "example.com,example.org"

这个配置解决了一部分问题,但最后发现是CoreDNS的配置问题,改了CoreDNS的配置才彻底解决。

网络策略冲突

Cilium的CNP和Kubernetes的NetworkPolicy会冲突。我们有一次写了一个Kubernetes NetworkPolicy,然后又写了一个CNP,结果两个都不生效。

最好的做法是:要么全部用Kubernetes NetworkPolicy,要么全部用Cilium的CNP和CCNP。不要混用。

Hubble的存储问题

Hubble的默认存储配置不太适合我们的场景,很快就满了。我们改了Hubble的配置:

apiVersion: v1
kind: ConfigMap
metadata:
  name: hubble-config
  namespace: kube-system
data:
  # 只存储最近7天的数据
  hubble-storage-retention-days: "7"
  # 只存储关键信息,不存储完整的payload
  hubble-storage-compression: "true"

这个配置减少了存储压力,但有些历史数据就看不到了。

最后的话

服务网格不是万能的,但它确实能解决一些问题。选择哪种服务网格,要看你的具体需求:

  • 如果你的服务数量大、调用关系复杂,Istio的功能会更合适
  • 如果你的服务数量不大、对性能敏感,Cilium会更轻量
  • 如果你的团队有eBPF经验,Cilium的深度优化会更有价值
  • 如果你需要多集群流量管理,Istio的解决方案更成熟

没有最好的服务网格,只有最适合的服务网格。我们的经验是:先明确你的需求,然后选一个能满足你需求的方案,不要盲目追求"最先进"或"最流行"。

我们的集群现在运行得很稳定,延迟降到了迁移前的水平,CPU使用率也下来了。这次迁移花了差不多一个月,但值得。

有时候,技术选型不是选最好的,而是选最合适的。

版权声明: 本文首发于 指尖魔法屋-服务网格实践笔记https://blog.thinkmoon.cn/post/122-service-mesh-istio-to-cilium/) 转载或引用必须申明原指尖魔法屋来源及源地址!