负载均衡与 Ingress 流量入口¶
云上点一下就能拿到的 LoadBalancer,在裸金属集群里默认永远是 Pending。本章先补齐四层:用 MetalLB 分配并通告 VIP;再补齐七层:用 ingress-nginx 做域名与路径路由,最后把一条流量按权重和请求头切给新版本。
你将学到¶
- 南北向流量路径、MetalLB 分配与通告模型,以及
IPAddressPool、L2Advertisement、BGPPeer的用法。 - 用 header、header-value、header-pattern、weight、cookie 五种方式做金丝雀发布。
为什么需要它¶
ClusterIP 只在集群内可达,NodePort 把服务暴露在 30000–32767 的高端口,既不好记也没法做健康检查。type: LoadBalancer 本是标准答案,但 Kubernetes 自己并不实现负载均衡器,它把这件事委托给云厂商的 cloud-controller-manager。裸金属集群没有这个组件,EXTERNAL-IP 就一直是 <pending>。
MetalLB 与 OpenELB 监听 LoadBalancer Service,从地址池分配 IP,再通过 ARP/NDP 或 BGP 通告出去。VIP 解决四层可达性,按域名、路径分流和 TLS 终止交给 Ingress Controller;两者串起来才是完整入口。
| 方案 | 工作层次 | 额外依赖 | 典型问题 |
|---|---|---|---|
| NodePort | L4,每节点同一端口 | 无 | 端口不可控,源 IP 易丢失 |
| MetalLB L2 | L4,VIP 漂移 | 与节点同网段的地址 | 单节点承载全部流量 |
| MetalLB BGP | L4,ECMP 多节点 | 支持 BGP 的交换机 | 需要网络团队配合 |
| ingress-nginx | L7,域名/路径/TLS | 一个 LoadBalancer 或 NodePort | 配置错误表现为 404/503 |
flowchart LR
C["集群外客户端"] --> D["DNS 解析到 VIP"]
D --> V["MetalLB 通告的 172.29.7.51"]
V --> N["持有 VIP 的节点或 ECMP 多节点"]
N --> K["kube-proxy ipvs 或 Cilium eBPF"]
K --> S["Service ClusterIP"]
S --> I["ingress-nginx Pod"]
I --> P["业务 Pod"]
图:南北向流量在四层与七层各经过一次转发,MetalLB 把包送到节点,Ingress 选后端。
核心概念¶
MetalLB 的两段式实现¶
MetalLB 把问题拆成「分配」和「通告」。分配由 IPAddressPool 描述地址范围,autoAssign: true 允许控制器自动取地址;通告由 L2Advertisement 或 BGPAdvertisement 描述,决定地址如何被外部看见。只建池不建通告,Service 能拿到 IP,但外部访问不通。
L2 模式通过 ARP/NDP 宣告 VIP,同一时刻只有一个节点响应,流量全落到该节点,再由 kube-proxy 转发。它零网络改造,但没有真正的负载分担,节点故障要靠成员选举重新宣告,切换期间会丢包。interfaces 限定从哪块网卡发包,多网卡机器必须写对。
BGP 模式把 VIP 以 /32 路由通告给上游交换机,多节点同时宣告形成 ECMP,流量真正分散。代价是需要交换机配合与 ASN 规划:BGPPeer 定义邻居与 myASN/peerASN,BGPAdvertisement 定义哪些池参与通告。
ipvs 模式必须开 StrictARP
kube-proxy 使用 ipvs 时,VIP 由 ipvs 接管,节点不会替 VIP 回答 ARP,MetalLB 的 L2 宣告就失效。仓库的做法是改 kube-proxy 的 ConfigMap,把 ipvs.strictARP 从 false 改成 true,再重启 kube-proxy。iptables 模式不需要这一步。
OpenELB 的定位¶
OpenELB 来自 KubeSphere 生态,能力与 MetalLB 高度重叠:Eip 资源描述地址池,支持 bgp、layer2、vip 三种协议,默认 BGP。仓库示例用 eip.openelb.kubesphere.io/is-default-eip: "true" 把某个 Eip 设为默认池,并用 interface 指定 L2 模式的网卡。
如果 webhook 因版本不匹配报 failed calling webhook,README 的做法是删掉 openelb-admission 的两个 WebhookConfiguration;这只是绕开校验,生产应优先修 CA bundle 与 Service 端口。
ingress-nginx 与 Ingress v1¶
ingress-nginx 由一个监听 80/443 的控制器 Deployment(通常以 LoadBalancer Service 暴露,正好接 MetalLB)和一组 admission webhook 组成。它默认只处理 ingressClassName: nginx 的对象;仓库 canary 清单里的 kubernetes.io/ingress.class 注解从 v1.18 起废弃,v1.31 上应改用 ingressClassName,照抄旧清单可能「创建成功却没有路由」。
Ingress v1 的后端结构也变了:serviceName/servicePort 被 service.name/service.port.number 取代,pathType 成为必填。Prefix 按路径段匹配,Exact 要求完全相等。TLS 通过 spec.tls 引用 kubernetes.io/tls 类型的 Secret,其中必须有 tls.crt 与 tls.key;实验里的 demoapp-tls 需提前创建,或交给 cert-manager 生成。
金丝雀发布的五种切流方式¶
创建一个与主 Ingress 同 host、同 path 的 canary Ingress,加 nginx.ingress.kubernetes.io/canary: "true",再选一种切流条件。多个条件同时存在时,优先级从高到低是 header-value、header、cookie、weight。
| 注解 | 取值 | 行为 |
|---|---|---|
canary-by-header |
请求头名 | 值为 always 进灰度,never 进主版本 |
canary-by-header-value |
精确值 | 请求头等于该值才进灰度 |
canary-by-header-pattern |
正则 | 请求头匹配正则才进灰度 |
canary-by-weight |
0–100 | 按权重随机分流,0 表示全量回滚 |
canary-by-cookie |
Cookie 名 | 值为 always/never 决定走向 |
一个 host 加一个 path 只能有一个 canary Ingress,多建不会叠加,行为未定义。灰度完成后应把主 Ingress 指向新 Service 再删掉 canary,而不是把权重长期挂在 100。
动手实验¶
实验一:部署 MetalLB 并拿到 EXTERNAL-IP
步骤 1 · 开启 StrictARP 并部署 MetalLB
kubectl get configmap kube-proxy -n kube-system -o yaml | \
sed -e "s/strictARP: false/strictARP: true/" | \
kubectl apply -f - -n kube-system
kubectl rollout restart ds/kube-proxy -n kube-system
METALLB_VERSION='v0.13.12'
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml
v0.13.12 已偏旧,新环境可用 v0.14.x,CRD 组与字段名不变。
步骤 2 · 创建地址池与 L2 通告
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: localip-pool
namespace: metallb-system
spec:
addresses:
- 172.29.7.51-172.29.7.80
autoAssign: true
avoidBuggyIPs: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: localip-pool-l2a
namespace: metallb-system
spec:
ipAddressPools:
- localip-pool
interfaces:
- enp1s0
步骤 3 · 创建 LoadBalancer Service
实验二:用 Helm 部署 ingress-nginx
helm install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx \
--set controller.metrics.enabled=true \
--create-namespace
kubectl -n ingress-nginx get svc ingress-nginx-controller
预期 EXTERNAL-IP 是 MetalLB 池中的地址(如 172.29.7.52)。metrics 在控制器 10254 端口;需要 Prometheus 自动发现时,再加 controller.podAnnotations."prometheus\.io/scrape"="true"。
实验三:按权重与 Header 灰度
步骤 1 · 部署两个版本并建立主 Ingress 与权重 canary
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp
spec:
ingressClassName: nginx
tls:
- hosts: ["demoapp.magedu.com"]
secretName: demoapp-tls
rules:
- host: demoapp.magedu.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demoapp-v10
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp-canary-by-weight
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
rules:
- host: demoapp.magedu.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demoapp-v11
port:
number: 80
kubectl apply -f ingress-canary-weight.yaml
for i in $(seq 1 20); do curl -s -H 'Host: demoapp.magedu.com' http://172.29.7.52/; echo; done
预期约 2 条来自 v1.1,其余来自 v1.0(正文以镜像实际返回为准)。也可以看控制器 access log,upstream 会在 default-demoapp-v10-80 与 default-demoapp-v11-80 之间切换。
步骤 2 · 把权重 canary 改成 Header canary
kubectl annotate --overwrite ingress demoapp-canary-by-weight \
nginx.ingress.kubernetes.io/canary-weight- \
nginx.ingress.kubernetes.io/canary-by-header=X-Canary \
nginx.ingress.kubernetes.io/canary-by-header-value=always
curl -s -H 'Host: demoapp.magedu.com' -H 'X-Canary: always' http://172.29.7.52/
curl -s -H 'Host: demoapp.magedu.com' http://172.29.7.52/
第一条固定返回 v1.1,第二条返回 v1.0。canary-weight- 末尾的减号表示删除该注解。把 canary-by-header-value 换成 canary-by-header-pattern: "(vip|VIP)_.*",即可按正则匹配灰度用户。
验证清单¶
-
EXTERNAL-IP是池中地址且集群外能 curl 通,ip neigh能看到对应节点 MAC。 - 权重 10 时约 1/10 请求命中 v1.1;带
X-Canary: always的请求 100% 命中 v1.1。
常见问题与排错¶
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
EXTERNAL-IP 一直 <pending> |
没有可用地址池,或池未 autoAssign |
看 controller 日志;检查池容量与 autoAssign |
| BGP 会话建立不起来 | ASN 不匹配、179 被拦、邻居地址写错 | 看 MetalLB 的 FRR 日志;核对 myASN/peerASN |
| Ingress 返回 404 | 没有匹配的 host/path,或 class 不匹配 | kubectl describe ingress;确认控制器 --ingress-class=nginx |
进阶挑战¶
- 把 L2 换成 BGP:用
BGPPeer与BGPAdvertisement通告同一个池,在交换机侧确认看到等价路由。 - 用
canary-by-cookie让测试账号保持一整天的灰度会话,并验证 cookie 与 weight 同时存在时的优先级。
小结¶
type: LoadBalancer只是声明,裸金属上必须有控制器分配并通告地址,两者缺一不可。- Ingress v1 的关键字段是
ingressClassName、pathType和嵌套的service,旧注解不再生效。 - canary 的优先级是 header-value > header > cookie > weight,一个 host/path 只保留一个 canary。
延伸阅读¶
- MetalLB 官方文档
- 原仓库 MetalLB 与 ingress-canary-demo
- 证书怎么自动签发给 Ingress?继续读证书管理与 cert-manager。