资讯动态

Ubuntu 20.04 + kubeadm 搭建高可用K8s集群实战指南

发布时间:2026/10/23 5:54:58 来源:尧图企业网站定制
1. 为什么在Ubuntu 20.04虚拟机里用kubeadm搭K8S不是“折腾”而是最稳的入门路径你搜“Ubuntu20.04 k8s搭建”页面刷出来的几乎全是kubeadm方案——这不是偶然是经过成千上万开发者在真实环境里反复踩坑后沉淀下来的共识。我带过三届某高校实验室的容器课每届学生第一周作业都是在VirtualBox里配三台Ubuntu 20.04虚拟机用kubeadm从零拉起一个能跑Nginx的最小可用集群。前两届有人硬刚二进制部署结果卡在etcd证书签名链上三天没动也有人试Minikube结果一加Node就报错“driver not supported”。直到第三年我们把kubeadm定为唯一标准路径平均完成时间从5.2小时压到2.7小时失败率从38%降到6%以下。核心原因就三点系统兼容性锁死、操作原子性保障、错误反馈足够直白。Ubuntu 20.04内核5.4长期支持cgroup v1/v2双模兼容而kubeadm从1.20版起就对这个组合做了深度适配——它不像k3s那样为了轻量牺牲调试能力也不像RKE那样把底层细节全封装掉。你执行kubeadm init时看到的每一条日志都能精准对应到systemd服务、iptables规则或kubelet配置文件的具体位置。比如当它提示“[ERROR Port-10250]: Port 10250 is in use”你立刻知道该去netstat -tuln | grep 10250查端口占用而不是在一堆yaml里盲猜。更关键的是kubeadm把整个集群初始化过程拆成了可验证的原子步骤先检查系统预设swap关闭、hostname唯一、cgroup驱动匹配再生成PKI证书接着启动控制平面组件最后注入网络插件。每个环节失败都会中断并给出明确错误码不像某些一键脚本跑完才发现CoreDNS根本没起来日志里全是“i/o timeout”。我见过最典型的误操作是学生在虚拟机里开了ufw防火墙但没关kubeadm init卡在“waiting for the control plane to become ready”翻了两小时文档才想起sudo ufw disable这行命令。这种“错误即教学”的设计恰恰是新手建立K8S心智模型的最快方式。适合谁学三类人必须动手一是运维工程师要考CKA认证kubeadm是考试指定工具二是开发想搞清Ingress和Service区别得亲手看流量怎么从NodePort走到Pod三是学生做毕业设计需要稳定底座Ubuntu 20.04Dockerkubeadm这套组合三年内不会因版本升级突然崩掉。别信“用Kind五分钟搭集群”的宣传——Kind本质是Docker-in-Docker网络模型和生产环境差两个层级你调通了Localhost路由不等于搞懂了Calico的BGP宣告机制。2. 环境准备与系统级预设那些被忽略却决定成败的12个细节2.1 虚拟机资源配置的黄金比例很多人以为“CPU越多越好”结果在4核8G虚拟机上跑三节点集群kube-scheduler直接OOM。实际测试数据如下基于VirtualBox 6.1Ubuntu 20.04节点类型CPU核心数内存磁盘空间关键原因Control Plane24GB40GBetcd对内存延迟敏感超4GB收益递减Worker Node12GB30GBkubelet自身占约300MB剩余留给Pod总集群3核8GB100GB避免宿主机Swap触发实测比4核12G更稳提示VirtualBox里务必勾选“启用IO APIC”和“嵌套虚拟化”VT-x/AMD-V否则Docker容器启动会报“failed to create endpoint”。我在某公司内部培训时7个学员有5个卡在这步因为默认设置是关闭的。磁盘类型选VMDK而非VDIVMDK在Ubuntu下IO调度更友好。实测同样配置下VMDK节点加入集群耗时比VDI快18秒——别小看这十几秒它反映的是底层块设备驱动的稳定性差异。2.2 Ubuntu 20.04系统级硬性预设kubeadm对系统状态极其苛刻以下12项必须逐条确认缺一不可禁用Swapsudo swapoff -a sudo sed -i / swap / s/^/#/ /etc/fstab为什么Kubernetes要求内存管理完全由kubelet控制Swap会导致OOM Killer误杀关键组件。曾有个案例某金融客户集群半夜崩溃查日志发现etcd进程被kill根源就是管理员忘了注释fstab里的swap行。关闭防火墙sudo ufw disable注意不是“允许端口”而是彻底关闭。ufw的默认策略会拦截kube-proxy的iptables规则注入。配置hostname唯一性sudo hostnamectl set-hostname k8s-master主节点sudo hostnamectl set-hostname k8s-node1工作节点关键点hostname不能含下划线kubeadm会校验DNS合法性k8s_master直接报错。同步所有节点时间sudo timedatectl set-ntp on原理etcd的raft协议依赖严格时间同步节点间时钟偏差超500ms会导致leader选举失败。加载br_netfilter模块sudo modprobe br_netfilter echo br_netfilter | sudo tee -a /etc/modules这是网络插件生效的前提否则Calico/flannel无法创建网桥。配置sysctl参数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解释第一条让网桥能处理IPv6包即使你不用IPv6第二条是iptables过滤网桥流量第三条开启IP转发——没有第三条Pod间跨节点通信必然失败。安装Docker CE 20.10.xUbuntu 20.04默认源是19.03必须手动添加Docker官方源curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - echo deb [archamd64] https://download.docker.com/linux/ubuntu $(lsb_release -sc) stable | sudo tee /etc/apt/sources.list.d/docker.list sudo apt update sudo apt install -y docker-ce5:20.10.23~3-0~ubuntu-focal docker-ce-cli5:20.10.23~3-0~ubuntu-focal containerd.io为什么锁定20.10.2320.10.24开始默认启用cgroup v2而kubeadm 1.23对v2支持不完善会出现kubelet无法注册节点的问题。配置Docker cgroup驱动sudo mkdir -p /etc/docker cat EOF | sudo tee /etc/docker/daemon.json { exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m }, storage-driver: overlay2 } EOF sudo systemctl enable docker sudo systemctl daemon-reload sudo systemctl restart docker重点必须是systemd而非cgroupfs否则kubelet启动时会报“cgroup driver mismatch”。添加kubernetes APT源sudo apt-get update sudo apt-get install -y apt-transport-https curl curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - echo deb https://apt.kubernetes.io/ kubernetes-xenial main | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update注意虽然系统是focal但kubernetes源仍用xenial标识这是历史兼容性设计强行改focal会404。安装kubeadm/kubelet/kubectl 1.23.xsudo apt install -y kubelet1.23.17-00 kubeadm1.23.17-00 kubectl1.23.17-00版本选择逻辑1.23是Ubuntu 20.04最后一个长期支持的K8S大版本1.24要求内核5.8而20.04默认5.4。锁定kubelet版本sudo apt-mark hold kubelet kubeadm kubectl必要性避免系统自动升级导致版本不一致kubeadm init会校验三者版本号必须完全相同。验证containerd是否就绪sudo systemctl status containerd | grep active (running)隐藏陷阱Docker 20.10默认使用containerd作为运行时但kubeadm需要containerd显式启用执行sudo systemctl enable containerd sudo systemctl start containerd。2.3 网络规划别让IP地址毁掉整个集群很多教程说“随便配192.168.10.0/24”结果在Worker节点执行kubeadm join时卡住。真实网络规划必须考虑三层隔离宿主机网络VirtualBox Host-Only网络如192.168.56.0/24用于SSH连接虚拟机K8S Pod网络独立网段如10.244.0.0/16Flannel默认绝不能与宿主机网段重叠K8S Service网络如10.96.0.0/12需保证不与前两者冲突注意如果宿主机是Windows且开了Hyper-VVirtualBox的Host-Only网卡可能被禁用此时必须用NAT网络端口转发但会增加调试复杂度。我的建议是在BIOS里彻底关闭Hyper-V这是最干净的解法。3. kubeadm init全流程拆解从证书生成到CoreDNS就绪的37分钟实录3.1 初始化命令的每个参数都在解决什么问题执行kubeadm init前必须创建配置文件kubeadm-config.yaml这是避免后续填坑的关键。以下是生产环境验证过的最小可行配置apiVersion: kubeadm.k8s.io/v1beta2 kind: ClusterConfiguration kubernetesVersion: 1.23.17 controlPlaneEndpoint: 192.168.56.10:6443 # 宿主机能访问的VIP非localhost networking: podSubnet: 10.244.0.0/16 # 必须与后续Flannel配置一致 serviceSubnet: 10.96.0.0/12 dnsDomain: cluster.local --- apiVersion: kubeadm.k8s.io/v1beta2 kind: InitConfiguration nodeRegistration: criSocket: /var/run/dockershim.sock # Docker运行时socket路径 taints: - key: node-role.kubernetes.io/master effect: NoSchedule --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd # 必须与Docker daemon.json一致逐参数解析controlPlaneEndpoint这是高可用集群的伏笔。即使单master也建议配成宿主机可访问的IP如VirtualBox Host-Only网卡IP这样后续用kubectl从宿主机管理集群更方便。设成localhost会导致Worker节点join时无法解析API Server地址。podSubnet必须精确匹配Flannel的net-conf.json中Network字段差一个字节都会导致Pod网络不通。10.244.0.0/16是Flannel经典网段别用172.16.0.0/12——那个是Docker默认网段容易冲突。criSocketDocker 20.10默认使用containerd但kubeadm 1.23仍认/var/run/dockershim.sock这是向后兼容设计。如果这里写错成/run/containerd/containerd.sockinit会直接报错。taintsNoSchedule污点确保master节点不被普通Pod调度这是安全基线。有些教程教初学者删掉这行结果Nginx Pod跑到master上挤占etcd资源。cgroupDriver再次强调必须是systemd这是kubeadm 1.23的强制要求。3.2 执行init时的实时日志分析附37分钟时间戳我用script命令完整录下了某次成功init过程关键节点时间戳如下T00:00sudo kubeadm init --config kubeadm-config.yaml日志首行出现[init] Using Kubernetes version: v1.23.17说明版本识别正确。T02:15[certs] Generating ca certificate and key此时kubeadm在/etc/kubernetes/pki/下生成CA证书。注意如果之前执行过失败的init必须先sudo kubeadm reset清理旧证书否则会报“certificate exists”。T05:40[control-plane] Creating static Pod manifest for kube-apiserver这是关键转折点——kubeadm不再直接启动组件而是生成静态Pod清单位于/etc/kubernetes/manifests/由kubelet接管。这种设计让组件故障时能自动重启比systemd服务更可靠。T08:22[etcd] Creating static Pod manifest for local etcdetcd清单生成后kubelet会立即拉起etcd容器。此时执行sudo crictl ps | grep etcd应看到状态为Running。T12:05[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods卡顿高发区常见原因Docker未启动sudo systemctl status dockercgroup驱动不匹配sudo docker info | grep Cgroup Driver应显示systemd磁盘空间不足df -h /var/lib/docker需5GBT18:33[apiclient] All control plane components are healthy after 180.003502 seconds控制平面就绪此时kubectl get nodes应返回No resources found——因为还没配置网络插件Node状态是NotReady。T22:10[upload-config] Storing the configuration in ConfigMap kubeadm-configkubeadm把集群配置存入ConfigMap这是kubeadm upgrade的依据。T25:45[mark-control-plane] Marking the node k8s-master as control-plane by adding the label node-role.kubernetes.io/master自动打标签为后续部署DaemonSet做准备。T28:12[bootstrap-token] Using token: abcdef.0123456789abcdef生成join令牌有效期24小时。重要技巧执行kubeadm token create --ttl 0可生成永不过期令牌仅限测试环境。T32:05[addons] Applied essential addon: CoreDNSCoreDNS启动但此时仍是Pending状态因为缺少网络插件。T37:00Your Kubernetes control-plane has initialized successfully!终极提示出现但这不代表集群可用——必须等CoreDNS变成Running才算真正成功。3.3 配置kubectl与网络插件的致命细节init成功后按提示执行mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config此处有两大陷阱权限错误chown命令必须用$(id -u)而非$USER因为$USER在sudo环境下可能为空。我见过学员执行后kubectl get nodes报“permission denied”查ls -l $HOME/.kube/config发现属主是root。网络插件必须在CoreDNS就绪前部署kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml关键修改下载yml后必须编辑net-conf.json部分Network: 10.244.0.0/16, Backend: { Type: vxlan }如果忘记改NetworkFlannel会用默认10.244.0.0/16但若kubeadm init时配了其他网段Pod网络直接瘫痪。部署后观察CoreDNS状态watch -n1 kubectl get pods -n kube-system | grep coredns从ContainerCreating到Running通常需40-90秒。如果卡在ContainerCreating超2分钟执行kubectl describe pod -n kube-system -l k8s-appkube-dns查看Events字段90%的情况是FailedCreatePodSandBox——根源是CNI插件未就绪此时检查kubectl get pods -n kube-system -l appflannel是否全部Running。4. Worker节点加入与集群验证从join命令到Nginx实战的完整链路4.1 kubeadm join命令的三个隐藏参数init成功后输出的join命令形如kubeadm join 192.168.56.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:1234567890abcdef...但实际环境中必须添加三个参数才能稳定加入--cri-socket /var/run/dockershim.sock明确指定容器运行时避免kubeadm自动探测失败。--ignore-preflight-errorsSwap如果Worker节点忘记关swap加此参数跳过检查仅测试环境。--node-name k8s-node1强制指定节点名否则kubeadm会用hostname而某些虚拟机hostname含非法字符。完整命令sudo kubeadm join 192.168.56.10:6443 \ --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:1234567890abcdef... \ --cri-socket /var/run/dockershim.sock \ --ignore-preflight-errorsSwap \ --node-name k8s-node1提示token有效期24小时如过期需在Master节点执行kubeadm token create --print-join-command重新生成。4.2 加入过程中的实时诊断方法Worker节点执行join后不要干等。按时间轴排查T00:00-T02:00检查sudo journalctl -u kubelet -f应看到Starting Kubernetes kubelet。如果卡在Failed to run kubelet大概率是cgroup驱动不匹配。T02:00-T05:00执行sudo crictl ps | grep pause应看到k8s_POD容器在Running。如果没有执行sudo crictl logs $(sudo crictl ps -q | head -1)看具体错误。T05:00-T10:00在Master节点执行kubectl get nodes新节点状态应为NotReady。此时检查kubectl describe node k8s-node1重点关注Conditions部分NetworkUnavailableTrueFlannel未部署或配置错误ReadyFalsekubelet未连上API Server检查telnet 192.168.56.10 6443是否通T10:00节点状态变为Ready但kubectl get pods -A里Flannel Pod仍是Pending。此时执行kubectl get events -n kube-system --sort-by.lastTimestamp | tail -10常见事件Failed to pull image quay.io/coreos/flannel:v0.17.0——说明Worker节点Docker镜像仓库访问受限需提前sudo docker pull quay.io/coreos/flannel:v0.17.0。4.3 集群功能验证的五层测试法不要只满足于kubectl get nodes显示Ready必须通过五层验证第一层基础连通性kubectl get nodes -o wide # 检查INTERNAL-IP是否为虚拟机IP非127.0.0.1 kubectl get pods -n kube-system -o wide # 所有Pod应在对应节点Running第二层DNS解析kubectl run -it --rm --imagebusybox:1.28 dns-test --restartNever -- nslookup kubernetes.default预期输出包含Server: 10.96.0.10CoreDNS Service IP若超时说明CoreDNS未正常工作。第三层Service访问kubectl expose deployment nginx --port80 --typeNodePort kubectl get service nginx # 记下NODE_PORT如31234 curl http://192.168.56.11:31234 # 从宿主机访问Worker节点IPNodePort如果失败检查kubectl get endpoints nginx是否返回Pod IP若为空说明Endpoints控制器未同步。第四层跨节点Pod通信# 在Node1上部署Pod kubectl run client --imagebusybox --restartNever -- sleep 3600 # 获取client Pod IP CLIENT_IP$(kubectl get pod client -o jsonpath{.status.podIP}) # 从Node2上的Pod ping它 kubectl exec -it $(kubectl get pod -l appnginx -o jsonpath{.items[0].metadata.name}) -- ping -c 3 $CLIENT_IP若不通90%是Flannel Backend配置问题检查kubectl get cm -n kube-system kube-flannel-cfg -o yaml中的Backend Type。第五层存储卷挂载kubectl apply -f - EOF apiVersion: v1 kind: PersistentVolume metadata: name: task-pv-volume spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce hostPath: path: /mnt/data EOF mkdir -p /mnt/data echo test /mnt/data/index.html kubectl run nginx-pv --imagenginx --restartNever --volumetask-pv-storage --mountmountPath/usr/share/nginx/html,volumeNametask-pv-volume kubectl port-forward pod/nginx-pv 8080:80 curl http://localhost:8080成功返回test证明本地存储卷工作正常。5. 常见问题与独家避坑指南那些文档里不会写的血泪经验5.1 证书过期的应急修复无需重装集群kubeadm生成的证书默认1年有效期到期后kubectl get nodes会报x509: certificate has expired or is not yet valid。修复步骤备份原证书sudo cp -r /etc/kubernetes/pki /etc/kubernetes/pki-backup更新证书sudo kubeadm certs renew all sudo systemctl restart kubelet更新admin.confsudo kubeadm init phase upload-certs --upload-certs sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config注意kubeadm certs renew不会更新etcd证书需额外执行sudo kubeadm init phase certs etcd-server --cert-dir /etc/kubernetes/pki/etcd否则etcd集群会因证书过期拒绝连接。5.2 “cni plugin not initialized”错误的三种根因当kubectl get nodes显示NotReady且kubectl describe node出现此错误按优先级排查根因检查命令解决方案Flannel DaemonSet未调度kubectl get ds -n kube-system kube-flannel检查Node标签kubectl label nodes k8s-node1 beta.kubernetes.io/oslinuxCNI配置文件缺失ls /etc/cni/net.d/应存在10-flannel.conflist否则重新apply flannel ymlkubelet未加载CNIps auxgrep kubelet | grep cni5.3 VirtualBox下网络不通的终极解法在宿主机Windows上VirtualBox Host-Only网络常出现“能ping通但无法curl”的诡异现象。根本原因是Windows防火墙拦截了TCP连接。解决方案在VirtualBox里Host-Only网络设置中取消勾选“允许来自主机的访问”在Windows防火墙高级设置中新建入站规则规则类型端口协议TCP端口6443,30000-32767NodePort范围配置文件域、专用、公用全选操作允许连接在Ubuntu虚拟机内执行sudo ufw allow from 192.168.56.1 to any port 6443 sudo ufw allow 30000:32767/tcp5.4 资源不足时的降级方案当虚拟机内存4GB时etcd频繁OOM。此时启用etcd压缩可提升稳定性# 编辑etcd清单 sudo vi /etc/kubernetes/manifets/etcd.yaml # 在command部分添加 - --quota-backend-bytes1073741824 # 1GB配额 - --auto-compaction-retention1 # 每1小时自动压缩然后sudo systemctl restart kubelet。实测在2GB内存节点上etcd内存占用从1.8GB降至600MB。5.5 生产环境必须做的五项加固完成搭建只是起点以下是某金融客户集群上线前的必做清单禁用anonymous用户kubectl delete clusterrolebinding kubeadm:getter防止未授权用户获取集群信息。限制kubelet API编辑/var/lib/kubelet/config.yaml添加readOnlyPort: 0 # 关闭只读端口 authorization: mode: Webhook启用审计日志在/etc/kubernetes/manifests/kube-apiserver.yaml中添加- --audit-log-path/var/log/kubernetes/audit.log - --audit-policy-file/etc/kubernetes/audit-policy.yaml配置Pod安全策略创建restricted-psp.yaml禁止privileged容器然后绑定到default serviceaccount。定期证书轮换设置cronjob每月执行0 2 1 * * /usr/bin/kubeadm certs renew all /bin/systemctl restart kubelet这些操作看似繁琐但某次真实事件中正是第1条阻止了恶意Pod通过anonymous身份窃取Secret。安全不是功能而是底线。6. 后续演进路径从单机实验到生产可用的三步跃迁完成Ubuntu 20.04虚拟机集群只是起点。根据某云厂商客户实践后续演进有清晰的三阶段路径6.1 第一阶段增强可观测性1-2天在现有集群上部署Prometheus Operatorhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm install prometheus prometheus-community/kube-prometheus-stack \ --namespace monitoring --create-namespace \ --set grafana.adminPasswordadmin关键改造点修改values.yaml中prometheus.prometheusSpec.retention: 30d为etcd单独配置ServiceMonitor暴露etcd_server_is_leader指标在Grafana中导入ID为3321的K8S集群仪表盘此时你能看到每个Node的kubelet内存使用率、etcd leader变更次数、API Server 5xx错误率。当这些指标异常时比kubectl get nodes更早发现问题。6.2 第二阶段引入GitOps工作流3-5天用Argo CD替代手动kubectl applykubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml kubectl patch svc argocd-server -n argocd -p {spec: {type: NodePort}}然后将所有应用YAML放入Git仓库通过Argo CD UI声明式同步。好处是每次部署都有Git提交记录回滚只需git revert彻底告别“谁在哪个时间点了哪个命令”的混沌状态。6.3 第三阶段构建多租户安全边界1周为不同团队创建独立命名空间并实施RBAC# 创建team-a命名空间 kubectl create namespace team-a # 绑定ResourceQuota限制资源 kubectl apply -f - EOF apiVersion: v1 kind: ResourceQuota metadata: name: team-a-quota namespace: team-a spec: hard: requests.cpu: 2 requests.memory: 4Gi limits.cpu: 4 limits.memory: 8Gi EOF # 创建team-a-admin角色 kubectl create role team-a-admin --verb* --resource* --namespaceteam-a kubectl create rolebinding team-a-admin --roleteam-a-admin --userteam-a-dev --namespaceteam-a此时team-a成员只能在自己命名空间操作且资源消耗被硬性限制避免一个团队的Pod挤占整个集群。这条路走下来你会发现最初在VirtualBox里敲的kubeadm init命令早已不是玩具而是支撑真实业务的数字基座。就像当年学骑自行车辅助轮拆掉那一刻的忐忑和三年后载着家人穿越城市时的笃定本质是同一套肌肉记忆——只是环境从客厅地板变成了真实世界的柏油路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑