3 节点 K8s 1.34 集群部署实录:Ubuntu 24.04 + Calico eBPF + Knative 就绪
背景与需求
在一台 MacBook Pro M4 Pro(24GB)上,通过 Parallels Desktop 创建 3 台 Ubuntu 24.04 ARM64 虚拟机,部署一套同时兼顾传统微服务与高性能 Knative Serverless 的 Kubernetes 集群。
基础设施规划
| 节点 | 主机名 | IP | 角色 |
|---|---|---|---|
| VM 1 | k8s-master | 10.211.55.6 | control-plane |
| VM 2 | k8s-worker1 | 10.211.55.7 | worker |
| VM 3 | k8s-worker2 | 10.211.55.8 | worker |
核心版本选型
- Kubernetes:v1.34.8
- Containerd:1.7.29(SystemdCgroup=true)
- Calico:v3.29.2(Operator 模式,eBPF 数据面)
- 内核:6.8.0-40-generic(BTF 已启用,eBPF CO-RE 可用)
- Pod 网络:192.168.0.0/16(VXLAN CrossSubnet)
- Service 网络:10.96.0.0/12
部署架构:模块化 Skill 脚本
将全套流程拆分为 3 个独立、幂等的 Shell 脚本,存放于项目仓库 boat/k8s-deploy/。
Skill 1:01-all-nodes-env.sh — 通用环境配置
在所有节点(包括 Master)上执行,接收主机名作为参数。
核心功能:
bash 01-all-nodes-env.sh k8s-master # Master 节点
bash 01-all-nodes-env.sh k8s-worker1 # Worker 节点
bash 01-all-nodes-env.sh k8s-worker2 # Worker 节点
- 主机名与 hosts 映射:自动修改 hostname,写入
/etc/hosts集群 IP 映射 - 关闭 Swap:
swapoff -a+ 注释/etc/fstab - 加载内核模块:
overlay、br_netfilter、nf_conntrack - sysctl 优化(Serverless 关键):
fs.inotify.max_user_watches = 524288← Pod 冷启动感知net.netfilter.nf_conntrack_max = 262144← 高弹缩连接跟踪fs.file-max = 2097152← 短生命周期 Pod 句柄
- 安装依赖:socat、conntrack、ipset、ipvsadm
- 配置 Containerd:
SystemdCgroup = true(与 K8s cgroup v2 对齐)sandbox_image = registry.aliyuncs.com/google_containers/pause:3.10.1(国内加速)
- 安装 nerdctl +
alias docker='nerdctl -n k8s.io'(兼容 Docker 肌肉记忆) - 安装 K8s 1.34:通过
pkgs.k8s.io官方源(国内 CDN 可达),使用apt-mark hold锁定版本
Skill 2:02-master-init.sh — 控制平面初始化
仅在 Master 执行一次。
bash 02-master-init.sh 10.211.55.6
kubeadm init指定 Pod CIDR + Service CIDR + 阿里云镜像仓库- 配置
~/.kube/config和 root 用户凭证 - 部署 Calico Tigera Operator + Installation CR
- Calico eBPF 数据面切换(关键优化):
bpfEnabled: true bpfExternalServiceMode: "DSR" bpfKubeProxyIptablesCleanupEnabled: true - 输出
kubeadm join完整令牌命令
Skill 3:03-worker-join.sh — 工作节点加入
在 Worker 节点上执行,支持交互式粘贴 join 命令或 auto 自动获取。
实战踩坑记录
坑 1:containerd 默认禁用 CRI 插件
现象:kubeadm init 报错 unknown service runtime.v1.RuntimeService
原因:containerd config default 生成的配置文件包含 disabled_plugins = ["cri"]
修复:
sudo sed -i 's/disabled_plugins = \["cri"\]/disabled_plugins = []/' /etc/containerd/config.toml
sudo systemctl restart containerd
坑 2:Pause 镜像无法拉取
现象:Failed to create sandbox: failed to pull image "registry.k8s.io/pause:3.8": i/o timeout
原因:国内无法直接访问 registry.k8s.io
修复:预配置 kubelet 的 pause 镜像为阿里云源
sudo mkdir -p /var/lib/kubelet
echo 'KUBELET_KUBEADM_ARGS="--pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.10.1"' \
| sudo tee /var/lib/kubelet/kubeadm-flags.env
坑 3:GPG 密钥导入 TTY 问题
现象:gpg: cannot open '/dev/tty': No such device or address
原因:非交互 SSH 会话没有 TTY
修复:添加 --batch --yes 参数
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.34/deb/Release.key \
| sudo gpg --dearmor --batch --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
坑 4:Calico 镜像国内不可达
现象:calico-node 持续 ImagePullBackOff,所有 docker.io 镜像超时
原因:阿里云、DaoCloud 等通用镜像源不缓存 Calico 镜像;K8s APT 包可走官方 CDN,但镜像不行
解决方案:利用已有的腾讯云 CVM(配置了 mirror.ccs.tencentyun.com 镜像加速)
# 在腾讯云 CVM 上拉取(使用 Tencent 企业镜像)
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/node:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/typha:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/kube-controllers:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/csi:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/cni:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/node-driver-registrar:v3.29.2
docker pull --platform linux/arm64 mirror.ccs.tencentyun.com/calico/pod2daemon-flexvol:v3.29.2
# 导出为 tar(549MB)
docker save mirror.ccs.tencentyun.com/calico/* -o calico-arm64.tar
# 传输到 K8s 节点并导入 containerd
scp calico-arm64.tar k8s-master:/tmp/
ssh k8s-master "sudo ctr -n k8s.io images import /tmp/calico-arm64.tar"
# Calico Installation 指定镜像仓库
kubectl apply -f - <<EOF
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
registry: mirror.ccs.tencentyun.com
calicoNetwork:
ipPools:
- cidr: 192.168.0.0/16
encapsulation: VXLANCrossSubnet
EOF
坑 5:Containerd 镜像加速器配置演变
注意:containerd 1.5+ 已弃用 mirrors 配置,必须使用 config_path 模式。
正确配置方式:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
# /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://docker.mirrors.ustc.edu.cn"
[host."https://registry-1.docker.io"]
capabilities = ["pull", "resolve"]
坑 6:2GB 内存不足以跑 eBPF
现象:启用 eBPF 后 kube-controller-manager 和 kube-scheduler 频繁 CrashLoopBackOff,节点内存仅剩 86MB
原因:eBPF 程序 + Calico-node + etcd + apiserver + controller + scheduler 全跑在 2GB Master 上,内存耗尽
解决:关闭 eBPF(bpfEnabled: false),对于 2GB 节点,iptables 模式更稳定。eBPF 模式建议节点 ≥ 4GB。
部署 Web 应用到集群
将 boat/web/index.html(STM32 蓝牙遥控小船项目介绍页)部署到 K8s:
apiVersion: apps/v1
kind: Deployment
metadata:
name: boat-web
spec:
replicas: 2
selector:
matchLabels:
app: boat-web
template:
spec:
containers:
- name: nginx
image: docker.io/library/nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: web-content
mountPath: /usr/share/nginx/html
volumes:
- name: web-content
hostPath:
path: /var/www/boat
---
apiVersion: v1
kind: Service
metadata:
name: boat-web
spec:
type: NodePort
selector:
app: boat-web
ports:
- port: 80
targetPort: 80
nodePort: 30080
访问地址:http://10.211.55.7:30080/
最终集群状态
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP
k8s-master Ready control-plane 91m v1.34.8 10.211.55.6
k8s-worker1 Ready <none> 90m v1.34.8 10.211.55.7
k8s-worker2 Ready <none> 90m v1.34.8 10.211.55.8
$ kubectl get pods -A
NAMESPACE NAME READY STATUS
calico-system calico-node-* 1/1 Running
calico-system calico-typha-* 1/1 Running
calico-system csi-node-driver-* 2/2 Running
default boat-web-* 1/1 Running
kube-system coredns-* 1/1 Running
kube-system etcd-k8s-master 1/1 Running
kube-system kube-apiserver-k8s-master 1/1 Running
快速开始(全新环境部署)
# 1. 配置 SSH 免密登录
ssh-copy-id k8s-master && ssh-copy-id k8s-worker1 && ssh-copy-id k8s-worker2
# 2. 配置免密 sudo(在每台 VM 上)
echo "parallels ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/parallels
# 3. 克隆部署脚本
git clone https://gitee.com/caobin/boat.git
cd boat/k8s-deploy
# 4. 三台节点并行执行环境配置
ssh k8s-master "bash 01-all-nodes-env.sh k8s-master" &
ssh k8s-worker1 "bash 01-all-nodes-env.sh k8s-worker1" &
ssh k8s-worker2 "bash 01-all-nodes-env.sh k8s-worker2" &
wait
# 5. Master 初始化
ssh k8s-master "bash 02-master-init.sh 10.211.55.6"
# 6. Worker 加入(使用 Master 输出的 join 命令)
ssh k8s-worker1 "bash 03-worker-join.sh auto"
ssh k8s-worker2 "bash 03-worker-join.sh auto"
# 7. 验证
kubectl get nodes -o wide
总结
| 维度 | 选型 | 说明 |
|---|---|---|
| 容器运行时 | containerd 1.7.29 | K8s 1.24+ 推荐,轻量高效 |
| 网络插件 | Calico v3.29.2 | 支持 eBPF 数据面,低延迟 |
| 数据面 | iptables(生产建议 eBPF ≥4GB) | 2GB 节点用 iptables 更稳 |
| 镜像加速 | Tencent 企业镜像 + USTC | 多级 fallback |
| 部署方式 | 模块化 Shell 脚本 | 幂等、可复用、注释详尽 |
Git 仓库:https://gitee.com/caobin/boat (k8s-deploy/ 目录)
本文完整的部署 Prompt:
「你是一个精通 Kubernetes、Linux 内核网络优化以及高级 Shell 脚本编写的资深 Devops 架构师。请为我编写一套在 3 台全新安装的 Ubuntu 24.04 虚拟机上部署 K8s 1.34 集群的完整、可直接复制运行的 Shell 脚本和操作指南。该集群需要同时完美兼顾"传统微服务部署"与"高性能 Knative Serverless 服务"的运行要求…」
附录:完整原始部署 Prompt
# Role
你是一个精通 Kubernetes、Linux 内核网络优化以及高级 Shell 脚本编写的资深 Devops 架构师。
# Task
请为我编写一套在 3 台全新安装的 Ubuntu 24.04 虚拟机上部署 K8s 1.34 集群的完整、可直接复制运行的 Shell 脚本和操作指南。该集群需要同时完美兼顾"传统微服务部署"与"高性能 Knative Serverless 服务"的运行要求。
# Infrastructure Info
- 节点规划与主机名修改逻辑:
- 主机 1:修改主机名为 `k8s-master` ip 10.211.55.6
- 主机 2:修改主机名为 `k8s-worker1` ip 10.211.55.7
- 主机 3:修改主机名为 `k8s-worker2` ip 10.211.55.8
- 所有主机的用户名 parallels/sudo 密码均为:`P@ssw0rdA`
- 所有主机名称修改完成后,使用ssh-copy-id 在~/.ssh/config配置免密登录
- 核心版本:Kubernetes v1.34 / Containerd 1.7+ / nerdctl 最新稳定版
- 网络配置:Pod 网络网段 (CIDR) 为 `192.168.0.0/16`
- 网络插件:Calico v3.27+,必须在初始化后自动或通过脚本引导切换为高性能 eBPF 路由模式。
# Script Design Requirements (模块化 Skill 技能包架构)
请将全套部署流程严格拆分为以下 3 个独立的脚本文件形成skill,确保具备基本的幂等性(重复执行不破坏系统),且注释清晰、可读性强:
## 【Skill 1:通用基础环境配置skill脚本】
- 文件名建议:`01-all-nodes-env.sh`
- 执行说明:在 3 台机器上均需运行,支持传入参数作为主机名(如 `bash 01-all-nodes-env.sh k8s-master`)。
- 核心功能:
1. 自动化修改主机名,并在 `/etc/hosts` 中预留集群 IP 映射模板。
2. 永久关闭 Swap 分区。
3. 加载 `overlay` 和 `br_netfilter` 内核模块。
4. 优化 `sysctl` 系统参数,针对 Serverless 高并发弹缩场景,大幅调高系统文件句柄、inotify 监视上限(`fs.inotify.max_user_watches = 524288`)以及连接跟踪表上限(`net.netfilter.nf_conntrack_max`)。
5. 安装 `socat`、`conntrack`、`ipset`、`ipvsadm` 等核心依赖。
6. 安装并配置 Containerd,必须显式开启 `SystemdCgroup = true`,并配置国内镜像加速器(如阿里云镜像源)。
7. 安装 `nerdctl` 并设置 shell 别名 `alias docker='nerdctl -n k8s.io'`,确保习惯使用 docker 命令的开发者拥有无缝的底层排查体验。
8. 引入国内 K8s APT 源,安装并锁定 `kubeadm`、`kubelet`、`kubectl` 的 1.34 版本。
## 【Skill 2:控制平面初始化与 Calico eBPF 优化skill脚本】
- 文件名建议:`02-master-init.sh`
- 执行说明:仅在 `k8s-master` 节点上执行一次。
- 核心功能:
1. 自动获取或允许定义 Master IP,执行 `kubeadm init` 初始化集群(指定 1.34 版本、正确的 Pod CIDR 和阿里云镜像仓库)。
2. 自动配置当前用户及 root 用户的 `.kube/config` 凭证。
3. 部署 Calico 官方 Operator 及自定义资源清单(确保网络网段严格匹配)。
4. **关键网络优化:** 包含完整的自动化指令或配置补丁,将 Calico 的数据面切换为 **eBPF 模式**(移除 Kube-Proxy 的 iptables 拦截,改为 BPF 驱动,开启 DSR 模式),以支撑 Serverless 服务极低的网络延迟和毫秒级冷启动就绪需求。
5. 在脚本末尾清晰打印用于 Worker 节点加入的 `kubeadm join` 完整令牌命令。
## 【Skill 3:工作节点加入skill脚本】
- 文件名建议:`03-worker-join.sh`
- 执行说明:在 `k8s-worker1` 和 `k8s-worker2` 上执行。
- 核心功能:
1. 提供干净、可交互的命令块,引导用户粘贴来自 Master 节点的 `kubeadm join` 认证命令并自动执行。
# Output Format
1. 请提供 3 个相互独立、结构清晰、带有对应文件名注释的skill代码块。
2. 脚本内部关键步骤必须包含中文详尽注释,阐明其设计原理(如为何开启 eBPF、为何锁定 K8s 版本等)。
Clash TUN 代理配置(解决 VM 网络受限)
VM 通过 Parallels NAT 共享 Mac 网络,但无法直连 Docker Hub。在 Mac 上开启 Clash (mihomo-party) 的 TUN 模式,VM 流量经 Mac 代理出口。
mihomo.yaml 关键配置
tun:
enable: true
stack: mixed
auto-route: true
dns-hijack:
- any:53
route-exclude-address:
- 10.211.55.0/24 # 排除 Parallels 虚拟机网段,避免 Parallels 误判盗版
- 10.37.129.0/24 # 排除 Host-Only 网段
⚠️ 必须添加
route-exclude-address排除 Parallels 网段,否则 Parallels Desktop 会因网络被代理而提示"非正版"。
containerd HTTP 代理(备选方案)
TUN 对大镜像(Dashboard 等走 CloudFront CDN 的)存在 EOF 断连问题。备选方案是配置 containerd 使用 Clash HTTP 代理:
# /etc/systemd/system/containerd.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://10.211.55.1:7890"
Environment="HTTPS_PROXY=http://10.211.55.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,192.168.0.0/16"
sudo -S -p '' systemctl daemon-reload && sudo -S -p '' systemctl restart containerd
实测 alpine、busybox 等小镜像可正常拉取;大镜像受限于 Clash 代理的 HTTPS CONNECT 转发稳定性。
Kubernetes Dashboard 部署
镜像导入
Dashboard 镜像在国内同样无法直拉,通过腾讯云 CVM 中转:
# 腾讯云拉取
ssh tencent-crm "docker pull --platform linux/arm64 kubernetesui/dashboard:v2.7.0"
ssh tencent-crm "docker pull --platform linux/arm64 kubernetesui/metrics-scraper:v1.0.9"
ssh tencent-crm "docker save kubernetesui/dashboard:v2.7.0 kubernetesui/metrics-scraper:v1.0.9 -o /tmp/dashboard-arm64.tar"
# 传输并导入到 3 节点
scp tencent-crm:/tmp/dashboard-arm64.tar /tmp/
for node in k8s-master k8s-worker1 k8s-worker2; do
scp /tmp/dashboard-arm64.tar $node:/tmp/
ssh $node "sudo ctr -n k8s.io images import /tmp/dashboard-arm64.tar && rm /tmp/dashboard-arm64.tar"
done
RBAC 与 Deployment
# ServiceAccount + ClusterRoleBinding
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kubernetes-dashboard
---
# Deployment(关键:mount /tmp emptyDir 避免非 root 容器日志写入失败)
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
replicas: 1
selector:
matchLabels:
k8s-app: kubernetes-dashboard
template:
spec:
serviceAccountName: admin-user
containers:
- name: kubernetes-dashboard
image: docker.io/kubernetesui/dashboard:v2.7.0
ports:
- containerPort: 8443
args:
- --auto-generate-certificates
- --namespace=kubernetes-dashboard
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
type: NodePort
ports:
- port: 443
targetPort: 8443
nodePort: 30443
selector:
k8s-app: kubernetes-dashboard
🐛 踩坑:Dashboard v2.7.0 非 root 容器 /tmp 不可写
现象:Pod 启动后立即 CrashLoopBackOff,日志显示 cannot create log: open /tmp/... no such file or directory
原因:Dashboard v2.7.0 默认以非 root 运行,/tmp 目录不可写导致日志初始化失败。
修复:mount emptyDir 到 /tmp(见上面 Deployment YAML 的 volumes 配置)。
🐛 踩坑:Token 末尾的 % 符号
现象:kubectl create token 输出的 Token 粘贴到 Dashboard 登录框提示 “Invalid credentials”
原因:zsh 在命令输出末尾自动添加 % 作为行尾标记(partial-line marker),复制时容易带进去。
修复:获取 Token 时过滤:
kubectl -n kubernetes-dashboard create token admin-user --duration=24h | tr -d '\n%'
访问方式
由于 Dashboard 使用自签名证书,通过 SSH 隧道 + kubectl proxy 绕过 HTTPS 证书问题:
# Mac 终端执行 SSH 隧道
ssh -L 8001:127.0.0.1:8001 k8s-master "kubectl proxy --port=8001"
浏览器打开:http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/,粘贴 Token 登录。
最终集群全景
| 组件 | 版本/配置 | 状态 |
|---|---|---|
| Kubernetes | v1.34.8 | ✅ 3 节点 Ready |
| containerd | 1.7.29 SystemdCgroup | ✅ |
| Calico | v3.29.2 iptables | ✅ VXLAN CrossSubnet |
| eBPF | 已启用 (4GB Master) | ✅ DSR + kube-proxy bypass |
| boat-web | NodePort 30080 | ✅ http://10.211.55.7:30080/ |
| Dashboard | v2.7.0 (kubectl proxy) | ✅ localhost:8001 |
| 镜像方案 | 腾讯云 tar 导入 + Clash TUN | ✅ |
生产高可用:MetalLB + NodePort 实现故障无感切换
问题
NodePort 模式下,用户必须知道具体节点 IP。如果访问的节点宕机,服务中断:
❌ http://10.211.55.7:30080/ ← worker1 关机 → 不可用
✅ http://10.211.55.8:30080/ ← worker2 正常 → 可用
生产环境必须通过域名 → 虚拟 IP → 健康检查,实现任意节点宕机后自动切换。
方案选型
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| MetalLB L2 模式 | 裸金属 / 本地 VM 集群 | ⭐⭐ 推荐 |
| 云 LB(SLB/ELB/CLB) | 云上 K8s | ⭐ |
| 外部反向代理(Nginx/HAProxy) | 已有独立 LB 基础设施 | ⭐⭐⭐ |
| Ingress Controller + 外部 LB | 需要 7 层路由/SSL 终止 | ⭐⭐⭐ |
MetalLB Layer 2 架构
用户浏览器
│
▼
boat.example.com ──DNS──▶ 192.168.0.100 (VIP, 由 MetalLB 分配)
│
┌─────────────┼─────────────┐
▼ ▼ ▼
k8s-master k8s-worker1 k8s-worker2
(NodePort) (NodePort) (NodePort)
│ │ │
└─────────────┼─────────────┘
▼
boat-web Pod
(任意存活节点)
工作流程:
- MetalLB 从地址池分配一个虚拟 IP(如
192.168.0.100) - 集群中某个存活节点通过 ARP 应答声称拥有此 IP
- 所有流量到达该节点 → kube-proxy/BPF → 分发到健康 Pod
- 当该节点宕机,另一个节点在几秒内接管 VIP
- DNS 始终指向同一 VIP,用户完全无感
部署步骤
# 1. 安装 MetalLB(K8s 1.34 兼容 v0.14+)
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml
# 2. 等待 MetalLB 就绪
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=120s
# 3. 配置 IP 地址池(与 Node 同网段)
kubectl apply -f - <<EOF
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: boat-pool
namespace: metallb-system
spec:
addresses:
- 10.211.55.100-10.211.55.110 # 与 VM 同网段,选未占用的 IP
EOF
# 4. 创建 L2 广播通告
kubectl apply -f - <<EOF
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: boat-l2
namespace: metallb-system
spec:
ipAddressPools:
- boat-pool
EOF
# 5. 将 Service 改为 LoadBalancer 类型
kubectl patch svc boat-web -p '{"spec":{"type":"LoadBalancer"}}'
# 6. 查看分配的 VIP
kubectl get svc boat-web
# EXTERNAL-IP: 10.211.55.100 ← 这就是虚拟 IP
故障切换测试
# 拿到 VIP 后,域名指向它
echo "10.211.55.100 boat.bmap.xyz" | sudo -S -p '' tee -a /etc/hosts
# 持续访问(不中断)
while true; do curl -s -o /dev/null -w "%{http_code}\n" http://boat.bmap.xyz:30080/; sleep 1; done
# 另开终端,关掉当前持有 VIP 的节点
# 观察 HTTP 状态码是否从 200 → 短暂中断(几秒) → 恢复 200
典型切换时间:2-5 秒(ARP 广播 + 新节点接管)。
与云 LB 对比
| 特性 | MetalLB L2 | 云 LB |
|---|---|---|
| 流量路径 | 所有流量经单节点 | 云平台分发到所有节点 |
| 带宽瓶颈 | 受单节点网卡限制 | 无单点瓶颈 |
| 切换速度 | 2-5 秒(ARP) | 通常 <1 秒 |
| 成本 | 免费 | 按流量/实例计费 |
| 适用场景 | 裸金属、VM、边缘 | 云上生产环境 |
结论:小规模集群(3-10 节点)MetalLB L2 完全够用。流量大到单节点网卡瓶颈时,升级到 Ingress + 云 LB 或 MetalLB BGP 模式。
K8s 1.34 集群部署实录:Ubuntu 24.04 ARM64 + Calico + eBPF
https://bmap.xyz/archives/k8s-deploy-post