Kubeadm+Docker搭建Kubernetes高可用集群实战:架构与避坑指南
2026/9/9 6:07:34 网站建设 项目流程

最近刚帮一个团队把测试环境的 Kubernetes 集群从单节点升级成了高可用模式。整个过程走下来,最大感触是:Kubeadm 官方文档写得确实完整,但真按文档一步一步点,坑比想象中多得多。特别是 Kubeadm 和 Docker 这条组合路线,在 Kubernetes 1.24 之后多了一个运行时适配层,很多人第一次搭高可用集群就栽在这里。

这篇博文我打算把基于 Kubeadm + Docker 部署 Kubernetes 高可用集群从架构设计到落地操作完整过一遍。你会搞清楚三个 master 节点加负载均衡到底怎么搭,为什么 Kubeadm 需要单独适配 Docker,以及部署过程中最容易被忽略的镜像加速、cgroup driver、证书哈希这些细节。适合准备在生产或测试环境搭一套原生 K8s 集群的运维、后端开发,也适合 K8s 入门者从 minikube 跨到真实集群这一步。

1. 架构规划与版本选型

1.1 为什么是 Kubeadm + Docker,而不是其他组合

先说选型思路。K8s 集群部署方式有很多种:二进制手动部署、Kubeadm、k3s、minikube、Rancher、kubekey 等等。如果你是第一次接触真实集群,Kubeadm 基本是绕不开的入门选项,因为它是 Kubernetes 官方提供的集群构建工具,也是从“单机学习”过渡到“生产环境编排”最平滑的一条路。

那为什么还特意提 Docker?因为很多团队内部本来就用 Docker 管理镜像和容器,开发环境、CI 流程全都是 docker build、docker push 那一套。直接把运行时切换到 containerd,虽然技术上是更现代的选择,但团队的学习成本和使用惯性不是一天能改的。所以这篇文章走的是 Docker + cri-dockerd 的兼容路线,让你继续用 Docker 写 Dockerfile、构建镜像、跑容器,同时底层通过适配层让 K8s 识别 Docker 运行时。

这里有个很关键的时间点:Kubernetes 在 1.24 版本正式移除了内嵌的 dockershim。也就是说,K8s 不再直接支持 Docker 作为容器运行时,要用 Docker 就必须额外装一个叫 cri-dockerd 的适配器。这个适配器的作用是把 CRI(Container Runtime Interface)调用转换成 Docker API 调用,让 kubelet 依然可以通过 Docker 来创建和管理容器。

如果你想跳过这个适配层,直接用 containerd,那 kubeadm 配置会简单一些,但 Docker 的生态和命令行习惯就需要调整。我的建议是,如果你已经熟练使用 Docker 命令,先走 cri-dockerd 这条路线,后续团队统一迁移到 containerd 也不迟。

1.2 高可用架构到底“高可用”在哪里

先理解一个概念:高可用不是多买几台机器就完了。Kubernetes 控制面由 API Server、etcd、Controller Manager、Scheduler 等组件组成,其中 API Server 是外界访问集群的唯一入口,etcd 负责保存整个集群的状态。如果这两个组件挂了,集群就瘫痪了。

控制面高可用的核心思路是:API Server 多副本部署,前面加一个负载均衡器,客户端通过负载均衡的虚拟 IP 访问 API Server;etcd 要么跟随控制面节点一起部署(堆叠模式),要么单独部署一组 etcd 集群(外部模式)。

我用的是堆叠模式,也就是三个 master 节点上都跑 etcd 和 API Server,然后通过 keepalived + haproxy 提供一个虚拟 IP。客户端访问 VIP 的 6443 端口时,haproxy 会把流量转发到其中一个健康的 master 节点的 6443 端口。

这里要解释一下 keepalived 和 haproxy 的分工。keepalived 负责虚拟 IP 的漂移,它基于 VRRP 协议,主节点挂了之后备用节点会自动抢到 VIP,这个过程通常在几秒内完成。haproxy 则负责对后端多个 API Server 做健康检查和负载分发。两者配合,就构成了控制面入口的高可用。

那为什么不直接用云平台的负载均衡?如果你的集群已经跑在云上,直接买一个 SLB(Server Load Balancer)转发到三个 master 的 6443 端口确实更省心。自建 keepalived + haproxy 的价值在于,它适用于裸金属机房、离线环境或不想依赖云厂商方案的场景。

1.3 主机与网络规划细节

我这次用的是三台 master 加两台 worker 的拓扑。生产环境如果资源充足,建议控制面 3 台起步,因为 etcd 需要奇数个节点来保证选主仲裁。两台 master 虽然也能跑,但 etcd 在其中一个节点宕机时会失去仲裁能力,严格来说不叫高可用。

主机名IP 地址角色配置建议
master01192.168.10.11控制面 + etcd4C8G
master02192.168.10.12控制面 + etcd4C8G
master03192.168.10.13控制面 + etcd4C8G
worker01192.168.10.21工作节点4C8G
worker02192.168.10.22工作节点4C8G
vip192.168.10.100虚拟 IP不占用实际机器

操作系统我这次用的是 Rocky Linux 9.3。CentOS 7 的源已经不怎么维护了,新部署不建议再碰。Ubuntu 22.04 也可以,下面的命令稍微改动一下包管理器就行。

网络规划上,有几个网段必须规划好:宿主机网段、Pod 网段、Service 网段。Pod 网段我习惯用 10.244.0.0/16,Service 网段用 10.96.0.0/12,这两个网段都必须和宿主机网段错开。之前见过有人把 Pod 网段直接设成 192.168.1.0/24,结果和办公网段撞了,网络路由一塌糊涂。

2. 基础环境准备与 Docker 运行时配置

2.1 系统初始化配置

基础环境部分建议先在每台机器上都操作一遍,不然等 kubeadm init 的时候才发现变量不一致,排查起来非常痛苦。

所有节点需要做这几件事:关闭 swap、配置主机名解析、加载内核模块、调整系统参数、关闭防火墙或放行端口。

关闭 swap 是硬性要求,kubelet 默认不支持启用 swap 的环境。执行swapoff -a并注释掉 /etc/fstab 里的 swap 行,同时运行下面的命令:

systemctl disable --now swap.target

配置 /etc/hosts,让每台机器都能通过主机名互相访问:

cat >> /etc/hosts <<EOF 192.168.10.11 master01 192.168.10.12 master02 192.168.10.13 master03 192.168.10.21 worker01 192.168.10.22 worker02 EOF

加载 Kubernetes 依赖的内核模块并启用网络转发:

cat > /etc/modules-load.d/k8s.conf <<EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter cat > /etc/sysctl.d/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

防火墙这里,如果是在内网测试环境,最简单的方式是直接关闭 firewalld;生产环境建议把下面这些端口在防火墙或安全组里放行:

端口用途
6443Kubernetes API Server
2379-2380etcd 客户端和集群通信
10250kubelet 通信
10251-10252kube-scheduler、kube-controller-manager
10257kube-controller-manager secure port
10259kube-scheduler secure port
30000-32767NodePort 服务端口范围

2.2 新版 Docker 安装与镜像加速配置

Docker 安装本身不复杂,主要是镜像源的问题。国内环境下直接从 Docker 官方源拉镜像确实很慢,这也是我这次部署时最头疼的一环,所以单独拿出来说。

先安装依赖包,然后配置 Docker 的 yum 源:

yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

接着安装新版 Docker:

yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker

Docker 装好之后,强烈建议先配置镜像加速。修改 /etc/docker/daemon.json,加上国内镜像仓库地址。以阿里云容器镜像服务为例,你可以在控制台申请一个专属加速地址,格式类似https://xxxx.mirror.aliyuncs.com,然后在 daemon.json 里配置:

cat > /etc/docker/daemon.json <<EOF { "exec-opts": ["native.cgroupdriver=systemd"], "registry-mirrors": ["https://你的专属加速地址.mirror.aliyuncs.com"], "data-root": "/data/docker" } EOF

注意里面那个exec-opts参数,它的作用是把 Docker 的 cgroup driver 改成 systemd。Kubernetes 从 1.22 版本开始,kubelet 推荐使用 systemd 作为 cgroup driver,统一之后能避免后面 kubelet 无法启动的问题。data-root 把 Docker 数据目录迁移到大容量磁盘,这个不是必须的,但如果根分区不大建议尽早改。

改完配置重启 Docker:

systemctl daemon-reload systemctl restart docker docker info | grep -A2 "Registry Mirrors"

2.3 安装 cri-dockerd 适配器

这一步是 Docker 路线和 Kubeadm 集成的关键,也是很多人会漏掉的一环。Kubernetes 1.24 之后不能再直接通过 dockershim 调用 Docker,所以需要单独安装 cri-dockerd 服务,让 kubelet 通过 cri-dockerd 找到 Docker。

cri-dockerd 可以从 GitHub 的 releases 页面下载对应系统的安装包。以 Rocky Linux 为例,安装 RPM 包:

wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.14/cri-dockerd-0.3.14-3.el9.x86_64.rpm yum install -y cri-dockerd-0.3.14-3.el9.x86_64.rpm

安装完成后启动并设置开机自启:

systemctl enable --now cri-docker.socket systemctl status cri-docker.socket

注意这里启动的是cri-docker.socket而不是cri-docker.service。cri-dockerd 默认监听在/var/run/cri-dockerd.sock,而 systemd 的 socket 激活机制会在收到连接时才拉起主进程。如果只启动了 service 而没启动 socket,kubeadm init 的时候很可能报找不到 CRI socket 的错误。

验证方式很简单,看看 socket 文件是否存在:

ls -l /var/run/cri-dockerd.sock

我这次部署时栽过一个坑:cri-dockerd 和 kubelet 的 cgroup driver 必须保持一致。前面 Docker 我设置成了 systemd,但 cri-dockerd 默认用的可能是 cgroupfs,结果 kubelet 一直报 mismatched cgroup driver。解决方法是修改 cri-dockerd 的启动参数,在 systemd service 文件里给 ExecStart 加上--cgroup-driver=systemd,或者直接用下面这个命令重启服务:

sed -i 's/-c\ \/etc\/crictl.yaml//' /usr/lib/systemd/system/cri-docker.service systemctl daemon-reload systemctl enable --now cri-docker.service cri-docker.socket

3. 高可用组件部署与控制面初始化

3.1 keepalived + haproxy 实现 VIP 负载均衡

高可用组件我放在最前面配置,因为后续的 kubeadm init 需要用到虚拟 IP 作为控制面端点。所有三个 master 节点上都要安装 haproxy 和 keepalived。

yum install -y keepalived haproxy

haproxy 的配置统一写到 /etc/haproxy/haproxy.cfg。核心思想是监听 6443 端口,把请求转发到后端三个 master 的 6443 端口:

global log /dev/log local0 maxconn 4096 defaults mode tcp log global retries 3 timeout connect 5s timeout client 50s timeout server 50s listen k8s-api-server bind 0.0.0.0:6443 mode tcp balance roundrobin option tcplog option tcp-check server master01 192.168.10.11:6443 check inter 3s fall 3 rise 2 server master02 192.168.10.12:6443 check inter 3s fall 3 rise 2 server master03 192.168.10.13:6443 check inter 3s fall 3 rise 2

这里要解释一下为什么用 TCP 模式而不是 HTTP 模式。Kubernetes API Server 走的是 HTTPS,负载均衡器只需要做四层转发即可,TCP 模式开销更低,也不需要对证书做额外处理。

keepalived 配置稍微复杂一点,因为三个节点需要区分主备角色。master01 配置为 MASTER,优先级设为 100;master02、master03 配置为 BACKUP,优先级设 90。虚拟 IP 地址统一写成 192.168.10.100。

master01 的 /etc/keepalived/keepalived.conf 配置如下:

global_defs { router_id LVS_K8S } vrrp_script check_haproxy { script "/usr/bin/killall -0 haproxy" interval 2 weight -2 } vrrp_instance VI_1 { state MASTER interface ens160 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass k8s-ha-123 } virtual_ipaddress { 192.168.10.100 } track_script { check_haproxy } }

master02 和 master03 的配置基本一样,只需把 state 改成 BACKUP、priority 改成 90。virtual_router_id必须在同一个局域网内唯一,auth_pass保持一致,否则 VRRP 报文会被丢弃。

特别提醒一下,track_script的作用是当 haproxy 进程异常时自动降低当前节点的 keepalived 优先级,触发 VIP 漂移到其他节点。这个脚本非常关键,没有它的话,即使 haproxy 挂了 VIP 也不会转移,客户端请求依然会打到坏节点上。

配置完成后启动服务并观察 VIP 落在哪台机器:

systemctl start haproxy systemctl start keepalived ip addr show

正常情况下 192.168.10.100 会出现在 master01 上。可以用 curl 测试一下 API Server 端口是否通:

curl -k https://192.168.10.100:6443/healthz

如果返回 ok,说明负载均衡这一层已经通了。

3.2 使用配置文件初始化第一个控制面节点

先在 master01 上安装 kubeadm、kubelet 和 kubectl。这三个组件的版本必须保持一致,我这次用的是 1.29.2。

配置 Kubernetes 的 yum 源:

cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.29/rpm/ enabled=1 gpgcheck=0 repo_gpgcheck=0 EOF

安装:

yum install -y kubeadm-1.29.2 kubelet-1.29.2 kubectl-1.29.2 systemctl enable --now kubelet

注意这一阶段 kubelet 启动后可能处于失败状态,因为集群还没有初始化,这属于正常现象。等 kubeadm init 完成后 kubelet 才能正常工作。

接下来是关键:生成 kubeadm 的配置文件。高可用集群的初始化强烈建议用配置文件代替一堆命令行参数,因为有很多配置项(比如 controlPlaneEndpoint、criSocket)命令行里也能写,但容易漏。

先查看需要哪些镜像:

kubeadm config images list --image-repository=registry.aliyuncs.com/google_containers

然后创建 kubeadm-init.yaml:

cat > /etc/kubernetes/kubeadm-init.yaml <<EOF apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock name: master01 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.29.2 controlPlaneEndpoint: "192.168.10.100:6443" imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd EOF

逐个解释这里的配置含义。localAPIEndpoint.advertiseAddress必须填当前机器的实际 IP,也就是 master01 的 IP。controlPlaneEndpoint填的是前面规划好的虚拟 IP 加端口,这个是高可用集群的关键,kubeadm 会把生成的证书和 kubeconfig 里的服务器地址都指向这个端点,后续 worker 节点 join 时也连这个地址。imageRepository配置成国内镜像仓库,否则 kubeadm 会尝试从registry.k8s.io拉取镜像,在国内网络环境下基本拉不动。criSocket指向 cri-dockerd 的监听路径。

配置文件准备好之后,先手动把镜像拉下来,避免 init 超时:

kubeadm config images pull --config /etc/kubernetes/kubeadm-init.yaml

然后执行初始化:

kubeadm init --config /etc/kubernetes/kubeadm-init.yaml --upload-certs

加上--upload-certs参数,是为了让后续其他 master 节点 join 时获取控制面证书,不需要手动拷贝证书文件。这一步会输出一段重要的信息,包括 kubectl 的配置命令、其他控制面节点加入集群的命令、worker 节点加入集群的命令。务必复制保存。

初始化完成后,按输出提示配置 kubectl:

mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config

3.3 扩容控制面节点和工作节点

先不要急着 join 第二个 master,我建议先在第一个节点上装好网络插件,确认集群正常后再扩容控制面。

不过如果网络插件还没装,第二个控制面节点 join 时,etcd 健康检查可以通过,但 kubelet 会因为 CNI 配置缺失而处于 NotReady 状态,不影响加入流程,只是节点状态不是 Ready。这里说下标准顺序:init 第一个 master -> 装 CNI -> join 第二个 master -> join 第三个 master -> join worker。这样每一步出问题都容易定位。

第二个 master 节点上执行以下命令(注意这是 init 时输出的 join 命令,token 和 hash 要替换成你自己的):

kubeadm join 192.168.10.100:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxx \ --control-plane --certificate-key xxxxxxxxx \ --cri-socket unix:///var/run/cri-dockerd.sock

这里有个小细节:--control-plane这个参数表示以控制面节点身份加入,不加的话就是 worker 节点。--cri-socket必须显式指定,因为集群里有多个 CRI socket(docker 的 crictl 默认也能识别),不指定的话 kubeadm 可能会找错。

worker 节点 join 时去掉--control-plane--certificate-key

kubeadm join 192.168.10.100:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxx \ --cri-socket unix:///var/run/cri-dockerd.sock

如果在扩容控制面节点时提示 token 过期,可以回到 master01 上重新生成 join 命令:

kubeadm token create --print-join-command

certificate-key 也可以通过kubeadm init phase upload-certs --upload-certs重新生成。

4. 网络插件选型与集群验证

4.1 安装 Calico 网络插件

CNI 插件是整个集群能跑起来的核心依赖之一,它负责给每个 Pod 分配网络地址和配置路由。不装网络插件的话,节点会一直处于 NotReady 状态,所有 Pod 都无法运行。

我这次选了 Calico。相比 Flannel,Calico 支持 NetworkPolicy(网络策略),可以在集群内部做更细粒度的访问控制。如果你只是搭个学习环境,Flannel 会更轻量,但生产环境建议直接上 Calico。

Calico 的安装是通过 kubectl 应用一个 manifest 文件,执行前要看清楚里面的网络配置。官方默认的 pod CIDR 是 192.168.0.0/16,但我的 podSubnet 配置的是 10.244.0.0/16,需要先修改。

先下载 Calico manifest:

wget https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/calico.yaml

国内网络拉取 GitHub 原地址可能比较慢,也可以从其他渠道获取同一份 manifest。下载后用编辑器查找CALICO_IPV4POOL_CIDR,把 192.168.0.0/16 改成 10.244.0.0/16:

sed -i 's/192.168.0.0\/16/10.244.0.0\/16/g' calico.yaml

然后应用:

kubectl apply -f calico.yaml

等一两分钟,查看节点状态:

kubectl get nodes -w

三个 master 节点应该陆续变成 Ready。这里要注意,如果 master 节点上有污点(taint),Pod 调度时默认不会调度到 master 上,但 Calico 和 CoreDNS 属于系统组件,kubeadm init 会自动给这些组件加容忍,所以不受影响。

4.2 验证高可用:模拟主节点故障

部署完网络插件,集群从形式上看已经正常了,但高可用是否真的生效,必须做故障演练。我的习惯是直接模拟最极端的情况:把第一个 master 也就是 VIP 当前所在的节点整个停掉,看请求是否还能正常到达 API Server。

先在 master01 上执行:

kubectl get nodes kubectl get pods -A

然后在 master01 上关闭 keepalived:

systemctl stop keepalived

这时 VIP 应该会在几秒内漂移到 master02 或 master03 上。可以在 master02 上查看:

ip addr show | grep 192.168.10.100

接着我在一台 worker 节点上试着访问 API Server 的健康检查接口:

curl -k https://192.168.10.100:6443/healthz

返回 ok 就说明负载均衡和 VIP 漂移正常。再验证一下 kubectl 命令是否还能正常执行:

kubectl get nodes

这里有个容易忽略的点:kubectl 使用的是admin.conf这个 kubeconfig,里面的 server 地址填的是 VIP。如果 VIP 漂移正常,kubectl 就不受单个 master 宕机影响。反过来,如果之前有人把 kubeconfig 里的 server 地址写成了某个具体 IP,那故障演练时 kubectl 就会随机失败,这也是排查时要留意的地方。

进一步验证 etcd 的健康状态,可以在任意 master 上执行:

ETCDCTL_API=3 etcdctl \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ --endpoints=https://192.168.10.11:2379,https://192.168.10.12:2379,https://192.168.10.13:2379 \ endpoint health

如果输出三个 healthy,说明 etcd 集群在单个节点停机时依然具备半数以上节点参与仲裁,数据一致性和可用性都有保障。

4.3 安装测试应用并验证 Service 和 DNS

集群 Ready 后,最好跑一个测试应用验证整个链路是否通畅。我习惯部署一个简单的 Nginx Deployment 和对应的 Service:

kubectl create deployment test-nginx --image=nginx kubectl expose deployment test-nginx --port=80 --type=NodePort

查看 Service 分配的 NodePort:

kubectl get svc test-nginx

然后用任意节点的 IP 加 NodePort 访问测试页:

curl http://192.168.10.21:30789

能返回 Nginx 默认页面,说明 Pod 网络、Service 转发、节点间路由都正常。

接着测试一下集群 DNS:

kubectl run curl-test --image=radial/busyboxplus:curl --command -- sleep 3600 kubectl exec -it curl-test -- nslookup kubernetes.default.svc.cluster.local

如果解析正常,说明 CoreDNS 工作正常。这里经常遇到的坑是 CoreDNS 一直 Pending 或者 CrashLoopBackOff,后面单独说排查方法。

5. 常见问题排查与避坑实录

5.1 镜像拉取超时或拉取失败

这是在国内环境部署集群时遇到最多的问题。现象是 kubeadm init 卡在 pulling images 阶段很久,最后报错failed to pull image ...

先说原因。kubeadm 默认的镜像仓库是registry.k8s.io,这个地址在国内访问速度非常不稳定。解决办法有两个层面:

第一个层面,在 kubeadm 配置里指定imageRepository为国内镜像仓库。比如阿里云的registry.aliyuncs.com/google_containers。配置好之后,先用kubeadm config images pull --config kubeadm-init.yaml手动预热,确认所有镜像都能拉下来再执行 init。

第二个层面,Docker 本身的镜像加速配置。前面在 daemon.json 里配置registry-mirrors就是为了解决这个问题,对一些公共镜像仓库的拉取会快很多。

另外,cri-dockerd 模式下,Kubelet 通过 CRI 拉取的镜像实际上还是由 Docker 执行,所以 Docker 镜像加速对 kubeadm 场景同样有效。我遇到过一次很奇怪的问题:kubeadm init 时其他镜像都正常,只有 sandbox-pause 这个镜像拉下来之后,crictl 识别不到镜像 tag。排查半天发现是 cri-dockerd 版本和 Kubernetes 版本有点不匹配,升级 cri-dockerd 后问题消失。遇到诡异问题优先检查组件版本。

5.2 kubelet 报 misconfiguration: kubelet cgroup driver: "cgroupfs"

kubelet 启动后一直失败,日志里出现 cgroup driver 不一致的报错,这是 Docker + K8s 组合的经典问题。

我在基础环境那节专门提过,Docker 的 daemon.json 里设置了native.cgroupdriver=systemd,cri-dockerd 启动参数也加了--cgroup-driver=systemd,kubelet 的 KubeletConfiguration 里也写了cgroupDriver: systemd。这样三者才一致。

如果你的系统里 kubelet 默认还是 cgroupfs,可以手动修改 kubelet 的配置文件/var/lib/kubelet/config.yaml,把 cgroupDriver 改掉后重启:

sed -i 's/cgroupfs/systemd/g' /var/lib/kubelet/config.yaml systemctl restart kubelet

注意一定要在 kubeadm init 之前就确认这个配置,否则初始化之后 kubelet 会一直处于异常状态,排查起来非常折磨人。

5.3 join 节点时提示 token 过期或证书哈希不匹配

kubeadm 的 bootstrap token 默认有效期是 24 小时,如果你隔了一天才来加第二个节点,输出的 join 命令大概率已经失效。这种情况在测试环境特别常见。

解决办法很简单,回到 master01 上重新生成:

kubeadm token create --print-join-command

这个命令会直接输出完整可用的 join 命令,包括新的 token 和 discovery-token-ca-cert-hash。注意 hash 是对应的 CA 证书的 sha256,token 更新了 hash 不需要变,因为 CA 证书没变。

另外还有一个小坑:执行 join 时--discovery-token-unsafe-skip-ca-verification这个参数虽然能跳过 CA 验证,但生产环境千万别用。一旦使用这个参数,新节点加入时不会校验控制面的 CA 证书,存在中间人攻击的风险。

5.4 CoreDNS 一直 Pending 或 CrashLoopBackOff

集群装上 Calico 后其他 Pod 都正常,唯独 CoreDNS 处于 Pending 状态,这种问题我见过不少次。

原因通常有两个:一个是 Pod 网段和宿主机网段冲突,导致 Calico 网络无法正常工作,节点无法分配 IP 给 CoreDNS;另一个是节点的污点问题,比如只有 master 节点可调度,而 CoreDNS 没有容忍对应污点,调度器没有可用节点。

排查思路:

kubectl describe pod -n kube-system -l k8s-app=kube-dns

看 Events 里有没有0/5 nodes are available之类的调度失败提示。如果是污点问题,检查节点 taints:

kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

如果只有 master 节点是 Ready,检查 kube-system 命名空间下的 Pod 有没有配置合适的 tolerations。大多数情况下 Calico 的 manifest 会给关键组件加上必要的容忍,但如果你用的是自定义 CNI,可能需要手动加。

5.5 etcd 备份与恢复注意事项

高可用集群不等于不会出数据问题。etcd 是集群的单一数据源,一旦损坏,整个集群的状态都救不回来。

生产环境建议每天对 etcd 做一次快照。在任一 master 节点执行:

ETCDCTL_API=3 etcdctl \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db

然后将快照文件同步到异机存储。恢复流程大致是:停止所有控制面组件,使用etcdctl snapshot restore恢复快照到新目录,再重新生成 etcd 数据目录并启动服务。具体步骤不在这篇里展开,但快照一定要记得做。

还有两个坑要提醒:第一,绝对不要直接对 etcd 节点做虚拟机整机快照然后恢复,这会导致 etcd 集群的成员信息和实际状态不一致,几乎必然损坏集群;第二,etcd 磁盘空间满了也会导致集群不可写,监控 etcd 所在磁盘的使用率要注意。

5.6 NodePort 访问不通,排查网络链路

如果测试应用部署了 NodePort,但通过节点 IP 访问不通,优先怀疑两个地方:防火墙放行规则和路由转发。

防火墙方面,我在基础环境那节列了需要放行的端口段,NodePort 的范围是 30000-32767。如果开通了 firewalld,执行:

firewall-cmd --permanent --add-port=30000-32767/tcp firewall-cmd --reload

iptables 的 FORWARD 链也需要允许数据包在节点之间转发。我排查过一个问题:节点上 iptables 的 FORWARD 策略是 DROP,导致 Pod 访问外网和 Service ClusterIP 都不通,但 Pod 本身创建成功。解决办法是在所有节点执行:

iptables -P FORWARD ACCEPT

这个设置在部分系统重启后会恢复默认策略,要持久化的话需要保存 iptables 规则,具体方式和发行版相关。

5.7 kubeconfig 中 server 地址写错导致 kubectl 间歇性失败

前面提过一嘴,这里详细说说。如果你在安装完成后把/etc/kubernetes/admin.conf拷贝到自己的机器上,直接用根证书签发用户权限去访问集群,那 server 地址必须配置成高可用 VIP。如果配置文件里写成了某个具体的 master 节点 IP,那么这个 master 宕机后,你的 kubectl 就会失联,即使整个集群还有两个 master 可用也无济于事。

正确做法是在生成 admin.conf 时注意查看server: https://192.168.10.100:6443这一行,确保是 VIP。如果配置错了,手动替换即可。

6. 部署过程中的几点实操体会

这套集群搭完之后,我多说几句个人体会。

第一,版本一致性真的能救命。kubeadm、kubelet、kubectl 这三个组件必须是同一个版本,Docker 和 cri-dockerd 也要和 K8s 主版本兼容。遇到过好多次,用户说自己按教程部署失败,最后发现是装了一个最新版 kubeadm 但 kubelet 是旧的,导致 join 时 API 版本不匹配。如果你不确定兼容关系,就用当时文档里标注的组合,别轻易尝试混搭。

第二,高可用不只是控制面的事。VIP 解决了 API Server 入口的高可用,但 keepalived 自身的脑裂问题、haproxy 的单点、CoreDNS 的资源限制、镜像仓库的可用性,任何一个环节掉链子都会影响集群可用性。我自己在后续维护里,还会给 keepalived 配置单播模式,避免交换机限制 VRRP 组播导致脑裂;给 haproxy 配上 rsyslog 独立日志,方便故障时回溯。

第三,快速获得完整 join 命令的小技巧。每次用 kubeadm init 或 join 之后,把输出的 join 命令完整保存到本地文件,尤其是 token、certificate-key、discovery-token-ca-cert-hash 这三样。另外,kubeadm token create --print-join-command这个命令非常实用,当 token 过期时不用翻找日志就能恢复现场。

第四,别把故障演习省掉。集群搭完不是终点,至少做一次“kill 一个 master”的演练,确认 VIP 能漂移、API Server 能正常响应、etcd 仍然健康,你才对这套高可用配置有信心。演练中发现的问题永远比线上突发事故要好处理。

这套 Kubeadm + Docker 的高可用方案,本质上是用 kubeadm 的标准化能力加上 keepalived 和 haproxy 的通用负载均衡手段,把 K8s 控制面从单点变成多副本。理解了这些底层组件的协作关系,后续再上 containerd、再对接云负载均衡,思路都是相通的。希望这篇文章能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询