1. 项目概述
前阵子帮团队把一套内部系统从单机Docker Compose迁移到K8s集群,整个过程中踩了不少坑,也沉淀了一套比较完整的实操思路。这篇博文就围绕“K8s集群搭建与Web服务部署”这个主题,从零开始梳理一套可直接复用的部署方案,适合正在学习K8s的运维、后端开发,以及准备把业务容器化上集群的团队参考。
先说清楚这套方案能解决什么问题。单机环境下跑Docker容器,最头疼的就是三件事:机器挂了服务就断了、流量上来没法横向扩展、发布新版本要停机维护。K8s集群恰恰就是围绕这三个痛点设计的。它能多台机器组成一个资源池,统一调度容器运行位置;通过副本机制保证服务不中断;支持滚动更新和回滚,发布过程对用户完全透明。本文会基于kubeadm工具搭建一套3节点集群,然后部署一个典型的Web服务Demo,把从环境准备到服务上线的完整链路走通。
本文涉及的核心技术栈包括:
- Linux系统基础操作与网络配置
- Docker容器运行时安装与配置
- kubeadm/kubelet/kubectl工具链
- Pod、Deployment、Service、Ingress等K8s核心资源对象
- 容器网络插件(以Calico为例)
如果你已经掌握Docker基本用法,但对集群编排还比较陌生,这篇文章就是为你准备的。下面直接进入正题。
2. 环境准备与架构设计
2.1 节点规划与角色分配
K8s集群最少需要2台机器(1个Master + 1个Worker),但生产环境建议至少3台,保证Master节点高可用。本次演示规划了3台虚拟机,角色分配如下:
| 节点角色 | 主机名 | 配置要求 | IP地址(示例) |
|---|---|---|---|
| Master | k8s-master | 4C8G,60G磁盘 | 192.168.1.10 |
| Worker | k8s-node1 | 4C8G,60G磁盘 | 192.168.1.11 |
| Worker | k8s-node2 | 4C8G,60G磁盘 | 192.168.1.12 |
关于配置选择,我这里多说几句。K8s本身对硬件要求不算苛刻,2C4G也能跑起来,但实际部署Web服务后,系统组件(etcd、kube-apiserver、kube-controller-manager等)会占用不少资源。实测下来,Master节点4C8G比较稳妥,Worker节点至少2C4G。磁盘方面,除了系统盘,最好给Docker单独挂一块数据盘,避免容器日志把系统盘写满。
操作系统选择了CentOS 7.9,虽然K8s官方已经逐步加大对其他发行版的支持,但CentOS 7的教程资料最丰富,踩坑时排查问题更容易。如果你用Ubuntu 22.04,命令上略有差异,但整体思路一致——网络插件、部署方式都是通用的。
2.2 版本选型:K8s与容器运行时
版本选择是整个集群搭建中最容易被忽视的环节。我见过不少新手直接装最新版,结果配套组件还没跟上,导致各种兼容性问题。这里给出一个经验性的选型建议:
- Kubernetes:1.28.x(当前稳定版,API无破坏性变更)
- kubeadm / kubelet / kubectl:与K8s版本保持一致
- Docker:24.0.x,或者使用containerd作为运行时
- Calico:3.27.x(网络插件)
- 操作系统:CentOS 7.9 / Ubuntu 22.04 LTS
这里需要特别说明一下K8s和Docker的关系。早期K8s直接调用Docker来管理容器,但后来K8s引入了CRI(Container Runtime Interface)标准,Docker由于不兼容该标准,K8s社区开发了containerd作为中间层适配。现在主流的做法是直接将containerd作为运行时,不再依赖Docker。但考虑到很多人习惯用Docker命令调试镜像,也可以在安装Docker的同时启用containerd模式,两者互不冲突。
2.3 系统初始化:三步搞定基础环境
所有节点都需要进行基础环境配置,我在脚本里整理了完整的初始化步骤:
#!/bin/bash # 1. 关闭防火墙与SELinux systemctl stop firewalld && systemctl disable firewalld setenforce 0 sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config # 2. 关闭swap(K8s要求必须关闭) swapoff -a sed -i '/ swap / s/^/#/' /etc/fstab # 3. 加载内核模块 cat > /etc/modules-load.d/k8s.conf << EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter # 4. 配置sysctl参数 cat > /etc/sysctl.d/k8s.conf << EOF net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system # 5. 配置hosts解析 cat >> /etc/hosts << EOF 192.168.1.10 k8s-master 192.168.1.11 k8s-node1 192.168.1.12 k8s-node2 EOF这几步操作很多人不理解为什么要做,我逐一解释:
- 关闭SELinux:SELinux的强制访问控制会干扰容器挂载目录的权限访问,在K8s场景下比较难配置,直接关闭最省事。
- 关闭swap:K8s调度器在计算节点资源时会参考节点可用内存,如果存在swap,调度器可能把Pod调度到内存不足的节点上,导致OOM风险。
- 加载br_netfilter:K8s的Service需要通过iptables做流量转发,这个模块负责处理桥接网络的iptables规则。
- 配置hosts:集群节点间通信基于主机名解析,提前配好避免后续因解析失败导致节点NotReady。
2.4 容器运行时安装:Docker与containerd双轨方案
由于后续要用kubeadm初始化集群,这里直接用containerd作为CRI运行时,同时安装Docker CLI方便日常调试。两个组件配合使用的方案,既保留了Docker的使用习惯,又确保K8s通过CRI标准与容器运行时交互。
# 安装Docker依赖 yum install -y yum-utils device-mapper-persistent-data lvm2 # 添加Docker仓库 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 安装Docker及containerd yum install -y docker-ce docker-ce-cli containerd.io # 配置containerd使用systemd cgroup驱动 mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml # 修改config.toml,找到SystemdCgroup这一行,改为true sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml # 镜像加速(可选,国内环境强烈建议配置) # 在config.toml中修改sandbox_image为国内镜像地址 systemctl daemon-reload systemctl enable --now docker containerd关于镜像加速的补充说明:由于K8s核心组件的镜像是托管在Google Container Registry上的,国内服务器直接拉取会超时。实测最有效的方案是配置阿里云或腾讯云的镜像加速器,或者使用代理节点提前拉取镜像再导出导入。本教程后续所有涉及镜像拉取的步骤,都会同步给出替代方案。
3. K8s集群搭建:从kubeadm init到节点加入
3.1 kubeadm、kubelet、kubectl安装
K8s的组件安装是我认为整个流程中最容易出现版本错乱的一步。这里必须强调一个原则:三个工具的版本必须完全一致,否则kubeadm init会直接报错。
# 配置K8s yum源 cat > /etc/yum.repos.d/kubernetes.repo << EOF [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=0 repo_gpgcheck=0 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg EOF # 安装指定版本 yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2 # 启动kubelet(此时会启动失败,属于正常现象,等kubeadm init后会恢复) systemctl enable kubelet && systemctl start kubelet这里解释一下为什么kubelet启动会失败。kubelet启动后需要连接kube-apiserver获取Pod清单,但此时控制面组件还没初始化,连不上是正常的。很多新手在这里卡住,看到kubelet状态是failed就以为装错了,实际上继续执行kubeadm init就行。
3.2 初始化控制面:kubeadm init全参数详解
在Master节点上执行初始化命令。这里建议提前准备好配置文件,比直接敲命令行参数更清晰,也方便重复执行。
# 生成默认配置文件 kubeadm config print init-defaults > kubeadm-init.yaml编辑生成的配置文件,将主要参数调整如下:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 # 修改为Master节点IP bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.28.2 # 与安装版本一致 controlPlaneEndpoint: 192.168.1.10:6443 networking: podSubnet: "10.244.0.0/16" # Pod网段,必须与网络插件一致 serviceSubnet: "10.96.0.0/12" # Service网段需要重点理解的是podSubnet这个参数。它定义了K8s集群内Pod的IP地址范围,所有容器都会被分配这个网段内的IP。Calico网络插件会在这个网段内做路由转发,所以这个网段不能和宿主机网段冲突,也不能和Service网段重叠。我这里用的10.244.0.0/16是Calico默认建议的,如果你有自己的规划,保持两处一致即可。
执行初始化:
kubeadm init --config kubeadm-init.yaml初始化成功后,屏幕会输出三部分关键信息:
- kubeconfig配置命令:需要将admin.conf复制到~/.kube/config才能使用kubectl
- Pod网络安装命令:需要安装CNI网络插件
- Worker节点加入命令:包含token和CA证书哈希
这三部分信息建议截图保存,后面每一步都要用到。如果初始化失败,可以先执行kubeadm reset清理现场,再根据错误信息调整配置。常见的失败原因包括镜像拉取超时、端口被占用、cgroup驱动不匹配等。
3.3 配置kubectl与安装Calico网络插件
初始化完成后,在Master节点执行:
# 配置kubectl mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config # 查看节点状态 kubectl get nodes此时节点状态是NotReady,原因是还没部署网络插件,这是正常现象。使用Calico作为CNI插件,它的职责是给每个Pod分配IP,并在集群节点间打通网络隧道。
# 下载Calico部署清单 curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml # 如果上面的地址无法访问,可以使用下面这个备用地址 # curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml # 安装Calico kubectl apply -f calico.yaml等待一两分钟后,查看节点状态应该会变为Ready:
kubectl get nodes如果节点长时间处于NotReady状态,通常是以下原因:
- 镜像拉取失败(calico镜像在docker hub上,国内需要提前拉取)
- Pod网段和Calico配置不一致
- 节点间网络不通,检查防火墙和路由
排查Pod状态:
kubectl get pods -n kube-system -o wide关注calico-node这个DaemonSet是否在所有节点上都运行,以及状态是否为Running。如果是ImagePullBackOff,那就优先解决镜像问题。
3.4 Worker节点加入集群
在Master节点上重新获取加入命令:
kubeadm token create --print-join-command在worker节点上执行输出的命令:
kubeadm join 192.168.1.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>加入成功后,在Master节点确认集群状态:
kubectl get nodes正常情况下,三个节点的状态都应该是Ready。如果worker节点状态是NotReady,大概率是网络插件没连通,或者在worker节点上没配置好kubelet启动参数。此时可以在worker节点上查看kubelet日志:
journalctl -u kubelet -f -n 100根据日志内容针对性排查。常见的情况包括主机名解析失败、CRI socket路径不对等。
4. Web服务部署与核心对象解析
4.1 部署策略选择:为什么推荐Deployment
集群就绪后,开始部署Web服务。这里选用一个简单的Nginx应用作为演示,后续可以替换成任意业务镜像。
K8s中管理无状态应用最常用的资源是Deployment,它负责声明期望的副本数,并维护Pod的创建、更新和删除。相比直接创建Pod,Deployment带来了几个关键能力:
- 自愈能力:Pod异常退出后,Deployment会自动重建
- 滚动更新:新版本发布时,逐步替换旧版本,业务不中断
- 版本回滚:发布出问题可以快速回退到上一版本
- 扩缩容:修改副本数即可实现水平伸缩
在真实业务中,你几乎不会直接创建裸Pod,而是通过Deployment这类控制器来管理Pod。这也是为什么所有K8s教程都强调从Deployment开始学习。
4.2 编写YAML清单:从零创建一个Web服务
下面是一个完整的Deployment清单:
apiVersion: apps/v1 kind: Deployment metadata: name: web-demo namespace: default labels: app: web-demo spec: replicas: 3 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 10 periodSeconds: 10这份清单里有几个值得展开的点:
资源请求与限制:requests是调度器为Pod预留的资源量,limits是Pod能使用的最大资源量。设置合理值很关键——requests设置太高会浪费集群资源,太低则可能导致节点资源超卖。上面设置的100m/128Mi表示这个Pod最少需要0.1核CPU和128MB内存,上限是0.5核和512MB。
探针配置:readinessProbe决定Pod是否可以被Service转发流量,livenessProbe决定是否重启容器。这两个配置对生产环境至关重要。没有readiness探针时,服务可能尚未就绪就被纳入负载均衡,导致部分请求503。没有liveness探针时,应用死锁后容器不会被自动重启。
执行部署:
kubectl apply -f deployment.yaml # 查看Deployment状态 kubectl get deployment web-demo # 查看Pod状态 kubectl get pods -l app=web-demo -o wide4.3 Service与Ingress:流量如何进入集群
Pod创建后,我们还需要考虑两个流量入口问题:集群内部访问和集群外部访问。这里用Service和Ingress解决。
首先创建Service,为三个Nginx Pod提供统一入口:
apiVersion: v1 kind: Service metadata: name: web-demo-service namespace: default spec: type: ClusterIP selector: app: web-demo ports: - port: 80 targetPort: 80Service通过selector将请求转发到匹配的Pod上,port是Service对外暴露的端口,targetPort是后端Pod监听的端口。这里有几种Service类型可选:
| 类型 | 适用场景 | 说明 |
|---|---|---|
| ClusterIP | 集群内部访问 | 默认类型,仅在集群内可达 |
| NodePort | 测试或临时暴露 | 在节点上开放一个端口,可外部访问 |
| LoadBalancer | 云环境 | 需要云厂商提供负载均衡器 |
| ExternalName | 外部服务映射 | 将Service映射到外部域名 |
对于生产Web服务,推荐使用Ingress统一管理外部流量。Ingress相当于集群内部的七层负载均衡,可以根据域名或路径分发请求:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-demo-ingress namespace: default spec: rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: web-demo-service port: number: 80使用Ingress的前提是集群内已安装了Ingress Controller,比如Nginx Ingress Controller或Traefik。Ingress只是声明规则,真正做流量转发的是Controller。
4.4 滚动更新与版本回滚实操
部署完成只是开始,日常频繁操作的是版本更新。假设我们要把Nginx镜像从1.25升级到1.26:
# 更新镜像版本 kubectl set image deployment/web-demo nginx=nginx:1.26-alpine # 查看滚动更新状态 kubectl rollout status deployment/web-demo # 如果有问题,回滚到上一个版本 kubectl rollout undo deployment/web-demo滚动更新时,Deployment默认配置下会先创建新版本的Pod,等新Pod就绪后,再逐个下线旧Pod。整个过程中服务不中断,用户体验无感知。这个策略的优势在于,如果新版镜像有问题,可以在回滚前及时发现,不会造成大规模故障。
5. 常用命令与故障排查技巧
5.1 kubectl高频命令速查
K8s学习曲线陡峭,很大一部分原因是kubectl命令太多。这里整理了一份我日常使用频率最高的命令清单:
# 查看资源 kubectl get nodes # 查看节点 kubectl get pods -o wide # 查看Pod详情 kubectl get deployment # 查看Deployment kubectl get svc # 查看Service kubectl get events --sort-by=.metadata.creationTimestamp # 查看集群事件 # 查看日志 kubectl logs -f pod-name # 实时查看Pod日志 kubectl logs pod-name --previous # 查看上一次崩溃的容器日志 # 进入容器 kubectl exec -it pod-name -- /bin/sh # 查看Pod详细描述 kubectl describe pod pod-name # 端口转发(临时访问集群内服务) kubectl port-forward svc/web-demo-service 8080:80 # 资源监控 kubectl top node kubectl top pod # 删除资源 kubectl delete -f deployment.yaml个人经验:遇到Pod异常时,第一步永远先describe,看Events里的记录;第二步看日志;第三步进容器检查应用本身。按照这个顺序排查,90%的问题都能解决。
5.2 排障必杀技:Pod状态与容器日志分析
K8s里Pod有几种常见状态,每种状态对应的问题方向不同:
| Pod状态 | 可能原因 | 排查重点 |
|---|---|---|
| Pending | 资源不足、调度失败 | 查看节点资源余量,确认是否被调度器拒绝 |
| ContainerCreating | 镜像拉取失败、Volumes挂载失败 | 查看Events,确认镜像地址和存储配置 |
| CrashLoopBackOff | 容器启动后立即退出 | 查看日志,确认应用配置和启动命令 |
| Running(0/1) | 容器启动但就绪探针失败 | 检查readinessProbe配置和应用实际监听端口 |
| Error | 容器运行过程报错 | 查看日志定位中断原因 |
下面演示一个典型的排障过程:
# 假设web-demo的Pod处于CrashLoopBackOff # 1. 查看Deployment下的Pod列表 kubectl get pods -l app=web-demo # 2. 查看Pod详情,关注Events区域 kubectl describe pod web-demo-xxxxx # 3. 查看容器日志 kubectl logs web-demo-xxxxx --previous在一次实际排障中,我发现容器不断重启,状态是CrashLoopBackOff,describe显示ContainerStatus有退出码127,日志提示exec user process caused "no such file or directory"。排查后发现是镜像的基础镜像使用了不同架构(比如AMD64的镜像跑在了ARM服务器上),换用匹配架构的镜像后问题解决。
另一个高频坑是就绪探针配置冲突。如果readinessProbe的path写成了/health,而应用实际没有这个接口,探针一直失败,Pod会永远处于未就绪状态,Service不会把流量转过来。此时应用本身是正常启动的,你还会收到一堆健康检查失败的告警。解决办法就是让探针路径和应用实际暴露的健康检查端点对齐。
5.3 etcd备份与集群升级
集群稳定运行一段时间后,必要的运维操作要跟上。etcd是K8s存储集群所有状态的关键组件,备份etcd是容灾恢复的核心环节。
# 使用etcdctl进行快照备份 ETCDCTL_API=3 etcdctl --endpoints=https://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 +%Y%m%d).db备份策略建议每天全量备份一次,保留最近7天。恢复etcd时,需要先停止kube-apiserver和etcd服务,然后使用etcdctl snapshot restore恢复数据,最后重启组件。如果只是单个节点故障,更快的做法是从集群中摘除节点,重新初始化后加入。
关于集群升级,推荐采用蓝绿升级法:先备份,再逐个节点升级。每个节点上执行yum update kubeadm kubelet kubectl,升级kubeadm后先执行kubeadm upgrade plan确认可用版本,再执行kubeadm upgrade apply。注意升级Master节点前要确认所有Worker节点健康,升级过程中不要同时发布新业务。
6. 集群运维心得与避坑记录
K8s集群跑起来只是第一步,稳定运维才是长期的挑战。分享几条最真实的实践心得。
其一,资源监控必须从第一天就做好。集群部署完成后,第一时间部署Metrics Server或Prometheus,设置好告警规则。有一次我的集群因为某个服务内存泄漏,导致节点内存耗尽,Pod被大量驱逐,业务直接抖动。如果提前配置了告警,完全可以在问题扩大之前发现。
其二,日志收集方案要提前规划。K8s的Pod会频繁重建,容器日志会跟随Pod销毁而丢失。生产环境建议部署EFK(Elasticsearch + Fluentd + Kibana)或Loki方案,将日志集中采集到持久化存储中。否则等线上出了故障再去翻日志,会发现关键Pod已经被调度到别的节点上,旧日志无处可查。
其三,命名空间隔离是团队协作的基础。多团队共用集群时,按项目或团队划分Namespace,并通过ResourceQuota限制每个Namespace的资源额度。这样既保证了资源分配公平,也避免了误操作影响其他业务。默认的所有资源都放在default命名空间下,时间一长会特别混乱。
其四,配置变更必须走GitOps审计链路。所有的Deployment、ConfigMap、Service等资源清单,都应该提交到Git仓库统一管理。有人修改了线上配置,可以通过历史记录回看和回滚。用kubectl apply -f直接改线上配置,当时是方便,但后续排查问题时往往说不清到底改了什么。
7. 常见问题速查:从集群搭建到运行期的坑
把实操中最常遇到的10个问题整理成速查表,方便大家对照排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| kubeadm init拉镜像超时 | 国内网络访问Google镜像仓库受限 | 配置镜像加速器,或提前拉取镜像后save/load |
| 节点状态NotReady | 网络插件未安装或podSubnet冲突 | 确认Calico路由正常,检查podSubnet配置 |
| Pod状态Pending | 资源不足或节点亲和调度不满足 | kubectl describe pod查看调度失败原因 |
| Pod反复重启 | 镜像启动即崩或Liveness探针失败 | 查看--previous日志,调整探针配置 |
| 删除Pod后立即重建 | 受Deployment/ReplicaSet管理 | 删除Deployment,不要直接删Pod |
| Service无法访问Pod | selector标签不匹配 | 核对Service的selector和Pod的labels |
| NodePort外部不通 | 安全组/防火墙未放行 | 打开节点端口和防火墙规则 |
| PV无法挂载 | 存储类或权限问题 | 检查StorageClass名称和PVC绑定状态 |
| CoreDNS CrashLoop | 网络插件与CoreDNS冲突 | 检查Pod网段,重启CoreDNS |
| kubectl无法连接集群 | kubeconfig失效 | 重新复制admin.conf到~/.kube/config |
补充两个非常隐蔽的坑。
第一个是关于kubelet的cgroup驱动。Docker默认使用cgroupfs,而K8s推荐使用systemd。如果不将containerd的SystemdCgroup设置为true,kubelet启动后检查节点信息时会报cgroup driver不一致的错误。这个坑在安装阶段不容易发现,等到节点加入集群才会暴露,排查起来特别费劲。
第二个是关于Service的externalTrafficPolicy。默认情况下,NodePort服务的流量可能被转发到任意节点上的Pod,对于需要保留客户端真实IP的场景(比如日志审计),需要将externalTrafficPolicy设置为Local。但这会导致流量被锁定在当前节点,无法实现跨节点负载均衡,需要综合考虑场景取舍。
回头来看,K8s集群搭建最难的环节其实不是命令本身,而是理解各组件之间的协作关系和网络流量路径。把kubelet如何感知Pod、kube-proxy如何实现Service转发、网络插件如何为Pod分配IP这几个问题想清楚,后面的运维会顺手很多。这套环境跑起来之后,我还陆续在集群上部署了监控告警、日志采集和CI/CD流水线,整个链路的运维效率比单机时代提升了不止一个量级。如果你正在规划上K8s,建议从最小集群开始,逐步接入业务,别一上来就追求生产级高可用——先把核心概念走通,再谈规模化。