做Kubernetes运维这几年,我最大的感受是:网上教程一大堆,但大部分都停留在“能用”的层面,离“敢上生产”还有很长一段距离。很多人照着文档把集群搭起来了,Pod也跑起来了,结果遇到节点重启、证书过期、镜像拉不下来、网络插件和云厂商冲突这些事,照样抓瞎。
这篇文章就围绕Kubernetes集群从安装到生产部署这条线,把我在实际运维中趟过的坑、验证过的方案、以及那些文档里不会写明白的判断逻辑,一次说清楚。适合刚把K8s列入学习计划的新手,也适合集群已经跑起来但总觉得不踏实的运维同学。文章里的操作步骤、参数配置都是我在真实环境里验证过的,你可以直接照着抄,但更重要的是理解每一步背后的为什么。
1. 安装前先想清楚:集群规划与版本选型
1.1 硬件配置怎么定,别等上了生产再后悔
很多人在安装K8s之前,第一个问题就是“需要几台机器”。这个真没有标准答案,但有一个基本的判断逻辑:看你的业务形态,再看你的高可用要求。
如果只是学习或者跑测试环境,三台2核4G的虚拟机就够用了,一台做控制平面,两台做工作节点。但如果是生产环境,我的建议是至少五台起步:三台控制平面节点做高可用,两台或以上的工作节点跑业务负载。控制平面节点规格不用太高,4核8G在大多数场景下都够用,因为真正消耗资源的是业务Pod和etcd;工作节点就要根据业务实际需求来算了,CPU、内存、磁盘都得预留至少30%的余量。这里的余量不是拍脑袋定的,你总得给系统组件、监控采集、日志收集留出运行空间,也要给突发流量和滚动更新时的资源峰值留足冗余。
磁盘这块,很多初次部署的人不重视,我见过太多集群因为etcd所在磁盘性能太差,导致整个APIServer响应变慢。etcd对磁盘延迟非常敏感,生产环境务必要用SSD,哪怕是云主机也要选择高性能云盘。工作节点上的容器运行目录,也就是docker或containerd的数据目录,同样建议独立挂载一块数据盘,避免和系统盘抢占I/O资源,也避免系统盘被写满后整个节点不可用。
1.2 版本选型与容器运行时的选择
版本选择这件事,原则是“选新不选旧,但别选最新”。Kubernetes的版本迭代速度很快,每年三个大版本,每个版本支持周期只有14个月左右。选版本的时候,第一看你的业务组件对K8s API的兼容性,第二看社区的主流使用情况,第三看云厂商托管的版本。
我个人的建议是,在当前时间点,选择社区已经发布半年以上、且还处于支持周期内的版本,比如1.28、1.29、1.30这些系列。太新的版本(比如刚发布不到一个月的)尽量不要在生产环境用,因为一些罕见的Bug和兼容性问题通常要等一两个补丁版本才会暴露出来。
容器运行时方面,Docker在1.24版本之后就被移除了对dockershim的支持,所以现在新部署集群直接用containerd是主流做法。很多人习惯用Docker,但containerd本身就是一个完整的容器运行时,而且更轻量,crictl命令虽然不像docker命令那么熟悉,但用法很接近。kubeadm工具在初始化集群的时候会默认去检测containerd的运行状态,如果你机器上装了Docker但也装了containerd,记得提前确认好kubelet用的是哪个运行时接口。
1.3 网络方案选型:Calico还是Flannel
网络插件是集群安装完之后的第一个关键决策点。Flannel和Calico是两种最常见的CNI插件,但它们的定位完全不同。
Flannel的特点是简单、轻量,只提供三层网络互通,也就是给每个Pod分配一个集群内IP,让Pod之间能互相访问。但它没有网络策略能力,也就是说你没法在集群层面做精细的访问控制。
Calico的功能就丰富多了,除了基本的Pod网络连通,它还实现了完整的NetworkPolicy,可以按命名空间、标签、IP段来做流量管控。如果你的集群要承载多租户业务,或者有等保合规需求,Calico基本是必选。代价是Calico的组件更多、运维复杂度更高,比如它依赖BGP(在某些数据封装模式下不依赖,但默认的BGP模式确实需要)来分发路由信息。
我自己的实践是:测试环境用Flannel足够了,生产环境直接上Calico。如果你用的是云主机,还要注意云厂商的安全组策略可能会拦截Calico的BGP端口(通常是179),这个坑在后面的疑难解答部分会细说。
2. 用kubeadm从零搭建一套可用集群
2.1 环境准备与组件安装细节
这部分我先把基础环境的要求列出来,这些前置条件不满足,后面会有各种莫名其妙的问题。
操作系统我推荐Ubuntu 22.04 LTS或CentOS 7.9/Stream 8(如果你还在用CentOS系的话)。内核版本至少在4.18以上,建议5.x。主机名要规范,最好用简短有意义的名称,比如k8s-master01、k8s-node01,因为主机名会写入节点的标签信息,之后排查问题的时候,一个清晰的主机名能省不少时间。
所有节点都需要做以下配置:
关闭swap。K8s的kubelet默认要求禁用swap,因为如果开启了swap,内存回收机制会和容器的内存限制产生冲突,导致Pod的内存QoS失效。永久关闭方法是修改/etc/fstab,把swap那一行注释掉,然后执行swapoff -a。
加载内核模块和调整系统参数。需要开启overlay和br_netfilter模块,否则容器网络通信会有问题。系统参数方面,net.bridge.bridge-nf-call-iptables、net.ipv4.ip_forward这几个是必须打开的,这些参数保证iptables规则能正确作用于桥接流量,也保证Pod的进出流量能正常转发。
配置时间同步。生产环境强烈建议部署chrony或systemd-timesyncd,因为Kubernetes的证书机制强依赖各节点时间的同步,节点间时间偏差超过5分钟,kubelet和APIServer之间的通信就会出现证书校验失败,表现出来就是节点反复NotReady。
组件安装很简单,三件套:kubeadm、kubelet、kubectl。用apt或yum安装的时候,需要先配置K8s的软件源,然后指定安装版本。这里有个小技巧,安装的时候把三者版本锁定,比如都用1.28.2,避免自动升级导致版本不一致。
2.2 初始化控制平面与工作节点加入
控制平面节点执行初始化之前,先准备好两个东西:一个是apiserver-advertise-address,也就是控制平面节点的内网IP;另一个是pod-network-cidr,这里是Pod的网段,要提前规划好,和你的VPC网段、Service网段都不能冲突。
初始化命令我习惯写成这样:
kubeadm init \ --apiserver-advertise-address=192.168.10.10 \ --image-repository=registry.aliyuncs.com/google_containers \ --kubernetes-version=v1.28.2 \ --service-cidr=10.96.0.0/12 \ --pod-network-cidr=10.244.0.0/16--image-repository参数很关键。默认情况下kubeadm会从registry.k8s.io拉取镜像,这个地址在海外的CDN节点上,国内网络环境经常拉不动。换成阿里云的镜像仓库后,拉取速度会快很多。
初始化成功后会输出一段join命令,里面带着token和证书hash,先复制保存好,这个在节点加入集群的时候要用。然后按照输出提示,把kubeconfig文件复制到当前用户的.kube目录下,这样才能用kubectl操作集群。
工作节点加入集群很简单,就是把kubeadm init输出的kubeadm join命令在每台工作节点上执行一遍。如果token过期了,可以在控制平面节点上执行kubeadm token create --print-join-command重新生成。
集群所有节点都Join进来之后,统一通过kubectl get nodes来确认节点状态。这里要注意一个现象:节点状态是NotReady是正常的,因为此时还没安装网络插件,Pod网络是不通的。安装完CNI插件之后,等一两分钟节点就会变成Ready。
2.3 安装Calico网络插件
Calico的安装其实是应用一堆YAML文件,官方推荐的方式是从Calico的GitHub仓库下载manifests文件,然后修改里面的Pod网段和你的初始化参数保持一致,再kubectl apply -f应用。
比如我上面规划的是10.244.0.0/16,那么在calico.yaml里找到CALICO_IPV4POOL_CIDR变量的位置,改成这个网段就行。如果你用的Pod网段和Calico的默认值不同,这个不改的话,后面Pod会一直拿不到IP。
安装完之后,通过kubectl get pods -n kube-system查看calico相关Pod都是Running状态,再看节点状态应该就是Ready了。
3. 核心概念与工作负载类型的生产解读
3.1 Pod、Deployment、Service的关系
这部分我给一个核心概念区分,方便新手快速建立认知框架。
Pod是Kubernetes调度的最小单元。一个Pod里可以有一个或多个容器,它们共享网络命名空间、共享存储卷。很多人以为Pod等同于容器,这个理解不准确。最典型的例子就是日志收集的Sidecar容器模式:业务容器负责处理业务逻辑,日志采集容器(如filebeat、fluentd)和它共享同一个Pod的存储卷,这样业务容器只需把日志写到本地文件,Sidecar容器就能把日志转发出去。这种“主容器+辅助容器”的搭配正是Pod这个抽象层存在的意义。
Deployment是用来管理无状态应用的控制器。你只需要声明“我要跑3个副本、每个副本用这个镜像、端口是8080”,Deployment就会帮你创建ReplicaSet,再由ReplicaSet创建出3个Pod。它还负责滚动更新:升级镜像版本的时候,新版本Pod一个一个起,旧版本Pod一个一个杀,整个过程对用户无感知。如果新的Pod启动失败,Deployment会自动回滚到上一个版本。
Service则是一个稳定的访问入口。Pod的IP是不固定的,今天这个Pod挂了,Deployment重新调度的Pod IP就变了。如果你直接通过Pod IP去访问业务,那必然要出问题。Service做的事情就是给一组Pod提供一个固定的虚拟IP(ClusterIP),并在它内部维护一个Endpoint列表,新Pod上线、旧Pod下线,Service会自动更新这个列表,对外部来说始终是同一个可访问地址。
3.2 配置管理:ConfigMap和Secret
ConfigMap就是用来解耦配置和镜像的。最常见的场景是:同一个镜像,在不同的环境(开发、测试、生产)里连的数据库地址不一样,数据库账号密码不一样。如果不引入ConfigMap,你就得为每个环境打一个镜像,这显然很低效。
ConfigMap可以挂载成环境变量,也可以挂载成文件。我个人的建议是:如果配置内容是结构化的(比如application.yml),尽量用文件挂载的方式;如果是简单键值对,用环境变量更省事。这里有个细节要注意,ConfigMap挂载成文件之后,如果你更新了ConfigMap,已经存在的Pod里的文件内容不会自动更新,除非Pod重启。一些开源项目(如nginx-ingress-controller)会监听ConfigMap变化并自动reload配置,但你自己写的业务应用不一定有这个能力。
Secret的结构和ConfigMap类似,但它保存的是敏感数据(密码、证书、Token),存储时做了Base64编码。要注意,Base64并不是加密,只是编码,任何能访问集群的人都可以直接解码Secret。所以生产环境建议开启etcd加密存储,或者用更专业的密钥管理方案,比如Sealed Secrets这类把Secret加密后再存进Git里的工具。
3.3 存储抽象:PV和PVC
有状态应用(数据库、消息队列)要持久化数据,这就涉及到存储。Kubernetes里存储的抽象方式很绕,但理解了之后会发现它是很优雅的。
PV(PersistentVolume)是集群里的存储资源,相当于一个存储池,它可以是NFS、Ceph的存储卷、云厂商的云盘或者本地磁盘。PVC(PersistentVolumeClaim)是用户对存储资源的申请,相当于你要从存储池里申请一块空间。Pod通过PVC来挂载存储,而不需要关心具体的存储后端是什么。
写PV的yaml时,需要指定容量、访问模式(ReadWriteOnce、ReadOnlyMany、ReadWriteMany)、存储类和回收策略。生产环境推荐用动态存储供给的方式,也就是StorageClass,这样就不用手动创建PV了。每次创建PVC,存储插件会自动从后端分配一块卷,做完用完释放。云厂商一般都有专门的CSI插件接入它们的云盘服务,比如阿里云有alicloud-disk,腾讯云有cbs-csi。
我自己踩过的一个存储坑是:云盘的访问模式有严格的地域限制,比如一块云盘只能被同一可用区的节点挂载。如果两个Pod副本被调度到了不同可用区的节点上,它们就无法共享同一块云盘。所以在设计有状态应用或多副本读写的存储方案时,一定要把可用区这个因素考虑进去。
4. 从“能跑”到“敢上线”:生产部署的硬门槛
4.1 资源配额、健康检查与调度约束
很多新手部署应用,yaml里只写了镜像名和副本数,这种部署方式在测试环境没问题,但离生产要求差得很远。生产部署的第一道门槛就是必须写清楚资源requests和limits。
requests是调度器做决策时看的,它告诉调度器这个Pod至少需要多少资源;limits是运行时限制,超过这个值的CPU会被限流,内存超了就会触发OOM Kill。不写requests的后果很严重:调度器会认为这个Pod对资源“没要求”,结果就是一堆Pod全被塞到一个节点上,资源超卖严重的时候,整台节点都可能被打挂。不写limits的后果是:某些应用出现内存泄漏时,它会无限吃内存,直到把节点内存耗尽。
这里我给一个经验值:业务应用的内存limits一般设为requests的1.5到2倍,但要结合实际业务来判断,不能套公式。Java应用因为JVM自带堆内存管理,内存limits要格外注意,设置的比堆内存小会导致容器频繁被杀。
健康检查是第二道门槛。readinessProbe(就绪探针)决定这个Pod要不要被放进Service的负载均衡池里,livenessProbe(存活探针)决定容器要不要被重启。接口健康检查的URL、端口、初始延迟时间initialDelaySeconds这些参数都需要根据你的应用启动耗时来调整。启动慢的应用,initialDelaySeconds一定要给足,否则应用还没起来就被livenessProbe判定失败然后反复重启,陷入CrashLoopBackOff的恶性循环。
调度约束这块,最常用的是nodeSelector和节点亲和性。比如你的业务对磁盘性能敏感,就给SSD节点打上标签,然后用nodeSelector指定业务Pod只能调度到这些节点上。还可以用PodAntiAffinity把同一应用的多个副本分散到不同节点,这样能避免一台节点挂了所有副本都没了。
4.2 接入层Ingress与证书自动化
Service的ClusterIP只能在集群内部访问,集群外的流量进来需要一个入口,这就是Ingress做的事。Ingress本身是一个反向代理,最常见的实现是nginx-ingress-controller和traefik。
Ingress配置里最关键的是域名、访问路径和后端Service的对应关系。生产环境还会涉及到HTTPS证书的管理。人工更新证书是最容易出错的事,证书忘了续期导致线上访问报安全错误,这种事故大家应该都遇到过。
推荐的做法是部署cert-manager来自动化证书管理。cert-manager配合Let's Encrypt,每90天自动续期一次证书,整个过程不需要人工干预。它的工作原理是:创建Certificate资源,cert-manager会去申请证书,并在到期前自动续期,然后把证书保存到同名的Secret里,Ingress引用这个Secret即可。
这里有一个坑要提醒:cert-manager在验证域名所有权时会发起HTTP-01或DNS-01挑战,HTTP-01挑战需要你的Ingress能从公网访问到相应的验证路径,DNS-01挑战则需要在域名服务商那里配置API Token。如果域名解析在有NS记录的托管服务商那里,建议直接走DNS-01,兼容性更好,也不依赖网络的入站状态。
4.3 监控告警与日志收集体系设计
集群上线了,业务跑起来了,接下来最重要的就是监控和日志。没有监控的集群就是盲人开车,宕机了都不知道为什么。
监控这块现在的主流方案是Prometheus + Grafana。Prometheus采集集群指标(节点CPU、内存、网络、Pod资源使用情况),Grafana做可视化展示。kube-prometheus-stack这个Helm Chart会把Prometheus、Alertmanager、Grafana、各种Exporter一起打包部署,适合快速落地。容器健康监控、节点资源监控、K8s组件监控在这个方案里都能覆盖。
告警规则是很多人容易忽略的地方。建议至少配置下面几条基础告警:
- 节点状态为NotReady持续超过1分钟;
- 节点内存使用率超过85%持续5分钟;
- Pod反复重启(CrashLoopBackOff持续10分钟);
- 集群中大量Pod处于Pending状态;
- etcd的leader切换次数异常。
告警阈值要基于实际集群规模调整,小集群和大集群的资源水位本来就不同,阈值设得太敏感会产生告警疲劳,团队慢慢就不看告警了,最后真正的故障也没人响应。
日志收集的生产方案基本上就是EFK或者Loki。EFK是Elasticsearch + Fluentd + Kibana,功能强大,但是Elasticsearch本身就很吃资源。Loki是Grafana出的解决方案,日志不建立索引,只做压缩存储,查询时再用标签过滤,资源消耗小得多。如果你的业务日志量不是特别大(每天几个GB到几十个GB),我推荐用Loki,运维成本低很多。
日志收集有个细节也不能疏忽:一定要做日志轮转和生命周期管理。日志保留天数是按业务需求定的,超过保留期的日志要自动清理或归档到冷存储,否则磁盘空间被日志占满,节点会陷入异常状态。
4.4 用Dashboard发布一个全新服务的实操演示
很多运维同学习惯了可视化界面操作,这里就演示一下通过Kubernetes Dashboard把一个新的Deployment部署上线。Dashboard本身可以通过kubectl proxy访问,生产环境建议用Ingress暴露并通过OIDC或RBAC做权限验证,不要直接暴露到公网。
假设我这边要发布一个基于nginx的web服务,在Dashboard里的操作流程是:
进入Dashboard的“工作负载”页面,点击“创建”按钮,可以切换到一个简单的表单模式。表单模式里我要填写应用名(web-demo)、镜像地址(nginx:1.25)、副本数量(3个副本)。这里还要把高级选项里的资源配额(limits和requests)填上,服务类型选择ClusterIP,端口映射里填上容器端口80。
提交之后,Dashboard会自动帮你生成并应用Deployment和Service这两个资源。切换到“服务”页面能看到这个ClusterIP服务创建成功,点进去能看到它关联的3个Pod都在Running状态。
如果是用YAML方式,对应的Deployment大概长这样:
apiVersion: apps/v1 kind: Deployment metadata: name: web-demo namespace: default spec: replicas: 3 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "200m"apiVersion: v1 kind: Service metadata: name: web-demo namespace: default spec: selector: app: web-demo ports: - port: 80 targetPort: 80最后还需要在Ingress里增加一条路由规则,把web.example.com这个域名指向web-demo这个Service。这样一个新服务就完整发布上线了。初始的时候如果只做验证,也可以先加一个临时隧道的svc类型(NodePort),从节点IP加端口直接访问,确认Pod起来了再切Ingress。
5. 踩过的坑与排查思路
5.1 镜像拉取慢或完全拉不动
这是新集群最普遍的问题。默认情况下kubelet从Docker Hub拉取镜像,而Docker Hub在国内网络环境下时快时慢,严重的时候直接超时。解决办法是给容器运行时配置镜像加速器。
containerd的镜像加速配置在/etc/containerd/config.toml的[plugins."io.containerd.grpc.v1.cri".registry.mirrors]字段里。编辑好之后要重启containerd服务。这块有一个容易踩的坑:修改config.toml之后,如果不重启containerd,配置不生效;但重启containerd会导致当前节点上所有存量容器被杀掉,所以生产环境最好找维护窗口操作,或者分批滚动操作。
业务镜像建议多使用云厂商的容器镜像服务,比如把镜像推送到阿里云ACR、腾讯云TCR,然后集群内从镜像仓库的内网地址拉取。这样既快又稳定,还能配合镜像仓库的免密配置做权限管理。
5.2 Pod一直Pending,卡在ContainerCreating
Pod卡在Pending,首先看这个事件:
kubectl describe pod <pod-name> -n <namespace>事件信息会明确告诉你原因。最常见的几类:
没有满足条件的节点:调度器找不到能容纳Pod的资源,通常是cpu或内存的requests超过了所有可用节点的可分配资源。解决办法是扩容节点、清理无用Pod,或者调低资源的requests配置。
镜像拉取失败(ImagePullBackOff):原因多半是镜像地址写错了、镜像不存在,或者镜像拉取的密钥没配。执行kubectl describe pod看事件里有没有401/403的提示,就能判断是不是认证问题。
PVC没有绑定到PV:就是存储依赖不满足调度的前提条件,导致Pod一直处于ContainerCreating状态。看事件里有没有waiting for PVC to be bound的相关提示,如果有,去排查PVC和PV的状态。
Pod一直ContainerCreating,还经常和网络插件的状态有关。Calico相关Pod如果没起来,新建Pod的沙箱容器一直无法创建成功,就会卡在ContainerCreating。先看kube-system里的Calico是否Running,再查节点上是否有没清理干净的残留路由规则。
5.3 kubelet证书过期
Kubernetes各组件之间的通信全靠证书,kubelet的证书默认有效期是一年。集群跑久了,一年之后会突然出现kubectl get nodes能看到节点,但kubectl logs、kubectl exec都失败的异常情况。原因就是kubelet的服务端证书过期了。
如果集群的证书由kubeadm管理,解决方式是在控制平面节点执行:
kubeadm certs renew all systemctl restart kubelet然后更新用户的kubeconfig文件。要注意的是,kubeadm certs renew all只会续期证书,不会重新生成CA证书,所以整个流程不用重新初始化集群,不影响已有的业务Pod。运维上强烈建议给证书过期加一个监控项,提前30天告警。
5.4 Calico BGP模式下的云主机安全组拦截问题
Calico默认的IPIP模式需要节点间通过BGP协议交换路由信息,BGP使用的TCP端口是179。在云主机上,云平台的安全组默认不会放通这个端口,导致各节点之间无法同步路由,表现为集群内跨节点的Pod网络不通。
这种问题的排查思路很清晰:先通过calicoctl node status查看BGP peer的状态,确认是不是Established;如果不是,就检查云平台安全组有没有放通179端口,以及节点防火墙(ufw、firewalld)是否拦截。解决办法很简单,在安全组里放通TCP 179端口即可。如果实在不想动安全组,另一个办法是把Calico切换成VXLAN模式,这种封装方式只需要UDP 4789端口,通常云平台默认放通。
写在最后的一些经验
这套集群部署和运维的流程,我前后在好几套环境里验证过,测试环境、生产环境、私有化环境都有。最大的感受是:K8s这套系统,安装只是开始,真正的运维挑战在后面。
像镜像加速配置、证书过期、资源配额阈值、网络插件与云平台安全组的兼容性,这些问题都不是装完集群就能发现的,而是在业务运行一段时间后才慢慢暴露。所以我也越来越认同一个观点:做K8s运维,最重要的能力不是会敲几个命令,而是能理解这套系统的设计逻辑,知道它每一个抽象层要解决什么问题,出了问题能顺着原理去排查,而不是靠瞎试。
最后再分享一个小技巧:我每次搭建环境的时候,都会把关键操作(初始化命令、join命令、网络配置、证书续期命令)整理成一个Cheat Sheet贴在笔记里,同时配合定时巡检脚本,把节点状态、证书过期时间、磁盘水位这些指标定期扫一遍。这样集群即使出问题,也能通过巡检记录快速定位变更时间和异常节点,比事后慢慢翻日志高效得多。