0%

基于 MetalLB 部署 k8s 的 LoadBalancer 服务

上篇文章我们已经 Kind 搭建了 k8s 环境,并且测试了 ClusterIP 和 NodePort 类型的 Service,这篇文章我们继续探索 k8s 中的 LoadBalancer 类型的 Service,之后我们还会简单介绍一下 k8s 的 ExternalName Service 和 Headless Service。

LoadBalancer Service

什么是 LoadBalancer Service

LoadBalancer 是 Kubernetes Service 的一种类型,用于将服务暴露给外部网络,下面简单对比了 k8s 的三种主要 Service 类型:

类型 外部访问方式 适用场景
ClusterIP 仅集群内部访问 内部服务通信
NodePort 节点 IP + 端口(30000-32767) 测试、简单暴露
LoadBalancer 外部负载均衡器分配的 IP 生产环境、云环境

当需要从外部访问 k8s 集群内的服务时,NodePort 类型的 Service 有很多 痛点

  • 单点故障风险:外部流量通常会硬编码指向某几个 Node 的 IP。如果那个特定的 Node 宕机了,用户的连接就会断开
  • 缺乏真正的负载均衡:NodePort Service 的流量转发路径是:流量到任意节点,节点再转发给 Port。如果大量流量涌入同一个 Node IP,这个节点的网络带宽会成为瓶颈
  • 端口难以管理:默认使用的是 30000-32767 范围内的端口,不能直接用标准的 80 或 443 端口

而 LoadBalancer 类型的 Service 则是为了解决这些问题而设计的。它具有如下优点:

  • 避免节点故障影响:云厂商的负载均衡器会自动监控所有后端的节点。如果一个节点健康检查失败,负载均衡器会自动停止向该节点转发流量,只发给健康的节点。对客户端来说,它始终访问同一个 IP(负载均衡器的 IP),完全感知不到后端节点的故障。
  • 提供一个稳定、统一的访问入口:客户端始终通过固定的、公开的外部 IP 地址来访问服务
  • 使用标准的服务端口 (80, 443):客户端看到的就是一个标准的网站地址

当你向 Kubernetes 提交一个 type: LoadBalancer 的 Service 的 YAML 文件时,集群内部会发生以下联动:

  • 分配内部资源: API Server 收到请求后,会先给这个 Service 分配一个内部的 ClusterIP,并在所有节点上随机开启一个 NodePort(比如 31080)
  • 云控制器介入:此时,集群中的一个核心组件 Cloud Controller Manager (CCM) 检测到了这个特殊类型的 Service。
  • 调用云厂商 API:CCM 会拿着你的凭证,通过 API 去呼叫底层的云提供商(如阿里云、AWS、腾讯云):请给我创建一个公网负载均衡实例,并将它的后端挂载到我这些 Kubernetes 节点的 31080 端口上
  • 回写状态: 云厂商创建完毕后,返回一个公网 IP。CCM 会把这个外部 IP 写回到你的 Service 的 EXTERNAL-IP 字段中

而当通过这个外部 IP 访问集群内的服务时:

  • 流量到达云厂商的负载均衡(Cloud LB)
  • LB 分发到 Node (利用 NodePort):云 LB 会根据自身的负载均衡算法(如轮询),将请求转发给集群中任意一个健康节点的宿主机 IP 和指定的 NodePort(如 Node-A-IP:31080)
  • Node 内部的拦截与分发 (kube-proxy 发力):流量进入节点后,该节点上的 kube-proxy 配置的 iptables/ipvs 规则会立刻拦截这个包:哦,你是访问 31080 端口的,我知道你想去哪个 Pod
  • 抵达目标 Pod:流量最终路由到真正运行代码的 Pod IP 上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
用户 -> (访问公网IP:80) -> 云厂商负载均衡器 (如 AWS ELB, 阿里云 SLB)
|
| (监听后端节点健康状态)
| (流量转发)
v
+----------------------+ | +----------------------+
| Kubernetes Node 1 |<-----| Kubernetes Node 2 |
| (健康) | | (不健康,被自动跳过) |
| NodePort: 30080 | | NodePort: 30080 |
+----------------------+ +----------------------+
| (流量不到达这里)
v (通过 kube-proxy)
+----------------------+
| ClusterIP: 10.96... |
+----------------------+
|
v (负载均衡到 Pod)
+-----+ +-----+ +-----+
| Pod | | Pod | | Pod |
+-----+ +-----+ +-----+

为什么需要 MetalLB?

在云环境(AWS、阿里云、GCP)中,创建 LoadBalancer Service 时,云厂商会自动创建一个外部负载均衡器(如 AWS ELB),并分配一个外部 IP。但在本地/裸金属环境(如 Kind、Minikube、自建集群)中,没有云厂商支持,LoadBalancer Service 的 EXTERNAL-IP 会一直处于 <pending> 状态。而 MetalLB 是面向裸金属 Kubernetes 集群的负载均衡器实现,基于标准路由协议,让裸金属 K8s 也能正常使用 Service: LoadBalancer 类型:

  • 为裸金属环境提供 LoadBalancer 功能
  • 分配虚拟 IP 给 LoadBalancer Service
  • 支持 Layer2(ARP)和 BGP 模式
模式 原理 适用场景
Layer2 通过 ARP 响应,让网络认为某节点拥有 LoadBalancer IP 小规模网络、简单测试
BGP 通过 BGP 协议向路由器广播 IP 大规模网络、生产环境

这篇文章我们将以 Layer2 模式来部署使用 MetalLB,如下简单介绍了 Layer2 模式工作流程:

  • MetalLB 从 IP 地址池中选择一个 IP 分配给 Service
  • MetalLB speaker(DaemonSet)在某节点上响应 ARP 请求
  • 网络流量到达该节点
  • kube-proxy 将流量转发到 Service 的后端 Pod

创建 LoadBalancer Service

首先我们看下在没有部署安装 MetalLB 时,如果创建一个 LoadBalancer 类型的 Service,会发生什么?环境仍然基于上篇文章所部署的 Kind 环境:

  • 创建 service 配置文件 nginx-lb-svc.yaml,Service 的 Type 为 LoadBalancer
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: nginx-lb
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80
  • type: LoadBalancer:Service 类型为 LoadBalancer

  • selector: app: nginx:选择标签为 app: nginx 的 Pod 作为后端

  • port: 80:Service 暴露的端口

  • targetPort: 80:Pod 内部应用监听的端口

  • 创建服务

1
kubectl apply -f nginx-lb-svc.yaml
  • 查看 Service 状态
1
2
3
4
# kubectl get svc nginx-lb

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb LoadBalancer 10.104.44.36 <pending> 80:32617/TCP 4s
字段 说明
NAME nginx-lb Service 名称
TYPE LoadBalancer Service 类型
CLUSTER-IP 10.104.44.36 集群内部虚拟 IP
EXTERNAL-IP <pending> 等待分配外部 IP(无负载均衡器实现)
PORT(S) 80:32617/TCP Service 端口:NodePort 端口
AGE 4s 创建时间

这里 EXTERNAL-IP: <pending> 表示没有负载均衡器来分配外部 IP。虽然 EXTERNAL-IP 为 <pending>,但 NodePort 已自动分配(32617),可通过 NodePort 方式访问。

  • 查看 Service 详细信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# kubectl describe svc nginx-lb

Name: nginx-lb
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=nginx
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.104.44.36
IPs: 10.104.44.36
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 32617/TCP
Endpoints: 10.244.2.2:80,10.244.1.2:80,10.244.2.3:80
Session Affinity: None
External Traffic Policy: Cluster
Internal Traffic Policy: Cluster
Events: <none>
字段 说明
Selector 选择 Pod 的标签匹配规则
IP/IPs Service 的 ClusterIP
NodePort 自动分配的节点端口(32617)
Endpoints 后端 Pod 的 IP:端口列表
Events 事件记录(当前无事件)

安装 MetalLB

因为我们将使用 Layer2 模式来部署 MetalLB,因此需要确保 MetalLB 的 IP 地址池必须与 Kind 集群在同一网络,否则无法通信。

实际部署

  • 我们首先确认 Kind Docker 网络范围:
1
2
3
4
docker network inspect kind --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

fc00:f853:ccd:e793::/64
172.19.0.0/16
  • 安装 MetalLB v0.15.3
1
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.3/config/manifests/metallb-native.yaml
  • 等待 MetalLB 就绪
1
kubectl wait --namespace metallb-system --for=condition=ready pod --selector=app=metallb --timeout=90s
  • 创建配置文件metallb-config.yaml,配置 MetalLB IP 地址池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 172.19.0.100-172.19.0.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- default-pool

IPAddressPool 详解

  • 定义 MetalLB 可以分配给 LoadBalancer Service 的 IP 范围
  • 每个 LoadBalancer Service 会从这个池中获取一个 IP
  • 这里 IP 范围必须与集群网络在同一网段

L2Advertisement 详解

  • Layer2 模式通过 ARP 协议让网络知道 LoadBalancer IP 在哪

  • speaker Pod 会响应 ARP 请求,告诉网络 这个 IP 在我这台节点上

  • 流量到达该节点后,由 kube-proxy 转发到 Service

  • 应用 metallb-config.yaml 配置文件:

1
kubectl apply -f metallb-config.yaml

检查 metallb 状态

我们来看一下 metallb 相关的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# kubectl get all -n metallb-system
NAME READY STATUS RESTARTS AGE
pod/controller-66bdd896c6-hvd6x 1/1 Running 0 62m
pod/speaker-5wqc5 1/1 Running 0 62m
pod/speaker-n77zs 1/1 Running 0 62m
pod/speaker-zw7rz 1/1 Running 0 62m

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/metallb-webhook-service ClusterIP 10.101.140.128 <none> 443/TCP 62m

NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 3 3 3 3 3 kubernetes.io/os=linux 62m

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 62m

NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-66bdd896c6 1 1 1 62m
1
2
3
4
5
6
#  kubectl get ipaddresspool,l2advertisement -n metallb-system
NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
ipaddresspool.metallb.io/default-pool true false ["172.19.0.100-172.19.0.200"]

NAME IPADDRESSPOOLS IPADDRESSPOOL SELECTORS INTERFACES
l2advertisement.metallb.io/default-advertisement ["default-pool"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# kubectl describe ipaddresspool default-pool -n metallb-system
Name: default-pool
Namespace: metallb-system
Labels: <none>
Annotations: <none>
API Version: metallb.io/v1beta1
Kind: IPAddressPool
Metadata:
Creation Timestamp: 2026-04-22T02:53:17Z
Generation: 1
Resource Version: 126786
UID: 5d2e1559-03f1-43a5-af13-f46e434821c0
Spec:
Addresses:
172.19.0.100-172.19.0.200
Auto Assign: true
Avoid Buggy I Ps: false
Status:
assignedIPv4: 1
assignedIPv6: 0
availableIPv4: 100
availableIPv6: 0
Events: <none>

这里,metallb-system 是 MetalLB 组件运行的 命名空间(Namespace),命名空间是 Kubernetes 中用于 隔离资源 的逻辑分区,例如 k8s 核心组件本身运行在 kube-system 命名空间,用户应用运行在 default 命名空间,而 MetalLB 组件运行在 metallb-system 命名空间。

  • controller:主要用于监听 Service,分配 IP,通过 Deployment(1个副本)运行

    • 监听 Kubernetes API,发现 type=LoadBalancer 的 Service
    • 如果 Service 的 EXTERNAL-IP 为空,从 IPAddressPool 选择 IP
    • 更新 Service 的 EXTERNAL-IP 字段
    • 记录分配状态
  • speaker:响应 ARP/BGP,宣布节点拥有 LoadBalancer IP,通过 DaemonSet(每个节点一个副本)运行。Layer2 模式工作 流程:

    • 监听 IP 分配事件
    • 当某节点被选中,开始响应 ARP 请求
    • ARP 响应告诉网络:LoadBalancer IP 在我这台节点上
    • 流量到达该节点,由 kube-proxy 转发

IPAddressPoolL2Advertisement 则是 MetalLB 自定义资源(Custom Resource Definition,CRD),用于配置 IP 地址池和 Layer2 通告。

检查 Service 状态

安装 MetalLB 后,之前创建的 LoadBalancer Service 会自动获得外部 IP:

1
2
3
4
# kubectl get svc nginx-lb

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb LoadBalancer 10.104.44.36 172.19.0.100 80:32617/TCP 3h23m

可以看到,MetalLB 从地址池分配为该 Service 分配了 172.19.0.100 IP 地址。

服务访问测试

接下来访问该 172.19.0.100 这个 EXTERNAL-IP IP 地址,可以看到页面正常返回:

1
2
3
4
5
# --noproxy 关闭宿主机的代理配置
# curl -I --noproxy "*" http://172.19.0.100
HTTP/1.1 200 OK
Server: nginx/1.29.8
......

整个请求流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
客户端请求 http://172.19.0.100/

│ ① ARP 请求: "谁是 172.19.0.100?"


┌───────────────────────────────────────────────────────────┐
│ 某 Worker 节点 (例如 worker-1) │
│ metallb-speaker (在此节点上) 应答 ARP: │
"172.19.0.100 的 MAC 地址是 节点网卡的 MAC"
└───────────────────────────────────────────────────────────┘

│ ② 流量被路由到该 Worker 节点的物理/虚拟网卡
│ (因为 ARP 告诉客户端这个 IP 的 MAC 属于这个节点)

┌───────────────────────────────────────────────────────────┐
│ Worker 节点网卡收到包,进入内核协议栈 │
│ iptables / IPVS 规则 (由 kube-proxy 维护) 捕获目的 IP │
│ 为 172.19.0.100:80 的流量 │
└───────────────────────────────────────────────────────────┘

│ ③ 规则将 VIP 转换为 Service 的 ClusterIP
│ (做 DNAT: 172.19.0.100:80 → 10.104.44.36:80)

┌───────────────────────────────────────────────────────────┐
│ nginx-lb Service │
│ ClusterIP: 10.104.44.36 (虚拟 IP,只存在于规则中) │
│ 选择器: app: nginx │
└───────────────────────────────────────────────────────────┘

│ ④ kube-proxy 规则再将 ClusterIP 负载均衡到后端 Pod
│ (再次 DNAT: 10.104.44.36:80 → 某个 Pod IP:80)

┌───────────────────────────────────────────────────────────┐
│ nginx Pod (例如 10.244.1.2) │
│ 返回 nginx 响应页面 │
└───────────────────────────────────────────────────────────┘

我们可以看下对应的 ARP 表,验证 Layer2 模式的工作原理:

1
2
3
4
5
6
# arp -an | grep 172.19.0.100
? (172.19.0.100) at 36:9d:8e:26:b2:43 [ether] on br-e8a629bdbe41

# docker exec -it k8s-cluster-worker ip link show | grep 36:9d:8e:26:b2:43 -B 1
2: eth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 36:9d:8e:26:b2:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

这里也可以看到 Layer2 模式的特点:

  • 单节点响应 ARP,非真正多节点负载均衡
  • 该节点故障时,speaker 会迁移到其他节点
  • 适合小规模测试环境

以上我们就通过 MetalLB 部署了 Kubernetes 的 LoadBalancer 服务。接下来简单介绍下 k8s 的 ExternalName 服务Headless 服务

ExternalName 服务

ExteranlName 服务 是 Kubernetes 提供的一种特殊类型的 Service,它允许你将服务映射到集群外部的域名,这对于需要将内部服务和外部系统集成时非常有用。ExternalName 服务 不指向集群内部的任何 Pod,而是指向集群外部的一个域名。

  • 它不涉及任何代理(Proxy)或转发(Forwarding),它在 DNS 层面上工作
  • 当你访问这个 Service 的域名时,Kubernetes DNS 会返回一个 CNAME 记录,指向你指定的外部域名

如下是一个配置示例:

1
2
3
4
5
6
7
kind: Service
apiVersion: v1
metadata:
name: my-database
spec:
type: ExternalName
externalName: rds.aliyun.com # 外部数据库地址

那为什么不直接在代码中编写外部域名呢:

  • 解耦(隔离变化):代码里只需要访问集群内部域名 http://my-database,如果以后数据库迁移了(比如从阿里云切到腾讯云),你只需要修改 Service 的配置,而不需要修改并重新发布代码
  • 统一管理:让外部资源在 K8s 内部像普通 Service 一样被管理

Headless 服务

Headless 服务(也称为无头服务)没有 ClusterIP。当你请求 DNS 查询这个 Service 的域名时,DNS 不会返回一个虚拟 IP,而是直接返回所有后端 Pod 的具体 IP 列表。要定义 Headless 服务,只需在 Service 的 YAML 文件中设置 clusterIP: None

Headless 服务 的核心特点:

  • 直接暴露 Pod:客户端可以绕过 Service 层的负载均衡,直接与特定的 Pod 建立连接
  • DNS 记录:DNS 会为每个 Pod 生成唯一的域名,格式为:<pod-name>.<service-name>.<namespace>.svc.cluster.local
  • 用于有状态应用:最常用于 StatefulSet(有状态应用,如 MongoDB、Redis 集群、Kafka)、客户端 LB(客户端自己决定访问哪个节点)、需要稳定的 Pod 域名
1
2
3
4
5
普通 Service:
Pod → Service 名称 → DNS 返回 ClusterIP → kube-proxy 负载均衡 → Pod

Headless Service:
Pod → Service 名称 → DNS 返回所有 Pod IP → 客户端自行选择 Pod

小结

这篇文章我们重点学习了 k8s 的 LoadBalancer 服务,并通过 MetalLB 在 Kind 环境中实际部署了一个 LoadBalancer 服务。我们还介绍了 k8s 的 ExternalNameHeadless 两种特殊类型的服务,他们都有各自的应用场景。

Reference