从零搭建K8s 集群

从零搭一套生产可用的 K8s 高可用集群(Rocky Linux 9 · kubeadm · 3 主 4 工)

面向:有 Linux 基础、想在自己机器/机房里落一套「能扛住单节点故障」的 K8s 集群的同学。

环境:Rocky Linux 9.x × 10 台(2C/8G 起)· Kubernetes v1.36 · containerd 2.x · Calico · Keepalived + HAProxy · Harbor。

目标:不是「跑个 demo」,而是 7 节点 Ready、VIP 可访问、私有仓库可拉、监控面板能用、能备份能恢复。


一、先想清楚:为什么要「高可用」而不是「单 Master」

单 Master 的 kubeadm 集群,学起来够用,但只要那台机器重启,kubectl 就全挂------控制面没有第二个副本,etcd 只有一份数据。生产环境里这不是「可用性差一点」,而是「一次内核升级就全网停摆」。

高可用要解决两件事:

问题 解法 本方案
API Server 挂了怎么办 多个 apiserver + 一个虚拟 IP 在前 3 台 Master + Keepalived VIP + HAProxy
etcd 数据只有一份 etcd 奇数副本(3/5) 3 台 Master 各跑一个 etcd

一句话记住顺序 :LB 必须先于 kubeadm init 。因为 controlPlaneEndpoint 指向 VIP,init 过程中会通过 VIP 去写 ClusterRoleBinding;VIP 还没挂上,init 就会卡在 upload-config 阶段报 context deadline exceeded。这个坑我踩过,排查了两小时。


二、规划表(先画表,再动手)

主机名 IP 角色 说明
lb01 192.168.5.20 Keepalived MASTER + HAProxy VIP 持有者
lb02 192.168.5.21 Keepalived BACKUP + HAProxy 秒级接管
k8s-m1 192.168.5.22 Master1 + etcd 第一个 init
k8s-m2 192.168.5.23 Master2 + etcd join
k8s-m3 192.168.5.24 Master3 + etcd join
k8s-w1~w4 192.168.5.25--27 / .29 Worker ×4 跑业务
harbor 192.168.5.28 私有镜像仓库 可选但强烈建议
  • VIP :192.168.5.200(必须和节点同网段、未被占用)
  • Pod 网段 :10.244.0.0/16;Service 网段 :10.96.0.0/12(与宿主机网段不冲突即可)
  • 规格底线 :Master 2C/4G 起(etcd 吃磁盘 IO,务必 SSD);Worker 按业务给。内存小于 2G 的节点 kubeadm 会直接拒绝。

三、十步落地

步骤 0:所有节点统一基础环境

bash 复制代码
# 关 swap(kubelet 硬要求)
swapoff -a && sed -ri 's/^([^#].*swap.*)$/#\1/' /etc/fstab

# 关 SELinux(或设 permissive)
setenforce 0 && sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

# 内核模块 + 转发
cat >/etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay && modprobe br_netfilter

cat >/etc/sysctl.d/99-k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system

# 时间同步(etcd 对时钟敏感)
dnf install -y chrony && systemctl enable --now chronyd

坑位 :模板机/克隆机常带失效的旧 yum 源 (指向已 404 的镜像站),dnf install 会卡 5 分钟才报错。先 dnf repolist 看一眼,把死的源 enabled=0。

步骤 1:换国内源 + 装 containerd

bash 复制代码
# Rocky 9 阿里云源(注意路径是 /rockylinux/9/,不是 /9.5/)
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
    -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
    -i.bak /etc/yum.repos.d/rocky*.repo
dnf makecache

# containerd(用 docker-ce 仓库里的包)
dnf config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
dnf install -y containerd.io

containerd 配置(2.x 关键差异):

bash 复制代码
containerd config default > /etc/containerd/config.toml

要改三处:

toml 复制代码
# 1) cgroup 驱动必须 systemd(否则 kubelet 起来节点 NotReady)
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
  SystemdCgroup = true

# 2) sandbox 镜像换成国内可达的
[plugins.'io.containerd.cri.v1.images'.pinned_images]
  sandbox = 'registry.aliyuncs.com/google_containers/pause:3.10'

# 3) 私有仓库证书目录(2.x 段名变了!)
[plugins.'io.containerd.cri.v1.images'.registry]
  config_path = '/etc/containerd/certs.d'

最容易翻车的一条 :containerd 1.x 的段名是 io.containerd.grpc.v1.cri,2.x 改成了 io.containerd.cri.v1.images 。段名写错,certs.d 静默不生效,私有仓库一直 ImagePullBackOff,而日志里一个字都不提示。

第二条 :sed 改配置千万别用 .. 通配。我用 s|config_path = ..|...| 时误伤了 plugin_config_path,把 containerd 配置写坏,全集群容器停摆 3 分钟。改前 cp config.toml config.toml.bak,改后 containerd config dump >/dev/null 校验再 restart。

步骤 2:装 kubeadm / kubelet / kubectl

bash 复制代码
cat >/etc/yum.repos.d/kubernetes.repo <<'EOF'
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core/stable/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core/stable/v1.36/rpm/repodata/repomd.xml.key
EOF
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
systemctl enable --now kubelet      # 此时会 CrashLoop,正常,等 init

步骤 3:先起 LB(顺序不能反)

lb01 / lb02 各装 keepalived + haproxy:

bash 复制代码
dnf install -y keepalived haproxy

/etc/keepalived/keepalived.conf(lb02 只改 state BACKUP 和 priority 90):

conf 复制代码
vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -20
}
vrrp_instance VI_1 {
    state MASTER
    interface ens160
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication { auth_type PASS; auth_pass k8s-lab }
    virtual_ipaddress { 192.168.5.200/24 }
    track_script { chk_haproxy }
}

/etc/haproxy/haproxy.cfg 关键段:

conf 复制代码
frontend k8s-api
    bind *:6443
    mode tcp
    default_backend k8s-masters

backend k8s-masters
    mode tcp
    balance roundrobin
    option tcp-check
    server k8s-m1 192.168.5.22:6443 check
    server k8s-m2 192.168.5.23:6443 check
    server k8s-m3 192.168.5.24:6443 check

验证:ip -o -4 addr show ens160 | grep 200 能看到 VIP;curl -k https://192.168.5.200:6443/version 此时还连不上(apiserver 没起),这是正常的。

步骤 4:kubeadm init(配置文件比命令行可靠)

kubeadm-config.yaml:

yaml 复制代码
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.5.22
  bindPort: 6443
nodeRegistration:
  name: k8s-m1                      # 不写就是 "node",后面改名很麻烦
  criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
clusterName: k8s-lab
controlPlaneEndpoint: 192.168.5.200:6443     # ← 必须写在 ClusterConfiguration 段!
kubernetesVersion: v1.36.0
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
apiServer:
  certSANs:
  - 192.168.5.200
  - 192.168.5.22
  - 192.168.5.23
  - 192.168.5.24
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
bash 复制代码
kubeadm init --config kubeadm-config.yaml --upload-certs | tee init.log

两个静默坑:

  1. controlPlaneEndpoint 属于 ClusterConfiguration 段。用 sed 插到 localAPIEndpoint 前面的话,它会落到 InitConfiguration 段里------kubeadm 不报错、静默忽略 ,join 命令退回单点 IP,高可用形同虚设。init 完一定 grep controlPlaneEndpoint init.log 验证 join 命令里是 VIP。
  2. nodeRegistration.name 默认是 node,不改的话第一个 Master 的节点名就叫 node。

步骤 5:装 CNI(Calico)

bash 复制代码
curl -fsSL https://cdn.jsdelivr.net/gh/projectcalico/calico@v3.32.2/manifests/calico.yaml -o calico.yaml
sed -i 's|value: "192.168.0.0/16"|value: "10.244.0.0/16"|' calico.yaml     # 用 | 做分隔符,别用 /
kubectl apply -f calico.yaml

raw.githubusercontent.com 和 ghproxy 在国内大概率不通,jsdelivr 实测稳定 。

sed 替换串里带 / 时,分隔符必须换 |,否则报 s 的未知选项。

步骤 6:其余节点 join

bash 复制代码
# 在 m1 上生成
kubeadm token create --print-join-command

# control-plane 节点额外带 --control-plane --certificate-key(init 时 --upload-certs 输出的那条)

步骤 7:私有仓库(Harbor)

bash 复制代码
# 自签证书必须带 SAN,否则 containerd 校验不过
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout harbor.key -out harbor.crt \
  -subj "/CN=harbor" -addext "subjectAltName=IP:192.168.5.28,DNS:harbor"

# 顺序很重要:先 load 镜像,再 prepare,最后 compose up
docker load -i harbor.v2.11.1.tar.gz
./prepare
docker compose up -d

每个 K8s 节点分发 CA:

bash 复制代码
mkdir -p /etc/containerd/certs.d/192.168.5.28
cp harbor.crt /etc/containerd/certs.d/192.168.5.28/ca.crt

坑 :docker push harbor/library/nginx:v1 会跑去 Docker Hub 找镜像------镜像名首段不含 . 或 : 且不是 localhost 时,docker 不认它是 registry 。统一用 IP:192.168.5.28/library/nginx:v1。

步骤 8:metrics-server(kubectl top 靠它)

三个必要件缺一不可:ClusterRole system:metrics-server、APIService v1beta1.metrics.k8s.io、Service selector 与 Pod 标签一致 。少任何一个的表现分别是:nodes is forbidden、Metrics API not available、MissingEndpoints。

步骤 9:监控面板(Prometheus + Grafana + node-exporter)

node-exporter 用 DaemonSet + hostNetwork,prometheus 用 kubernetes_sd_configs 自动发现,grafana 走 NodePort 暴露。数据源用 ConfigMap 预置,省掉手工点。

bash 复制代码
kubectl apply -f monitoring-stack.yaml
kubectl get pods -n monitoring        # 期望 9 个 Running(7 node-exporter + 1 prom + 1 grafana)
curl -s http://<任一worker>:30300/api/health    # {"database":"ok",...}

验收别只看 Pod Running。要看 Prometheus 的 up 指标 :count(up==1) 应该等于 节点数 × 2(node-exporter + cadvisor 各一条)。只看 Pod Ready,很容易漏掉「抓不到数据」这种半残状态。

步骤 10:验收清单(逐条打勾,别跳)

# 检查项 命令 期望
1 节点 kubectl get nodes 全部 Ready
2 控制面 kubectl -n kube-system get pod -l tier=control-plane 每 Master 3 组件 Running
3 etcd kubectl -n kube-system get pod -l component=etcd 3/3
4 网络 kubectl -n kube-system get pod -l k8s-app=calico-node 与节点数一致
5 VIP curl -k https://<VIP>:6443/version 版本 JSON
6 业务 curl http://<worker>:<NodePort>/ 200
7 LB 漂移 lb01 systemctl stop keepalived VIP <2s 漂到 lb02,服务不断
8 私有仓库 用镜像地址建 Deployment Pod Running
9 监控 count(up==1) 节点数×2
10 备份 etcd snapshot + 配置打包 可 restore

四、故障演练:不做这一步,等于没验收

层级 动作 实测结果(2C/8G × 7)
Pod kubectl delete pod --force 3 秒重建,且漂到其它节点
Worker vmrun stop soft 60s 内 NotReady;重开 4s 起、约 6 分钟 Ready
Master vmrun stop soft 关机期间其余 6 节点全 Ready、kubectl 全程可用;重开约 2 分钟 Ready

Master 关机慢(93s vs Worker 9s),因为 etcd 要走完 raft 收尾,这是正常的。

Worker 挂掉超过 5 分钟(默认 tolerationSeconds=300)才会驱逐 Pod;节点在 5 分钟内回来,Pod 不会迁移------这是设计行为,不是 bug。


五、备份与恢复(生产的分水岭)

一套能恢复的集群,才算交付。备份三件套:

bash 复制代码
# 1) etcd 快照(数据面)
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /var/lib/etcd/snap-$(date +%Y%m%d).db

# 2) 配置面
tar czf etc-kubernetes.tar.gz -C / etc/kubernetes

# 3) 各节点 containerd/kubelet/sysctl/LB/Harbor 配置

坑 :etcd 官方镜像是 distroless ,里面没有 /bin/sh、没有 ls、没有 which 。kubectl exec ... -- /bin/sh -c "etcdctl ..." 会报 stat /bin/sh: no such file or directory。必须直接 exec 绝对路径:kubectl exec <pod> -- /usr/local/bin/etcdctl ...。

恢复时三台 Master 用同一份快照 各自 snapshot restore,--name 和 --initial-advertise-peer-urls 按本机改,数据目录属主保持 root:root 700。


六、国内网络下的下载源实测(2026-10 结论)

目标 结果
mirrors.aliyun.com/rockylinux/9/ ✅(/9.5/ 是 404,用 /9/)
pkgs.k8s.io ✅ 302 可跟随
download.docker.com ❌ SSL 重置 → 用阿里云 docker-ce 镜像
raw.githubusercontent.com ❌ 超时
ghproxy.net ❌ 502
cdn.jsdelivr.net/gh/... ✅ 稳定(Calico 清单走这里)
docker.m.daocloud.io ✅ 可直接 crictl pull
quay.io ✅ 常可达(Calico 镜像备用)
registry.aliyuncs.com/google_containers ✅ metrics-server 等直接拉

镜像拉不动的三招 :① 国内 CDN 全路径 docker.m.daocloud.io/library/x:tag;② quay.io 拉下来 ctr -n k8s.io images tag 改成期望的名字;③ 离线 ctr images export/import。


七、一张纸的排错顺序

节点 NotReady 时,按这个顺序看,基本 5 分钟内能定位:

bash 复制代码
systemctl status kubelet                  # 1. kubelet 活不活
journalctl -u kubelet -n 100 --no-pager   # 2. 报错第一行
crictl ps -a | head                       # 3. 容器运行时通不通
crictl logs <sandbox-id>                  # 4. pause 容器起不来 = CNI/镜像问题
grep -i "cgroup\|sandbox" /var/log/messages
kubectl get node <n> -o yaml | grep -A5 Conditions
  • cgroup driver mismatch → containerd 的 SystemdCgroup 没开
  • failed to get sandbox image → pause 镜像拉不到,换阿里云
  • network plugin not ready → CNI 没装或 Pod 网段与 calico 配置不一致
  • x509: certificate signed by unknown authority → 自签 CA 没分发到 /etc/containerd/certs.d/<registry>/ca.crt

八、这套东西的边界

  • 能扛:单 Master 宕机、单 Worker 宕机、单 LB 宕机、API 短时抖动。
  • 不能扛:两台 Master 同时挂(etcd 失去多数派)、存储单点(本地盘 PV 挂了数据就没了)、机柜断电。
  • 真要上生产,还得补:外部 etcd 集群、etcd 定时快照到对象存储、kubeadm certs renew 的证书轮换日历、NetworkPolicy、RBAC 最小权限、审计日志落盘。

参考实现(脚本化、可重复):deploy-k8s.sh 把上面十步按 run <stage> 串起来,10 台机器一遍过;monitoring-stack.yaml 是上面第九步的完整清单。所有命令都是实机跑过的,报错原文见配套的《实机经验与坑位清单》。

相关推荐
吴声子夜歌2 小时前
Docker入门与实战——热门镜像介绍
运维·docker·容器
方便面不加香菜2 小时前
容器虚拟化基础--CGroups资源控制
运维·docker·容器
吴声子夜歌5 小时前
Docker入门与实战——Docker命令查询
docker·容器
时间售卖铺6 小时前
MySQL DML 与 DQL 语句详解
运维·数据库·mysql·云原生
孔令飞7 小时前
1024程序员节,OneX平台送3套免费Go实战项目
云原生·go·ai编程
吴声子夜歌7 小时前
Docker入门与实战——常见问题总结
运维·docker·容器
丨晨 丨7 小时前
K8s控制器天团图鉴-五大控制器整活
云原生·容器·kubernetes
时间售卖铺8 小时前
MySQL 库表设计:数据库范式详解
运维·数据库·mysql·云原生
马立杰9 小时前
Docker Build 编译补丁版 PHP7.4 FPM 容器镜像教程(CVE-2026-109618 CVE-2026-109619)
运维·docker·容器·php