1. 整体思路与部署架构选型1.1 为什么选择离线手动搭建先聊一个大家都会遇到的问题:学习K8S到底应该从哪里入手?很多人第一反应是装个minikube或者kind,一条命令拉起来,kubectl get nodes能看到一个Ready的节点,就觉得"我会K8S了"。但等真正上了生产,出了问题才发现自己完全不知道集群内部是怎么运转的。这就跟开车一样,你在驾校开自动挡和上了路开手动挡完全是两码事。K8S这个东西,如果不去手动搭建一次,你就永远停留在"会用工具"的层面,没有真正理解"工具"本身。我这次选择离线手动搭建,其实是被环境"逼"出来的。某公司的内网服务器完全不能访问外网,但又需要一套K8S环境来做容器化部署的测试和验证。最开始我也想过曲线救国:在一台能上网的机器上装好所有镜像,然后打包带进内网。后来发现这个思路本身就是K8S离线部署的标配玩法。索性就把整个过程完整走了一遍,顺便把之前的很多知识盲区补上了。手动搭建的好处在哪里?首先是你能真正搞清楚每个组件是干什么的。etcd存什么数据、kube-apiserver怎么和etcd交互、kube-scheduler怎么调度Pod、kubelet怎么和容器运行时通信。这些东西在自动化的安装工具里全都是黑盒,你只知道装完就能用,但出了问题就抓瞎。其次是手动搭建的过程中,你会被迫理解证书体系、网络插件的选择、Pod网段和Service网段的规划,这些才是K8S运维的核心能力。1.2 核心组件与架构设计在动手之前,先把K8S的架构理清楚。我这次搭建的是最经典的单Master双Worker架构,三个节点分别是master01、worker01、worker02。为什么不用高可用?因为这是做学习和验证用的环境,单Master足够覆盖绝大部分测试场景。如果以后要上生产,再做HA,原理是一样的,只是多了负载均衡这一层。K8S的核心组件可以分成两个部分:控制平面和节点平面。控制平面包含kube-apiserver、kube-controller-manager、kube-scheduler、etcd四个组件,节点平面包含kubelet、kube-proxy和容器运行时。组件角色一览:组件职责关键点etcd集群状态存储所有数据的唯一来源,坏了集群就瞎了kube-apiserver所有请求的入口唯一能直接访问etcd的组件kube-controller-manager维持期望状态节点控制器、副本控制器等一堆控制器kube-scheduler决定Pod跑在哪个节点根据资源和约束做调度决策kubelet节点上的“眼睛和手”负责Pod的生命周期管理kube-proxyService流量转发实现负载均衡和服务发现这里最需要理解的是kube-apiserver的地位。所有的组件——kubelet、kube-scheduler、kube-controller-manager、kubectl——要读写集群状态,都必须经过kube-apiserver。它就像整个集群的“总闸门”,所有请求进来先经过认证、授权、准入控制三道关卡,然后才会真正写入etcd。理解了这一点,你就知道为什么在排查问题的时候,第一步总是看kube-apiserver是否正常。1.3 版本选型与配套组件版本选型这个问题,网上说法很多,但我给的建议很简单:选一个经过时间验证的稳定版本。我这次用的是1.23.17,为什么选这个版本?因为1.24开始,K8S把Dockershim移除了,如果你还习惯用Docker作为容器运行时,就需要额外安装CRI-Dockerd这个适配层。而1.23及之前的版本,Docker还是"原生支持"的,对初学者来说少了一道折腾。当然如果你已经适应containerd作为运行时,用更新的版本也没问题,只是我这次为了减少变量,选择了更稳妥的1.23。配套组件版本也是一个萝卜一个坑:组件版本说明Kubernetesv1.23.17主版本etcdv3.5.6存储集群数据containerdv1.6.19容器运行时,替代Dockercrictlv1.24.0命令行调试工具calicov3.25.0CNI网络插件这里多说一句为什么要用containerd而不是Docker。刚开始我其实想用Docker,毕竟熟悉嘛。但研究了一下发现,在当前这个阶段,containerd才是更"正统"的容器运行时。K8S通过CRI(Container Runtime Interface)和containerd直接交互,中间少了一个Docker的转换层,更稳定也更高效。而且后面如果想把集群升级到更新版本,用containerd可以省掉很多麻烦。2. 离线环境的准备与前置条件2.1 离线安装包的准备策略离线安装最大的难点不是"装",而是"怎么把需要的东西都提前准备好"。首先明确需要准备的几个包:K8S的三个核心组件包——kubelet、kubeadm、kubectl,而且必须指定版本。这三个东西在K8S官方的yum仓库或者apt仓库里都有,关键是要在能联网的机器上先下载好。我用的是yum方式,所以需要下载kubelet-1.23.17、kubeadm-1.23.17、kubectl-1.23.17这三个rpm包,以及它们的一堆依赖。然后就是容器运行时。我选了containerd,需要下载containerd.io这个包。这里必须提醒一下:containerd的版本和K8S版本要兼容,1.6.x的containerd配合1.23.x的K8S是经过验证的组合。接下来是镜像。K8S集群跑起来需要一堆镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause、coredns,还有网络插件calico需要的几个镜像。这一块是离线部署的重头戏,也是最容易出问题的地方。提示:镜像的获取有一个小技巧,通过kubeadm config images list可以列出指定版本K8S需要的全部镜像清单,然后逐个拉取、打包、拷贝。这一步必须在能联网的机器上提前完成。2.2 服务器基础环境配置三台服务器的操作系统我用的都是CentOS 7.9,配置是4核8G、100G磁盘。这个配置跑一个测试集群完全够了,但如果想让集群运行得更从容,建议Master节点给到8核16G,毕竟etcd和kube-apiserver都是吃内存的主儿。系统层面的准备工作集中在三个方面:主机名、IP和防火墙。主机规划:主机名IP地址角色master01192.168.1.101控制平面节点worker01192.168.1.102工作节点worker02192.168.1.103工作节点设置主机名很简单,hostnamectl set-hostname master01,然后三台机器的/etc/hosts都要加上三行映射关系。这一步很多人会忽略,但非常重要——K8S组件之间通信依赖主机名解析,如果你不配hosts,后面kubeadm init的时候就会报"hostname not found"的错误。然后就是关闭防火墙。很多人对"关闭防火墙"这件事有心理障碍,觉得不安全。这里要说明一下:在K8S集群内部,组件之间需要大量端口互通,你要是开着firewalld,就得额外配置一堆端口放行规则,把K8S的端口清单一个个加进去。对于学习和测试环境,直接systemctl stop firewalld加systemctl disable firewalld是最省事的。生产环境当然不能这么干,需要按端口白名单严格配置,这是后话。SELinux也要关掉。这个说法可能有人不认同,但我的经验是:在CentOS 7上用K8S,关SELinux能省掉80%的疑难杂症。设置setenforce 0,然后修改/etc/selinux/config把SELINUX=enforcing改成permissive。网上也有不关SELinux硬上的,但那是给追求极致安全的强迫症准备的,不适合我们这种要快速落地场景。2.3 内核参数与系统调优这一块是很多人容易忽略的。K8S对系统的要求不只是"能跑",还需要一些内核参数的配合。核心是开启iptables对桥接流量的处理能力。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 vm.swappiness = 0 EOF sysctl --system逐条解释一下。bridge-nf-call-iptables表示让iptables能够处理经过Linux网桥的流量。这是K8S网络能不能正常工作的关键:Pod之间的通信要经过CNI网桥,如果这个参数没开,流量就无法被netfilter正确过滤和转发。ip_forward是开启IP转发,这个几乎是所有容器网络方案的硬性前提。vm.swappiness=0说得直白点就是尽量别用swap,因为容器场景下内存被swap出去会导致性能急剧下降,而且K8S在计算节点内存资源时,默认是不考虑swap的。还有一个容易被忽视的点:需要加载br_netfilter模块。虽然在CentOS 7.9上默认会加载,但稳妥起见还是提前确认一下:modprobe br_netfilter lsmod | grep br_netfilter3. 容器运行时与K8S组件安装3.1 手动安装containerdcontainerd的安装方式其实很简单,就是一个rpm包。但在装完之后,有两件重要的事情要做:一是配置cgroup驱动,二是配置系统服务。containerd默认的cgroup驱动是systemd还是cgroupfs?这里有一个历史问题。K8S从1.21版本开始,kubelet默认使用systemd作为cgroup驱动,而containerd默认用cgroupfs。这俩如果不一致,就会出现一个让人很困惑的现象:kubelet起来之后报"failed to run Kubelet: failed to validate kubelet flags"或者cgroup相关的错误。解决办法是修改containerd的配置文件,让它也用systemd。containerd的默认配置文件可以通过containerd config default > /etc/containerd/config.toml生成,然后修改两个地方。一个是SystemdCgroup,把它从false改成true;另一个是sandbox_image,这是Pause镜像的地址。sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml sed -i 's#sandbox_image = "registry.k8s.io/pause.*#sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6"#' /etc/containerd/config.toml这里要补充说一下sandbox_image这个参数的含义。Pause容器是每个Pod里的"根容器",它不干任何正事,就是占个位置,负责创建和维护Pod的网络命名空间,Pod里其他容器才能共享网络。如果Pause镜像拉不下来,整个Pod就创建不起来。所以在离线环境里,这一步提前在配置里指定好镜像源位置,能避免很多麻烦。3.2 K8S核心组件包的安装在安装之前,先把chache的/etc/yum.repos.d/kubernetes.repo配置好。但离线环境是访问不了这个仓库的,所以正确姿势是:在联网机器上把rpm包下载好,然后拷贝到三台服务器上,用yum install -y ./kubelet-1.23.17.rpm ./kubeadm-1.23.17.rpm ./kubectl-1.23.17.rpm本地安装。如果遇到依赖问题,比如某个so文件缺失,我的建议是直接使用yum install --downloadonly方式把依赖也一并拉下来。装完之后把kubelet、kubeadm、kubectl加开机自启并启动:systemctl enable kubelet && systemctl start kubelet注意一点:现在kubelet会起得很早,它会因为找不到集群而疯狂报错,刷一堆"failed to connect to API server"的日志。这是正常的,不用慌。kubelet本身就是个不断重试的"打不死的小强",等kubeadm init完成之后它自然就正常了。3.3 镜像的导入与校验离线环境最大的坎儿就是镜像。K8S核心组件加上Calico网络插件,总共需要大概十四五个镜像,每一个都必须提前准备好。具体流程是这样:在联网机器上,先拉取所有需要的镜像,然后通过docker save -o k8s-images.tar或者ctr -n k8s.io images export把镜像打包成一个tar文件,拷贝到内网服务器后再导入。如果你用的是containerd,导入命令是:ctr -n k8s.io images import k8s-images.tar注意-n k8s.io这个命名空间参数,这是K8S专用的镜像命名空间,如果不指定默认是default,导进去的镜像在K8S里根本看不到。之前提到的kubeadm config images list是获取镜像清单的快捷方式。列出后逐个拉取即可,如下是1.23.17核心组件需要的镜像列表:registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-controller-manager:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6 registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.5.6-0 registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.6Calico需要的镜像相对独立,后面网络配置部分再单独说。4. 证书体系与控制平面初始化4.1 手动生成PKI证书K8S的证书体系是整个集群安全的基础,众多组件之间通信全靠TLS双向认证。手动搭建的好处是在这个环节你会被迫搞清楚谁给谁签发证书,以及每个证书的用途。大体的证书逻辑是:有一个集群的CA根证书,然后给apiserver签发一个服务端证书,给etcd签发证书,给每个节点的kubelet签发客户端证书。K8S集群里的所有组件都信任这个根CA,只要持有根CA签发的证书,就能互相认证。证书要放在/etc/kubernetes/pki目录下。核心的证书文件如下:证书文件用途说明ca.crt, ca.key集群根CA所有证书的信任根apiserver.crt, apiserver.keyAPI Server服务端证书需要包含MasterIP和Service网段apiserver-kubelet-client.crt连接kubelet的客户端证书API Server访问kubelet时使用front-proxy-ca.crt聚合层CA扩展API Server时使用etcd/ca.crtetcd集群CAetcd组件专用其实手动生成这些证书并不算太复杂,关键在于别把SAN(Subject Alternative Name)搞错。apiserver的证书SAN必须包含所有可能访问apiserver的地址,包括Master节点的IP、集群的Service网段第一个地址(通常是10.96.0.1)、还有"kubernetes"和"kubernetes.default"这些内置域名。漏掉任何一个,后面都会出现诡异的证书校验失败。4.2 kubeadm init的执行与参数解析说起来,手动搭建K8S其实很少有人真的去逐个证书手动用openssl生成,更多是用kubeadm init来"半自动"完成。这里的"手动"指的是不用自动化脚本来做,而不是说连kubeadm都不用。你依然需要自己规划IP、网段、镜像仓库,只是证书、配置文件的生成这一步kubeadm帮我们完成了大部分工作。执行初始化:kubeadm init \ --apiserver-advertise-address=192.168.1.101 \ --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers \ --kubernetes-version=v1.23.17 \ --service-cidr=10.96.0.0/12 \ --pod-network-cidr=10.244.0.0/16逐个参数拆解一下。apiserver-advertise-address指定apiserver对外广播的地址,必须填Master节点的内网IP,不然其他节点找不到它。image-repository是镜像仓库地址,因为离线环境我们已经手动导入镜像了,这里其实不拉取,但这个地址会被写入kubeadm的配置里,后面token验证的时候还会用到。service-cidr是Service的虚拟IP网段,pod-network-cidr是Pod的IP网段,这两个地址池不能和物理机网段冲突,相互之间也不能重叠。我这次选的10.244.0.0/16是Calico默认推荐的Pod网段,10.96.0.0/12是K8S默认的Service网段。提示:Pod网段和Service网段一旦确定,后期修改极麻烦,会牵涉到CNI配置和CoreDNS配置。集群一经初始化,这两个网段基本就锁死了。所以动手之前一定想清楚。成功初始化之后,kubeadm会在末尾输出一串关键信息,包括:加入集群的token和证书校验哈希值、kubectl的config文件位置。务必截图保存,后面两个Worker节点加入集群就靠它了。4.3 部署CoreDNS扩展组件CoreDNS是K8S集群内部DNS服务,负责Service名的解析。如果你不在集群里部署CoreDNS,Pod之间就不能通过Service名访问彼此,只能靠IP,这在微服务架构中是不可接受的。部署CoreDNS其实很简单,因为kubeadm会在init完成后自动部署,但前提是网络插件已经就绪。在没有CNI的情况下,CoreDNS会一直处于Pending状态。所以正确的顺序是先部署网络插件,再等CoreDNS正常。在初始化之后检查:kubectl get pods -n kube-system如果看到coredns的Pod是Pending,不用着急,这是正常的。后面部署好Calico之后,它会自动从Pending变成Running。5. 网络插件的部署与集群联调5.1 Calico的离线部署集群的网络方案我选了Calico,它的优势在于支持多种网络模式,性能在CNI插件里属于第一梯队,而且配置灵活,既能做纯三层的BGP网络,也能用Overlay模式。离线部署Calico的流程是:先把calico.yaml配置文件拿下来,然后把配置里镜像地址改成本地已经导入的镜像地址,再kubectl apply -f calico.yaml。在配置Calico之前,有一个重要的参数要确认:CALICO_IPV4POOL_CIDR。这个参数是Calico里面Pod地址池的配置,必须和之前kubeadm init时设置的pod-network-cidr一致。比如我之前设置的Pod网段是10.244.0.0/16,那么calico.yaml里的CALICO_IPV4POOL_CIDR也要改成10.244.0.0/16,集群的网络才能正常通信。注意:Calico部署的先后顺序虽然可以在kubeadm init之前或之后,但有一个大原则是必须在kubelet和kube-proxy工作正常之后、CoreDNS调度之前完成。因为在没有任何CNI插件的情况下,K8S是没有能力为Pod分配IP的,所有Pod都只能卡在ContainerCreating状态。5.2 Worker节点加入集群Worker节点的加入是整个过程中最流畅也最容易出错的环节。kubeadm init结束时的输出里有完整的join命令模板,把token和ca-cert-hash替换成自己环境的实际值即可:kubeadm join 192.168.1.101:6443 \ --token l5d3qn.abcdef1234567890 \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx这个命令在worker01和worker02上各执行一次。执行成功之后,Master节点上验证:kubectl get nodes正常情况下会看到三个节点处于NotReady状态。为什么是NotReady?因为kubelet虽然起来了,但它并不会主动告诉apiserver"我准备好了",要由kubelet上报自己的资源信息和状态,由controller-manager给它打上Ready标签。如果节点上网络插件还没就绪,或者kubelet上报有问题,节点就会一直停留在NotReady。如果join的时候提示token过期或者证书校验失败,不要慌。直接用下面的命令在Master节点上重新生成:kubeadm token create --print-join-command这条命令会输出一个新的join命令,直接复制就行。5.3 网络状态验证与联通性测试集群初始化的最后一步,是验证网络是否真的通了。我的验证方法是三板斧:看节点状态、看系统Pod状态、跑一个测试Deployment。第一步,kubectl get nodes,看到三个节点都是Ready,说明节点层面的网络配置正确了。第二步,kubectl get pods -n kube-system,看到所有Pod都是Running,包括calico-kube-controllers和calico-node。第三步,创建测试用Pod,验证跨节点网络互通:kubectl run test-nginx --image=nginx如果这个Pod能进入Running状态,说明整个网络链路已经打通了。再进一步做一次跨节点连通性测试:kubectl exec -it test-nginx -- curl <pod_ip>能通,说明Calico的路由规则已经生效,Pod之间的三层网络没有问题了。6. 实战坑点与排查经验6.1 版本不匹配问题在手动搭建过程中,版本不匹配是最折磨人的一类问题。这里分享一个我踩过的具体案例。某次初始化的时候,kubeadm init报了一个"error execution phase preflight: unable to fetch the kubeadm-config ConfigMap"的错误。排查了很久,最后发现是kubeadm和kubelet的版本不一致。kubeadm是1.23.17,但kubelet被yum自动升级到了1.24。版本不同导致kubeadm生成的配置和kubelet实际行为不一致,最终卡在preflight检查。这个问题常规文档不太会提,因为它和"正常安装流程"没关系,纯粹是离线环境里手忙脚乱搞出来的版本错乱。排查思路是:kubeadm version kubelet --version kubectl version三个版本必须完全一致,差一个minor版本都不行。解决方法是把kubelet降级到和kubeadm一致的版本:yum install -y kubelet-1.23.17 --nogpgcheck systemctl restart kubelet6.2 镜像拉取失败与命名空间陷阱离线环境里镜像拉取失败是另一个高频问题。这里的坑点往往不在"拉取"本身,而在于镜像导入的命名空间不对。之前提过,K8S使用的是containerd的k8s.io命名空间。如果你导入镜像时用了默认命名空间ctr images import而不是ctr -n k8s.io images import,kubelet在拉取镜像时会报"image not found",但实际上镜像已经在本地了。这个问题我至少遇到过三次,每次都是因为命令少写了-n k8s.io参数。还有一个容易忽略的点:如果K8S组件使用默认的registry.k8s.io仓库,而你导入镜像时用的是阿里云仓库路径,也会导致镜像找不到。一定要保证镜像仓库地址一致。比如配置里写着registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6,那么导入的镜像也必须是这个完整路径。6.3 节点NotReady的排查路径节点一直NotReady是最让人头疼的问题,因为它可能由多个原因导致。我的排查路径有一个固定的优先顺序:第一步,看kubelet状态:systemctl status kubelet,看有没有报错。如果是证书相关错误,去检查/etc/kubernetes/kubelet.conf是否存在且权限是否正确。第二步,看节点上的容器运行时:crictl ps,如果这个命令报错,说明crictl连接不上containerd,检查/run/containerd/containerd.sock是否存在、crictl的runtime-endpoint配置是否正确。第三步,看CNI插件是否就绪:ls /opt/cni/bin/,如果没有calico相关的可执行文件,说明CNI还在启动中或安装不完整。第四步,看apiserver上的节点事件:kubectl describe node worker01,这里能看到具体的时间线,比如哪个探针失败、什么原因导致的NotReady。这套排查路径在我手动搭建过程中多次验证过,效率很高。你不需要一开始就去翻日志,按照这个顺序一步步排除,基本十分钟内能定位到根因。6.4 常见问题速查表把这次手动搭建过程中遇到的坑整理成了一个速查表,方便大家对照:现象可能原因解决方案kubeadm init失败:port 6443 already in use端口被占用ss -lntp查端口占用的进程,释放端口节点一直NotReadyCNI未就绪或kubelet异常先看CNI状态,再看kubelet日志镜像报not found导入命名空间不对用ctr -n k8s.io images import重新导入CoreDNS一直Pending网络插件没跑起来确认Calico部署成功后再观察kubectl无法连接apiserverkubeconfig配置错误检查~/.kube/config的server字段和证书路径跨节点Pod网络不通Calico配置的Pod网段不一致核对CALICO_IPV4POOL_CIDR和kubeadm的pod-network-cidrkubelet疯狂重启版本不一致或缺少依赖核对kubelet/kubeadm/kubectl版本,检查依赖库7. 常用运维命令与集群日常管理7.1 节点与Pod的日常操作集群搭好之后,日常管理主要是通过kubectl完成。这里把我常用的命令整理一下,都是一些高频操作。查看节点状态:kubectl get nodes -o wide查看集群里所有命名空间的Pod:kubectl get pods -A -o wide查看某个Pod的详细信息和事件:kubectl describe pod <pod-name> -n <namespace>查看Pod日志:kubectl logs -f <pod-name> -n <namespace> --tail=200进入Pod内执行命令:kubectl exec -it <pod-name> -n <namespace> -- /bin/bash这些命令是基础中的基础,但恰恰是日常最常用的部分。建议大家把这些命令的-o wide参数用起来,它能直接显示Pod跑在哪个节点、IP是什么,排查问题的时候非常高效。7.2 证书过期与更新策略K8S的证书有效期默认是一年。这个时间说长不长,说短不短,很多集群跑着跑着突然出现一堆奇怪的报错,一查才发现是证书过期了。如果是kubeadm部署的集群,更新证书比较简单:kubeadm certs renew all执行完之后重启相关组件:systemctl restart kubelet对于自签的apiserver证书,更新之后需要重启kube-apiserver。但要注意,kube-apiserver是静态Pod,修改/etc/kubernetes/manifests/kube-apiserver.yaml之后会自动重启,不需要手动干预。7.3 节点维护与移除有时候需要把某个节点摘下来做维护,这时候要让K8S把Pod都赶走:kubectl drain worker02 --ignore-daemonsets --delete-emptydir-data维护完成后让节点重新参与调度:kubectl uncordon worker02如果要彻底移除一个节点,顺序是:先drain、再删除节点、然后在该节点上执行kubeadm reset清空本地数据。这两个命令在运维中必不可少,特别是当某个Worker节点要换硬件的时候,drain + uncordon的组合能让业务无感知地完成迁移。8. 手动搭建的经验总结与后续扩展这次离线手动搭建K8S集群,前后花了两天时间。第一天准备环境、拉镜像、踩版本坑,第二天正式部署然后反复调试网络。整个过程走完之后,我对K8S的理解和在"一条命令装好"时代形成的认知完全不一样了。你开始明白每个组件为什么要这么设计,哪些参数是牵一发动全身的,哪些错误信息背后真正的原因是什么。如果让我给准备入门的同学一个建议:即便你以后用开源的集群管理平台做生产部署,也一定要找一套离线环境手动把集群搭一遍。因为你在手动搭建过程中积累的底层认知,会在未来每一次集群故障排查中变成你的直觉。关于后续扩展,我打算在搭建好的集群上做三件事。第一件事是部署一套完整的微服务Demo,把Ingress、ConfigMap、Secret、PV/PVC这些核心资源都用起来,看看它们在真实业务中的应用方式。第二件事是尝试把集群升级到更新的版本,练练兵,毕竟版本升级在生产中是个高频话题。第三件事是试一下集群监控那套体系,看看从零开始构建可观测性的完整路径。这一趟下来,我现在碰到网络问题第一反应不是去看什么知名博主写的"十种排查方法",而是自己从Pod网段、Service网段、Calico的路由规则这几个维度去推,反而解决问题更快一些。这就是手动搭建带来的底层能力,也是我愿意把这套流程完整记录下来的原因。希望这篇实践记录能给同样准备手动搭建K8S集群的同学一点参考,少走一些我曾经踩过的弯路。
1. 整体思路与部署架构选型1.1 为什么选择离线手动搭建先聊一个大家都会遇到的问题:学习K8S到底应该从哪里入手?很多人第一反应是装个minikube或者kind,一条命令拉起来,kubectl get nodes能看到一个Ready的节点,就觉得"我会K8S了"。但等真正上了生产,出了问题才发现自己完全不知道集群内部是怎么运转的。这就跟开车一样,你在驾校开自动挡和上了路开手动挡完全是两码事。K8S这个东西,如果不去手动搭建一次,你就永远停留在"会用工具"的层面,没有真正理解"工具"本身。我这次选择离线手动搭建,其实是被环境"逼"出来的。某公司的内网服务器完全不能访问外网,但又需要一套K8S环境来做容器化部署的测试和验证。最开始我也想过曲线救国:在一台能上网的机器上装好所有镜像,然后打包带进内网。后来发现这个思路本身就是K8S离线部署的标配玩法。索性就把整个过程完整走了一遍,顺便把之前的很多知识盲区补上了。手动搭建的好处在哪里?首先是你能真正搞清楚每个组件是干什么的。etcd存什么数据、kube-apiserver怎么和etcd交互、kube-scheduler怎么调度Pod、kubelet怎么和容器运行时通信。这些东西在自动化的安装工具里全都是黑盒,你只知道装完就能用,但出了问题就抓瞎。其次是手动搭建的过程中,你会被迫理解证书体系、网络插件的选择、Pod网段和Service网段的规划,这些才是K8S运维的核心能力。1.2 核心组件与架构设计在动手之前,先把K8S的架构理清楚。我这次搭建的是最经典的单Master双Worker架构,三个节点分别是master01、worker01、worker02。为什么不用高可用?因为这是做学习和验证用的环境,单Master足够覆盖绝大部分测试场景。如果以后要上生产,再做HA,原理是一样的,只是多了负载均衡这一层。K8S的核心组件可以分成两个部分:控制平面和节点平面。控制平面包含kube-apiserver、kube-controller-manager、kube-scheduler、etcd四个组件,节点平面包含kubelet、kube-proxy和容器运行时。组件角色一览:组件职责关键点etcd集群状态存储所有数据的唯一来源,坏了集群就瞎了kube-apiserver所有请求的入口唯一能直接访问etcd的组件kube-controller-manager维持期望状态节点控制器、副本控制器等一堆控制器kube-scheduler决定Pod跑在哪个节点根据资源和约束做调度决策kubelet节点上的“眼睛和手”负责Pod的生命周期管理kube-proxyService流量转发实现负载均衡和服务发现这里最需要理解的是kube-apiserver的地位。所有的组件——kubelet、kube-scheduler、kube-controller-manager、kubectl——要读写集群状态,都必须经过kube-apiserver。它就像整个集群的“总闸门”,所有请求进来先经过认证、授权、准入控制三道关卡,然后才会真正写入etcd。理解了这一点,你就知道为什么在排查问题的时候,第一步总是看kube-apiserver是否正常。1.3 版本选型与配套组件版本选型这个问题,网上说法很多,但我给的建议很简单:选一个经过时间验证的稳定版本。我这次用的是1.23.17,为什么选这个版本?因为1.24开始,K8S把Dockershim移除了,如果你还习惯用Docker作为容器运行时,就需要额外安装CRI-Dockerd这个适配层。而1.23及之前的版本,Docker还是"原生支持"的,对初学者来说少了一道折腾。当然如果你已经适应containerd作为运行时,用更新的版本也没问题,只是我这次为了减少变量,选择了更稳妥的1.23。配套组件版本也是一个萝卜一个坑:组件版本说明Kubernetesv1.23.17主版本etcdv3.5.6存储集群数据containerdv1.6.19容器运行时,替代Dockercrictlv1.24.0命令行调试工具calicov3.25.0CNI网络插件这里多说一句为什么要用containerd而不是Docker。刚开始我其实想用Docker,毕竟熟悉嘛。但研究了一下发现,在当前这个阶段,containerd才是更"正统"的容器运行时。K8S通过CRI(Container Runtime Interface)和containerd直接交互,中间少了一个Docker的转换层,更稳定也更高效。而且后面如果想把集群升级到更新版本,用containerd可以省掉很多麻烦。2. 离线环境的准备与前置条件2.1 离线安装包的准备策略离线安装最大的难点不是"装",而是"怎么把需要的东西都提前准备好"。首先明确需要准备的几个包:K8S的三个核心组件包——kubelet、kubeadm、kubectl,而且必须指定版本。这三个东西在K8S官方的yum仓库或者apt仓库里都有,关键是要在能联网的机器上先下载好。我用的是yum方式,所以需要下载kubelet-1.23.17、kubeadm-1.23.17、kubectl-1.23.17这三个rpm包,以及它们的一堆依赖。然后就是容器运行时。我选了containerd,需要下载containerd.io这个包。这里必须提醒一下:containerd的版本和K8S版本要兼容,1.6.x的containerd配合1.23.x的K8S是经过验证的组合。接下来是镜像。K8S集群跑起来需要一堆镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause、coredns,还有网络插件calico需要的几个镜像。这一块是离线部署的重头戏,也是最容易出问题的地方。提示:镜像的获取有一个小技巧,通过kubeadm config images list可以列出指定版本K8S需要的全部镜像清单,然后逐个拉取、打包、拷贝。这一步必须在能联网的机器上提前完成。2.2 服务器基础环境配置三台服务器的操作系统我用的都是CentOS 7.9,配置是4核8G、100G磁盘。这个配置跑一个测试集群完全够了,但如果想让集群运行得更从容,建议Master节点给到8核16G,毕竟etcd和kube-apiserver都是吃内存的主儿。系统层面的准备工作集中在三个方面:主机名、IP和防火墙。主机规划:主机名IP地址角色master01192.168.1.101控制平面节点worker01192.168.1.102工作节点worker02192.168.1.103工作节点设置主机名很简单,hostnamectl set-hostname master01,然后三台机器的/etc/hosts都要加上三行映射关系。这一步很多人会忽略,但非常重要——K8S组件之间通信依赖主机名解析,如果你不配hosts,后面kubeadm init的时候就会报"hostname not found"的错误。然后就是关闭防火墙。很多人对"关闭防火墙"这件事有心理障碍,觉得不安全。这里要说明一下:在K8S集群内部,组件之间需要大量端口互通,你要是开着firewalld,就得额外配置一堆端口放行规则,把K8S的端口清单一个个加进去。对于学习和测试环境,直接systemctl stop firewalld加systemctl disable firewalld是最省事的。生产环境当然不能这么干,需要按端口白名单严格配置,这是后话。SELinux也要关掉。这个说法可能有人不认同,但我的经验是:在CentOS 7上用K8S,关SELinux能省掉80%的疑难杂症。设置setenforce 0,然后修改/etc/selinux/config把SELINUX=enforcing改成permissive。网上也有不关SELinux硬上的,但那是给追求极致安全的强迫症准备的,不适合我们这种要快速落地场景。2.3 内核参数与系统调优这一块是很多人容易忽略的。K8S对系统的要求不只是"能跑",还需要一些内核参数的配合。核心是开启iptables对桥接流量的处理能力。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 vm.swappiness = 0 EOF sysctl --system逐条解释一下。bridge-nf-call-iptables表示让iptables能够处理经过Linux网桥的流量。这是K8S网络能不能正常工作的关键:Pod之间的通信要经过CNI网桥,如果这个参数没开,流量就无法被netfilter正确过滤和转发。ip_forward是开启IP转发,这个几乎是所有容器网络方案的硬性前提。vm.swappiness=0说得直白点就是尽量别用swap,因为容器场景下内存被swap出去会导致性能急剧下降,而且K8S在计算节点内存资源时,默认是不考虑swap的。还有一个容易被忽视的点:需要加载br_netfilter模块。虽然在CentOS 7.9上默认会加载,但稳妥起见还是提前确认一下:modprobe br_netfilter lsmod | grep br_netfilter3. 容器运行时与K8S组件安装3.1 手动安装containerdcontainerd的安装方式其实很简单,就是一个rpm包。但在装完之后,有两件重要的事情要做:一是配置cgroup驱动,二是配置系统服务。containerd默认的cgroup驱动是systemd还是cgroupfs?这里有一个历史问题。K8S从1.21版本开始,kubelet默认使用systemd作为cgroup驱动,而containerd默认用cgroupfs。这俩如果不一致,就会出现一个让人很困惑的现象:kubelet起来之后报"failed to run Kubelet: failed to validate kubelet flags"或者cgroup相关的错误。解决办法是修改containerd的配置文件,让它也用systemd。containerd的默认配置文件可以通过containerd config default > /etc/containerd/config.toml生成,然后修改两个地方。一个是SystemdCgroup,把它从false改成true;另一个是sandbox_image,这是Pause镜像的地址。sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml sed -i 's#sandbox_image = "registry.k8s.io/pause.*#sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6"#' /etc/containerd/config.toml这里要补充说一下sandbox_image这个参数的含义。Pause容器是每个Pod里的"根容器",它不干任何正事,就是占个位置,负责创建和维护Pod的网络命名空间,Pod里其他容器才能共享网络。如果Pause镜像拉不下来,整个Pod就创建不起来。所以在离线环境里,这一步提前在配置里指定好镜像源位置,能避免很多麻烦。3.2 K8S核心组件包的安装在安装之前,先把chache的/etc/yum.repos.d/kubernetes.repo配置好。但离线环境是访问不了这个仓库的,所以正确姿势是:在联网机器上把rpm包下载好,然后拷贝到三台服务器上,用yum install -y ./kubelet-1.23.17.rpm ./kubeadm-1.23.17.rpm ./kubectl-1.23.17.rpm本地安装。如果遇到依赖问题,比如某个so文件缺失,我的建议是直接使用yum install --downloadonly方式把依赖也一并拉下来。装完之后把kubelet、kubeadm、kubectl加开机自启并启动:systemctl enable kubelet && systemctl start kubelet注意一点:现在kubelet会起得很早,它会因为找不到集群而疯狂报错,刷一堆"failed to connect to API server"的日志。这是正常的,不用慌。kubelet本身就是个不断重试的"打不死的小强",等kubeadm init完成之后它自然就正常了。3.3 镜像的导入与校验离线环境最大的坎儿就是镜像。K8S核心组件加上Calico网络插件,总共需要大概十四五个镜像,每一个都必须提前准备好。具体流程是这样:在联网机器上,先拉取所有需要的镜像,然后通过docker save -o k8s-images.tar或者ctr -n k8s.io images export把镜像打包成一个tar文件,拷贝到内网服务器后再导入。如果你用的是containerd,导入命令是:ctr -n k8s.io images import k8s-images.tar注意-n k8s.io这个命名空间参数,这是K8S专用的镜像命名空间,如果不指定默认是default,导进去的镜像在K8S里根本看不到。之前提到的kubeadm config images list是获取镜像清单的快捷方式。列出后逐个拉取即可,如下是1.23.17核心组件需要的镜像列表:registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-controller-manager:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy:v1.23.17 registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6 registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.5.6-0 registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.6Calico需要的镜像相对独立,后面网络配置部分再单独说。4. 证书体系与控制平面初始化4.1 手动生成PKI证书K8S的证书体系是整个集群安全的基础,众多组件之间通信全靠TLS双向认证。手动搭建的好处是在这个环节你会被迫搞清楚谁给谁签发证书,以及每个证书的用途。大体的证书逻辑是:有一个集群的CA根证书,然后给apiserver签发一个服务端证书,给etcd签发证书,给每个节点的kubelet签发客户端证书。K8S集群里的所有组件都信任这个根CA,只要持有根CA签发的证书,就能互相认证。证书要放在/etc/kubernetes/pki目录下。核心的证书文件如下:证书文件用途说明ca.crt, ca.key集群根CA所有证书的信任根apiserver.crt, apiserver.keyAPI Server服务端证书需要包含MasterIP和Service网段apiserver-kubelet-client.crt连接kubelet的客户端证书API Server访问kubelet时使用front-proxy-ca.crt聚合层CA扩展API Server时使用etcd/ca.crtetcd集群CAetcd组件专用其实手动生成这些证书并不算太复杂,关键在于别把SAN(Subject Alternative Name)搞错。apiserver的证书SAN必须包含所有可能访问apiserver的地址,包括Master节点的IP、集群的Service网段第一个地址(通常是10.96.0.1)、还有"kubernetes"和"kubernetes.default"这些内置域名。漏掉任何一个,后面都会出现诡异的证书校验失败。4.2 kubeadm init的执行与参数解析说起来,手动搭建K8S其实很少有人真的去逐个证书手动用openssl生成,更多是用kubeadm init来"半自动"完成。这里的"手动"指的是不用自动化脚本来做,而不是说连kubeadm都不用。你依然需要自己规划IP、网段、镜像仓库,只是证书、配置文件的生成这一步kubeadm帮我们完成了大部分工作。执行初始化:kubeadm init \ --apiserver-advertise-address=192.168.1.101 \ --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers \ --kubernetes-version=v1.23.17 \ --service-cidr=10.96.0.0/12 \ --pod-network-cidr=10.244.0.0/16逐个参数拆解一下。apiserver-advertise-address指定apiserver对外广播的地址,必须填Master节点的内网IP,不然其他节点找不到它。image-repository是镜像仓库地址,因为离线环境我们已经手动导入镜像了,这里其实不拉取,但这个地址会被写入kubeadm的配置里,后面token验证的时候还会用到。service-cidr是Service的虚拟IP网段,pod-network-cidr是Pod的IP网段,这两个地址池不能和物理机网段冲突,相互之间也不能重叠。我这次选的10.244.0.0/16是Calico默认推荐的Pod网段,10.96.0.0/12是K8S默认的Service网段。提示:Pod网段和Service网段一旦确定,后期修改极麻烦,会牵涉到CNI配置和CoreDNS配置。集群一经初始化,这两个网段基本就锁死了。所以动手之前一定想清楚。成功初始化之后,kubeadm会在末尾输出一串关键信息,包括:加入集群的token和证书校验哈希值、kubectl的config文件位置。务必截图保存,后面两个Worker节点加入集群就靠它了。4.3 部署CoreDNS扩展组件CoreDNS是K8S集群内部DNS服务,负责Service名的解析。如果你不在集群里部署CoreDNS,Pod之间就不能通过Service名访问彼此,只能靠IP,这在微服务架构中是不可接受的。部署CoreDNS其实很简单,因为kubeadm会在init完成后自动部署,但前提是网络插件已经就绪。在没有CNI的情况下,CoreDNS会一直处于Pending状态。所以正确的顺序是先部署网络插件,再等CoreDNS正常。在初始化之后检查:kubectl get pods -n kube-system如果看到coredns的Pod是Pending,不用着急,这是正常的。后面部署好Calico之后,它会自动从Pending变成Running。5. 网络插件的部署与集群联调5.1 Calico的离线部署集群的网络方案我选了Calico,它的优势在于支持多种网络模式,性能在CNI插件里属于第一梯队,而且配置灵活,既能做纯三层的BGP网络,也能用Overlay模式。离线部署Calico的流程是:先把calico.yaml配置文件拿下来,然后把配置里镜像地址改成本地已经导入的镜像地址,再kubectl apply -f calico.yaml。在配置Calico之前,有一个重要的参数要确认:CALICO_IPV4POOL_CIDR。这个参数是Calico里面Pod地址池的配置,必须和之前kubeadm init时设置的pod-network-cidr一致。比如我之前设置的Pod网段是10.244.0.0/16,那么calico.yaml里的CALICO_IPV4POOL_CIDR也要改成10.244.0.0/16,集群的网络才能正常通信。注意:Calico部署的先后顺序虽然可以在kubeadm init之前或之后,但有一个大原则是必须在kubelet和kube-proxy工作正常之后、CoreDNS调度之前完成。因为在没有任何CNI插件的情况下,K8S是没有能力为Pod分配IP的,所有Pod都只能卡在ContainerCreating状态。5.2 Worker节点加入集群Worker节点的加入是整个过程中最流畅也最容易出错的环节。kubeadm init结束时的输出里有完整的join命令模板,把token和ca-cert-hash替换成自己环境的实际值即可:kubeadm join 192.168.1.101:6443 \ --token l5d3qn.abcdef1234567890 \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx这个命令在worker01和worker02上各执行一次。执行成功之后,Master节点上验证:kubectl get nodes正常情况下会看到三个节点处于NotReady状态。为什么是NotReady?因为kubelet虽然起来了,但它并不会主动告诉apiserver"我准备好了",要由kubelet上报自己的资源信息和状态,由controller-manager给它打上Ready标签。如果节点上网络插件还没就绪,或者kubelet上报有问题,节点就会一直停留在NotReady。如果join的时候提示token过期或者证书校验失败,不要慌。直接用下面的命令在Master节点上重新生成:kubeadm token create --print-join-command这条命令会输出一个新的join命令,直接复制就行。5.3 网络状态验证与联通性测试集群初始化的最后一步,是验证网络是否真的通了。我的验证方法是三板斧:看节点状态、看系统Pod状态、跑一个测试Deployment。第一步,kubectl get nodes,看到三个节点都是Ready,说明节点层面的网络配置正确了。第二步,kubectl get pods -n kube-system,看到所有Pod都是Running,包括calico-kube-controllers和calico-node。第三步,创建测试用Pod,验证跨节点网络互通:kubectl run test-nginx --image=nginx如果这个Pod能进入Running状态,说明整个网络链路已经打通了。再进一步做一次跨节点连通性测试:kubectl exec -it test-nginx -- curl <pod_ip>能通,说明Calico的路由规则已经生效,Pod之间的三层网络没有问题了。6. 实战坑点与排查经验6.1 版本不匹配问题在手动搭建过程中,版本不匹配是最折磨人的一类问题。这里分享一个我踩过的具体案例。某次初始化的时候,kubeadm init报了一个"error execution phase preflight: unable to fetch the kubeadm-config ConfigMap"的错误。排查了很久,最后发现是kubeadm和kubelet的版本不一致。kubeadm是1.23.17,但kubelet被yum自动升级到了1.24。版本不同导致kubeadm生成的配置和kubelet实际行为不一致,最终卡在preflight检查。这个问题常规文档不太会提,因为它和"正常安装流程"没关系,纯粹是离线环境里手忙脚乱搞出来的版本错乱。排查思路是:kubeadm version kubelet --version kubectl version三个版本必须完全一致,差一个minor版本都不行。解决方法是把kubelet降级到和kubeadm一致的版本:yum install -y kubelet-1.23.17 --nogpgcheck systemctl restart kubelet6.2 镜像拉取失败与命名空间陷阱离线环境里镜像拉取失败是另一个高频问题。这里的坑点往往不在"拉取"本身,而在于镜像导入的命名空间不对。之前提过,K8S使用的是containerd的k8s.io命名空间。如果你导入镜像时用了默认命名空间ctr images import而不是ctr -n k8s.io images import,kubelet在拉取镜像时会报"image not found",但实际上镜像已经在本地了。这个问题我至少遇到过三次,每次都是因为命令少写了-n k8s.io参数。还有一个容易忽略的点:如果K8S组件使用默认的registry.k8s.io仓库,而你导入镜像时用的是阿里云仓库路径,也会导致镜像找不到。一定要保证镜像仓库地址一致。比如配置里写着registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6,那么导入的镜像也必须是这个完整路径。6.3 节点NotReady的排查路径节点一直NotReady是最让人头疼的问题,因为它可能由多个原因导致。我的排查路径有一个固定的优先顺序:第一步,看kubelet状态:systemctl status kubelet,看有没有报错。如果是证书相关错误,去检查/etc/kubernetes/kubelet.conf是否存在且权限是否正确。第二步,看节点上的容器运行时:crictl ps,如果这个命令报错,说明crictl连接不上containerd,检查/run/containerd/containerd.sock是否存在、crictl的runtime-endpoint配置是否正确。第三步,看CNI插件是否就绪:ls /opt/cni/bin/,如果没有calico相关的可执行文件,说明CNI还在启动中或安装不完整。第四步,看apiserver上的节点事件:kubectl describe node worker01,这里能看到具体的时间线,比如哪个探针失败、什么原因导致的NotReady。这套排查路径在我手动搭建过程中多次验证过,效率很高。你不需要一开始就去翻日志,按照这个顺序一步步排除,基本十分钟内能定位到根因。6.4 常见问题速查表把这次手动搭建过程中遇到的坑整理成了一个速查表,方便大家对照:现象可能原因解决方案kubeadm init失败:port 6443 already in use端口被占用ss -lntp查端口占用的进程,释放端口节点一直NotReadyCNI未就绪或kubelet异常先看CNI状态,再看kubelet日志镜像报not found导入命名空间不对用ctr -n k8s.io images import重新导入CoreDNS一直Pending网络插件没跑起来确认Calico部署成功后再观察kubectl无法连接apiserverkubeconfig配置错误检查~/.kube/config的server字段和证书路径跨节点Pod网络不通Calico配置的Pod网段不一致核对CALICO_IPV4POOL_CIDR和kubeadm的pod-network-cidrkubelet疯狂重启版本不一致或缺少依赖核对kubelet/kubeadm/kubectl版本,检查依赖库7. 常用运维命令与集群日常管理7.1 节点与Pod的日常操作集群搭好之后,日常管理主要是通过kubectl完成。这里把我常用的命令整理一下,都是一些高频操作。查看节点状态:kubectl get nodes -o wide查看集群里所有命名空间的Pod:kubectl get pods -A -o wide查看某个Pod的详细信息和事件:kubectl describe pod <pod-name> -n <namespace>查看Pod日志:kubectl logs -f <pod-name> -n <namespace> --tail=200进入Pod内执行命令:kubectl exec -it <pod-name> -n <namespace> -- /bin/bash这些命令是基础中的基础,但恰恰是日常最常用的部分。建议大家把这些命令的-o wide参数用起来,它能直接显示Pod跑在哪个节点、IP是什么,排查问题的时候非常高效。7.2 证书过期与更新策略K8S的证书有效期默认是一年。这个时间说长不长,说短不短,很多集群跑着跑着突然出现一堆奇怪的报错,一查才发现是证书过期了。如果是kubeadm部署的集群,更新证书比较简单:kubeadm certs renew all执行完之后重启相关组件:systemctl restart kubelet对于自签的apiserver证书,更新之后需要重启kube-apiserver。但要注意,kube-apiserver是静态Pod,修改/etc/kubernetes/manifests/kube-apiserver.yaml之后会自动重启,不需要手动干预。7.3 节点维护与移除有时候需要把某个节点摘下来做维护,这时候要让K8S把Pod都赶走:kubectl drain worker02 --ignore-daemonsets --delete-emptydir-data维护完成后让节点重新参与调度:kubectl uncordon worker02如果要彻底移除一个节点,顺序是:先drain、再删除节点、然后在该节点上执行kubeadm reset清空本地数据。这两个命令在运维中必不可少,特别是当某个Worker节点要换硬件的时候,drain + uncordon的组合能让业务无感知地完成迁移。8. 手动搭建的经验总结与后续扩展这次离线手动搭建K8S集群,前后花了两天时间。第一天准备环境、拉镜像、踩版本坑,第二天正式部署然后反复调试网络。整个过程走完之后,我对K8S的理解和在"一条命令装好"时代形成的认知完全不一样了。你开始明白每个组件为什么要这么设计,哪些参数是牵一发动全身的,哪些错误信息背后真正的原因是什么。如果让我给准备入门的同学一个建议:即便你以后用开源的集群管理平台做生产部署,也一定要找一套离线环境手动把集群搭一遍。因为你在手动搭建过程中积累的底层认知,会在未来每一次集群故障排查中变成你的直觉。关于后续扩展,我打算在搭建好的集群上做三件事。第一件事是部署一套完整的微服务Demo,把Ingress、ConfigMap、Secret、PV/PVC这些核心资源都用起来,看看它们在真实业务中的应用方式。第二件事是尝试把集群升级到更新的版本,练练兵,毕竟版本升级在生产中是个高频话题。第三件事是试一下集群监控那套体系,看看从零开始构建可观测性的完整路径。这一趟下来,我现在碰到网络问题第一反应不是去看什么知名博主写的"十种排查方法",而是自己从Pod网段、Service网段、Calico的路由规则这几个维度去推,反而解决问题更快一些。这就是手动搭建带来的底层能力,也是我愿意把这套流程完整记录下来的原因。希望这篇实践记录能给同样准备手动搭建K8S集群的同学一点参考,少走一些我曾经踩过的弯路。