跳转至

负载均衡与 Ingress 流量入口

云上点一下就能拿到的 LoadBalancer,在裸金属集群里默认永远是 Pending。本章先补齐四层:用 MetalLB 分配并通告 VIP;再补齐七层:用 ingress-nginx 做域名与路径路由,最后把一条流量按权重和请求头切给新版本。

难度 · 进阶 预计 75 分钟 前置:Service、CNI 与 DNS

你将学到

  • 南北向流量路径、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 通告

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)_.*",即可按正则匹配灰度用户。

验证清单

  • 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

进阶挑战

  1. 把 L2 换成 BGP:用 BGPPeer 与 BGPAdvertisement 通告同一个池,在交换机侧确认看到等价路由。
  2. 用 canary-by-cookie 让测试账号保持一整天的灰度会话,并验证 cookie 与 weight 同时存在时的优先级。

小结

  • type: LoadBalancer 只是声明,裸金属上必须有控制器分配并通告地址,两者缺一不可。
  • Ingress v1 的关键字段是 ingressClassName、pathType 和嵌套的 service,旧注解不再生效。
  • canary 的优先级是 header-value > header > cookie > weight,一个 host/path 只保留一个 canary。

延伸阅读