服务网格实践笔记
很多人一上来就讲服务网格的全景图;我更想先把这次卡住的点说清楚。"
第一次听到这句话的时候,我正在给一个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
这个配置能工作,但有几个问题:
- VirtualService和DestinationRule太容易搞混:新人每次都要看半天文档才能搞明白这两个东西的关系
- 配置太容易出错:一个weight写成100而不是10,瞬间就把所有流量切过去了
- 调试很痛苦:配置不生效,你不知道是CRD没生效,还是Sidecar没更新,还是路由规则写错了
我们踩过的一个坑:VirtualService的host字段一定要写成服务名(不带namespace),但DestinationRule的host字段可以写完整的服务名。文档里有写,但不仔细看就会搞混。
性能问题的真相
真正让我们头疼的是性能问题。上线一周后,我们发现了几个异常现象:
- P99延迟比之前增加了30-50ms
- CPU使用率明显上升:每个Pod的CPU使用率上升了15-20%
- 内存泄漏:有个服务隔几天就会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证书管理理论上应该自动化的,但实际上我们遇到了一些问题:
- 证书轮换失败:有时候证书到期了,但新的证书没有自动签发,服务之间就无法通信
- 证书验证失败:有个第三方服务要调用我们的服务,配置了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的指标,发现:
- 指标太多:Envoy暴露的指标有上千个,但大部分都用不上
- 指标不清晰:有些指标的名字和标签很难理解,比如
istio_requests_total和istio_request_duration_milliseconds_bucket的区别 - 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的界面很强大:
- 实时流量监控:可以看到每个请求的详细信息,包括HTTP头、响应码、延迟
- 追踪功能:可以追踪一个请求从进入到退出的完整链路
- 告警:可以配置告警,比如某个服务的错误率超过阈值就告警
而且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,然后逐步迁移服务:
- 先迁移不重要的服务:比如一些内部工具、后台任务
- 再迁移一般的服务:比如用户中心、配置中心
- 最后迁移核心服务:比如订单服务、支付服务
第二步:并行运行
我们有两个选择:
- 一下子全部迁移,风险太大
- 先并行运行Istio和Cilium,逐步切换
我们选择了第二种。但这遇到了一个问题:Istio和Cilium的Network Policy不兼容。
最终我们是这样做的:
- 先在测试环境关闭Istio的mTLS,让所有服务之间的通信都是明文的
- 然后在生产环境安装Cilium,但只用于网络策略,不用于服务网格
- 逐步把服务从Istio的VirtualService和DestinationRule切换到Cilium的CNP和CCNP
- 最后移除Istio
第三步:验证
每迁移一个服务,我们都要验证:
- 流量正常:所有API都能正常调用
- 延迟正常:P99延迟在可接受范围内
- 错误率正常:没有出现新的错误
- 监控正常: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适合什么场景?
- 复杂的微服务架构:如果你的服务超过50个,而且服务之间的调用关系很复杂,Istio的功能会很有用
- 多集群流量管理:如果你有多个Kubernetes集群,需要在这些集群之间做流量管理,Istio的多集群功能很强大
- 高级可观测性:如果你需要非常详细的追踪和监控,Istio的集成会更好
- 企业级功能:如果你需要授权策略、JWT认证、灰度发布等企业级功能,Istio更成熟
Cilium适合什么场景?
- 轻量级服务网格:如果你的服务数量在50个以内,而且只需要基本的服务网格功能,Cilium更轻量
- 性能要求高:如果你的应用对延迟和CPU使用率很敏感,Cilium的性能会更好
- 网络策略需求:如果你主要需要网络策略和安全隔离,Cilium的CNP和CCNP更简单
- 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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。