
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通过CRIContainer 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把SELINUXenforcing改成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.swappiness0说得直白点就是尽量别用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组件专用其实手动生成这些证书并不算太复杂关键在于别把SANSubject 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-address192.168.1.101 \ --image-repositoryregistry.cn-hangzhou.aliyuncs.com/google_containers \ --kubernetes-versionv1.23.17 \ --service-cidr10.96.0.0/12 \ --pod-network-cidr10.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名的解析。如果你不在集群里部署CoreDNSPod之间就不能通过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 --imagenginx如果这个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 importkubelet在拉取镜像时会报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查看集群里所有命名空间的Podkubectl get pods -A -o wide查看某个Pod的详细信息和事件kubectl describe pod pod-name -n namespace查看Pod日志kubectl logs -f pod-name -n namespace --tail200进入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集群的同学一点参考少走一些我曾经踩过的弯路。