使用 MetalLB 和 Envoy Gateway 实现多个域名对应多个公网 IP
· 1538 words · ~ 8 min read
Last modified:
本文介绍两种在 Kubernetes 中实现"多个域名对应多个公网 IP"的方案,均基于 MetalLB 和 Envoy Gateway,适用于裸金属或私有云环境。
目标
实现效果:
|
|
每个 IP 对外暴露 80/443。多个域名通过 DNS 多 A 记录同时指向这组 IP,客户端选择其中一个建连。
前置条件
- 集群已安装 MetalLB,并配置包含目标 IP 的
IPAddressPool。 - 集群已安装 Envoy Gateway。
- DNS 支持为同一域名配置多条 A 记录。
- 目标 IP 在集群外部网络可达。
方案一:多 Gateway,每 IP 一个 Envoy 实例
原理
每个公网 IP 对应一组独立资源:EnvoyProxy + Gateway + Envoy Gateway 自动生成的 LoadBalancer Service。多个域名作为 listener 聚合到每个 Gateway 上,每条 HTTPRoute 通过多个 parentRefs 同时绑定所有 Gateway。
架构
flowchart TD
Client["Client"]
DNSA["DNS: *.jimyagtest1.com"]
DNSB["DNS: *.jimyagtest2.com"]
IP1["192.0.2.10"]
IP2["192.0.2.11"]
IP3["192.0.2.12"]
SVC1["LoadBalancer Service\n192.0.2.10"]
SVC2["LoadBalancer Service\n192.0.2.11"]
SVC3["LoadBalancer Service\n192.0.2.12"]
GW1["Gateway gw-192-0-2-10\nEnvoy 实例 A"]
GW2["Gateway gw-192-0-2-11\nEnvoy 实例 B"]
GW3["Gateway gw-192-0-2-12\nEnvoy 实例 C"]
RouteA["HTTPRoute jimyagtest1-route\nparentRefs: GW1, GW2, GW3"]
RouteB["HTTPRoute jimyagtest2-route\nparentRefs: GW1, GW2, GW3"]
BackendA["Backend Service A"]
BackendB["Backend Service B"]
Client --> DNSA & DNSB
DNSA & DNSB --> IP1 & IP2 & IP3
IP1 --> SVC1 --> GW1
IP2 --> SVC2 --> GW2
IP3 --> SVC3 --> GW3
GW1 & GW2 & GW3 --> RouteA --> BackendA
GW1 & GW2 & GW3 --> RouteB --> BackendB
资源关系
每个 IP 一组(以三个 IP 为例):
| 资源 | 名称 | 说明 |
|---|---|---|
| EnvoyProxy | gw-192-0-2-10 | 控制 Envoy Service 的 IP 和 annotation |
| Gateway | gw-192-0-2-10 | 引用对应 EnvoyProxy |
| Service | (自动生成) | LoadBalancer IP = 192.0.2.10 |
业务路由(所有 IP 共用):
| 资源 | parentRefs |
|---|---|
| HTTPRoute jimyagtest1-route | gw-192-0-2-10, gw-192-0-2-11, gw-192-0-2-12 |
| HTTPRoute jimyagtest2-route | gw-192-0-2-10, gw-192-0-2-11, gw-192-0-2-12 |
配置示例
EnvoyProxy(每个 IP 一个,以 192.0.2.10 为例):
|
|
说明:新版 MetalLB 使用 metallb.io/* annotation;旧版集群需改为 metallb.universe.tf/*。
Gateway(每个 IP 一个):
|
|
同理为 192.0.2.11、192.0.2.12 各创建一组。参考:Envoy Gateway - Gateway Address。
HTTPRoute(每个域名一条,绑定所有 Gateway):
|
|
jimyagtest2-route 结构相同,sectionName 改为 https-jimyagtest2。
运维负担:parentRefs 批量更新
这是方案一的主要代价。每条 HTTPRoute 显式列出所有 Gateway,新增或删除一个 IP 时必须同步 patch 所有相关路由:
| 操作 | 步骤 | patch 次数 |
|---|---|---|
| 新增 IP | 创建 EnvoyProxy + Gateway → patch 所有 HTTPRoute 加一条 parentRef | = HTTPRoute 数量 |
| 删除 IP | patch 所有 HTTPRoute 删一条 parentRef → 删 Gateway + EnvoyProxy | = HTTPRoute 数量 |
推荐将这组操作封装在管理层(控制器或 API),通过 label 批量查找需要更新的 HTTPRoute,避免人工逐条维护。
更新流程
新增 IP:
sequenceDiagram
participant Mgmt as 管理层
participant K8s as Kubernetes API
participant EG as Envoy Gateway
participant ML as MetalLB
participant DNS as DNS
Mgmt->>K8s: Create EnvoyProxy for new IP
Mgmt->>K8s: Create Gateway for new IP
EG->>K8s: Reconcile Envoy data plane Service
ML->>K8s: Assign LoadBalancer IP
Mgmt->>K8s: List HTTPRoutes by label, patch all parentRefs
Mgmt->>DNS: Add A record
删除 IP:
- 从 DNS 删除该 IP 的 A 记录。
- 等待至少一个 DNS TTL。
- 管理层通过 label 查出所有 HTTPRoute,批量删除对应
parentRef。 - 删除对应 Gateway。
- 删除对应 EnvoyProxy。
方案一小结
优点:资源全部由 Envoy Gateway 控制器管辖,Gateway.status 准确反映外部 IP,行为可预期。
缺点:每个 IP 独立一套 Envoy 实例,CPU/内存冗余;新增/删除 IP 需要 O(n) 次 HTTPRoute patch。
方案二:单 Gateway + 手动 LoadBalancer Service
原理
只创建一个 Gateway 和一套 Envoy 数据面。将 EnvoyProxy 的 envoyService.type 设为 ClusterIP,Envoy Gateway 控制器只生成集群内部可达的 ClusterIP Service,不分配外部 IP。
为每个公网 IP 手动创建一个 type: LoadBalancer 的 Service,用 MetalLB annotation 绑定指定 IP,selector 指向 Envoy pods。所有外部流量经各自的 LoadBalancer Service 打到同一套 Envoy 数据面。
HTTPRoute 只引用这一个 Gateway,增删 IP 只操作 LoadBalancer Service,不涉及 HTTPRoute。
架构
flowchart TD
Client["Client"]
DNSA["DNS: *.jimyagtest1.com"]
DNSB["DNS: *.jimyagtest2.com"]
IP1["192.0.2.10"]
IP2["192.0.2.11"]
IP3["192.0.2.12"]
SVC1["LoadBalancer Service\n192.0.2.10(手动)"]
SVC2["LoadBalancer Service\n192.0.2.11(手动)"]
SVC3["LoadBalancer Service\n192.0.2.12(手动)"]
GW["Gateway my-gateway\n单套 Envoy 实例"]
RouteA["HTTPRoute jimyagtest1-route\nparentRefs: my-gateway"]
RouteB["HTTPRoute jimyagtest2-route\nparentRefs: my-gateway"]
BackendA["Backend Service A"]
BackendB["Backend Service B"]
Client --> DNSA & DNSB
DNSA & DNSB --> IP1 & IP2 & IP3
IP1 --> SVC1
IP2 --> SVC2
IP3 --> SVC3
SVC1 & SVC2 & SVC3 --> GW
GW --> RouteA --> BackendA
GW --> RouteB --> BackendB
资源关系
控制器管理:
| 资源 | 名称 | 说明 |
|---|---|---|
| EnvoyProxy | my-proxy | envoyService.type: ClusterIP |
| Gateway | my-gateway | 引用 my-proxy |
| Service | (自动生成) | ClusterIP,仅集群内可达 |
| Deployment | envoy-my-gateway | Envoy 数据面 |
手动管理(每个 IP 一个):
| 资源 | 名称 | 说明 |
|---|---|---|
| Service | envoy-ip-192-0-2-10 | type: LoadBalancer,selector → Envoy pods |
| Service | envoy-ip-192-0-2-11 | 同上 |
| Service | envoy-ip-192-0-2-12 | 同上 |
业务路由(只引用一个 Gateway):
| 资源 | parentRefs |
|---|---|
| HTTPRoute jimyagtest1-route | my-gateway |
| HTTPRoute jimyagtest2-route | my-gateway |
Envoy Pod 的 Selector Labels
Envoy Gateway 生成的 Envoy pods 上带有以下标签,手动 Service 的 selector 使用这两个:
|
|
可以通过查看控制器生成的 ClusterIP Service 确认实际 selector 和 targetPort:
|
|
配置示例
EnvoyProxy(关闭控制器自动分配外部 IP):
|
|
Gateway:
|
|
手动 LoadBalancer Service(每个 IP 一个,targetPort 需与实际 Envoy 容器端口对齐):
|
|
为 192.0.2.11、192.0.2.12 各创建一个相同结构的 Service。
HTTPRoute(只引用一个 Gateway):
|
|
潜在问题
升级时静默失效(最高风险)
Envoy Gateway 升级后若变更了 pod label scheme 或容器端口,手动 Service 的 selector 或 targetPort 不再匹配,流量静默丢弃,没有控制器告警。每次升级 Envoy Gateway 后需要人工验证 selector 和 targetPort 是否仍然正确。
新增 listener 不自动同步
Gateway 新增 listener port 后,手动 Service 的 ports 不会自动跟进,需要同步 patch 所有 LoadBalancer Service。漏掉则该端口流量不通,且不报错。
Gateway status 不反映外部 IP
Gateway.status.addresses 只显示 ClusterIP,不包含手动 Service 的外部 IP。依赖 Gateway status 的工具(external-dns、告警)会读到错误结论。
生命周期解绑
删除 Gateway 时,控制器自动清理 ClusterIP Service 和 Envoy Deployment,但手动创建的 LoadBalancer Service 不受控制器管理,会孤立留在集群中,需要手动清理。
externalTrafficPolicy 影响源 IP
使用 Cluster 模式时 Envoy 看到的是 SNAT 后的 IP。要保留真实客户端 IP 需要改用 Local,但 Local 模式只有运行 Envoy pod 的节点接收流量,需要确认节点分布和故障切换行为。
更新流程
新增 IP:
sequenceDiagram
participant Mgmt as 管理层
participant K8s as Kubernetes API
participant ML as MetalLB
participant DNS as DNS
Mgmt->>K8s: Create LoadBalancer Service for new IP
ML->>K8s: Assign LoadBalancer IP
Mgmt->>DNS: Add A record
删除 IP:
- 从 DNS 删除该 IP 的 A 记录。
- 等待至少一个 DNS TTL。
- 删除对应的 LoadBalancer Service。
方案二小结
优点:一套 Envoy 实例,没有冗余部署;HTTPRoute 只引用一个 Gateway,增删 IP 不涉及 HTTPRoute。
缺点:手动 Service 游离于控制器管辖之外,升级时存在静默失效风险,Gateway status 不准确,生命周期需要自行维护。
方案对比
| 维度 | 方案一(多 Gateway) | 方案二(单 Gateway) |
|---|---|---|
| Envoy 实例数 | N 个(每 IP 一个) | 1 个 |
| HTTPRoute parentRefs | N 条(引用所有 Gateway) | 1 条 |
| 增删 IP 操作范围 | EnvoyProxy + Gateway + patch 所有 HTTPRoute | 1 个 LoadBalancer Service |
| 新增 listener 影响 | 只改 Gateway 定义 | 需同步 patch 所有手动 Service |
| 控制器可见性 | 全部受控 | 手动 Service 不受控 |
| Gateway status 准确性 | 准确 | 仅显示 ClusterIP |
| 升级风险 | 低 | 中(selector/targetPort 可能变) |
| 适用场景 | 优先考虑,稳定可预期 | 资源受限或要求单 Envoy 实例时 |
社区现状
envoyproxy/gateway#7892 提议在 KubernetesProxyProvider 里增加 envoyServices 字段,允许单个 Gateway 原生对应多个 LoadBalancer IP,或由控制器在 spec.addresses 包含多个 IP 时自动拆分创建 Service。如果该特性落地,方案二的手动部分可以被控制器接管,两个方案的缺点都会消失。目前该 issue 状态为 open + stale,尚未开发。
关键点补充
为什么不是一个 Service 配多个 IPv4
MetalLB 的 metallb.io/loadBalancerIPs 支持逗号分隔,但主要用于 dual-stack 场景。“一个域名负载到多个 IPv4"的正确模型是多个 Service 各持一个 IP,再通过 DNS 多 A 记录分散。参考:MetalLB Usage - IPv6 and dual stack services。
IP sharing
如果需要让其他 LoadBalancer Service 与 Envoy Service 共享同一个 IP,需满足 MetalLB IP sharing 条件:sharing key 相同、端口不冲突、都使用 externalTrafficPolicy: Cluster 或 selector 完全相同。参考:MetalLB Usage - IP address sharing。
验证命令
查看 LoadBalancer Service 是否拿到指定 IP:
|
|
查看 Gateway 是否 Accepted:
|
|
查看 HTTPRoute 状态:
|
|
指定 IP 测试连通性:
|
|
查看 DNS 返回:
|
|