1. 项目缘起与整体架构设计
1.1 为什么选择自建集群而不是托管方案
最早接触容器编排是在一个内部测试环境里,当时用单节点跑几个Demo服务,觉得这东西确实方便。后来业务侧要求把一批有状态服务也迁进来,单节点扛不住,才开始认真考虑多节点集群的事。摆在面前的有两条路:一是直接用云厂商的托管K8s服务,二是自己从裸机开始搭。托管方案省心,控制面不用自己维护,但有几个现实问题绕不开——测试环境预算有限,托管集群的控制面费用按小时计费,长期跑下来成本不低;另外有些内部网络策略和存储挂载方式在托管环境里受限,调试起来反而更麻烦。所以最终决定自己搭一套,用三台物理机做基础,后续再按需扩容。
这个选择背后的逻辑其实很简单:可控性和成本。自建集群意味着从etcd到kubelet的每一层你都能碰,出问题能查到根上,而不是对着云厂商的控制台干瞪眼。代价就是运维复杂度上来了,升级、备份、证书轮换都得自己管。所以这套方案更适合有一定Linux基础、愿意花时间折腾的团队,纯小白或者业务压力极大的生产环境,我还是建议先评估托管方案。
1.2 集群拓扑与节点角色划分
三台机器的规划是这样的:一台做控制面(Control Plane),跑kube-apiserver、kube-scheduler、kube-controller-manager和etcd;另外两台做工作节点(Worker Node),跑业务Pod。有人会问为什么不做三控制面高可用,答案很直接——测试环境资源有限,三台机器如果都做控制面,剩下的计算资源跑不了几个Pod。单控制面在测试环境完全够用,挂了重启就行,生产环境再考虑堆叠式etcd或者外部etcd集群。
节点角色划分上有个细节要注意:控制面节点默认带一个污点(taint),不允许普通Pod调度上去。这个设计是为了保护控制面资源不被业务抢占。如果你资源实在紧张,可以手动去掉这个污点,但我不建议这么做,控制面一旦被业务Pod拖垮,整个集群就失联了。工作节点则要打上标签(label),方便后续用nodeSelector或者亲和性调度把特定业务固定到特定节点。
网络规划方面,我用了三个网段:节点物理网络用192.168.1.0/24,Pod网络用10.244.0.0/16,Service网络用10.96.0.0/12。这三个网段不能重叠,否则路由会出问题。Pod网段和Service网段的具体值可以改,但一旦集群初始化完成就不能再动,所以规划阶段就要想清楚。
1.3 容器运行时与网络插件的选型考量
容器运行时这块,早期Docker是默认选择,但从K8s 1.24开始dockershim被移除,containerd成了主流。我选containerd的原因很简单:它更轻量,调用链更短,出问题时排查路径清晰。Docker作为运行时多了一层dockerd,虽然对开发者友好,但在K8s场景下这层封装反而增加了不确定性。安装containerd之后需要配置SystemdCgroup驱动,这一步如果漏了,kubelet启动时会报cgroup不匹配的错误,节点会一直NotReady。
网络插件我选了Calico,而不是Flannel。Flannel配置简单,但只提供基本的Overlay网络,没有网络策略(NetworkPolicy)支持。Calico用BGP协议做路由分发,性能更好,而且自带网络策略功能,后续如果要做Pod间的访问控制,不用再换插件。Calico的IPIP模式在跨网段通信时会有封装开销,但在同网段内可以直接路由,实测下来延迟比Flannel的VXLAN模式低不少。如果你集群节点都在同一个二层网络里,Calico的BGP模式甚至不需要封装,性能接近原生网络。
2. 系统初始化与运行时环境准备
2.1 操作系统层面的基础配置
三台机器统一装的是Ubuntu 22.04 LTS,内核版本5.15。选这个版本是因为它对cgroup v2支持完善,containerd和kubelet都能直接用。装完系统后第一件事是关swap,这不是可选项,是必须项。kubelet在swap开启的情况下行为不可预测,调度器计算资源时也会出错。临时关闭用swapoff -a,永久关闭要注释掉/etc/fstab里的swap行,两处都做才彻底。
接下来是内核参数调整。br_netfilter模块要加载,否则iptables看不到桥接流量,Pod之间通信会断。net.bridge.bridge-nf-call-iptables和net.bridge.bridge-nf-call-ip6tables都要设为1。net.ipv4.ip_forward也要开,这是Pod跨节点通信的基础。这些参数写进/etc/sysctl.d/k8s.conf,用sysctl --system生效。我见过有人只改了/etc/sysctl.conf,结果重启后配置丢失,节点直接失联,所以配置文件的位置要记牢。
时间同步容易被忽略,但etcd对时间敏感,节点间时间偏差超过一定阈值会导致选举异常。装个chrony或者systemd-timesyncd,确保三台机器时间一致。主机名也要规划好,用hostnamectl set-hostname设置成有意义的名字,比如k8s-master、k8s-node1、k8s-node2,后面加入集群时靠主机名区分节点,比记IP方便。
2.2 containerd安装与关键配置项
containerd的安装有两种方式:apt直接装或者下载二进制包。apt装的是系统仓库里的版本,可能偏旧;二进制包能选最新稳定版。我用的二进制包,从官方release页面下载后解压到/usr/local/bin,然后写一个systemd unit文件。unit文件里ExecStart指向/usr/local/bin/containerd,Delegate设为yes,这样containerd能自己管理cgroup。
配置文件/etc/containerd/config.toml是重点。先用containerd config default生成默认配置,然后改两个地方:一是SystemdCgroup设为true,二是sandbox_image改成国内能拉到的pause镜像地址。SystemdCgroup不设true的话,kubelet和containerd的cgroup驱动不一致,节点会报failed to run Kubelet: misconfiguration: kubelet cgroup driver: "cgroupfs" is different from docker cgroup driver: "systemd"这类错误。pause镜像地址改掉是因为默认的registry.k8s.io在国内拉取不稳定,换成阿里云或者腾讯云的镜像仓库地址,能省很多等待时间。
改完配置后systemctl restart containerd,然后用ctr命令验证一下。ctr version能看到客户端和服务端版本,ctr images pull拉一个测试镜像,能拉下来说明运行时没问题。这里有个小坑:ctr默认用default命名空间,而k8s用的是k8s.io命名空间,用ctr -n k8s.io images list才能看到k8s拉下来的镜像。
2.3 kubelet、kubeadm、kubectl三件套安装
这三个组件的版本要一致,不能kubeadm用1.28而kubelet用1.27。安装方式我用的是apt仓库,先加Google的apt key,再加源,然后apt install kubelet kubeadm kubectl。加key的时候如果网络不通,可以用国内镜像源的key。装完后systemctl enable kubelet,注意这时候kubelet还起不来,因为还没初始化集群,这是正常的,不用慌。
kubelet的配置文件在/var/lib/kubelet/config.yaml,这个文件是kubeadm初始化时生成的,不用手动改。但有一个参数值得关注:cgroupDriver。如果containerd配的是systemd,这里也必须是systemd,kubeadm会自动检测,但如果你手动改过containerd配置,最好确认一下。另外resolvConf指向/run/systemd/resolve/resolv.conf,如果系统用的是其他DNS管理方式,这个路径要相应调整,否则Pod里的DNS解析会出问题。
kubectl是客户端工具,装在哪台机器都行,我一般装在控制面节点上,方便直接操作。装完后配一下bash补全,source <(kubectl completion bash),能省不少敲命令的时间。kubectl的配置文件在/etc/kubernetes/admin.conf,普通用户想用的话要拷贝到~/.kube/config并改权限。
3. 集群初始化与网络插件部署
3.1 kubeadm init参数详解与执行过程
初始化命令我用的是kubeadm init配合一个配置文件,而不是一长串命令行参数。配置文件的好处是可读、可版本控制、可复用。配置文件里几个关键字段:kubernetesVersion指定版本号,controlPlaneEndpoint填控制面节点的IP和端口(默认6443),podSubnet填10.244.0.0/16,serviceSubnet填10.96.0.0/12。imageRepository改成国内镜像仓库地址,否则拉镜像会卡住。
执行kubeadm init --config kubeadm-config.yaml之后,它会做一系列检查,然后拉镜像、生成证书、启动控制面组件。这个过程大概三到五分钟,取决于网络速度。看到Your Kubernetes control-plane has initialized successfully!就说明成功了。这时候它会输出两段重要信息:一段是配置kubectl的命令,一段是工作节点加入集群的token。这两段都要保存下来,尤其是token,默认24小时过期,过期后要用kubeadm token create重新生成。
初始化完成后,kubectl get nodes应该能看到控制面节点,状态是NotReady。这是正常的,因为网络插件还没装。kubectl get pods -n kube-system能看到apiserver、scheduler、controller-manager、etcd都在跑,coredns是Pending状态,等网络插件装好就会起来。
3.2 Calico部署与网络连通性验证
Calico的部署用kubectl apply一个yaml文件就行,但yaml里的Pod网段要和kubeadm配置的一致。官方提供的calico.yaml默认用192.168.0.0/16,和我的10.244.0.0/16不一样,所以要改。改的地方在CALICO_IPV4POOL_CIDR这个环境变量,找到它把值改掉。另外IP_AUTODETECTION_METHOD可以设成interface=eth0或者can-reach=192.168.1.1,让Calico自动选对网卡,避免选到docker0或者别的虚拟网卡上。
apply之后等一两分钟,kubectl get pods -n kube-system看到calico-node和calico-kube-controllers都Running,kubectl get nodes状态变成Ready,就说明网络通了。验证网络连通性可以起两个测试Pod,一个在node1一个在node2,互相ping一下。kubectl run test1 --image=busybox --overrides='{"spec":{"nodeName":"k8s-node1"}}' -- sleep 3600,类似地起test2在node2上,然后kubectl exec进去ping对方的Pod IP。能ping通说明跨节点网络没问题。
Calico的BGP模式在同网段下不需要IPIP封装,性能好。但如果节点跨网段,就要开IPIP或者VXLAN。判断方法很简单:kubectl get ippool -o yaml看ipipMode字段,如果是Never就是纯BGP,Always就是全封装。同网段建议用Never,跨网段用CrossSubnet,只在跨网段时封装。
3.3 工作节点加入与集群状态检查
工作节点加入集群用kubeadm init输出的kubeadm join命令,带上token和discovery-token-ca-cert-hash。这个hash是控制面CA证书的指纹,用来验证控制面身份,不能省。join命令执行后,节点会拉镜像、启动kubelet、向apiserver注册。在控制面上kubectl get nodes能看到新节点,状态从NotReady变成Ready大概需要一两分钟。
节点加入后有几个检查项:kubectl get nodes -o wide看内网IP对不对,kubectl describe node k8s-node1看Conditions里MemoryPressure、DiskPressure、PIDPressure是不是False,kubectl get pods -n kube-system -o wide看calico-node是不是在新节点上也Running了。如果节点一直NotReady,大概率是网络插件没起来,或者containerd配置有问题,journalctl -u kubelet -f看日志能定位到具体原因。
有个常见问题是节点加入后Pod调度不上去,kubectl describe pod看到0/3 nodes are available: 3 node(s) had taint。这是因为控制面有污点,工作节点如果也有污点就要检查是不是kubeadm join时带了--taint参数。默认工作节点没有污点,但如果之前手动加过,用kubectl taint nodes k8s-node1 key:NoSchedule-去掉。
4. Kubesphere安装与功能模块配置
4.1 Kubesphere与K8s的版本匹配策略
Kubesphere对K8s版本有要求,不是随便哪个版本都能装。装之前先去官方文档查兼容性矩阵,比如Kubesphere 3.4支持K8s 1.21到1.26,3.3支持1.19到1.24。版本不匹配的话,安装过程中会报各种奇怪的错误,比如CRD不兼容、API版本找不到。我用的K8s 1.26配Kubesphere 3.4,这个组合是官方验证过的,相对稳。
安装方式有两种:一是用ks-installer,在K8s集群里跑一个安装器Pod,通过改ConfigMap来启用各个组件;二是用KubeKey,从裸机开始一键装K8s加Kubesphere。我选的是ks-installer,因为集群已经搭好了,不想重装。ks-installer的原理是在集群里创建一堆CRD和Operator,然后根据ConfigMap里的开关决定装哪些组件。这种方式对已有集群侵入性小,卸载也方便。
安装前要确认集群资源够用。Kubesphere最小化安装(只装核心组件)大概需要4核8G,全功能安装(加上监控、日志、DevOps、ServiceMesh)要8核16G以上。我的三台机器每台4核8G,只装了核心加监控,DevOps和ServiceMesh没开,资源刚好够。如果资源不够,安装过程中Pod会一直Pending,看事件是Insufficient cpu或者Insufficient memory。
4.2 ks-installer部署与核心组件启用
安装第一步是apply两个yaml:一个是kubesphere-installer.yaml,创建Namespace和Deployment;另一个是cluster-configuration.yaml,定义要启用哪些组件。apply之后kubectl get pods -n kubesphere-system能看到ks-installer的Pod在跑,它会根据cluster-configuration里的配置去装各个组件。这个过程比较长,核心组件装完大概10到15分钟,如果开了监控和日志,可能要20分钟以上。
cluster-configuration.yaml里几个关键开关:etcd.monitoring控制etcd监控,local_store控制本地存储,monitoring控制Prometheus监控栈,logging控制日志收集,devops控制DevOps流水线,servicemesh控制Istio。每个开关打开都会部署对应的Operator和CR,资源消耗不小。建议第一次装只开核心加monitoring,跑稳了再逐步开其他。
安装过程中用kubectl logs -n kubesphere-system deploy/ks-installer -f看进度,看到Installation is successful就完成了。然后kubectl get svc -n kubesphere-system找到ks-console的NodePort,用浏览器访问http://节点IP:30880,默认账号admin,密码P@88w0rd。第一次登录会要求改密码,改完就进控制台了。
4.3 存储、监控与日志模块的落地配置
Kubesphere的存储模块依赖StorageClass。测试环境没有独立存储设备,我用的是local path provisioner,把节点上的某个目录当存储池。装完Kubesphere后,kubectl get sc应该能看到local的StorageClass。如果没有,可以手动装一个local-path-provisioner,或者用OpenEBS的hostpath模式。存储这块要注意的是,local volume和节点绑定,Pod漂移到其他节点后数据就没了,所以只适合测试,生产要用网络存储。
监控模块装好后,Kubesphere控制台里能看到集群的CPU、内存、磁盘、网络监控图表。底层是Prometheus加Grafana,Kubesphere做了封装。如果监控数据出不来,检查kubectl get pods -n kubesphere-monitoring-system,看prometheus-k8s的Pod是不是Running。常见问题是Prometheus的PVC没绑定,kubectl get pvc -n kubesphere-monitoring-system看状态是不是Bound。没绑定的话检查StorageClass和PVC的storageClassName是否匹配。
日志模块用的是Fluent Bit加Elasticsearch。Fluent Bit以DaemonSet形式跑在每个节点上,收集容器日志发到Elasticsearch。装完后在控制台里能按Pod、按命名空间查日志。如果日志查不到,先看Fluent Bit的Pod日志,kubectl logs -n kubesphere-logging-system -l app.kubernetes.io/name=fluent-bit,常见错误是Elasticsearch连不上或者索引创建失败。Elasticsearch的资源消耗比较大,如果节点内存不够,可以调小JVM堆大小,在kubectl edit statefulset elasticsearch-logging-data -n kubesphere-logging-system里改ES_JAVA_OPTS。
5. 常见故障排查与运维经验
5.1 节点NotReady的排查路径
节点NotReady是最常见的问题,排查思路是从下往上:先看kubelet服务状态,systemctl status kubelet,如果服务没跑起来,journalctl -u kubelet -n 100看报错。常见原因有:containerd没启动、cgroup驱动不匹配、证书过期、配置文件路径错误。containerd的问题用systemctl status containerd和journalctl -u containerd查,cgroup驱动不匹配的报错信息很明确,直接改配置就行。
如果kubelet在跑但节点还是NotReady,看kubectl describe node的Conditions。Ready为False时,Message字段会说明原因,比如container runtime not ready或者network plugin not ready。container runtime not ready一般是containerd的问题,network plugin not ready是Calico没起来。Calico没起来的原因可能是镜像拉不下来、RBAC权限不够、或者网段配置冲突。kubectl logs -n kube-system -l k8s-app=calico-node能看到具体报错。
还有一种情况是节点状态是Ready但Pod调度不上去,kubectl describe pod看到node(s) had taint或者Insufficient cpu。taint的问题用kubectl describe node看Taints字段,有污点就根据情况去掉。资源不足的话,kubectl describe node看Allocated resources,对比Capacity和Allocatable,如果Allocatable很小,可能是kubelet预留资源设得太大,改/var/lib/kubelet/config.yaml里的systemReserved和kubeReserved。
5.2 Pod启动失败的典型场景与处理
Pod一直Pending,先看kubectl describe pod的Events。0/3 nodes are available后面跟的原因很关键:Insufficient cpu是CPU不够,node(s) had taint是污点问题,node(s) didn't match node selector是标签不匹配,pod has unbound immediate PersistentVolumeClaims是PVC没绑定。PVC没绑定的问题,kubectl get pvc看状态,Pending的话kubectl describe pvc看Events,常见原因是StorageClass不存在或者Provisioner没跑。
Pod一直ContainerCreating,看Events里有没有Failed to pull image或者Failed to create pod sandbox。拉镜像失败检查镜像地址和网络,crictl pull手动拉一下试试。sandbox创建失败一般是CNI插件的问题,ls /etc/cni/net.d/看有没有Calico的配置文件,没有的话说明Calico没装好。还有一种情况是failed to set up sandbox container network: failed to allocate for range,这是IP池耗尽,kubectl get ippool -o yaml看IP池大小,不够就扩。
Pod CrashLoopBackOff,看kubectl logs的报错。如果是应用自身的问题,日志里会有明确信息。如果是OOMKilled,说明内存超了,调大resources.limits.memory或者优化应用内存占用。如果是Error: cannot find module之类的,是镜像里缺依赖,重新打镜像。CrashLoopBackOff的退避时间会越来越长,从10秒到5分钟,调试时可以用kubectl run起一个带sleep的临时Pod,把镜像挂进去手动跑命令,比反复重启快。
5.3 集群证书过期与etcd备份恢复
K8s的证书默认有效期一年,到期后apiserver会拒绝连接,kubectl报Unable to connect to the server: x509: certificate has expired。检查证书有效期用kubeadm certs check-expiration,续期用kubeadm certs renew all。续期后要重启控制面组件,docker ps或者crictl ps找到apiserver、scheduler、controller-manager、etcd的容器,crictl rm删掉,kubelet会自动重建。注意etcd的证书续期后也要重启etcd容器,否则etcd还是用旧证书。
etcd备份是运维的底线。备份命令用etcdctl snapshot save,要指定endpoint和cacert、cert、key。这些文件在/etc/kubernetes/pki/etcd/目录下。备份文件存到集群外的机器上,别放本地,节点挂了备份也没了。恢复的时候用etcdctl snapshot restore,恢复到一个新的data目录,然后改etcd的static pod yaml里的--data-dir指向新目录,重启etcd。恢复操作风险高,建议先在测试环境练几遍。
Kubesphere的备份主要是备份它的CRD和ConfigMap。kubectl get crd | grep kubesphere能看到一堆CRD,用kubectl get导出成yaml存起来。ConfigMap在kubesphere-system和kubesphere-monitoring-system命名空间下,也导出备份。Kubesphere本身没有一键备份恢复的功能,所以定期手动备份是必要的。如果集群要重建,先装K8s,再装Kubesphere,然后把备份的CRD和ConfigMap apply回去,能恢复大部分配置。
5.4 资源规划与性能调优的实操建议
测试环境资源有限,调优的目标是用最少资源跑最多东西。kubelet的--eviction-hard参数控制驱逐阈值,默认memory.available<100Mi,磁盘nodefs.available<10%。如果节点磁盘小,这个阈值要调低,否则Pod频繁被驱逐。--image-gc-high-threshold和--image-gc-low-threshold控制镜像回收,默认85%和80%,磁盘紧张的话调到70%和60%,让kubelet更早清理旧镜像。
Calico的FelixConfiguration可以调iptablesRefreshInterval和routeRefreshInterval,默认90秒和90秒,调大到300秒能减少CPU占用。prometheus的采集间隔默认30秒,调到60秒能省一半的存储和CPU。Kubesphere的监控数据保留时间默认7天,改kubectl edit prometheus -n kubesphere-monitoring-system里的retention字段,改成3天能省不少磁盘。
节点上的系统预留资源要设合理。systemReserved给系统进程留,kubeReserved给kubelet和容器运行时留。4核8G的节点,systemReserved设cpu: 500m, memory: 1Gi,kubeReserved设cpu: 500m, memory: 1Gi,这样Allocatable大概是3核6G,能跑不少Pod。不设的话,系统进程和业务Pod抢资源,节点容易卡死。改完systemctl restart kubelet生效,然后kubectl describe node确认Allocatable变了。
6. 个人实操体会与后续扩展方向
这套集群从裸机到Kubesphere跑起来,前后折腾了大概两天,其中大部分时间花在排查网络和存储问题上。最大的体会是:日志和事件是第一手资料,遇到问题先看kubectl describe和journalctl,比在网上搜半天管用。另一个体会是版本匹配比什么都重要,K8s、containerd、Calico、Kubesphere这四个组件的版本要互相兼容,装之前花十分钟查兼容性矩阵,能省后面几小时的排查时间。
后续扩展的话,有几个方向可以考虑。一是加节点做高可用控制面,把etcd从单点变成三节点集群,这样控制面挂一台不影响集群。二是接外部存储,用NFS或者Ceph做StorageClass,解决local volume不能漂移的问题。三是开DevOps模块,把CI/CD流水线跑起来,配合GitOps做应用发布。四是开ServiceMesh,用Istio做流量管理和可观测性,不过这个对资源要求高,节点配置得跟上。
最后分享一个小技巧:Kubesphere的ks-installer在安装过程中如果卡住,不要急着删Pod重来。先看kubectl logs -n kubesphere-system deploy/ks-installer的日志,很多时候是某个镜像拉不下来或者某个Job超时,手动拉一下镜像或者调大超时时间就能过。删了重来反而可能因为残留资源导致新问题。另外cluster-configuration.yaml改完之后,ks-installer会自动检测变化并重新执行,不用手动重启Pod,等几分钟就行。