Kubernetes双节点集群搭建:从单节点到真正调度

发布时间:2026/10/16 16:34:28
Kubernetes双节点集群搭建:从单节点到真正调度
1. 为什么“K8s - 2”这一篇一定要聊清楚双节点1.1 单节点集群到底少了什么先亮个底这篇是 Kubernetes 系列笔记的第二篇主角就是“第二台机器”。很多朋友搭完第一个 K8s 集群后都会面临同一个问题——kubectl get nodes里孤零零只有一个节点Deployment 也能跑Service 也能通但总觉得这跟“容器编排”四个字对不上。原因很简单单节点集群本质上只是一台机器上开了几个管理系统所谓的“调度”根本没有发生。单节点集群最直接的痛点有三个。第一控制面组件apiserver、scheduler、controller-manager、etcd和业务 Pod 挤在同一台机器上CPU 和内存稍微一紧张kubelet 就开始驱逐 Pod你根本分不清是业务吃资源还是系统组件吃资源。第二节点一旦宕机或者重启整个集群就是一张废牌没有第二个节点可以接管任何工作负载。第三你没法体验多副本调度、节点亲和性、滚动更新时的跨节点迁移而这些恰恰是 K8s 最值钱的特性。所以把单节点扩展成双节点不是简单“再装一台机器跑个 join”而是让集群第一次拥有真正的“分布式”语义控制面与业务分离调度器有多个目标节点可选网络插件开始在主机之间打通容器网络。可以说第二台节点的加入才是 K8s 从“玩具”走向“工具”的分水岭。1.2 双节点架构里的角色切分双节点集群最常见也最合理的布局是一台控制面节点control-plane加一台工作节点worker。控制面节点跑 kube-apiserver、kube-scheduler、kube-controller-manager 和 etcd工作节点只跑 kubelet、kube-proxy、容器运行时以及真正要被调度的业务 Pod。这个切分背后有一套很朴素的设计逻辑控制面组件对网络和存储的要求高但本身不承载业务流量工作节点则恰恰相反。把两者混在一起生产环境早晚会出问题。所以在实验环境里从“第二台”开始就养成角色隔离的习惯后面迁移到三节点、五节点时思路会非常顺。我见过有人直接把第二台也设成控制面节点组成双控制面。这个做法在纯学习场景里可以理解但要注意一个技术细节etcd 集群的可用性算法要求多数节点存活双节点 etcd 在故障时的容错能力反而比单节点更尴尬——1/2不满足多数派要求一台挂了整个控制面 still 不可用。因此如果你只有两台机器我更推荐“一控制面、一工作节点”的组合把控制面资源留给系统工作节点专心跑业务后面再去考虑三控制面高可用的问题。2. 加入第二节点之前三份“环境准备”最容易踩坑2.1 两个节点的基础环境必须对齐很多人在第二节点上装完 Docker 或 containerd直接拿 join 命令贴上去然后发现 kubelet 报各种莫名其妙的错。根因基本都是环境不一致。第一件事确认两个节点的操作系统版本、内核版本尽量保持一致至少不要跨大版本。第二件事把两台机器的主机名、/etc/hosts都配好比如控制面节点叫 k8s-master工作节点叫 k8s-node01并且互相能解析。第三件事关闭 swap。Kubelet 默认不允许在启用 swap 的机器上运行如果你没关节点会一直 NotReady。可以临时关swapoff -a但为了重启后依然生效最好注释掉/etc/fstab里的 swap 行。第四件事确保时间同步。K8s 对证书敏感节点时间偏差超过 5 分钟TLS 握手就会失败表现是 kubelet 反复报 x509 证书错误。装个 chrony 或者 ntpd 就行。这些看起来都是琐事但我在实操里踩过不止一次尤其是克隆虚拟机导致的 machine-id 重复问题。注意如果你用虚拟机模板克隆出工作节点克隆后一定要重置/etc/machine-id和 SSH host key。否则两个节点的 machine-id 相同kubelet 注册时可能串场节点状态非常诡异。2.2 容器运行时和 cgroup 驱动的坑第二节点同样要装容器运行时如今 kubeadm 默认用 containerd而不是 Docker。安装后别急着 start先确认一个关键配置SystemdCgroup。用普通用户方式拉起的 containerd默认 cgroup 驱动往往不匹配而 kubelet 在 v1.26 之后基本都要求 systemd cgroup 驱动。操作路径是先containerd config default /etc/containerd/config.toml生成默认配置然后在配置文件里找到SystemdCgroup改成true。这句话不是可有可无它决定后续 kubelet 能不能通过健康检查。cgroup 驱动不一致的典型表现是节点注册成功但过几分钟就 NotReady日志里出现CRI v1 runtime API is not implemented这类提示或者failed to run Kubelet: misconfiguration: kubelet cgroup driver: cgroupfs is different from docker cgroup driver: systemd。官方组件 kubeadm、kubelet、kubectl 三个包的版本必须锁死。别在工作节点上随手装 latest否则控制面是 v1.28工作节点是 v1.31kubelet 与 apiserver 的版本协商可能出问题。用apt-cache madison kubeadm或yum list kubeadm查看可安装版本指定版本号安装。2.3 网络插件选型决定 Pod 网段别想当然另一个“看似无关实则致命”的准备工作是集群网络插件的选型。网络插件要管两件事给 Pod 分配 IP、在节点之间打通这些 IP。大多数网络插件都会预设一个默认网段比如 Flannel 默认的10.244.0.0/16Calico 默认的192.168.0.0/16。问题来了如果kubeadm init时 Pod 网段和网络插件默认网段不一致CNI 会反复申请 IP 失败Pod 一直 ContainerCreating网络插件自己也可能 CrashLoopBackOff。我的推荐是 Flannel。双节点小集群、学习场景Flannel 的 VXLAN 模式足够用配置简单排错容易。控制面初始化时指定--pod-network-cidr10.244.0.0/16然后安装 Flannel 的 manifests 文件即可。如果你业务网络规划特殊比如要跟现有物理网络重叠那就要仔细算 CIDR别直接撞车。总体思路是先定网络插件再反推 kubeadm init 参数顺序不能反。3. 控制面初始化把这台机器变成“调度中心”3.1 kubeadm init 的关键参数这样给假设控制面节点 IP 是192.168.10.10工作节点 IP 是192.168.10.11。初始化命令我通常会写成sudo kubeadm init \ --apiserver-advertise-address192.168.10.10 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12三个参数各有用处。--apiserver-advertise-address是告诉集群其他组件通过哪个 IP 访问 apiserver一定要填控制面节点内网固定 IP别用 127.0.0.1也别用动态 IP。--pod-network-cidr是 Pod 网段必须跟网络插件一致上面已经说过。--service-cidr是 Service 网段默认是10.96.0.0/12一般不需要改除非你公司内网网段跟它冲突。初始化成功后会输出三段关键信息一是kubeadm join命令二是 join token三是 CA 证书的 hash 值。这是后面第二节点入伙的“门票”。很多人忽略了一件事kubeadm init完成后要在控制面节点上执行以下命令让当前用户能连上 apiservermkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后kubectl get nodes应该能看到控制面节点处于NotReady这正常——网络插件还没装。3.2 安装 Flannel 网络插件这里我建议直接下载官方提供的 kube-flannel.yml然后 applykubectl apply -f https://raw.githubusercontent.com/.../kube-flannel.yml注意要拉取 GitHub 上的清单文件现实中需要确保网络可达或者提前把 yaml 下载到本地再 apply。别在未联网的离线环境里硬等会卡到怀疑人生。安装后稍等一分钟再kubectl get pods -n kube-system看到kube-flannel-ds-*处于 Running控制面节点的状态会自动变成Ready。这个过程中最值得观察的是Flannel 会在每个节点上以 DaemonSet 形式运行一个 Pod也就是说第二节点加入后Flannel 会自动部署上去不用手动再去装一次。3.3 让控制面“愿意”调度业务 Pod双节点集群一个隐蔽的认知误区在这里。默认情况下控制面节点带有一个污点taintkubectl describe node k8s-master | grep Taints # Taints: node-role.kubernetes.io/control-plane:NoSchedule这个污点的意思是普通 Pod 不允许调度到控制面节点。所以如果你初始化好之后马上创建一个有 2 个副本的 Deployment两个副本只会在唯一的工作节点上挤着控制面节点看起来“闲着”却没人干活。这其实是设计好的行为生产环境本来就不希望业务 Pod 跟控制面抢资源。但实验环境只有两台机器面对这种局面有一个很常见的处理临时移除控制面污点让控制面也参与调度。kubectl taint nodes k8s-master node-role.kubernetes.io/control-plane:NoSchedule-把后面的减号去掉污点其实就是“同意这台机器可以跑业务 Pod”。这样做之后两个节点都能调度多副本才有机会真正分布在两台机器上。正式环境不建议这么干但如果你的目的就是验证跨节点调度这是个非常实用的操作。4. 第二节点加入一条 join 命令背后的三个机制4.1 生成加入令牌和 CA 证书 Hash在控制面节点上重新生成 join 命令比翻历史记录更快更干净kubeadm token create --print-join-command这会输出一条完整的命令类似kubeadm join 192.168.10.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxx有两个细节要知道。第一token 默认有效期是 24 小时如果集群搭了一半隔天才继续token 已经过期用kubeadm token list查不到有效 token 时就得重新生成。调试阶段想省事可以用kubeadm token create --ttl 0 --print-join-command创建一个永不过期的 token先跑通流程再学习生命周期管理。第二--discovery-token-ca-cert-hash是控制面 CA 证书的指纹作用是让第二节点确认自己在跟“正确的集群”通信防止中间人。如果命令弄丢了可以通过以下方式重新计算openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \ openssl rsa -pubin -outform der 2/dev/null | \ openssl dgst -sha256 -hex | sed s/^.* //4.2 第二节点上的 join 执行流程第二节点上只要 containerd、kubeadm、kubelet 都装好直接把 join 命令贴上去执行即可。执行过程中会在接受 apiserver 下发的证书kubelet 注册节点然后加入到集群并从控制面拉取配置。注意新版 kubeadm 一般会自动检测 containerd socket不用手动指定--cri-socket。我在实践中见过一个典型错误直接把控制面节点的kubeadm join命令原封不动复制到工作节点结果工作节点报[preflight] Some fatal errors或kubelet cgroup driver问题。这种报错通常不是在 join 阶段而是 preflight 检查阶段暴露环境不一致。处理办法就是回到第 2 节老老实实检查 swap、cgroup、机器 ID、端口占用。join 成功后工作节点上 kubelet 会以 systemd 服务方式运行并自动加载/etc/kubernetes/kubelet.conf持续向控制面汇报心跳。整个过程不需要你手动启动任何东西这是 kubeadm 设计得很顺滑的地方。4.3 验证节点状态别只看“Ready”回控制面节点执行kubectl get nodes -o wide正常情况下能看到两行节点状态都是Ready并且VERSION列应该一致或非常接近。这里多一个建议把-o wide当成默认习惯因为它会额外输出节点的内网 IP、容器运行时版本、操作系统信息。如果第二节点很长时间卡在NotReady先看它是Unknown还是NotReady——Unknown多半是 apiserver 等不到 kubelet 心跳NotReady多半是 kubelet 起来了但健康检查没过。同一时间再看一下 kube-system 命名空间里的 Flannel Podkubectl get pods -n kube-system -o wide你会发现 kube-flannel-ds 自动在新节点上多了一个 Pod这就是 DaemonSet 的能力每个符合匹配条件的节点都会自动部署一个副本。到这里集群在逻辑上已经真正变成“双节点”了。5. 双节点跑通业务从“能跑”到“知道为什么能跑”5.1 创建多副本 Deployment看调度器怎么“分活”为了验证调度我建议创建一个有 2 个副本的简单应用。写一个名为nginx-deploy.yaml的清单apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80执行kubectl apply -f nginx-deploy.yaml后用kubectl get pods -o wide观察输出。如果你已经在第 3.3 节去掉了控制面污点两个 Pod 大概率会分别落在k8s-master和k8s-node01上。如果没有去掉污点两个 Pod 全部落在同一个工作节点上控制面节点则一直空着。这两种结果用kubectl describe pod里的Events都能看到调度器的决策记录比如Successfully assigned default/nginx-demo-xxx to k8s-node01。这个步骤最有意义的体验在于Pod 与节点不是绑死的Pod 的“家”由调度器决定。你删除一个 Pod它会自动在另一个节点上重建你 drain 一个节点Pod 会被驱逐到可用节点上。这就是编排和传统“在某台机器上起容器”的本质区别。5.2 跨节点访问 ClusterIP 与 Pod IP业务跑起来之后再做一个经典验证从任意一个节点访问 Service 的 ClusterIP。先创建一个 Servicekubectl expose deployment nginx-demo --port80 --target-port80 --namenginx-svc kubectl get svc nginx-svc拿到 ClusterIP比如10.96.200.1。然后进入一个 Podkubectl exec -it nginx-demo-xxx -- curl http://10.96.200.1/ --max-time 3你会发现即使这个 Pod 跟另一个后端 Pod 不在同一个节点上请求也能通。这里面有两套网络逻辑在同时工作Flannel 负责 Pod 之间跨主机的二层网络互通kube-proxy 负责把 ClusterIP 转发到后端 Pod。生活化一点理解ClusterIP 像公司的总机号码拨进去被转接到具体的工位Pod IP 则像工位电话跨楼层也能直拨因为 Flannel 在两层楼之间拉了条虚拟网线。顺手验证 DNS在 Pod 里执行nslookup nginx-svc.default.svc.cluster.local能解析到 ClusterIP 说明 CoreDNS 工作正常。这个验证很重要因为很多业务接入集群时第一件事就是依赖服务名互相调用。5.3 给节点打标签实现“这活儿只给第二台机器干”双节点不仅有“两个节点都能跑”的玩法还能实现“指定给某个节点跑”。比如工作节点是一台高性能物理机或者挂着 GPU可以在控制面节点上打标签kubectl label node k8s-node01 locationrack-a然后在 Deployment 里加上nodeSelectorspec: nodeSelector: location: rack-a这样调度器会把 Pod 严格限制到打了locationrack-a标签的节点上。除此之外还有更细粒度的nodeAffinity、podAntiAffinity可以做“不在同一个节点上同时跑两个副本”这类控制。这些配置在生产里非常常用但它们的承载基础都是一个至少有两个节点供你区分和选择。单节点集群根本没法演练这些特性这也是我坚持让你先把双节点跑通的原因。6. 常见问题与排查实录6.1 节点 NotReady这是双节点集群排在第一的高频问题。遇到先分两类看控制面节点 NotReady几乎一定是网络插件没装好优先查kubectl get pods -n kube-system里 Flannel 的状态工作节点 NotReady优先查工作节点上的 kubeletjournalctl -u kubelet -fkubelet 日志会直接给出线索。最常见的三种原因一是 cgroup 驱动不一致前面已经强调过二是节点时间不同步导致证书校验失败三是网络插件无法跨节点通信VXLAN 端口被云安全组或防火墙挡掉。注意 Flannel 用的是 UDP 8472 端口控制面和节点之间必须放通。6.2 token 过期或证书 Hash 不匹配kubeadm join报 token 无效十有八九是 token 过了 24 小时。直接重新生成即可。报证书 hash 不匹配是你的--discovery-token-ca-cert-hash跟当前 CA 证书算出来的 hash 不一致。如果你是在完全干净的工作节点上 join通常不会出这种问题如果工作节点之前加入过其他集群残留/etc/kubernetes配置会导致 preflight 失败建议先备份后清理/etc/kubernetes再 join。6.3 Pod 一直 Pending 或 CrashLoopBackOffPod 一直 Pending先看调度器怎么想的kubectl describe pod pod-nameEvents 里如果出现FailedScheduling通常是资源不足、端口冲突、或者没有节点满足污点容忍。Windows 系统上常见的是工作节点内存只有 2GPod 一多节点资源就不够。Pod 反复 CrashLoopBackOff则去kubectl logs查容器日志如果日志里出现Address already in use多半是端口被占用改一下 NodePort 或 hostPort 就好。6.4 快速排查一种“玄学”情况如果你确认所有配置都对但双节点之间 Pod 就是不通先别急着改业务配置试试从两个节点分别ping对方的 Pod IP。这能快速区分是 Pod 自身服务问题还是底层 Flannel 隧道问题。VXLAN 网络有个特点Pod IP 层面是通的但ping看到的源地址可能是 VTEP 的地址这是正常现象。不要被“ping 通但源地址不对”吓到只要业务流量通网络层就没毛病。排查完把这些经验整理成一张小表贴在笔记里会非常提神现象可能原因优先排查手段节点 NotReady网络插件、cgroup、时间journalctl -u kubelet -fjoin 报 token 无效token 过期kubeadm token create 重新生成Pod Pending污点、资源不足kubectl describe podPod CrashLoopBackOff配置错误、端口冲突kubectl logs跨节点 Pod 不通VXLAN 端口、CIDR 冲突ping Pod IP查防火墙7. 最后再分享几点实际体会双节点集群最大的价值不是“有两台机器”这个事实而是它让你第一次真正看见 K8s 里的“调度”发生了什么。我第一次看到两个副本分别落到不同节点时瞬间理解了为什么大家都在说“不要手动指定容器跑在哪台机器上”——调度器比你更清楚全局状态。有一个踩过几次坑之后总结出来的习惯所有节点的主机名、机器 ID、时间、内核版本、kubeadm 版本最好在开工前一次性核对完别边搭边补。很多双节点问题的根子都在初始环境差异上看似是 Kubernetes 的报错其实是系统管理员偷懒留下的债。如果你已经顺利跑通这一篇的内容下一步建议往三个方向走一是把控制面节点数扩到三个理解真正的高可用是怎么通过多数派协商实现的二是引入 Ingress Controller把集群内部 Service 暴露到外部三是做一次完整的节点维护演练比如kubectl drain一个节点观察业务如何无缝迁移。这个过程会比“照着文档敲命令”痛苦但收获也会扎实得多。