1. 项目概述:为什么K8s集群里LoadBalancer不是“配个IP就完事”的事
在K8s生态里,“部署LoadBalancer”这六个字,表面看是网络层的一次配置动作,实则是一场横跨云厂商、CNI插件、Service类型、外部LB设备与内核转发机制的协同作战。我带过三轮K8s生产环境迁移,每次讲到LoadBalancer,总有运维同事拍着桌子说:“不就是把Service type改成LoadBalancer,云平台自动给个公网IP吗?”——这话在公有云单集群小规模场景下勉强成立,但一旦涉及混合云架构、自建IDC接入、多可用区容灾、TLS终止卸载、健康检查定制或灰度流量调度,这个“自动”二字背后全是手抖就断服的坑。LoadBalancer本质不是K8s原生组件,而是K8s Service抽象与外部负载均衡器之间的契约接口;它不负责转发逻辑,只负责“告诉谁来干活、怎么连上它、怎么确认它还活着”。所以标题里那个“六”字很关键——它暗示这是系列实践的第六篇,前五篇必然已铺垫了集群网络模型(CNI选型与Pod通信)、Service ClusterIP原理、NodePort暴露方式、Ingress控制器选型及EndpointSlice机制。没有这些底座,直接上LoadBalancer,就像没打地基就砌承重墙。本文聚焦纯技术落地:不依赖特定云厂商控制台,不假设你用的是AWS ALB或阿里云SLB,而是从K8s原生API出发,拆解LoadBalancer Service如何通过cloud-controller-manager与外部设备联动,如何手动对接裸金属环境下的HAProxy集群,以及当云厂商LB服务异常时,如何用MetalLB实现秒级故障接管。核心关键词——LoadBalancer Service、externalIPs、healthCheckNodePort、service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path——这些不是YAML里的装饰字段,而是决定流量能否真正抵达Pod的生命线参数。
2. 核心设计思路:为什么必须绕开“云平台自动创建”这一条捷径
2.1 LoadBalancer的本质:一个需要双向握手的“外包合同”
K8s中Service类型为LoadBalancer时,其行为完全由cloud-controller-manager(CCM)驱动。很多人误以为CCM是K8s核心组件,其实它是云厂商提供的适配层——AWS有aws-cloud-controller-manager,Azure有azure-cloud-controller-manager,OpenStack有openstack-cloud-controller-manager。它的核心职责只有三件事:监听Service变更、调用云API创建/删除LB资源、将LB后端节点(即Node IP)注册到云LB的Target Group。注意,这里注册的是Node IP,不是Pod IP。这意味着流量路径是:Client → 云LB → Node IP:NodePort → kube-proxy → Pod IP:TargetPort。整个链路存在两个关键耦合点:一是CCM必须能稳定访问云厂商API(网络策略、IAM权限、API限流都可能中断);二是云LB必须支持将Node IP作为后端(部分老旧LB仅支持IP+Port组合,而K8s NodePort是动态分配的)。我曾遇到某金融客户因云厂商API网关升级导致CCM连续37分钟无法同步Service状态,期间新创建的LoadBalancer Service始终卡在Pending状态,kubectl get svc显示EXTERNAL-IP <pending>。此时若团队只会等云平台告警,业务就已中断。因此,本方案设计的第一原则是:解耦CCM依赖,让LoadBalancer能力可降级、可验证、可替换。
2.2 自建LB方案的三种技术路径对比
| 方案类型 | 适用场景 | 部署复杂度 | 故障恢复时间 | 流量可见性 | 典型代表 |
|---|---|---|---|---|---|
| 云厂商托管LB | 公有云单集群、无定制需求 | ★☆☆☆☆(极低) | 5~30分钟(依赖云平台) | 仅提供基础监控指标 | AWS ALB/NLB、GCP L4/L7 LB |
| MetalLB + BGP | 裸金属集群、需BGP路由通告 | ★★★★☆(高) | <10秒(BGP会话收敛) | 完全可控,可集成Prometheus | MetalLB v0.13+ |
| MetalLB + Layer2 | 小规模测试集群、无BGP支持交换机 | ★★☆☆☆(中) | <3秒(ARP广播) | 可抓包分析,但无会话保持 | MetalLB Layer2模式 |
我们最终选择MetalLB Layer2模式作为主方案,原因很务实:第一,客户IDC交换机不支持BGP,且采购新设备流程需3个月;第二,Layer2模式无需修改网络架构,仅需在K8s节点上启用ARP响应;第三,它完美复现了云LB的核心语义——为Service分配静态IP,并通过ARP广播宣告该IP归属当前活跃节点。但必须强调:Layer2模式有天然限制——同一子网内只能有一个Active节点持有该IP,因此它不适合多可用区跨子网场景。如果你的集群节点分布在不同VLAN,必须切回BGP模式或改用其他方案(如Keepalived+HAProxy)。这个取舍不是技术优劣,而是对现实网络基础设施的诚实面对。
2.3 关键设计决策:为什么放弃Ingress,坚持Service LoadBalancer
常有人问:“既然Ingress能做七层负载均衡,为啥还要搞LoadBalancer?”答案藏在协议栈层级里。Ingress工作在OSI第七层(应用层),它解析HTTP/HTTPS请求头,根据Host或Path路由到后端Service;而LoadBalancer工作在第四层(传输层),它只看TCP/UDP连接的源IP+端口,不关心报文内容。这意味着:
- gRPC、WebSocket、MySQL、Redis等非HTTP协议必须走LoadBalancer;
- 需要客户端真实IP透传时,LoadBalancer可通过PROXY协议或X-Forwarded-For头实现,而Ingress需额外配置
use-forwarded-headers: "true"且依赖后端应用解析; - TLS终止位置不同:LoadBalancer可在云LB或MetalLB前端卸载,Ingress通常在Ingress Controller(如Nginx)卸载。
我们有个实时风控系统,要求客户端IP毫秒级透传至业务Pod做地理围栏校验。若走Ingress,Nginx默认会覆盖X-Real-IP为自身IP,需在ConfigMap中显式开启real-ip-header: "X-Real-IP"并配置set-real-ip-from网段,稍有疏忽就丢失原始IP。而MetalLB Layer2模式下,客户端直连Node IP,kube-proxy通过iptables DNAT转发,netstat -tnp | grep :<NodePort>能看到真实的客户端IP和端口。这种确定性,是Ingress无法替代的。
3. 实操细节解析:从零部署MetalLB并打通全链路
3.1 环境准备:五个不可跳过的前置检查项
部署MetalLB前,必须确认以下五点,缺一不可。我见过太多团队卡在第3步,折腾两天才发现是交换机禁用了ARP响应:
K8s版本兼容性:MetalLB v0.13+要求K8s ≥ v1.20。执行
kubectl version --short确认,若为v1.19或更低,必须先升级集群。旧版kube-proxy的iptables规则与MetalLB冲突,会导致ARP广播被丢弃。节点网络互通性:所有Worker节点必须在同一二层广播域(即同一VLAN/子网)。执行
ping <其他节点IP>和arping -c 3 <其他节点MAC>验证。若arping返回Timeout,说明交换机ACL阻止了ARP请求,需联系网络管理员放行。内核参数校准:Linux内核需允许节点响应非本机IP的ARP请求。在每台节点执行:
# 检查当前值 sysctl net.ipv4.conf.all.arp_ignore sysctl net.ipv4.conf.all.arp_announce # 正确值应为1和2(MetalLB官方要求) echo "net.ipv4.conf.all.arp_ignore = 1" >> /etc/sysctl.conf echo "net.ipv4.conf.all.arp_announce = 2" >> /etc/sysctl.conf sysctl -parp_ignore=1表示只响应目标IP是本机地址的ARP请求;arp_announce=2表示优先使用与目标IP同网段的本地地址响应ARP。这两个参数是MetalLB能“冒充”Service IP的底层保障。防火墙放行:确保节点防火墙(如firewalld或ufw)未拦截ARP包。临时关闭测试:
systemctl stop firewalld。生产环境需添加规则:iptables -I INPUT -p arp -j ACCEPT。CCM卸载确认:若集群已部署云厂商CCM,必须先停用,否则它会与MetalLB争抢Service管理权。执行
kubectl get pods -n kube-system | grep cloud-controller,若有输出,需按云厂商文档卸载CCM。否则会出现kubectl get svc中EXTERNAL-IP反复在<pending>和IP间切换的诡异现象。
提示:以上检查项建议写成Shell脚本自动化执行。我们封装了
check-metallb-prereq.sh,包含超时检测和失败项高亮,5分钟内完成全集群扫描。
3.2 MetalLB部署:三步完成核心组件安装
MetalLB采用Helm Chart方式部署最稳妥,避免YAML手工编辑遗漏RBAC权限。以下是经过27个集群验证的标准化流程:
第一步:添加Helm仓库并更新
helm repo add metallb https://metallb.github.io/metallb helm repo update第二步:创建命名空间与配置文件
# 创建独立命名空间,避免资源污染 kubectl create namespace metallb-system # 编写config.yaml,定义IP地址池(关键!) cat > metallb-config.yaml << 'EOF' apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: default-pool namespace: metallb-system spec: addresses: - 192.168.10.200-192.168.10.250 # 生产环境建议预留20个IP防突发扩容 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: default-l2 namespace: metallb-system EOF注意addresses字段必须是集群所在子网的空闲IP段,且不能与DHCP服务器地址池重叠。我们曾因填错成10.0.0.100-10.0.0.200(实际节点在192.168.10.0/24),导致MetalLB持续发送ARP宣告,但无人响应,kubectl logs -n metallb-system <speaker-pod>日志满屏failed to send ARP reply。
第三步:Helm安装并验证
helm install metallb metallb/metallb \ --namespace metallb-system \ --create-namespace \ --values metallb-config.yaml \ --set controller.image.repository="quay.io/metallb/controller" \ --set speaker.image.repository="quay.io/metallb/speaker" # 等待Pod就绪(约30秒) kubectl wait --namespace metallb-system \ --for=condition=ready pod \ --selector=app=metallb,component=controller \ --timeout=60s kubectl wait --namespace metallb-system \ --for=condition=ready pod \ --selector=app=metallb,component=speaker \ --timeout=60s安装后执行kubectl get pods -n metallb-system,应看到controller-xxx和多个speaker-xxx(每个节点一个)。若speaker状态为CrashLoopBackOff,大概率是步骤3.1中的内核参数未生效,需重启节点或执行sysctl -p后kill -HUP $(pidof kubelet)。
3.3 Service LoadBalancer实战:从创建到流量验证的完整闭环
现在创建一个真实的LoadBalancer Service,以Nginx为例,全程演示流量路径:
Step 1:部署Nginx应用
# nginx-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.21 ports: - containerPort: 80 name: http --- apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - name: http port: 80 targetPort: 80 protocol: TCP type: LoadBalancer # 关键!指定类型执行kubectl apply -f nginx-deploy.yaml。
Step 2:观察IP分配过程
# 查看Service状态,等待EXTERNAL-IP出现 kubectl get svc nginx-service -w # 初始状态 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx-service LoadBalancer 10.96.123.45 <pending> 80:31234/TCP 10s # 约5秒后变为 nginx-service LoadBalancer 10.96.123.45 192.168.10.200 80:31234/TCP 15s这个192.168.10.200正是MetalLB从default-pool中分配的IP。此时MetalLB Speaker组件已在节点上执行arping -U -c 3 -I eth0 192.168.10.200宣告该IP归属。
Step 3:验证ARP宣告是否生效在任意一台集群外的Linux机器(如Jump Server)执行:
# 清空ARP缓存 ip neigh flush dev eth0 # 发送ARP请求 arping -c 3 192.168.10.200 # 正常输出应类似: ARPING 192.168.10.200 from 192.168.10.50 eth0 Unicast reply from 192.168.10.200 [00:11:22:33:44:55] 1.234ms Unicast reply from 192.168.10.200 [00:11:22:33:44:55] 1.345ms若收到Unicast reply,证明ARP宣告成功,交换机已学习到192.168.10.200对应节点MAC地址。
Step 4:抓包确认流量路径在持有192.168.10.200的节点上抓包:
# 监听Service IP的80端口 tcpdump -i any host 192.168.10.200 and port 80 -nn -A # 从Jump Server curl测试 curl http://192.168.10.200 # 抓包输出应显示: # 1. 客户端SYN包发往192.168.10.200:80 # 2. 节点内核通过iptables DNAT转到NodePort(如31234) # 3. kube-proxy将包转发至某个Pod的10.244.1.10:80这才是完整的流量闭环。如果抓包只看到SYN但无SYN-ACK,说明kube-proxy规则未生效,需检查iptables -t nat -L KUBE-SERVICES是否包含对应规则。
3.4 高级配置:让LoadBalancer不止于“通”,更要“稳”和“智”
仅仅分配IP只是起点。生产环境必须配置健康检查与会话保持,否则节点宕机时流量仍会打到故障节点。MetalLB通过Service的Annotation实现:
# nginx-service-enhanced.yaml apiVersion: v1 kind: Service metadata: name: nginx-service annotations: # 启用健康检查:MetalLB会定期探测NodePort metallb.universe.tf/allow-shared-ip: "nginx-cluster" # 会话保持:基于客户端IP哈希,保证同一IP始终路由到同一Pod service.beta.kubernetes.io/aws-load-balancer-type: "nlb" # 此处仅为示意,MetalLB不支持此Annotation # 正确做法:通过Ingress Controller或Service Mesh实现 spec: selector: app: nginx ports: - name: http port: 80 targetPort: 80 protocol: TCP type: LoadBalancer # 关键:指定健康检查端口(必须是NodePort范围内的端口) healthCheckNodePort: 32000healthCheckNodePort是K8s原生字段,MetalLB会向该端口发送TCP探测。需确保你的应用在该端口返回HTTP 200(如Nginx可配置location /healthz { return 200; })。若探测失败,MetalLB Speaker会停止宣告该IP,流量自动切到其他健康节点。
注意:
healthCheckNodePort必须是30000-32767范围内的整数,且不能与现有NodePort冲突。我们习惯固定用32000,并在所有应用Deployment中统一添加livenessProbe指向该端口。
4. 常见问题排查:那些让你凌晨三点爬起来的典型故障
4.1 故障现象:kubectl get svc中EXTERNAL-IP始终为<pending>,日志无报错
这是最高频问题。排查顺序必须严格遵循:
确认MetalLB组件状态
kubectl get pods -n metallb-system # 若controller或speaker未Running,先看Events kubectl describe pod -n metallb-system <pod-name>检查IPAddressPool资源是否存在且有效
kubectl get ipaddresspool -n metallb-system # 输出应为: # NAME AGE # default-pool 2d # 若为空,说明config.yaml未正确应用 kubectl apply -f metallb-config.yaml验证节点是否加入L2Advertisement
kubectl get l2advertisement -n metallb-system # 必须有default-l2记录,且STATUS为Accepted # 若为Pending,检查speaker日志: kubectl logs -n metallb-system deploy/metallb-speaker | grep -i "l2advertisement"终极手段:手动触发ARP宣告
进入speaker容器,执行:kubectl exec -it -n metallb-system deploy/metallb-speaker -- sh # 在容器内运行 arping -U -c 3 -I eth0 192.168.10.200若返回
Sent 3 probes (3 broadcast(s))但无Reply,证明网络层阻断,需检查交换机ARP ACL。
4.2 故障现象:流量能通,但随机50%请求超时,curl -v显示TCP连接建立缓慢
这是典型的ARP缓存老化问题。Linux默认ARP缓存超时为30秒,若MetalLB切换节点(如原节点宕机),客户端ARP缓存仍指向旧MAC,导致半数包丢失。解决方案:
- 客户端侧:在Jump Server等测试机执行
ip neigh change 192.168.10.200 lladdr 00:11:22:33:44:55 nud permanent,强制绑定MAC。 - 服务端侧:调整MetalLB Speaker的ARP宣告频率。编辑
metallb-config.yaml,增加:apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: default-l2 namespace: metallb-system spec: ipAddressPools: - default-pool # 每5秒宣告一次,加速客户端ARP更新 arpInterval: 5s
4.3 故障现象:Service IP能ping通,但curl http://IP返回Connection refused
这说明ARP宣告成功,但四层流量未到达Pod。分两步定位:
Step 1:确认NodePort是否监听
# 查看Service对应的NodePort kubectl get svc nginx-service -o wide # 在节点上检查该端口是否被监听 ss -tlnp | grep :31234 # 替换为实际NodePort # 正常输出:LISTEN 0 128 *:31234 *:* users:(("kube-proxy",pid=1234,fd=5))若无输出,说明kube-proxy未生成规则,检查kubectl get nodes是否所有节点Ready,或kube-proxyPod是否正常。
Step 2:检查iptables DNAT链
# 查看KUBE-SERVICES链是否包含该Service iptables -t nat -L KUBE-SERVICES | grep nginx-service # 正常应有类似: # KUBE-MARK-MASQ all -- 192.168.10.200 anywhere /* default/nginx-service:http */ # KUBE-SVC-XXXXXX tcp -- anywhere 192.168.10.200 /* default/nginx-service:http */ tcp dpt:http若缺失,重启kube-proxy:kubectl delete pod -n kube-system -l component=kube-proxy。
4.4 故障现象:多节点集群中,Service IP总是在两个节点间漂移,导致客户端连接中断
这是Layer2模式的固有缺陷——它依赖ARP宣告抢占,无主从选举。解决方案只有两个:
短期应急:在
metallb-config.yaml中为每个节点指定静态IP,避免竞争:apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: node1-pool namespace: metallb-system spec: addresses: - 192.168.10.201 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: node1-l2 namespace: metallb-system spec: ipAddressPools: - node1-pool # 绑定到特定节点 nodeSelectors: - matchExpressions: - key: kubernetes.io/hostname operator: In values: ["node1"]长期方案:迁移到BGP模式,通过BGP路由属性(如Local Preference)控制主备。需网络团队配合配置BGP邻居,但稳定性提升10倍。
5. 运维进阶:监控、告警与容量规划的硬核实践
5.1 必须监控的五个黄金指标
MetalLB本身不提供Prometheus指标,但可通过以下方式采集关键数据:
| 指标名称 | 数据来源 | 查询PromQL | 告警阈值 | 业务含义 |
|---|---|---|---|---|
| Speaker ARP宣告成功率 | kubectl logs解析 | rate(metallb_speaker_arp_replies_total[1h]) | <0.95 | 表明网络层ARP阻断,需立即检查交换机 |
| Service IP分配延迟 | kubectl get svc时间戳差 | histogram_quantile(0.95, sum(rate(kube_service_created_timestamp_seconds_bucket[1h])) by (le)) | >30s | MetalLB控制器性能瓶颈或etcd压力大 |
| NodePort健康检查失败率 | kubectl get endpointslice | count by (service) (kube_endpointslice_condition{condition="Ready", status="False"}) | >0 | 后端Pod全部不健康,业务已不可用 |
| IP地址池剩余率 | kubectl get ipaddresspool | 1 - (sum(metallb_ipaddresspool_used_addresses) by (pool)) / sum(metallb_ipaddresspool_total_addresses) by (pool) | <0.1 | 地址池即将耗尽,需扩容 |
| L2Advertisement状态异常数 | kubectl get l2advertisement | count(kube_l2advertisement_status_phase{phase!="Accepted"}) | >0 | MetalLB配置错误,需人工介入 |
我们用Fluentd采集speaker日志,正则提取arp_replies_total计数器,写入Prometheus。告警规则直接关联企业微信机器人,消息模板为:“🚨 MetalLB告警:192.168.10.200ARP宣告失败率98%,请检查交换机ACL”。
5.2 容量规划:一个IP地址池能支撑多少Service?
别被“200个IP”迷惑。真实容量取决于三个维度:
并发连接数限制:Linux内核
net.core.somaxconn默认128,单个NodePort最大连接数≈65535(端口范围)。但MetalLB分配的每个IP对应一个NodePort,因此理论最大Service数=IP数×65535。然而,实际受限于:- kube-proxy性能:iptables模式下,每增加1个Service,iptables规则增加约20行。当Service数>500时,
iptables-save耗时超10秒,导致规则同步延迟。 - etcd压力:每个Service对象存储约2KB,1000个Service占用2MB etcd空间,读写QPS激增。
- kube-proxy性能:iptables模式下,每增加1个Service,iptables规则增加约20行。当Service数>500时,
我们的经验值:
- 小型集群(≤10节点):IP池50个,Service数≤200
- 中型集群(11~50节点):IP池100个,Service数≤500
- 大型集群(>50节点):必须切BGP模式,IP池不限,但需规划BGP ASN和路由反射器
弹性扩容操作:
# 扩容IP池(无需重启MetalLB) kubectl patch ipaddresspool -n metallb-system default-pool \ --type='json' \ -p='[{"op": "replace", "path": "/spec/addresses", "value": ["192.168.10.200-192.168.10.299"]}]'
5.3 灾备演练:当MetalLB完全失效时,如何30秒内切回NodePort
任何高可用方案都需验证降级能力。我们每月执行一次“MetalLB熔断演练”:
- 模拟故障:
kubectl scale deploy -n metallb-system metallb-speaker --replicas=0 - 验证降级:
kubectl patch svc nginx-service -p '{"spec":{"type":"NodePort"}}' - 获取NodePort:
kubectl get svc nginx-service -o jsonpath='{.spec.ports[0].nodePort}' - 客户端切换:
curl http://<任意节点IP>:<NodePort> - 恢复MetalLB:
kubectl scale deploy -n metallb-system metallb-speaker --replicas=3
整个过程记录在SOP文档中,要求全员能在2分钟内完成。真正的稳定性,不在于“永不故障”,而在于“故障时人人知道怎么做”。
6. 最后分享一个血泪教训:关于“云厂商LB与MetalLB共存”的幻觉
曾有客户提出:“我们既要用云厂商ALB处理公网流量,又用MetalLB处理IDC内网流量,能否共存?”听起来合理,实则埋雷。问题出在cloud-controller-manager与metallb-speaker对同一Service的争夺上。K8s API Server不会拒绝重复控制器,结果是:
- CCM创建ALB后,将
EXTERNAL-IP设为ALB DNS名(如abc.elb.amazonaws.com) - MetalLB Speaker发现
EXTERNAL-IP非IP格式,尝试覆盖为192.168.10.200 - 两者反复拉锯,
kubectl get svc中EXTERNAL-IP在DNS名和IP间疯狂跳变
最终解决方案只有两个:
- 物理隔离:公网Service全部用
type: LoadBalancer交由CCM管理;内网Service全部用type: ClusterIP+externalIPs: [192.168.10.200],由MetalLB接管externalIPs字段。 - 逻辑隔离:通过Label Selector区分,MetalLB配置
address-pools时指定serviceSelector,只管理带network/internal: "true"标签的Service。
这个教训让我明白:K8s的“声明式”不是魔法,而是要求你对每个字段的控制者有清醒认知。当你写下type: LoadBalancer,就要想清楚——此刻,是谁在为你打工?