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。

注解

取值

行为

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。

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 通告

metallb-pool-l2.yaml
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
ingress-canary-weight.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. 常见问题与排错

现象

可能原因

处理方式

EXTERNAL-IP 一直 <pending>

没有可用地址池,或池未 autoAssign

看 controller 日志;检查池容量与 autoAssign

BGP 会话建立不起来

ASN 不匹配、179 被拦、邻居地址写错

看 MetalLB 的 FRR 日志;核对 myASN/peerASN

Ingress 返回 404

没有匹配的 host/path,或 class 不匹配

kubectl describe ingress;确认控制器 --ingress-class=nginx

3.8.6. 进阶挑战

  1. 把 L2 换成 BGP:用 BGPPeer 与 BGPAdvertisement 通告同一个池,在交换机侧确认看到等价路由。

  2. 用 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. 延伸阅读