简介:这是一套面向云原生初学者与运维工程师的二进制高可用Kubernetes集群一键部署工具,专为深入理解k8s控制平面组件原理而设计,解决手动部署etcd、kube-apiserver、scheduler等组件流程繁琐、易出错的痛点。资源共13个文件,包含4个核心Shell脚本(如install_HA_k8s.sh、install_etcd.sh)、2个二进制压缩包(etcd与kubernetes-server)、2个CNI网络配置(calico.yaml、coredns.yaml)、3个CFSSL证书工具(cfssl_linux-amd64等)及readme.txt说明文档,整体包大小398.89MB,覆盖证书生成、高可用主节点初始化、工作节点加入、VIP漂移与网络插件安装全流程。已有1574人学习下载,用户可直接执行脚本完成从环境准备到集群验证的完整闭环,配套清晰步骤注释与典型排错提示,特别适合动手实践、面试复盘或企业轻量级生产环境快速搭建。 回想第一次在客户现场部署高可用K8s集群的场景,我到现在还记得当时的狼狈:三台Master、两台Node,加上独立etcd集群和VIP,整套下来每台机器都要敲上百条命令。仅证书签发这一步,就因为SAN漏掉了VIP,导致apiserver地址对不上,又重签了一遍。凌晨两点半,我坐在机房里泡着浓茶想,这套流程明明可以用脚本固化下来,为什么还要让后来的人继续重复踩坑?
后来我花了大约一周时间,把整套二进制高可用K8s集群的部署流程拆解、重写、反复测试,最终沉淀成一套一键部署脚本。本文就是这套脚本从架构设计、实现逻辑到落地排障的完整复盘。内容涵盖高可用架构的底层机制、脚本分层设计思路、证书与网络插件的关键细节,以及集群上线后最常见的故障排查链路。适合有一定Linux基础、想在离线内网或生产环境自建K8s集群的运维和交付同学参考。
1. 为什么是二进制:被kubeadm和RKE2都“坑”过之后的选择
1.1 三种部署方式的真实差异
开始写脚本之前,我先明确了一个问题:为什么不用现成的kubeadm,也没有直接用RKE2,而是选二进制?
很多人觉得kubeadm是官方推荐,RKE2是Rancher的轻量级发行版,这两个方案已经足够成熟,自己再折腾二进制有点重复造轮子。但我在实际交付过程中,两种方式都遇到过比较麻烦的场景。
kubeadm最大的问题在于依赖镜像仓库。初始化集群时要拉取kube-apiserver、kube-controller-manager、kube-scheduler、coredns、pause等一组镜像,升级时还要拉新版本镜像。生产环境里很多机房是隔离网络,就算有内网镜像仓库,也需要先把所有镜像搬运进去。如果赶上交付现场临时发现某个镜像校验值不对,或者仓库同步延迟,整个时间表都会被拖垮。
RKE2的思路是把K8s组件打包成单个二进制,内置containerd和k3s风格的目录结构,对运维来说确实省心。但它有个隐性成本:版本跟随Rancher的发布节奏走。企业内部如果有安全合规要求,需要固定某个K8s小版本、自己维护补丁,或者要跟内部监控、日志、安全Agent做深度适配时,RKE2的封装反而成了阻碍——很多东西不开放让你改。
二进制部署的本质,是把所有组件从官方Release页下载下来,自己生成证书、自己写systemd配置、自己组装集群。它不适合所有人,但适合以下场景:
| 部署方式 | 镜像依赖 | 版本可控性 | 故障排查友好度 | 对网络环境要求 |
|---|---|---|---|---|
| kubeadm | 强依赖镜像仓库 | 中,受发行版约束 | 中,组件被容器封装 | 需要镜像源 |
| RKE2/RKE | 相对少 | 低,跟随上游发行版节奏 | 中,封装较多 | 需要官方二进制包 |
| 二进制手动部署 | 无 | 高,完全自主可控 | 高,所有日志直接可见 | 只要有tar包即可 |
1.2 二进制部署的本质:把“黑盒”变“白盒”
我后来跟朋友聊天时打过一个比方:kubeadm像是在帮你组装一台品牌机,双击安装就能开机,但你想看看内存插槽走线、电源模组怎么设计,它不让你拆;二进制部署更像是你从京东买齐了CPU、主板、电源、机箱,自己动手装一台。装的过程更费劲,但装完之后,每根线是谁接的、每个零件什么规格,你心里一清二楚。
这个“白盒”属性在排障时价值极大。K8s集群出问题时,大约有七成故障出在“组件之间的衔接层”——kubelet和容器运行时对不上、apiserver连不上etcd、证书过期、CNI网络没起来。如果是kubeadm部署,你要先钻进容器里看日志、看配置;如果是二进制部署,所有组件进程直接挂在systemd下,配置就在/etc/kubernetes目录里,日志直接打在journalctl里,哪里断了改哪里,整个过程非常直接。
1.3 什么样的环境适合二进制方案
从我实际经手的项目看,二进制方案在下面几类环境中出镜率最高:
- 独立交付、信创替代、政企内网项目:网络隔离严重,镜像仓库不一定可用,但服务器上能上传tar包。
- 多集群统一版本管理的平台团队:需要把几十套集群钉在同一个K8s版本上,kubeadm自动升级会引入版本漂移。
- 深度定制场景:比如需要替换默认调度器、自定义kubelet启动参数、对接企业内部CA体系。
- 学习K8s原理的技术团队:手动部署一遍,对apiserver、etcd、controller-manager、scheduler之间关系的理解,比只看文档深刻得多。
我写这套脚本的定位很明确:不追求覆盖所有花式功能,而是把一个三Master两Node的高可用集群,做到“给一份IP清单,一条命令跑完,半小时内交付可用集群”。
2. 高可用架构的底层逻辑:VIP、负载均衡和选主机制
2.1 部署拓扑:三Master双Worker的最小高可用形态
在动手写脚本之前,我先把目标架构画了出来。没有用mermaid,直接看文字描述也足够直观。
集群最少需要五台机器:
- 三台Master节点,跑kube-apiserver、kube-controller-manager、kube-scheduler、etcd;
- 两台Worker节点,跑kubelet、kube-proxy和业务负载。
为什么Master要三台?因为etcd要用Raft协议保证数据一致性,Raft要求多数派才能写入。三节点集群允许挂掉一台,剩下两台仍是多数派;如果只有两台,挂掉一台就只剩一台,不满足多数派条件,整个集群的写入会全部卡住。
三台Master同时承担etcd角色,省下了单独部署etcd集群的机器成本。对于生产环境规模不大、几百个Pod以内的集群,这种“叠放”架构足够稳定。如果业务量极大,etcd和apiserver之间会产生资源竞争,那时候再拆成独立etcd集群也不迟。
2.2 etcd由Raft协议决定的三节点奇数规则
规划etcd时有一个点必须想明白:etcd选主和数据写入的规则,决定了集群节点数必须是奇数。
Raft协议里,一个写请求要提交成功,必须得到超过半数的节点确认。三节点集群允许挂一台,五节点集群允许挂两台。如果部署成四节点,允许挂一台,但四台机器只比三台多承担了三分之一的存储成本,却没有提升任何可用性——所以四节点etcd是运维里最常见的浪费型设计。
我在脚本里同时处理了etcd的两个核心运维参数,这两个参数也建议你在自己的集群里提前配好:
--quota-backend-bytes=8589934592 --auto-compaction-mode=periodic --auto-compaction-retention=72hquota-backend-bytes是etcd存储的配额上限,默认2G很容易写满,我直接给到8G。auto-compaction-retention=72h表示每72小时自动压缩一次历史数据,防止etcd的数据文件无限膨胀。这两个参数不配,集群跑上几个月后很容易出现“数据目录超大但看不到什么日志”的诡异故障。
2.3 关键决策:kube-apiserver访问etcd到底该走哪条路径
很多人第一次搭高可用K8s集群时会在这里卡很久:kube-apiserver连接etcd时,地址列表里到底填什么?
- 方案A:填三个Master节点的物理IP,比如
https://192.168.10.11:2379,https://192.168.10.12:2379,https://192.168.10.13:2379; - 方案B:填VIP,比如
https://192.168.10.100:2379。
我实际测试下来,方案A更稳。因为kube-apiserver和etcd同机部署时,直连本机etcd不但延迟最低,而且即使VIP发生漂移,apiserver和etcd之间的连接也不会断。
方案B的问题在于,HAProxy如果你只把2379端口也纳入VIP转发,那么VIP抖一下,所有apiserver到etcd的连接都要重连。虽然K8s能自动重试,但生产现场任何一次不必要的抖动都可能引发雪崩。所以我的脚本里,etcd服务直接暴露在三台Master的物理IP上,VIP只转发6443端口。
2.4 规划清单:IP、主机名、CIDR、端口
写脚本前还有一份清单必须确定,否则后面改起来非常痛苦:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| VIP | 192.168.10.100 | kube-apiserver的负载入口 |
| Master节点IP | 192.168.10.11/12/13 | 三台,跑etcd和K8s控制面组件 |
| Worker节点IP | 192.168.10.21/22 | 业务负载节点 |
| Service CIDR | 10.96.0.0/12 | K8s Service虚拟IP段 |
| Pod CIDR | 10.244.0.0/16 | Pod容器IP段,CNI默认用这个段 |
| 节点名称 | k8s-master-01/02/03, k8s-node-01/02 | 必须全局唯一,不能重名 |
| DNS服务器 | 无特殊要求 | 推荐内网DNS,全节点能互通 |
这里有一个经验要重点说:主机名、IP和CIDR一定要在跑脚本前确定好,并且写进环境配置文件,不要部署到一半再改。主机名重复、IP错位、Service CIDR和Pod CIDR冲突,这三类问题在K8s排障里占比极高。脚本里我做了前置校验,发现IP不连通、主机名重名、端口被占用就直接报错退出,这也是“一键部署”能在各种现场稳定复现的关键。
3. 一键部署脚本的分层设计:从入口到落地的完整链路
3.1 入口脚本:交互式参数采集与配置生成
我把整个部署流程拆成了两层。第一层是一份全局环境配置文件,我习惯命名为cluster.env;第二层是一串带编号的模块脚本,从01到08按顺序执行。
入口脚本本身不干重活,只做三件事:读取配置、校验环境、按顺序调用子模块。这种做法最大的好处是:如果在某个模块执行失败,你可以单独重新跑那一个模块,不用从头再来一遍。
#!/bin/bash # deploy-k8s.sh set -o errexit set -o pipefail source ./cluster.env echo "[INFO] 开始部署 Kubernetes ${K8S_VERSION} 集群..." ./scripts/00-check-env.sh ./scripts/01-init-system.sh ./scripts/02-gen-certs.sh ./scripts/03-install-etcd.sh ./scripts/04-install-master.sh ./scripts/05-install-node.sh ./scripts/06-install-cni.sh ./scripts/07-verify-cluster.sh入口脚本里的set -o errexit非常关键,意思是任何一条命令失败就让脚本整体退出。K8s部署是链条式依赖,前面失败后面继续跑只会产生一堆半成品配置,重启后更难排查。我见过很多朋友的部署脚本不加这个参数,结果某个证书生成失败,后面的安装流程照样跑了一整轮,最后整个集群状态混乱到只能重装系统。
3.2 幂等化的奥秘:怎么做到脚本反复执行不出乱子
“幂等”这个词听起来抽象,但理解起来很简单:同一个脚本,在已经部署过的机器上再跑一遍,不会破坏已有环境,不会重复生成一堆垃圾配置。
K8s部署里最容易出现重复执行问题的是证书生成和systemd配置。如果脚本每次执行都重新生成CA证书,那么旧证书全部失效,集群里所有组件之间的信任关系直接崩掉。所以我的证书模块开头会先检查目标路径是否存在:
if [ -f "/etc/kubernetes/pki/ca.crt" ]; then echo "[WARN] 已检测到CA证书,跳过证书生成。" else ./gen-certs.sh fisystemd unit文件也一样。一个服务已经在运行,你又往/usr/lib/systemd/system/下覆盖了一份新的unit文件,再执行systemctl daemon-reload,正在运行的服务状态可能变成activating或直接重启,对线上集群来说这是不可接受的。所以脚本里每个服务模块都做了同样的判断:配置文件存在且服务状态为active (running),则跳过。
这套幂等逻辑让我在后续排障时代价极低:现场出了问题,丢一个模块重跑,而不是整台机器重装。
3.3 证书模块:所有网络通信的安全底座
证书是K8s二进制部署里最绕不开、也最容易出错的部分,我单独花了一整节讲它。
K8s集群内部通信全部走TLS加密,整个证书体系分三套:
- etcd证书:etcd节点之间的peer通信、etcd对外服务的client通信;
- Kubernetes组件证书:kube-apiserver对外提供服务的server证书,以及controller-manager、scheduler、kubelet、kube-proxy等组件的client证书;
- kubeconfig证书:kubectl、kubelet等客户端访问apiserver时使用的用户证书。
三套证书共用同一个CA,还是各自独立的CA?我选择的是etcd单独一套CA,K8s单独一套CA。好处是职责隔离——如果某个etcd节点被攻破或者证书误发,K8s控制面证书不受影响;反过来也一样。
生成K8s apiserver证书时,有一个隐藏的大坑就是SAN(Subject Alternative Name)。apiserver的证书必须包含所有可能的访问地址,包括:
- 三个Master节点的物理IP;
- VIP地址;
- Service CIDR里的第一个IP,通常是
10.96.0.1,这是kube-apiserver在集群内部的Service地址; - 本机回环地址
127.0.0.1; - 域名,比如
kubernetes.default.svc.cluster.local、kubernetes.default.svc、kubernetes.default、kubernetes。
我最开始手动部署时漏掉了VIP,导致kubectl通过VIP访问apiserver时报证书校验失败,那个错误信息还特别不直观——x509: certificate is valid for 10.96.0.1, not 192.168.10.100。看到这个你才能反应过来是SAN没配全。所以脚本里我把SAN列表定义成了数组,并且强制把VIP、MASTER_IPS、SERVICE_CIDR首地址都加进去:
SAN_LIST=( "127.0.0.1" "10.96.0.1" "${VIP}" "${MASTER_IPS[@]}" "kubernetes" "kubernetes.default" "kubernetes.default.svc" "kubernetes.default.svc.cluster.local" )3.4 组件安装与systemd托管:让二进制程序变成可靠服务
证书生成完之后,就是安装组件。Master节点的组件从官方Release页下载对应的tar包,解压后把二进制文件放到/usr/local/bin/目录下,再为每个组件写一个systemd unit文件。
以kube-apiserver为例,它的systemd单元文件核心内容大概长这样:
[Unit] Description=Kubernetes API Server Documentation=https://github.com/kubernetes/kubernetes After=network.target [Service] ExecStart=/usr/local/bin/kube-apiserver \ --bind-address=0.0.0.0 \ --secure-port=6443 \ --etcd-servers=https://192.168.10.11:2379,https://192.168.10.12:2379,https://192.168.10.13:2379 \ --service-cluster-ip-range=10.96.0.0/12 \ --service-node-port-range=30000-32767 \ --client-ca-file=/etc/kubernetes/pki/ca.crt \ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key \ --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \ --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \ --allow-privileged=true \ --service-account-key-file=/etc/kubernetes/pki/sa.pub \ --kubelet-preferred-address-types=InternalIP,Hostname,ExternalIP Restart=on-failure RestartSec=10 LimitNOFILE=65535 [Install] WantedBy=multi-user.target有一个微小的启动参数值得注意:--kubelet-preferred-address-types=InternalIP,Hostname,ExternalIP。如果不指定这个参数,新版K8s默认会优先用Hostname去连接kubelet,而在没有内网DNS的环境里,Hostname解析不出来,apiserver访问kubelet就会超时,表现为kubectl logs、kubectl exec经常卡住。我见过太多集群没配这个参数,排查半天最后发现是DNS解析问题。
controller-manager和scheduler的systemd配置相对简单,核心是加--leader-elect=true。这两个组件不支持多实例同时工作,必须靠leader选举机制保证同一时刻只有一个实例在真正干活。三台Master上各跑一个实例,宕机一台,其他实例立刻顶上,这就是Master节点组件高可用的实现原理。
Worker节点上的kubelet和kube-proxy也用systemd托管。kubelet的配置里有一个参数跟容器运行时直接相关,我拿到下一节专门讲——因为这个地方错一步,整个集群的Pod都起不来。
4. 三个最容易翻车的环节:证书SAN、网络插件和运行时对接
4.1 证书SAN漏配导致apiserver访问异常
前面说了SAN漏配的坑,这里再展开一个真实排障过程。
现象:集群部署完成后,在任意一台Master上执行kubectl get nodes,报错Unable to connect to the server: x509: certificate is valid for 10.96.0.1, not 192.168.10.100。
这个报错信息的意思是:你拿着证书去访问192.168.10.100,但证书里写明的合法地址只有10.96.0.1。如果你之前手动生成证书时忘了加VIP,那么通过VIP访问就永远不可能成功。
处理方式也很直接:重新生成apiserver证书,把VIP加进SAN,然后用新证书重启kube-apiserver组件。
cfssl gencert -ca=ca.crt -ca-key=ca.key \ -profile=kubernetes \ -hostname=127.0.0.1,10.96.0.1,192.168.10.11,192.168.10.12,192.168.10.13,192.168.10.100,kubernetes,kubernetes.default \ kubernetes-csr.json | cfssljson -bare apiserver这条命令里,-hostname参数就是决定证书“允许哪些地址访问”的关键。我在脚本里把它参数化,每次生成前自动拼接,彻底杜绝手写漏项。
4.2 容器运行时cgroup驱动要和kubelet保持一致
K8s部署的第二个高频翻车点,是kubelet和容器运行时之间的cgroup驱动不一致。
cgroup是Linux内核用来限制进程资源使用量的机制。K8s要控制Pod的CPU、内存使用上限,kubelet必须用cgroup去约束容器。containerd和kubelet都有两套驱动可以选择:systemd和cgroupfs。只要kubelet和容器运行时用的驱动不一致,kubelet就会报错,节点状态直接NotReady。
我在脚本里统一处理成systemd驱动,这是目前所有主流发行版推荐的方案。
kubelet侧在配置文件中指定:
apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemdcontainerd侧在/etc/containerd/config.toml中指定:
[plugins."io.containerd.grpc.v1.cri"] systemd_cgroup = true如果这两个参数不一致,kubelet启动时会报一个非常明确但很容易被忽略的错误:
failed to run Kubelet: failed to validate kubelet flags: cgroup-driver does not match the runtime cgroup-driver看见这个报错,先别慌,对照一下两边驱动配置,改成一致即可。
另外,使用containerd作为容器运行时,还有一个镜像源问题。K8s每个Pod启动前都要先拉一个pause镜像,这个镜像默认地址是registry.k8s.io/pause:3.9。在完全离线的环境里,必须提前把pause镜像导入到每个节点的containerd里,并且把sandbox_image改成内网仓库地址。我的脚本里专门有一个模块做镜像预加载,部署前先ctr images import,确保节点就绪后有镜像可用。
4.3 CNI网络选型和IPVS内核模块加载
集群节点状态变成Ready之后,最激动人心的时刻是创建第一个Pod。但很多二进制部署的集群,Pod创建了却一直ContainerCreating,卡在沙箱创建或网络设置阶段。这时候八成是CNI网络插件没有正确部署。
CNI是K8s容器网络的接口标准,常见的实现有Flannel、Calico、Cilium。我的脚本默认用Flannel,原因是它最简单、最稳定、能满足绝大多数场景的需求——Pod互通、Service后端的负载均衡,Flannel的VXLAN模式都能覆盖。
Flannel部署上去之后,有一个重要的前置条件:kube-proxy如果要开启IPVS模式,内核必须加载相应的IPVS模块。IPVS是Linux内核里比iptables更高效的负载均衡方案,K8s访问Service流量时默认会走这里。
脚本里我写了一段前置检查,确保以下内核模块全部存在:
ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack_ipv4如果模块缺失,用modprobe加载:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack_ipv4注意,nf_conntrack_ipv4在新版内核里已经改名成nf_conntrack了,脚本里要做一个版本判断。这部分细节网上教程很少提到,但部署时遇到modprobe: FATAL: Module nf_conntrack_ipv4 not found的概率并不低。
另外还要确认ip_forward开启:
sysctl -w net.ipv4.ip_forward=1这个参数不开启,容器内部的流量转发会失败,Pod能创建但网络完全不通。
4.4 部署脚本执行后的验证指标
脚本最后一定要有验证环节,不能跑完就算完。我的07-verify-cluster.sh模块会自动执行以下几项检查:
kubectl get nodes,确认所有节点状态为Ready;kubectl get pods -n kube-system,确认coredns、kube-proxy、flannel等系统组件全部Running;- 创建一个测试Pod,等待它完全启动,再删除;
kubectl get svc -A,确认Service网络分配正常。
如果检查失败,脚本会输出对应组件的日志路径,并提示用户用journalctl -u kubelet -f或kubectl describe进一步排查。
5. 集群上线之后的排障经验:从NodeNotReady到服务访问不通
5.1 NodeNotReady的完整排查链路
集群部署完并不代表万事大吉,NodeNotReady是运维群里出现频率最高的问题。我总结出一条固定排查链路,每次遇到都能快速定位。
第一步,在Master上执行kubectl describe node <节点名>,看Conditions里的信息。但这里有一句大实话:describe输出的内容经常不够具体,只能告诉你节点处于什么状态,没法告诉你根因。
第二步,登录到出问题的节点上,执行systemctl status kubelet -l。这一步能确定kubelet到底有没有在运行。如果kubelet根本没起来,再看日志:
journalctl -u kubelet -f --no-pagerkubelet日志里出现频率最高的几个根因,我整理成了一张速查表:
| 日志关键字 | 根因 | 处理方式 |
|---|---|---|
cgroup-driver does not match | kubelet与容器运行时cgroup驱动不一致 | 统一改成systemd并重启 |
Container runtime network not ready | CNI网络插件未部署或异常 | 检查flannel/calico Pod状态 |
Error getting node | kubeconfig证书或RBAC权限问题 | 检查kubelet.kubeconfig证书有效性 |
Failed to get system container stats | 节点资源异常或cgroup管理失控 | 检查磁盘和inode使用率 |
PLEG is not healthy | 容器运行时响应超时 | 排查containerd状态,必要时重启 |
第三步,确认容器运行时状态:
systemctl status containerd crictl pskubelet和containerd之间存在一个“容器运行时接口”,如果containerd挂掉或响应缓慢,kubelet会把节点标记为NotReady。这里有个容易忽略的点:containerd异常通常是因为磁盘满了,所以排查时先df -h和df -i看一眼,百分之六十的运行时假死都是磁盘写满导致的。
5.2 镜像拉取失败和DNS解析异常的常见根因
节点状态恢复Ready之后,第二步通常是部署业务应用。这里最常见的两个问题是ImagePullBackOff和DNS解析异常。
ImagePullBackOff的排查思路很直白:执行kubectl describe pod <pod名>,看Events里拉镜像时具体报什么错。常见原因无非三种:
- 镜像地址写错,或者tag不存在;
- 私有仓库需要认证,但Pod没有配置
imagePullSecrets; - 节点无法访问镜像仓库,离线环境或网络策略拦截。
离线环境下我用脚本部署时,会先在所有节点上预加载业务镜像。具体做法是把镜像打成tar包,然后每个节点执行ctr -n k8s.io images import xxx.tar。注意,containerd的命名空间默认是k8s.io,如果用ctr images import不带-n参数,镜像会被导入到默认命名空间,K8s根本看不到。
DNS解析异常的表现是:Pod能启动,但Service域名访问不通,比如curl http://my-service.default.svc一直超时。
排查链路是:
- 先确认CoreDNS Pod是否Running:
kubectl get pods -n kube-system | grep coredns; - 再确认CoreDNS日志里有没有报错:
kubectl logs -n kube-system -l k8s-app=kube-dns; - 下一步检查kube-proxy是否正常工作:
kubectl logs -n kube-system -l k8s-app=kube-proxy; - 最后看节点上IPVS规则是否生成:
ipvsadm -L -n,如果没有任何规则,说明kube-proxy的IPVS模式初始化失败。
DNS解析异常还有一个很容易被忽略的坑:CoreDNS的副本数如果超过一个,且它们之间网络互通正常、但上游DNS配置不一致,会导致部分Pod解析正常、部分Pod解析超时。我建议把CoreDNS副本数固定为2,并检查CoreDNS ConfigMap里的forward配置是否指向了正确的上游DNS。
5.3 高可用组件选主异常时的特征与处理
高可用集群里,controller-manager和scheduler通过leader选举机制实现多活,但它们选主异常的隐蔽性很强。节点都显示Ready,Pod也都在跑,但某些功能时好时坏。
比如kubectl delete pod之后,Pod被删了却迟迟没有新Pod创建,或者控制器长期不响应期望状态。这时候要看controller-manager的日志:
journalctl -u kube-controller-manager -f如果出现大量leaderelection lost或failed to acquire lease,说明选主机制出了问题。常见原因有两个:
- 三个Master节点时钟不同步。leader选举依赖租约过期时间,时钟偏差太大会导致租约频繁过期、反复选主。
- etcd写入延迟太高。controller-manager选主要往etcd写一个Lease记录,如果etcd响应慢,租约续不上,就会不断丢主。
处理方案也很明确:先在所有节点上配置chrony或ntp时间同步;然后检查etcd健康状态:
etcdctl --endpoints=https://192.168.10.11:2379,https://192.168.10.12:2379,https://192.168.10.13:2379 \ --cacert=/etc/etcd/pki/ca.crt \ --cert=/etc/etcd/pki/server.crt \ --key=/etc/etcd/pki/server.key \ endpoint health --cluster看到三端全部返回healthy,再观察选主日志是否恢复。这一套组合拳打下来,大部分选主异常都能解决。
6. 把脚本沉淀成运维资产:日常管理与后续扩展
6.1 节点的增删改:脚本如何支持纳管新节点
集群交付后第一个常见需求就是扩容加节点。我脚本里单独写了一个add-node.sh,它做的事情可以概括为:
- 解压K8s Node组件二进制包;
- 生成kubelet、kube-proxy需要的kubeconfig文件;
- 从已有Master节点拷贝CA证书和配置;
- 启动kubelet和kube-proxy;
- 在Master上执行
kubectl label node添加角色标签。
新节点的证书不用重新生成。K8s的证书体系里,kubelet的证书有两种方式:一种是手动签发长期证书,另一种是通过kubelet TLS Bootstrapping机制自动申请。我脚本里默认采用bootstrap方式:
kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubeconfig \ --kubeconfig=/etc/kubernetes/kubelet.kubeconfig这样一来,新节点只需要一个bootstrap token,kubelet启动后会自动跟apiserver申请证书,并把申请到的证书写入kubelet.kubeconfig。后续就算证书到期,也能自动续期,省去了每过一年就得手动重签证书的麻烦。
6.2 从集群到业务:部署LNMP、Spring Boot、Redis的衔接
集群搭好之后,下一步就是往里面运行业务。搜热词里能看出,很多人关心K8s部署LNMP、Spring Boot项目、Redis集群。这里我给一个通用的思维框架:K8s部署业务的核心不是“把应用塞进去”,而是把应用拆解成部署、服务、配置三块。
- 无状态应用用Deployment管理,Pod挂了自动拉起;
- 有状态应用用StatefulSet管理,比如Redis集群、Kafka集群,每个Pod有固定网络标识和存储;
- 配置和敏感信息用ConfigMap和Secret管理,不要写死在镜像里。
以部署一个Spring Boot项目为例,最简化的yaml文件至少包含:
apiVersion: apps/v1 kind: Deployment metadata: name: springboot-app spec: replicas: 3 selector: matchLabels: app: springboot-app template: metadata: labels: app: springboot-app spec: containers: - name: app image: registry.internal/springboot-app:1.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: springboot-app spec: selector: app: springboot-app ports: - port: 80 targetPort: 8080这套脚本交付的集群自带Service负载均衡、健康检查和自动恢复能力。后面再往集群里接Ingress、Prometheus监控、日志采集,都是在现有底座上长新枝的事。
6.3 升级策略与备份容灾
二进制部署有个一直被人诟病的点:升级麻烦。其实一旦脚本化之后,升级过程反而比kubeadm更可控。
我的做法是:先在一台测试节点上升级kubelet二进制,观察日志和Pod稳定性,然后逐台滚动替换。控制面组件升级则按“先备节点、后主节点”的顺序,每台替换后要等它重新加入集群、选主成功,再操作下一台。
备份方面,K8s集群最重要的备份对象不是各组件配置,而是etcd数据。etcd里保存了集群的全部状态——所有Pod、Service、Deployment、ConfigMap、Secret。我建议每天做一次快照,保留最近7天:
etcdctl snapshot save /backup/etcd-snapshot-$(date +%F).db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/etcd/pki/ca.crt \ --cert=/etc/etcd/pki/server.crt \ --key=/etc/etcd/pki/server.key恢复时使用etcdctl snapshot restore,然后把恢复出来的数据目录替换到目标节点上。整个恢复流程我在测试环境演练过很多次,如果数据备份完整,从宕机到恢复,半小时内能拉起一个可用集群。
我在使用中发现,二进制部署这套方案最值钱的地方,不是“不依赖kubeadm”这个形式,而是它逼着你把集群的每一个组件都理解透了。等你在生产环境遇到过几次NodeNotReady、证书过期、etcd容量告警之后,就会明白:所有能用脚本覆盖的复杂流程,背后都藏着一套可以复用的底层认知。
最后分享一个小技巧:脚本维护时,尽量把版本号、IP、CIDR这类变量收敛到最顶层的cluster.env文件里,不要散落在各个模块脚本中。这样每次版本升级或者换一套环境重新交付,只需要改一份配置文件,剩下的事情交给脚本去跑。这套脚本帮我完成过多次现场交付,从机器准备到集群可用,最快一次大约二十五分钟。希望能帮你少踩一些我当年踩过的坑。
本文还有配套的精品资源,点击获取