3.8. 负载均衡与 Ingress 流量入口
云上点一下就能拿到的 LoadBalancer,在裸金属集群里默认永远是 Pending。本章先补齐四层:用 MetalLB 分配并通告 VIP;再补齐七层:用 ingress-nginx 做域名与路径路由,最后把一条流量按权重和请求头切给新版本。
- 层级:
L3 · 平台
- 难度:
进阶
- 预计:
75 分钟
- 前置:
Service、CNI 与 DNS
3.8.1. 你将学到
南北向流量路径、MetalLB 分配与通告模型,以及
IPAddressPool、L2Advertisement、BGPPeer的用法。用 header、header-value、header-pattern、weight、cookie 五种方式做金丝雀发布。
3.8.2. 为什么需要它
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 |
图:南北向流量在四层与七层各经过一次转发,MetalLB 把包送到节点,Ingress 选后端。
3.8.3. 核心概念
3.8.3.1. 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 模式不需要这一步。
3.8.3.2. 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 端口。
3.8.3.3. 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 生成。
3.8.3.4. 金丝雀发布的五种切流方式
创建一个与主 Ingress 同 host、同 path 的 canary Ingress,加 nginx.ingress.kubernetes.io/canary: "true",再选一种切流条件。多个条件同时存在时,优先级从高到低是 header-value、header、cookie、weight。
注解 |
取值 |
行为 |
|---|---|---|
|
请求头名 |
值为 |
|
精确值 |
请求头等于该值才进灰度 |
|
正则 |
请求头匹配正则才进灰度 |
|
0–100 |
按权重随机分流,0 表示全量回滚 |
|
Cookie 名 |
值为 |
一个 host 加一个 path 只能有一个 canary Ingress,多建不会叠加,行为未定义。灰度完成后应把主 Ingress 指向新 Service 再删掉 canary,而不是把权重长期挂在 100。
3.8.4. 动手实验
实验一:部署 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
kubectl apply -f metallb-pool-l2.yaml
步骤 3 · 创建 LoadBalancer Service
kubectl create deployment demoapp --image=ikubernetes/demoapp:v1.0 --replicas=2
kubectl create service loadbalancer demoapp --tcp=80:80
kubectl get svc demoapp -w
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
demoapp LoadBalancer 10.97.7.114 172.29.7.51 80:30072/TCP 8m
实验二:用 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
kubectl apply -f deploy-demoap-v1_0.yaml
kubectl apply -f deploy-demoap-v1_1.yaml
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)_.*",即可按正则匹配灰度用户。
3.8.4.1. 验证清单
EXTERNAL-IP是池中地址且集群外能 curl 通,ip neigh能看到对应节点 MAC。权重 10 时约 1/10 请求命中 v1.1;带
X-Canary: always的请求 100% 命中 v1.1。
3.8.5. 常见问题与排错
现象 |
可能原因 |
处理方式 |
|---|---|---|
|
没有可用地址池,或池未 |
看 controller 日志;检查池容量与 |
BGP 会话建立不起来 |
ASN 不匹配、179 被拦、邻居地址写错 |
看 MetalLB 的 FRR 日志;核对 |
Ingress 返回 404 |
没有匹配的 host/path,或 class 不匹配 |
|
3.8.6. 进阶挑战
把 L2 换成 BGP:用
BGPPeer与BGPAdvertisement通告同一个池,在交换机侧确认看到等价路由。用
canary-by-cookie让测试账号保持一整天的灰度会话,并验证 cookie 与 weight 同时存在时的优先级。
3.8.7. 小结
type: LoadBalancer只是声明,裸金属上必须有控制器分配并通告地址,两者缺一不可。Ingress v1 的关键字段是
ingressClassName、pathType和嵌套的service,旧注解不再生效。canary 的优先级是 header-value > header > cookie > weight,一个 host/path 只保留一个 canary。
3.8.8. 延伸阅读
证书怎么自动签发给 Ingress?继续读证书管理与 cert-manager。