1. 项目概述与核心痛点
最近在给团队搭建一套新的Kubernetes(K8s)测试集群,从零开始走了一遍部署流程。虽然官方文档和各种教程满天飞,但真到自己动手,从环境准备到组件拉起,再到集群可用,每一步都可能遇到意想不到的“坑”。这次部署的目标是一个三节点的生产级测试集群(1 Master + 2 Worker),使用containerd作为容器运行时,Calico作为网络插件。整个过程下来,最大的感触就是:K8s部署的成功,往往不在于你记住了多少命令,而在于你是否理解每个命令背后的原理,以及当命令失败时,你能否快速定位到那个“捣鬼”的配置项。这篇文章,我就把这次部署中遇到的关键问题、排查思路和最终解决方案,以“踩坑记录”的形式整理出来,希望能帮你绕过我走过的弯路。
对于刚接触K8s的朋友来说,部署集群是第一个“下马威”。它不像安装一个单机软件那么简单,涉及到操作系统配置、容器运行时、网络、存储等多个层面的协调。常见的痛点集中在几个方面:系统参数配置不当(如关闭swap、配置内核模块)、容器运行时与kubelet的cgroup驱动不一致导致节点无法注册、网络插件安装后Pod之间无法通信、以及证书相关的问题导致组件间认证失败。这些问题往往相互关联,一个环节出错,表象可能出现在另一个完全不同的地方,排查起来非常考验对K8s架构的理解。
2. 环境准备与前置条件梳理
部署K8s集群,第一步不是急着运行kubeadm init,而是要把基础环境打磨好。这就像盖房子前要打好地基,地基不稳,后面砌再高的墙也容易塌。
2.1 操作系统与内核要求
我们选用的是Ubuntu 22.04 LTS。选择稳定的、K8s社区支持良好的Linux发行版至关重要。首先,必须确保所有节点(Master和Worker)的系统主机名、/etc/hosts文件配置正确且能够互相解析。我遇到过因为主机名带下划线而导致kubelet启动失败的问题,所以最好使用标准的DNS命名规则(仅包含字母、数字、连字符和点)。
接下来是内核参数的调整。K8s对Linux内核有一些硬性要求,比如必须关闭swap。这不仅仅是为了性能,更是因为kubelet的默认行为假设节点有充足的非交换内存。关闭swap可以通过sudo swapoff -a临时生效,并编辑/etc/fstab永久注释掉swap分区行。此外,还需要加载一些必需的内核模块,如br_netfilter、ip_vs等,并配置sysctl参数以启用IP转发和桥接流量。
# 加载内核模块 sudo modprobe br_netfilter sudo modprobe ip_vs sudo modprobe ip_vs_rr sudo modprobe ip_vs_wrr sudo modprobe ip_vs_sh sudo modprobe nf_conntrack # 配置sysctl参数 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf br_netfilter EOF cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system注意:
sysctl --system会重新加载所有配置文件,确保上述配置生效。如果遇到bridge-nf-call参数报“文件不存在”的错误,通常是因为br_netfilter模块没有成功加载,请先用lsmod | grep br_netfilter检查。
2.2 容器运行时选型与安装:Containerd实战
K8s早已不再绑定Docker,事实上,从1.24版本开始,Dockershim已被移除,你需要直接选择一种容器运行时接口(CRI)兼容的运行时。主流的选项是containerd和CRI-O。我们选择containerd,因为它性能优秀、资源占用少,且是Docker的底层运行时,生态成熟。
安装containerd推荐使用官方二进制包或从发行版仓库安装。这里以官方二进制为例。下载解压后,需要生成默认配置文件/etc/containerd/config.toml。这里藏着第一个大坑:cgroup驱动。
# 安装containerd wget https://github.com/containerd/containerd/releases/download/v1.7.13/containerd-1.7.13-linux-amd64.tar.gz sudo tar Cxzvf /usr/local containerd-1.7.13-linux-amd64.tar.gz sudo systemctl enable --now containerd # 生成默认配置 sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml生成配置后,关键一步是修改cgroup驱动。K8s的kubelet默认使用systemd作为cgroup驱动,而containerd默认生成的配置中,SystemdCgroup参数是false。如果不一致,kubelet在启动时会报错,节点状态一直是NotReady。
# 编辑 /etc/containerd/config.toml [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] ... [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true # 将这里改为 true修改后,重启containerd:sudo systemctl restart containerd。务必使用sudo systemctl status containerd检查服务状态,确保没有错误日志。
2.3 K8s组件安装与版本锁定
安装kubeadm, kubelet和kubectl。建议使用阿里云或谷歌的镜像源来加速下载。这里又一个细节:在安装kubelet后,不要立即启动它。因为此时集群还未初始化,kubelet会不断尝试连接一个不存在的API Server,产生大量错误日志。
# 添加阿里云镜像源 sudo apt-get update && sudo apt-get install -y apt-transport-https curl curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add - echo "deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list # 安装指定版本(保持各节点版本一致) sudo apt-get update sudo apt-get install -y kubelet=1.28.0-00 kubeadm=1.28.0-00 kubectl=1.28.0-00 sudo apt-mark hold kubelet kubeadm kubectl # 锁定版本,防止意外升级apt-mark hold命令非常重要,它能防止系统自动更新时升级K8s组件,避免因版本不一致导致集群故障。在所有节点上重复上述环境准备步骤,确保配置完全一致。
3. 集群初始化与首个“拦路虎”
基础环境就绪后,就可以在Master节点上执行初始化命令了。这是最激动人心也最容易出错的一步。
3.1 kubeadm init 命令参数解析
不要直接运行sudo kubeadm init,建议使用配置文件来初始化,这样参数清晰、可重复。创建一个kubeadm-config.yaml文件:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.100 # 替换为Master节点的实际IP bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/containerd/containerd.sock imagePullPolicy: IfNotPresent taints: [] --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.0 controlPlaneEndpoint: "192.168.1.100:6443" # 高可用时可配置负载均衡器VIP networking: podSubnet: "192.168.0.0/16" # 必须与后续安装的CNI插件(如Calico)的默认网段匹配 serviceSubnet: "10.96.0.0/12" apiServer: extraArgs: authorization-mode: Node,RBAC timeoutForControlPlane: 4m0s imageRepository: registry.aliyuncs.com/google_containers # 使用国内镜像源加速 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd # 明确指定kubelet使用systemd cgroup驱动重点关注几个参数:
advertiseAddress:Master节点的IP,其他节点将通过这个地址连接API Server。podSubnet:这是Pod的IP地址范围。这个值必须与你将要安装的网络插件(CNI)的默认网段一致。例如,Calico的默认IPPool是192.168.0.0/16。如果不一致,网络插件将无法正确分配IP,导致Pod无法启动。imageRepository:墙内必备。指向阿里云镜像仓库,可以极大加快镜像拉取速度,避免初始化卡在Pull镜像阶段。cgroupDriver: systemd:在KubeletConfiguration中显式声明,与containerd的配置遥相呼应,双重保险。
3.2 初始化过程详解与常见报错
执行初始化:sudo kubeadm init --config=kubeadm-config.yaml --upload-certs | tee kubeadm-init.log。使用tee命令将输出同时保存到文件,方便后续排查。
初始化过程会依次进行预检、拉取镜像、生成证书和静态Pod清单等。这里最容易卡住的地方是镜像拉取。即使配置了国内镜像源,也可能因为网络波动导致某个镜像拉取失败。观察输出日志,如果卡在某个镜像上,可以手动去其他节点docker pull或crictl pull对应的镜像,然后打tag。
另一个常见错误是[ERROR Port-6443]或[ERROR Port-10259]等端口占用。这通常是因为之前部署失败后,相关组件没有清理干净,或者有其他服务占用了K8s默认端口(6443, 10250, 10259等)。使用sudo netstat -tlnp | grep <端口号>检查并解决冲突。
如果初始化失败,可以使用sudo kubeadm reset -f进行清理,它会重置kubeadm所做的更改。但注意,kubeadm reset不会清除iptables规则和CNI插件创建的网桥设备,这些需要手动清理。
# 清理网络残留(在重置后执行) sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X sudo ip link delete cni0 2>/dev/null sudo ip link delete flannel.1 2>/dev/null sudo ip link delete cali* 2>/dev/null 2>/dev/null # 如果用了Calico初始化成功后,会输出加入集群的命令,类似于kubeadm join ...。务必把这段信息保存好。按照提示,配置kubectl:
mkdir -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,应该能看到Master节点,但状态是NotReady,因为网络插件(CNI)还没有安装。
4. 网络插件部署与跨节点通信难题
CNI插件是K8s集群的“神经系统”,负责Pod之间的网络通信。没有它,集群就是瘫痪的。我们选择Calico,功能强大且文档完善。
4.1 Calico安装与关键配置核对
安装Calico通常很简单,一条命令即可:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.4/manifests/calico.yaml但是,魔鬼在细节里。直接应用这个官方yaml,很可能导致Pod网络与你的kubeadm配置冲突。最大的坑就是Pod子网(CIDR)不匹配。在初始化时我们设置了podSubnet: 192.168.0.0/16,而Calico默认的IP池(IPPool)可能也是这个网段,这看起来没问题。但如果你的物理网络恰好也是192.168.0.0/16,就会产生路由冲突。或者,你之前初始化时用了别的网段,那Calico Pod将无法分配IP。
解决方案:在安装前,先下载Calico的yaml文件,检查并修改其IP池配置。
wget https://raw.githubusercontent.com/projectcalico/calico/v3.26.4/manifests/calico.yaml用编辑器打开calico.yaml,搜索CIDR或IPPOOL。你会找到一个Calico自定义资源定义(CRD)的配置部分。确保其cidr字段与kubeadm配置中的podSubnet完全一致。
apiVersion: crd.projectcalico.org/v1 kind: IPPool metadata: name: default-ipv4-ippool spec: blockSize: 26 cidr: 192.168.0.0/16 # 确认这里与 podSubnet 一致 ipipMode: Never natOutgoing: true nodeSelector: all() vxlanMode: Always修改保存后,再应用:kubectl apply -f calico.yaml。应用后,使用kubectl get pods -n kube-system观察calico-node-xxx和calico-kube-controllers-xxx这些Pod的状态,直到全部变为Running。
4.2 节点NotReady问题深度排查
即使Calico Pod跑起来了,kubectl get nodes可能依然显示NotReady。这是最让人头疼的阶段,需要系统化排查。
- 检查kubelet状态:在问题节点上运行
sudo systemctl status kubelet -l。查看是否有红色错误日志。常见错误是“cgroup driver mismatch”或“failed to run kubelet”。 - 检查容器运行时:运行
sudo crictl ps,看pause容器和k8s核心组件(如etcd, apiserver)的容器是否正常运行。如果crictl命令报错或没有容器,说明containerd没有正确服务kubelet。 - 检查CNI插件:在节点上查看CNI配置和日志。
ls -la /etc/cni/net.d/ # 应该能看到Calico的配置文件 sudo journalctl -u kubelet -f | grep -i cni # 查看kubelet日志中关于CNI的错误 - 检查Calico Node DaemonSet:
kubectl describe pod calico-node-xxxx -n kube-system。关注Events部分和容器状态。常见问题是镜像拉取失败、权限不足(需要挂载/var/run/calico等目录)或节点标签不匹配。 - 检查IP分配:Calico为每个节点分配一个Pod网段块。在Master节点上运行
kubectl get ipamblock -o yaml可以查看分配情况。如果某个节点的块没有分配成功,该节点上的Pod就无法获取IP。
我遇到的一个典型问题是,Worker节点一直NotReady,kubelet日志显示network plugin is not ready: cni config uninitialized。检查/etc/cni/net.d/发现是空的。原因是Calico的calico-nodePod在该节点上启动失败。进一步describe该Pod,发现事件是Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox ...。这通常指向更底层的网络问题,比如主机iptables规则冲突、内核模块缺失,或者节点防火墙(如ufw)没有关闭。
实操心得:遇到节点
NotReady,不要慌,按照“kubelet日志 -> 容器运行时状态 -> CNI配置与日志 -> 网络插件Pod状态”这条链路,自上而下或自下而上进行排查,总能定位到问题根源。善用journalctl和kubectl describe、kubectl logs这三个命令。
5. Worker节点加入与认证故障
Master节点Ready后,就可以让Worker节点加入了。在每个Worker节点上,运行Master初始化成功后给出的kubeadm join命令。
5.1 join命令执行与令牌管理
kubeadm join命令包含API Server地址、令牌和CA证书哈希。令牌默认24小时有效。如果令牌过期,可以在Master节点上重新生成:
# 生成新的令牌 kubeadm token create --print-join-command # 获取CA证书哈希 openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'将新令牌和哈希值组合成新的kubeadm join命令。Worker节点执行join后,使用kubectl get nodes在Master上观察,新节点会先处于NotReady状态,待Calico等Pod调度到该节点并运行后,状态会变为Ready。
5.2 节点加入失败的认证与网络排查
Worker节点加入失败,常见原因有:
- 网络不通:Worker节点无法访问Master节点的6443端口。用
telnet <master-ip> 6443或curl -k https://<master-ip>:6443测试。 - 令牌或证书哈希无效:确保复制的命令完整无误,特别是超长的证书哈希。
- 时间不同步:所有节点的时间必须基本同步,否则TLS握手会失败。使用
chronyd或ntpd服务确保时间同步。 - Hostname冲突:集群内节点主机名必须唯一。
如果join命令看似成功,但节点一直不出现,检查Master节点上kube-apiserver的日志:sudo journalctl -u kube-apiserver -f,看是否有来自Worker节点的连接请求和认证错误。
6. 核心组件状态监控与问题修复
集群初步就绪后,需要确保所有核心系统Pod都健康运行。这些Pod都在kube-system命名空间下。
6.1 系统Pod健康检查清单
运行kubectl get pods -n kube-system,你应该看到以下核心组件(具体名字前缀可能不同):
coredns-xxx:DNS服务,通常有两个副本。如果处于Pending或CrashLoopBackOff,通常是网络问题。etcd-<master-hostname>:键值存储数据库,Master节点独有。如果异常,整个集群控制面将瘫痪。kube-apiserver-<master-hostname>:API服务器。kube-controller-manager-<master-hostname>:控制器管理器。kube-scheduler-<master-hostname>:调度器。calico-node-xxx:每个节点上都有一个,负责本节点网络。calico-kube-controllers-xxx:Calico的控制器,通常一个副本。
任何一个组件异常,都需要立即排查。使用kubectl describe pod <pod-name> -n kube-system和kubectl logs <pod-name> -n kube-system查看详细原因。
6.2 DNS与CoreDNS排错实战
CoreDNS是最容易出问题的地方之一。症状是:Pod内无法解析集群内服务名(如kubernetes.default.svc.cluster.local)或外部域名。
诊断步骤:
- 在任意Pod内(或临时运行一个
busyboxPod),执行nslookup kubernetes.default。 - 如果解析失败,检查CoreDNS Pod日志:
kubectl logs -l k8s-app=kube-dns -n kube-system。 - 检查CoreDNS的ConfigMap:
kubectl get configmap coredns -n kube-system -o yaml。默认配置通常没问题,但如果你自定义了上游DNS或域名,需要仔细核对。 - 检查节点上的
/etc/resolv.conf。kubelet会使用该文件中的nameserver作为Pod的默认上游DNS。如果这里面是127.0.0.1,而节点本地没有运行DNS服务,就会导致解析失败。解决方法是在kubelet启动参数中配置--resolv-conf指向正确的文件,或者确保本地有可用的DNS转发服务。
我遇到过一个经典案例:所有Pod内部DNS解析超时。检查CoreDNS Pod日志,发现大量read udp i/o timeout错误。原因是节点防火墙规则丢弃了UDP 53端口(DNS查询端口)的流量。虽然Calico管理了Pod网络,但Pod访问节点本地DNS服务器(如/etc/resolv.conf中的8.8.8.8)的流量仍然会经过主机的网络栈,受主机防火墙规则影响。解决方案是调整主机防火墙,允许DNS查询流量。
7. 存储、Ingress与后续组件部署考量
基础集群稳定后,就可以考虑部署有状态应用了,这涉及到存储和外部访问。
7.1 存储类(StorageClass)与本地路径供给器
测试环境中,最简单的存储方案是使用local-path-provisioner。它能为每个节点提供基于本地路径的动态持久卷(PV)。
# 部署 local-path-provisioner kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-storage.yaml # 将其设为默认StorageClass kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'部署后,创建PVC(PersistentVolumeClaim)时,如果不指定storageClassName,就会自动使用local-path来动态创建PV。注意,这种存储是节点亲和的,Pod被调度到哪个节点,存储卷就在该节点的本地目录创建。如果Pod被重新调度到其他节点,将无法访问原有数据,因此仅适用于测试或可丢失的数据。
7.2 Ingress Controller选型与部署
要让外部流量访问集群内的服务,需要Ingress Controller。我们选择Nginx Ingress Controller,它功能全面、社区活跃。
# 使用Helm安装(需先安装Helm) helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace部署后,需要确定Ingress Controller的Service类型。在云环境通常用LoadBalancer,在裸机环境则常用NodePort。如果使用NodePort,可以通过任意节点的IP和分配的端口(如http://<node-ip>:30080)来访问Ingress。为了有一个固定的访问入口,我们可以在前面再部署一个负载均衡器(如HAProxy)或者使用MetalLB(一个裸机负载均衡器实现)。
部署Ingress时常见的问题是404或503错误。排查思路:
kubectl get ingress:检查Ingress资源是否被正确创建,ADDRESS字段是否为空。kubectl describe ingress <name>:查看Events,是否有配置错误。kubectl get pods -n ingress-nginx:检查Ingress Controller Pod是否Running。- 查看Ingress Controller Pod的日志:
kubectl logs -l app.kubernetes.io/name=ingress-nginx -n ingress-nginx,里面会有详细的转发规则和错误信息。 - 检查Ingress中定义的
backendserviceName和servicePort是否与集群内真实的Service完全匹配(包括命名空间)。
8. 日常维护与故障排查工具箱
集群跑起来不是终点,日常维护和故障排查能力同样重要。
8.1 必备的kubectl诊断命令
掌握以下命令组合,能解决80%的日常问题:
- 资源概览:
kubectl get all -A(查看所有命名空间所有资源) - Pod详情:
kubectl describe pod <pod-name> -n <namespace>(看事件、状态详情) - Pod日志:
kubectl logs -f <pod-name> -n <namespace>(实时查看日志) - 进入Pod:
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh(用于调试) - 资源YAML:
kubectl get <resource-type> <resource-name> -n <namespace> -o yaml(获取配置) - 节点资源:
kubectl describe node <node-name>(查看节点资源使用、事件、污点) - 服务端点:
kubectl get endpoints <service-name>(查看Service背后真实的Pod IP和端口)
8.2 集群重置与安全清理指南
当集群彻底混乱需要推倒重来时,请按顺序执行:
- 驱逐节点(可选):
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data - 重置节点:在每个节点上执行
sudo kubeadm reset -f - 清理网络(每节点):
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X sudo ip link delete cni0 2>/dev/null sudo ip link delete flannel.1 2>/dev/null # 清理CNI配置和二进制文件 sudo rm -rf /etc/cni/net.d sudo rm -rf /opt/cni/bin - 清理配置文件:
rm -rf $HOME/.kube sudo rm -rf /etc/kubernetes - 卸载软件包(如需要):
sudo apt-get purge kubeadm kubectl kubelet kubernetes-cni
重要提醒:
kubeadm reset和清理操作会永久删除所有集群数据和应用,仅用于测试环境或确定要销毁集群时。生产环境务必先备份ETCD和数据卷。
部署Kubernetes集群就像完成一个精密的拼图,每一块(组件、配置)都必须严丝合缝。这次踩坑经历让我深刻体会到,理解其架构原理(如kubelet与容器运行时的交互、CNI插件的工作机制、kube-apiserver的认证流程)远比死记硬背命令更重要。当遇到问题时,顺着日志和事件这条“线索”,结合对原理的理解,总能找到问题的根源。最后,保持耐心,善用社区和文档,每一个坑踩过去,都是对这套强大系统更深入的认识。