Cilium Service Mesh:用 CiliumEnvoyConfig 实现 L7 负载均衡与 URL 重写完整实践
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
本文基于 Cilium 官方文档《L7 Load Balancing and URL re-writing》展开,介绍 Cilium Service Mesh 中CiliumEnvoyConfig/CiliumClusterwideEnvoyConfigCRD 的完整实战流程:部署测试工作负载、通过 Hubble 观测流量、添加 L7 网络策略让内置 Envoy 代理介入流量路径,最终用CiliumClusterwideEnvoyConfig定义 Envoy Listener、Route 和 Cluster 资源,在两个后端服务之间做 50/50 加权负载均衡并将路径/foo重写为/。读完本文,你可以掌握在 Cilium 集群中用声明式 xDS 资源实现 L7 流量管理(负载均衡、URL 重写、重试策略、异常检测)的全部操作步骤,并理解 Envoy 资源在 Cilium Agent 中的加载方式与校验限制。
Cilium Service Mesh 定义了CiliumEnvoyConfigCRD,允许用户为嵌入 Cilium Agent 的 Envoy 组件设置配置。本文的示例场景就是:配置一个 Envoy Listener,将流量在两个后端服务echo-service-1和echo-service-2之间负载均衡。
部署测试应用
先部署测试工作负载,对应的清单文件位于 examples/kubernetes/servicemesh/envoy/test-application.yaml:
kubectl apply -f examples/kubernetes/servicemesh/envoy/test-application.yaml该清单包含以下资源:
- 两个客户端 Deployment:
client和client2(使用quay.io/cilium/alpine-curl镜像,以sleep方式常驻,供kubectl exec发起请求); - 两个 Echo 服务:
echo-service-1和echo-service-2(基于quay.io/cilium/json-mock镜像,监听 8080 端口,返回 JSON 回显;echo-service-1额外带other=echo标签并设置了与 client 的 podAffinity,两个 echo Pod 内还各带一个 sidecar CoreDNS 容器用于 DNS 测试); - 两个
NodePort类型的 Service:echo-service-1、echo-service-2,均暴露 8080/TCP。
查看这些 Pod 的信息:
kubectl get pods --show-labels -o wide输出示例(kind=client,name=client2,other=client这类标签组合决定了后续策略的选择器):
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS client-7568bc7f86-dlfqr 1/1 Running 0 100s 10.0.1.8 minikube-m02 <none> <none> kind=client,name=client,pod-template-hash=7568bc7f86 client2-8b4c4fd75-xn25d 1/1 Running 0 100s 10.0.1.24 minikube-m02 <none> <none> kind=client,name=client2,other=client,pod-template-hash=8b4c4fd75 echo-service-1-97748874-4sztx 2/2 Running 0 100s 10.0.1.86 minikube-m02 <none> <none> kind=echo,name=echo-service-1,other=echo,pod-template-hash=97748874 echo-service-2-76c584c4bf-p4z4w 2/2 Running 0 100s 10.0.1.16 minikube-m02 <none> <none> kind=echo,name=echo-service-2,pod-template-hash=76c584c4bf可以注意到:只有client2带有other=client标签——这个标签稍后会用在CiliumNetworkPolicy的endpointSelector中,使 L7 策略只作用于client2这个端点。
把client2的 Pod 名保存到环境变量中,后文命令会反复用到:
export CLIENT2=$(kubectl get pods -l name=client2 -o jsonpath='{.items[0].metadata.name}')用 Hubble 开始观测流量
先按照 Hubble 安装文档的说明在集群中启用 Hubble(见 Hubble 安装文档)。然后在另一个终端启用 Hubble 端口转发,并持续观测来自client2Pod 的流量:
kubectl -n kube-system port-forward deployment/hubble-relay 4245:4245 & hubble observe --from-pod $CLIENT2 -f此时从client2直接向两个后端服务发请求,应该都能各自获得响应(尚未经过代理):
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/ kubectl exec -it $CLIENT2 -- curl -v echo-service-2:8080/观察 Hubble 输出,你会发现这些 Pod 之间的所有流都标记为to/from-stack、to/from-overlay或to/from-endpoint——此时还没有任何流量被标记为流向/来自代理(to/from-proxy)。这一观察结果成立的前提是集群中尚不存在其他会影响该流量的 L7 策略。
再验证一个基线事实:向这两个服务的不存在 URL/foo发请求会得到 404 响应:
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/foo kubectl exec -it $CLIENT2 -- curl -v echo-service-2:8080/foo添加 L7 策略:让 Envoy 代理进入流量路径
添加 L7 策略会使 Envoy 代理被引入该流量的转发路径。需要应用两个策略文件:
kubectl apply -f examples/kubernetes/servicemesh/envoy/client-egress-l7-http.yaml kubectl apply -f examples/kubernetes/servicemesh/envoy/client-egress-only-dns.yaml第一个文件 client-egress-l7-http.yaml 是对 L7 策略的完整定义,值得逐段解读:
apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: client-egress-l7-http spec: description: "Allow GET one.one.one.one:80/ and GET <echo>:8080/ from client2" endpointSelector: matchLabels: other: client # 只选中 client2(唯一带 other=client 标签的 Pod) egress: # 允许 GET / 请求流向 echo Pod - toEndpoints: - matchLabels: k8s:kind: echo toPorts: - ports: - port: "8080" protocol: TCP rules: http: - method: "GET" path: "/" # 只允许 GET 方法 + 根路径,其他路径全部丢弃 # 仅允许对 one.one.one.one 的 GET / 请求(80 端口) - toFQDNs: - matchName: "one.one.one.one" toPorts: - ports: - port: "80" protocol: TCP rules: http: - method: "GET" path: "/"第二个文件 client-egress-only-dns.yaml 允许所有kind: client端点向 kube-system 命名空间中的 kube-dns/coredns 端点发起任意 DNS 查询(matchPattern: "*",53 端口 ANY 协议),它启用的是 DNS 层解析能力,是toFQDNs类策略能工作的前提。
添加 L7 策略后再次发请求:
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/ kubectl exec -it $CLIENT2 -- curl -v echo-service-2:8080/foo此时 Hubble 输出出现了关键变化:
- 出现了
to-proxy流向——说明流量被重定向到了本地 Envoy 代理,L7 可见性被激活; - 输出中出现了第 7 层的 HTTP 协议信息,例如
HTTP/1.1 GET http://echo-service-1:8080/。
关于 X-Forwarded-For 与 Envoy 头部清理(Sanitizing)
需要注意:Envoy 出于安全考虑会对部分 HTTP 头部做清理(sanitize),例如丢弃入站请求中由客户端伪造的X-Forwarded-For。如果后端服务需要感知真实客户端 IP,可以让 Envoy 信任前序跳数、阻止其重写这些头部。做法是通过 Helm values 设置:
envoy.xffNumTrustedHopsL7PolicyIngress:ingress L7 策略 Envoy 监听器信任的跳数;envoy.xffNumTrustedHopsL7PolicyEgress:egress L7 策略 Envoy 监听器信任的跳数。
从 install/kubernetes/values.yaml.tmpl 可见,这两个值默认都是0(不信任任何前序跳数)。语义上的区别是:
- egress 策略中,前序跳数就是源 Pod 本身;
- ingress 策略中,前序跳数可能是源 Pod、"egress policy transparent proxy"、Cilium Ingress Controller、Cilium Gateway API,或任何其他 Ingress 代理/基础设施。
是否信任前序跳数存在安全含义(客户端可伪造头部),应结合部署环境自行评估。
验证 L7 策略强制执行
由于策略只允许对/路径的 GET 请求,对其他任何 URL 的请求都会被丢弃。例如:
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/fooHubble 输出会显示该 HTTP 请求被丢弃:
Jul 7 08:40:15.076: default/client2-8b4c4fd75-6pgvl:58586 -> default/echo-service-1-97748874-n7758:8080 http-request DROPPED (HTTP/1.1 GET http://echo-service-1:8080/foo)同时 curl 端会收到403 Forbidden响应。至此验证了:L7 策略由代理路径上的 Envoy 实例强制执行,未匹配的请求在 L7 层被拦截。
添加 Envoy 负载均衡与 URL 重写
现在应用核心配置文件 envoy-traffic-management-test.yaml,它定义了一个CiliumClusterwideEnvoyConfig(集群级 CRD,与命名空间级的CiliumEnvoyConfig对应,CRD 定义见 examples/crds/v2/ciliumclusterwideenvoyconfigs.yaml):
kubectl apply -f examples/kubernetes/servicemesh/envoy/envoy-traffic-management-test.yaml该配置的完整内容如下(这也是理解 Cilium 如何声明 Envoy xDS 资源的最佳范例):
apiVersion: cilium.io/v2 kind: CiliumClusterwideEnvoyConfig metadata: name: envoy-lb-listener spec: services: - name: echo-service-1 namespace: default - name: echo-service-2 namespace: default resources: # 1) Listener:HTTP 连接管理器,指向名为 lb_route 的路由配置 - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: envoy-lb-listener filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: envoy-lb-listener rds: route_config_name: lb_route use_remote_address: true skip_xff_append: true http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router # 2) RouteConfiguration:按前缀 / 匹配,加权转发并做正则重写 - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: lb_route virtual_hosts: - name: "lb_route" domains: [ "*" ] routes: - match: prefix: "/" route: weighted_clusters: clusters: - name: "default/echo-service-1" weight: 50 - name: "default/echo-service-2" weight: 50 retry_policy: retry_on: 5xx num_retries: 3 per_try_timeout: 1s regex_rewrite: pattern: google_re2: { } regex: "^/foo.*$" substitution: "/" # 3) 两个 EDS Cluster:后端由 Cilium 自动同步 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/echo-service-1" connect_timeout: 5s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/echo-service-2" connect_timeout: 3s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2结合 CRD schema(examples/crds/v2/ciliumenvoyconfigs.yaml)可以明确spec中三个关键字段的分工:
| 字段 | 作用 |
|---|---|
services | 指定流量应被重定向到 Envoy Listener 做 L7 负载均衡的 Kubernetes Service。这些服务的后端会自动通过 EDS 同步到 Envoy;listener字段可指定重定向到哪个 Listener(缺省用resources中的第一个 Listener);ports可限定仅重定向哪些前端端口(缺省全部) |
backendServices | 指定后端自动通过 EDS 同步到 Envoy 的 Service,但不将这些服务的流量转发到 Listener——即允许自定义 Listener 做加权负载均衡,同时保留 Cilium 原生的 Service 负载均衡,二者并存 |
resources | Envoy xDS 资源列表,仅接受五种类型:envoy.config.listener.v3.Listener、envoy.config.route.v3.RouteConfiguration、envoy.config.cluster.v3.Cluster、envoy.config.endpoint.v3.ClusterLoadAssignment、envoy.extensions.transport_sockets.tls.v3.Secret |
另外两个与集群级 CRD 相关的细节来自 schema 描述:namespace字段在CiliumEnvoyConfig中默认取 CEC 自身命名空间,而在CiliumClusterwideEnvoyConfig中默认取default;nodeSelector(标准 K8s 标签选择器)可限定配置生效的节点,为 nil 时作用于所有节点。
注意(官方警告):这些 Envoy 资源完全不被 Kubernetes 校验(schema 使用
x-kubernetes-preserve-unknown-fields: true)。Envoy 资源中的任何错误只会在 Cilium Agent 观察这些 CRD 时暴露——kubectl apply会报告成功,而节点本地 Envoy 实例的解析/安装可能已经失败。目前唯一的验证手段是查看 Cilium Agent 日志中的错误与警告(Agent 还会对集群中相互冲突的 Envoy 资源打印警告日志)。此外,Cilium Ingress Controller 会在底层自行配置所需的 Envoy 资源;如果你显式创建 Envoy 资源,务必检查 Agent 日志确认没有冲突。原文见 warning.rst。
配置效果:50/50 负载均衡 + 路径重写
该配置监听发往两个echo-服务之一的流量,并:
- 在两个后端
echo-服务之间做 50/50 加权负载均衡; - 将路径
/foo重写为/(正则^/foo.*$→ 替换为/); - 附带
retry_policy:遇到 5xx 时最多重试 3 次,单试超时 1 秒; - 两个 Cluster 均配置了
outlier_detection(连续 2 次本地发起失败即将端点剔除),并采用ROUND_ROBIN负载均衡与 EDS 端点发现。
由于路径重写,之前被 403/404 挡住的/foo请求现在应该成功:
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/foo但网络策略仍然会拦截任何没有被重写成/的路径。例如这个请求会导致数据包被丢弃并返回 403:
kubectl exec -it $CLIENT2 -- curl -v echo-service-1:8080/barHubble 输出:
Jul 7 08:43:47.165: default/client2-8b4c4fd75-6pgvl:33376 -> default/echo-service-2-76c584c4bf-874dm:8080 http-request DROPPED (HTTP/1.1 GET http://echo-service-1:8080/bar)注意这里还有一个微妙但重要的行为:请求虽然发往echo-service-1,但策略判定依据的 HTTP 目标仍然是原始 URIhttp://echo-service-1:8080/bar——重写发生在 Envoy 转发层,策略评估看到的是代理解析出的 L7 请求,二者协作才实现"重写放行、未重写拦截"的效果。
观察负载均衡行为
对同一个后端服务连续发起多次请求,你会在 Hubble 输出中观察到大约一半的请求实际由另一个后端处理,例如:
Jul 7 08:45:25.807: default/client2-8b4c4fd75-6pgvl:37388 -> kube-system/coredns-64897985d-8jhhn:53 L3-L4 REDIRECTED (UDP) Jul 7 08:45:25.807: default/client2-8b4c4fd75-6pgvl:37388 -> kube-system/coredns-64897985d-8jhhn:53 to-proxy FORWARDED (UDP) Jul 7 08:45:25.807: default/client2-8b4c4fd75-6pgvl:37388 -> kube-system/coredns-64897985d-8jhhn:53 dns-request FORWARDED (DNS Query echo-service-1.default.svc.cluster.local. AAAA) Jul 7 08:45:25.807: default/client2-8b4c4fd75-6pgvl:37388 -> kube-system/coredns-64897985d-8jhhn:53 dns-request FORWARDED (DNS Query echo-service-1.default.svc.cluster.local. A) Jul 7 08:45:25.808: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 none REDIRECTED (TCP Flags: SYN) Jul 7 08:45:25.808: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 to-proxy FORWARDED (TCP Flags: SYN) Jul 7 08:45:25.808: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 to-proxy FORWARDED (TCP Flags: ACK) Jul 7 08:45:25.808: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 to-proxy FORWARDED (TCP Flags: ACK, PSH) Jul 7 08:45:25.809: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 L3-L4 REDIRECTED (TCP Flags: SYN) Jul 7 08:45:25.809: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 to-endpoint FORWARDED (TCP Flags: SYN) Jul 7 08:45:25.809: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 to-endpoint FORWARDED (TCP Flags: ACK) Jul 7 08:45:25.809: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 to-endpoint FORWARDED (TCP Flags: ACK, PSH) Jul 7 08:45:25.809: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 http-request FORWARDED (HTTP/1.1 GET http://echo-service-1:8080/) Jul 7 08:45:25.811: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 to-proxy FORWARDED (TCP Flags: ACK, FIN) Jul 7 08:45:25.811: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-1:8080 to-proxy FORWARDED (TCP Flags: ACK) Jul 7 08:45:30.811: default/client2-8b4c4fd75-6pgvl:57942 -> default/echo-service-2-76c584c4bf-874dm:8080 to-endpoint FORWARDED (TCP Flags: ACK, FIN)这段输出清晰地还原了整条数据路径:客户端 SYN 被REDIRECTED到本地代理(to-proxy),Envoy 通过weighted_clusters把连接交给echo-service-2的端点(to-endpoint),L7 请求http-request FORWARDED中显示的 URI 仍为重写前的http://echo-service-1:8080/。这就是"客户端只看到一个 Service,实际流量被 Envoy 按权重分摊到两个 Service"的完整证据链。
小结:这套机制的关键点
- 两层职责分离:L7 网络策略(
CiliumNetworkPolicy的rules.http)负责"允许/丢弃",CiliumEnvoyConfig负责"如何转发"(加权、重写、重试、剔除异常端点)——二者叠加生效,路径重写不能绕过策略评估。 - 声明式 xDS:Cilium 把 Envoy 的 Listener/Route/Cluster 作为 CRD 字段直接暴露(
spec.resources),后端端点通过 EDS 自动同步,无需手写ClusterLoadAssignment,也无需独立部署 Envoy 控制面。 - 无 schema 校验的现实约束:
resources字段不经过 K8s 校验,配置正确性最终靠 Cilium Agent 日志确认,这是运维该特性时最重要的检查动作。 - 可观测性闭环:Hubble 的
to-proxy/http-request事件让"代理是否介入、L7 内容是什么、策略在哪一层丢弃了请求"全部可视,是调试 L7 流量管理问题的第一入口。
相关文件索引:
- 文档源文件:envoy-traffic-management.rst、warning.rst
- 示例清单:test-application.yaml、client-egress-l7-http.yaml、client-egress-only-dns.yaml、envoy-traffic-management-test.yaml
- CRD 定义:ciliumenvoyconfigs.yaml、ciliumclusterwideenvoyconfigs.yaml
- 相关主题(同一 servicemesh 目录下的延伸阅读):Envoy 负载均衡、L7 流量管理、流量染色/灰度
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考