首页 / 资讯中心 / 文章详情

kubeadm实战:从零搭建Kubernetes多节点集群并跑通Nginx

kubeadm实战:从零搭建Kubernetes多节点集群并跑通Nginx ★ FEATURED ARTICLE
上一篇刚把Pod调度策略讲完这篇直接进入Kubernetes集群部署的完整实战。很多朋友手上有《深入理解Kubernetes源码》但我的建议很直接源码可以慢慢啃集群先给我跑起来。你连一个多节点集群都没有读调度器源码就像没摸过方向盘先看发动机原理效果极差。这篇是Kubernetes基础系列的第四篇核心只做一件事从几台裸机或虚机开始用kubeadm搭出一个可用的Kubernetes集群并且让Nginx真实跑起来。适合已经玩过minikube、正准备搭多节点环境或者被各种安装教程坑过几次的人。这篇文章我尽量保持“给同事讲方案”的口吻不会只丢命令会把每个关键参数的来历和选择理由说清楚。整条链路包括环境准备、控制平面初始化、工作节点加入、CNI网络选型、业务应用部署和日常排障。看完之后你应该能在半天内独立复现一套集群并且以后遇到节点NotReady、镜像拉不下来这类问题知道先查哪里、再查哪里而不是漫无目的地百度。1. 部署方式选型为什么要以kubeadm为主线1.1 四种常见部署方式的对比与选择Kubernetes集群的部署方式五花八门新手很容易被绕晕。我简单归纳成四类各有各的适用场景二进制部署从GitHub下载kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy等二进制文件手动生成证书、配置systemd服务。这种方式最大的优点是“看得见每一块组件”出了问题你知道是哪个服务挂了排障思路特别清晰。但它的缺点也极其明显部署时间极长几百个步骤不能错一步而且证书体系复杂手动维护非常痛苦。我不推荐新手用这种方式作为学习路径光生成CA证书和各个组件的kubeconfig就得花掉一整天。kubeadm部署这是CNCF官方推荐的引导工具。它把二进制部署里最繁琐的部分——证书生成、kubeconfig配置、控制平面组件静态Pod编排——全部自动化了。你只需要初始化控制平面拿token让工作节点加入再装一个CNI插件集群就活了。我在这篇里主推的就是这种方式也是目前社区里使用最广的部署方式。它足够透明kubeadm生成的每个配置文件都在/etc/kubernetes目录下摆着你随时可以查看。发行版打包部署比如RKE2、K3s、MicroK8s这类。K3s尤其适合边缘计算和资源受限的环境把控制平面组件压缩进单个进程安装一条命令就完成。这类工具的优点是快缺点是抽象层太厚很多底层细节被掩盖了。如果你刚开始学Kubernetes用K3s搭集群很容易“知其然不知其所以然”。云托管服务比如各大云厂商的托管Kubernetes服务你只需要创建集群控制平面由平台帮你维护。这是生产环境最省心的选择但完全屏蔽了集群创建过程。作为学习者你不能只依赖它因为一旦集群出问题你根本不知道控制平面组件之间是怎么协作的。1.2 kubeadm适合什么场景不适合什么场景kubeadm对自建集群来说是个“甜点级”工具。它默认把控制平面组件以静态Pod的方式托管给kubelet这意味着apiserver、etcd这些组件在集群里是以容器形式运行的。这带来一个好处以后控制平面组件升级本质上是更换镜像版本配合kubeadm upgrade命令就能一键完成。它的适用场景非常明确中小规模的自建集群、离线环境、混合部署环境以及所有想深入理解Kubernetes工作原理的学习场景。如果你需要高可用kubeadm也支持只要在init时指定control-plane-endpoint指向一个负载均衡器再在另外两台机器上执行kubeadm join --control-plane即可这是生产环境比较推荐的组合。不适合的场景也有超大规模集群千节点以上建议走二进制或云托管因为网络和etcd性能需要非常细致的调优对非root权限环境、不可变基础设施要求高的场景kubeadm的默认配置可能需要大量改写。另外如果你完全没有Linux基础也别急着上kubeadm先把systemd、iptables、磁盘分区这些基本功补一补。2. 动手前的主机规划与系统初始化2.1 节点分工与网络CIDR设计部署Kubernetes之前先花十分钟规划网络这个环节省掉后面全是大坑。以三节点集群为例标准分工是一台控制平面节点master两台工作节点worker。生产环境建议控制平面至少三台做高可用但对于学习环境一台控制平面加两台工作节点完全够用。网络规划核心是三段地址不能重叠物理机网段、Pod网段、Service网段。物理机网段是你SSH连机器用的假设是192.168.1.0/24。Pod网段是集群内部给每个Pod分配的IP地址池kubeadm默认不指定的话是10.244.0.0/16或10.96.0.0/12视CNI组件而定。Service网段是给Service虚拟IP用的默认是10.96.0.0/12。这两段地址必须和物理机网段、以及你公司内部其他网络完全隔离否则路由器会把去往Pod的流量当成普通内网流量转发造成诡异的路由冲突。Pod网段的CIDR大小直接影响集群规模。10.244.0.0/16这个地址段一共包含65536个IP假设每个节点默认最多跑110个Podkubelet的maxPods默认值理论支撑500多个节点。如果你的规划超500节点建议把Pod网段放大到/14甚至/12同时把每节点Pod数上限下调避免单节点资源争抢。Service网段设计要留余量因为Service和Pod不同Service本身数量有限/12完全够用。2.2 系统初始化五件事内核模块、swap、防火墙、主机名、IP在安装kubeadm之前每个节点必须完成五项基础配置我按顺序说明第一加载内核模块。Kubernetes依赖iptables和IPVS做流量转发需要确保br_netfilter、overlay等模块已经加载。建议在/etc/modules-load.d/k8s.conf里写入这两个模块然后systemctl restart systemd-modules-load生效再用lsmod | grep br_netfilter确认。第二关闭swap。这是新手最容易忽略的一步。kubelet默认要求swap必须为0否则节点加入后kubelet会报错。虽然新版K8s支持Swap特性门控但生产环境没人用它。执行swapoff -a只是临时关闭必须把/etc/fstab里swap那行注释掉否则重启后swap恢复集群又挂。这一步不做后续节点状态会反复NotReady。第三调整内核参数。重点设置net.bridge.bridge-nf-call-iptables1这个参数保证经过Linux bridge的流量能被iptables规则处理是集群网络正常工作的前提。直接写进/etc/sysctl.d/k8s.conf并sysctl --system加载。第四hosts和主机名。每一台节点要有唯一的主机名不能有重复。把集群所有节点的IP和主机名映射写进/etc/hosts避免组件之间靠DNS解析互相找不到。第五防火墙和SELinux。协议层面Kubernetes需要放行6443apiserver、2379/2380etcd、10250kubelet、以及Service NodePort的30000-32767端口段。很多人在集群外访问不到Pod就是防火墙把这几个端口拦了。SELinux在RHEL系系统上建议先设为permissiveKubernetes官方文档对SELinux的支持说明里permissive是最稳妥的。2.3 容器运行时选型与cgroup驱动对齐容器运行时的选择直接影响kubelet能否正常监控容器资源。早期大家习惯用Docker但Kubernetes从1.24版本开始移除了dockershim现在主流是containerd和CRI-O。以containerd为例它会随kubeadm一起安装或单独安装。安装完成后有一件事必须做修改/etc/containerd/config.toml把SystemdCgroup设置为true同时把sandbox_image换成国内可访问的镜像地址。为什么要这么改Kubernetes的kubelet和容器运行时必须使用相同的cgroup驱动否则kubelet无法准确感知容器占用的CPU和内存。用systemd作为cgroup驱动的逻辑是systemd是pid 1由它统一管理cgroup容器运行时和kubelet都作为systemd的子进程存在这样资源统计才准确。如果你用cgroupfs驱动和systemd时不时产生“双重管理”的问题轻则日志报错重则节点资源统计错乱。我的建议非常直接所有节点、所有组件无脑统一SystemdCgrouptrue这是当前最不容易踩坑的配置。3. 用kubeadm跑通控制平面初始化3.1 安装kubeadm、kubelet、kubectl并锁定版本安装这几个组件时最大的坑是版本漂移。我见过很多人执行了yum install kubeadm kubelet kubectl以后过一段时间跑yum update结果kubelet升级而kubeadm还是旧版或者kubelet和kubernetes控制平面版本不一致整个集群翻车。正确的做法是安装时明确指定版本并且把这些软件包加入yum的版本锁定列表。以RHEL系的安装为例先配置国内可访问的软件源kubeadm官方源在部分网络环境下可能不稳定再执行yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2 systemctl enable kubelet注意这个阶段只需要enable kubelet不要start。因为kubelet还没有拿到集群配置启动起来会一直报错找不到静态Pod配置这会干扰后续初始化。等到kubeadm init完成kubelet自动被唤醒。用apt的Debian系也一样关键是三个组件的版本必须一致。我的习惯是记一个版本组合比如1.28.2就都是1.28.2小版本差异也可能导致API兼容问题。3.2 kubeadm init核心参数拆解与镜像源设置环境准备好以后在控制平面节点上执行初始化。这条命令我拆开讲解每个参数都有自己的使命kubeadm init \ --kubernetes-versionv1.28.2 \ --apiserver-advertise-address192.168.1.10 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12 \ --image-repositoryregistry.aliyuncs.com/google_containers--apiserver-advertise-address指定apiserver对外广播的地址必须是控制平面节点的内网IP。注意不要用公网IP除非你有特殊需求。--pod-network-cidr是Pod地址池这里选的10.244.0.0/16是Calico默认推荐的网段如果把Pod网段改了后面安装Calico时也要同步改。--image-repository的作用是换镜像仓库。kubeadm默认从registry.k8s.io拉取控制平面镜像这个域名在国内的网络环境下经常超时。换成国内可访问的镜像仓库后kubeadm会从对应镜像库拉取apiserver、etcd、coredns等镜像速度稳定很多。企业环境更推荐的方式是提前用kubeadm config images pull把镜像拉到本地再推到自建的Harbor私有仓库集群安装时通过--image-repository指向内网地址完全离线部署。初始化成功后会输出一段提示包括三个信息/etc/kubernetes/admin.conf的位置、kubelet已启动的确认、以及一段worker节点加入的命令。这段join命令要保存下来后面加节点要用。如果你当场忘了也可以随时用kubeadm token create --print-join-command重新生成。3.3 安装CNI插件以Calico为例kubeadm init完成后集群的网络处于“没有路”的状态——Pod之间无法通信。这一步由CNI插件负责主流选择是Calico和Flannel。我首选Calico原因有三性能好基于BGP路由而非纯overlay封装、支持NetworkPolicy、排障工具链完整。安装Calico有两种方式。老版本是直接kubectl apply它的operator或manifest文件新版本推荐用operator方式kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.0/manifests/tigera-operator.yaml kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.0/manifests/custom-resources.yaml如果之前的Pod网段不是10.244.0.0/16需要先下载custom-resources.yaml把里面的cidr字段改成你自己的网段再apply。Calico安装后可以观察calico-system命名空间下的Pod是否全部Running然后查看节点是否变为Ready。如果IPVS或iptables有历史残留配置Calico可能会起不来通常把节点重启一次就能解决。4. 工作节点加入与集群状态验证4.1 获取和重建join token工作节点加入集群靠的是token和证书哈希。kubeadm init成功后会打印完整的join命令格式类似kubeadm join 192.168.1.10:6443 --token token \ --discovery-token-ca-cert-hash sha256:hash这个token默认有效期是24小时。如果你第二天才加节点或者token过期了不必重新初始化集群。在控制平面节点上重新生成即可kubeadm token create --print-join-command这条命令非常高频值得记死。它会自动生成新的token并且把discovery-token-ca-cert-hash也打印出来直接复制到工作节点执行就行。有一种情况容易让人困惑如果你给工作节点加了--control-plane参数那就是要把这个节点变成第二个控制平面节点用于高可用而不是普通工作节点。只有需要扩展控制平面时才能用这个参数。4.2 节点加入后的验证清单从节点状态到Pod调度节点加入集群后不能只看状态Ready就完事。我习惯按一个固定清单逐项验证第一步在控制平面执行kubectl get nodes确认所有节点状态都是Ready并且角色标识正确。如果某个节点状态是NotReady大概率是CRI配置或网络插件问题先去排查kubelet日志。第二步查看节点详细信息kubectl describe node node-name重点看Conditions部分是不是都显示True以及底部的Events里有没有warning。Events里如果出现Failed to create pod sandbox基本可以判定containerd配置没改对。第三步验证Pod能否跨节点调度。在部署Nginx时设置replicas为3观察这3个Pod是不是分布到不同节点上。如果全部挤在同一个节点建议检查节点是否有污点taint没去除。默认情况下控制平面节点有污点工作节点没有所以3个副本应该分布在两台worker上。第四步验证DNS是否工作。进入一个测试Pod执行nslookup kubernetes.default.svc.cluster.local能解析出来说明CoreDNS正常。DNS是集群内部服务发现的基础这一步挂了会导致各种奇怪问题。5. 跑起第一个业务Nginx从YAML到外部访问5.1 用Deployment声明Nginx实例集群搭好接下来让业务跑起来。Deployment是Kubernetes里最常用的工作负载类型它管理一组副本Pod并且支持滚动更新和回滚。创建一个nginx-demo.yaml文件apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo namespace: default spec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80执行kubectl apply -f nginx-demo.yaml后用kubectl get pods -o wide查看。这里有个细节为什么image要用nginx:1.25-alpine而不是nginx:latest因为latest标签不具备可重复性今天拉的和明天拉的可能不是同一个版本。生产环境里镜像必须指定精确版本这是最基础的可复现性要求。另外selector.matchLabels必须和template.metadata.labels完全匹配否则Deployment创建时会报错。这个字段的设计是为了让Deployment能够找到它管理的Pod两者的关系就像“招聘要求”和“候选人简历”匹配不上就没法管理。5.2 Service的三种暴露方式怎么选Pod本身是“短命”的随时可能被重新调度IP地址会变。如果让外部流量直接访问Pod IP那等于别人给你一个随时会变的电话号码。Service就是Pod前端的固定入口它把一组Pod的流量汇聚到同一个虚拟IP上。Service的类型选择我按场景区分ClusterIP默认类型只能在集群内部访问。适合服务间互相调用比如后端API给前端提供数据。你无法从集群外直接访问ClusterIP。NodePort在ClusterIP基础上每个节点开放一个端口默认范围30000-32767流量经过节点端口转发到Service再转发到Pod。适合测试环境和小规模暴露比如开发阶段调试Nginx页面。它的劣势是每个服务都要占一个节点端口服务多了端口管理混乱。LoadBalancer云环境下专用会调用云厂商的负载均衡器把NodePort暴露到公网。自建集群通常没有这个外部组件除非你装了MetalLB这类软件负载均衡。三者关系可以理解为ClusterIP是“内部电话分机”NodePort是“外线总机”LoadBalancer是“前台秘书”。生产环境推荐用Ingress统一入口这个下面单独讲。5.3 NodePort端口段与Ingress的关系以Nginx为例如果用NodePort暴露服务yaml是这样的apiVersion: v1 kind: Service metadata: name: nginx-demo-svc spec: type: NodePort selector: app: nginx-demo ports: - port: 80 targetPort: 80 nodePort: 30080端口字段有讲究port是Service对外暴露的端口targetPort是容器监听的端口nodePort是每个节点上开给外部访问的端口。nodePort如果不写系统会在30000-32767里随机分配。显式指定30080的好处是可预期、方便防火墙放行。这里必须提醒NodePort的端口范围由apiserver的--service-node-port-range参数控制如果想改范围需要在kubeadm init或apiserver配置里预先设置集群初始化以后再改非常麻烦通常要改静态Pod参数并重启apiserver。那Ingress和NodePort是什么关系Ingress本质上是集群内部的七层负载均衡器它通过一个统一的入口通常也是NodePort或LoadBalancer暴露的入口接收HTTP流量再根据域名和路径路由到不同Service。比如nginx.example.com路由到Nginx服务api.example.com路由到API服务只需要一个入口IP。这就是生产环境服务多了以后NodePort不够用的终极解法。部署Ingress Controller比如nginx-ingress或traefik的时候它自身会暴露一个NodePort端口后续所有服务都通过路径或域名转发不需要每个服务单独申请端口。这套架构值得专门写一篇这里先理解概念。6. 集群日常运维命令速查与证书管理6.1 kubectl高频查询命令速查集群部署完以后日常用得最多的就是下面这组命令我直接整理成速查表查询目标命令说明全部节点状态kubectl get nodes -o wide查看节点IP、内核版本、运行时版本命名空间下所有资源kubectl get all -n ns包含Pod、Service、Deployment等查看Pod日志kubectl logs pod -c container --tail100多容器时必加-c参数进入Pod交互kubectl exec -it pod -- /bin/sh注意镜像里有没有shell查看事件kubectl get events --sort-by.metadata.creationTimestamp排障时第一手线索查看资源YAMLkubectl get deploy nginx-demo -o yaml对比实际状态与期望状态有一个技巧特别实用kubectl get pod -o wide显示出的是Pod的IP和所在节点但如果要确认Pod是不是真的在预期节点上更准确的方式是kubectl get pod -o custom-columnsNODE:.spec.nodeName,POD:.metadata.name。这可以避免你对着-o wide的NODE列猜半天尤其在网络组件有特定调度需求时非常有用。再说一个很多人不知道的kubectl top node可以看到节点CPU和内存实时使用率前提是集群装了metrics-server。没装的话这条命令会报错。metrics-server是生产集群的基本组件强烈建议装一条kubectl apply就能搞定它会通过kubelet的Summary API采集资源数据不依赖第三方存储。6.2 证书续期、版本升级与备份要点Kubernetes的证书有效期问题非常经典。kubeadm默认给apiserver、etcd等组件签发的证书有效期是一年。集群跑了一年以后你会发现kubectl突然报证书过期。这就是没有提前做证书管理的后果。检查证书有效期的方法kubeadm certs check-expiration续期的标准动作kubeadm certs renew all systemctl restart kubelet这两个命令需要在控制平面节点执行并且建议先备份/etc/kubernetes目录。kubeadm certs renew all会把所有证书统一续期然后重启kubelet让组件加载新证书。注意证书续期后不需要重新初始化集群也不需要重新加入工作节点但如果你使用的是高可用控制平面每台控制平面节点都要做一次续期。版本升级方面我的经验是升级前先把文档读三遍再在测试环境完整演练一遍。kubeadm升级路径是先升级kubeadm再kubeadm upgrade plan查看可升级版本然后执行kubeadm upgrade apply 版本最后逐个节点升级kubelet。升级过程中最怕的是跨大版本升级比如1.27直接跳到1.29很多API版本不兼容遇到问题极难排查。最后强调备份。etcd是集群的“数据库”备份etcd是重中之重。最简单的方式ETCDCTL_API3 etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/server.crt \ --key/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot-$(date %F).db把这条命令加进crontab每天凌晨执行一次再结合对象存储异地保存遇到误删资源或升级翻车时能救命。7. 集群部署高频故障排查实录7.1 镜像拉不下来的合规解决方式新装集群时最常遇到的现象是Pod卡在ImagePullBackOff。拉取失败的原因通常有两类一是镜像仓库网络不通二是镜像名写错或源仓库不存在。合规且高效的解决方案有两个思路。第一个是用国内可访问的公共镜像仓库比如阿里云的容器镜像服务。kubeadm可以指定--image-repositoryregistry.aliyuncs.com/google_containers对于业务镜像可以在拉取前给镜像地址加前缀比如docker pull registry.aliyuncs.com/google_containers/nginx:1.25-alpine再通过docker tag打成不带前缀的名字。第二个方案是建立私有镜像仓库比如Harbor把需要的镜像提前pull下来再push到内网仓库所有节点从内网拉取。这个方案对企业用户最友好速度最快而且不受公网环境影响。排查时先kubectl describe pod pod看Events里ImagePullBackOff的完整报错再在有问题的节点上手动执行crictl images查看节点本地已有镜像。注意crictl是containerd的命令行工具不是docker。很多用containerd的人习惯性敲docker images发现什么都看不到其实不是没有镜像是工具用错了。7.2 节点NotReady的排查顺序kubelet、CRI与CNI节点NotReady是集群运维里出现频率最高的问题。我的排查顺序是先kubelet再CRI最后CNI。绝对不要一上来就重启节点也不要直接重装系统。第一步登录节点执行journalctl -u kubelet -f --since 10min看kubelet日志。最典型的报错是failed to initialize top level QOS containers或container runtime is not running。前者通常是cgroup驱动不一致后者是containerd没启动或配置坏了。cgroup驱动不一致的修复办法检查/etc/containerd/config.toml里SystemdCgroup是不是true以及kubelet的启动参数里cgroup-driver是不是cgroupfs两者必须一致。第二步执行crictl ps和crictl pods如果connect失败那就是CRI的问题。常见原因是containerd服务挂了或者containerd的socket路径不对。检查systemctl status containerd如果服务正常但crictl连不上大概率是把socket路径配错了。第三步如果kubelet正常、CRI也正常但节点依然NotReady看CNI有没有问题。ls /etc/cni/net.d/目录是否存在配置kubectl get pods -n kube-system看Calico相关Pod的状态。如果你之前装了Flannel又换成Calico旧配置文件没删干净CNI插件会加载混乱节点状态直接给你好看。解决方式是删除旧的CNI配置文件和残留的虚拟网卡比如flannel.1重启kubelet等Calico重新创建路由。7.3 Pod Pending和CrashLoopBackOff的定位思路Pod卡在Pending状态代表它还没被调度到任何节点上。这时候kubectl describe pod显示的Events是排障的第一手资料0/3 nodes are available: 3 node(s) had untolerated taint表示节点有污点你的Pod没有对应的容忍。最常见的是控制平面节点自带的node-role.kubernetes.io/control-plane污点业务Pod默认调度不到控制平面节点上。Insufficient cpu或Insufficient memory表示节点资源不够要么减少副本数要么给节点扩容。FailedScheduling且没有其他详细原因可能是某个节点的调度器异常检查kube-scheduler Pod状态。CrashLoopBackOff则是Pod启动后立刻退出然后又反复重启。这一步先看日志kubectl logs pod如果日志为空加--previous参数看上一次容器的日志。这个参数非常重要因为容器crash后新容器启动默认看的是新容器的日志往往是空的。加上--previous才能看到崩溃时的完整输出里面通常直接写着报错原因。这类问题九成是配置错误环境变量写错、配置文件挂载路径不对、启动命令里的占位符没替换。先把日志里的报错关键词搜索一遍再检查启动命令和入参最后才考虑是不是镜像本身有问题。我见过不少人一看到CrashLoopBackOff就重新构建镜像实际上是yaml里少了几个引号白折腾半天。最后分享一点个人体会Kubernetes集群的部署本质上是一个“解决依赖”的过程系统依赖、运行时依赖、网络依赖、调度依赖。你一步步把这些依赖理顺集群自然就起来了。我前前后后搭过不下几十次集群从最初的二进制逐组件拼装到后来熟悉kubeadm的每个参数和默认值最大的体会是千万别急着“背命令”而是要把每个操作背后的“为什么”想明白。比如为什么要关swap、为什么要让cgroup驱动统一、为什么Pod网段不能跟物理网段重叠——这些原理想通了你遇到问题就有方向而不是靠猜。kubeadm这套部署流程到现在依然是社区最稳的基线方案。照着这篇的顺序从头到尾走一遍你会对“声明式API”有非常具体的体感当你用kubectl apply一个YAML文件集群自发地把Pod调度到了预期节点网络策略生效流量正常流转那种“一切尽在掌握”的感觉是看多少文档都换不来的。后续我会再抽时间写一篇关于单节点集群的性能优化和控制平面高可用的进阶文章先把基础地基打扎实再说。
阅读完成 · 觉得有帮助?
咨询建站