1. 从一个真实困惑说起:为什么大家都在聊云原生和K8S
如果你最近逛技术社区,或者参加一些架构评审会,大概率会反复听到两个词:云原生和K8S。有人把它们说得像银弹,仿佛业务一上云原生就能自动扛住双十一流量;也有人觉得这就是运维圈造出来的新概念,跟业务开发没多大关系。我刚开始接触的时候也是这种感受,直到自己亲手把一个单体应用拆成容器化部署,踩了一堆坑之后,才真正理解这套东西到底在解决什么问题。
先把结论摆在前面:云原生不是某一项具体技术,而是一套让应用从设计、开发、交付到运维都围绕“云环境”来思考的方法论;而K8S(Kubernetes)是这套方法论里目前最主流的容器编排系统,负责把一堆散落的容器组织成一个能自愈、能扩缩、能滚动升级的集群。这篇文章我会从零开始,把云原生的核心思想、K8S 的关键组件、实际部署流程、常见坑点全部拆开讲清楚,适合刚入门的后端开发、运维同学,也适合想搞清楚“我们公司到底该不该上 K8S”的技术负责人。
我写这篇的出发点很简单:网上讲 K8S 的文章要么是官方文档式的命令罗列,要么是概念堆砌,看完还是不知道从哪下手。所以我打算用从业者的视角,把“为什么这么设计”“实际怎么操作”“哪里容易翻车”这三件事讲透。你不需要有容器基础,但最好对 Linux 基本命令和网络概念有点了解,这样读起来会更顺。
2. 云原生到底在解决什么问题:拆解核心思路
2.1 云原生的四个核心支柱
很多人以为云原生就是“把应用打成 Docker 镜像”,这个理解太窄了。云原生的核心可以归纳为四个相互支撑的方向,我习惯把它们叫做四个支柱:
- 容器化:把应用和它的依赖打包成一个标准单元,解决“在我机器上能跑,在你机器上跑不起来”的经典问题。
- 微服务:把一个大单体拆成多个独立部署的小服务,每个服务可以单独升级、单独扩容。
- 持续交付:通过自动化流水线,让代码从提交到上线的时间从几天缩短到几分钟。
- 动态编排:也就是 K8S 这类系统,负责在成百上千台机器上自动调度、监控、恢复这些容器。
这四个支柱不是必须全部做到才叫云原生。我见过不少团队只做了容器化和编排,就已经获得很大收益。关键是要理解它们之间的依赖关系:没有容器化,微服务的部署成本会高到无法承受;没有编排,容器数量一多就管不过来;没有持续交付,微服务的迭代速度优势就发挥不出来。
2.2 为什么传统部署方式越来越吃力
要理解云原生的价值,得先看看传统部署方式在规模变大后遇到的瓶颈。假设你有一个 Java 应用,早期只有两台服务器,手动传个 jar 包、重启一下进程就完事了。但当业务增长到几十个服务、上百台机器时,问题就来了:
第一,环境不一致。开发环境是 JDK 8,测试环境是 JDK 11,生产环境又装了不同的系统库,导致同一个包在不同环境表现不一样。第二,资源利用率低。每个服务都按峰值流量预留机器,平时 CPU 利用率可能只有 5%,浪费严重。第三,扩缩容慢。大促前要提前几天申请机器、装环境、部署,流量过去了机器又闲置。第四,故障恢复靠人。半夜某个进程挂了,得靠值班人员手动重启,恢复时间完全取决于人多久能醒。
云原生的思路是把这些问题交给平台层解决:容器保证环境一致,编排系统负责调度和自愈,声明式配置让扩缩容变成改一个数字。这不是说运维人员就没事干了,而是他们的工作从“手动操作”转向“设计平台和规则”。
2.3 容器和虚拟机的本质区别
说到容器,很多人会拿它和虚拟机对比。我用一个生活化的类比来解释:虚拟机像是租一整栋房子,你有独立的墙、独立的门、独立的水电系统,隔离性很好,但每栋房子都要配一套基础设施,开销大。容器更像是合租公寓里的一个房间,大家共享厨房和卫生间(也就是宿主机的操作系统内核),但每个房间有自己的私人物品(应用和依赖),隔离性够用,开销小得多。
这个区别带来的直接后果是:虚拟机启动通常要几十秒到几分钟,容器启动往往只要几百毫秒到几秒;同样一台物理机,能跑的容器数量通常比虚拟机多一个数量级。当然,容器的隔离性不如虚拟机强,所以对于多租户、强安全隔离的场景,虚拟机仍然有它的位置。实际生产中,很多团队是两者结合:用虚拟机做节点,在虚拟机上跑容器。
理解了这一点,你就能明白为什么 K8S 选择以容器为基本调度单元——因为容器的轻量特性,才使得“快速创建、快速销毁、频繁调度”成为可能。
3. K8S 核心组件拆解:它凭什么能管住成千上万个容器
3.1 控制平面:集群的大脑
K8S 集群分为控制平面(Control Plane)和工作节点(Worker Node)两部分。控制平面是大脑,负责决策;工作节点是四肢,负责干活。控制平面里最核心的几个组件,我一个个说:
API Server是整个系统的入口,所有操作——无论是你用 kubectl 敲命令,还是其他组件之间通信——都要经过它。它本质上是一个 RESTful 接口,负责认证、授权、校验请求,然后把数据存到 etcd 里。你可以把它理解成公司的前台,所有事情都要先到它这里登记。
etcd是一个分布式键值存储,保存着整个集群的所有状态数据。它是 K8S 里唯一的有状态组件,所以它的稳定性至关重要。生产环境一般要部署 3 个或 5 个节点组成集群,保证高可用。我踩过的坑是:etcd 对磁盘 IO 非常敏感,如果用普通机械硬盘,集群规模一大就会出现写入延迟,导致整个控制平面响应变慢。所以生产环境强烈建议用 SSD。
Scheduler负责决定新创建的 Pod 该放到哪个节点上。它会综合考虑节点的资源余量、亲和性规则、污点容忍等条件,选出一个最合适的节点。这个过程叫“调度”,是 K8S 最核心的能力之一。
Controller Manager是一组控制器的集合,每个控制器负责一种“期望状态”的维护。比如 Deployment 控制器确保实际运行的 Pod 数量等于你声明的副本数,Node 控制器负责在节点失联时做处理。它们的工作模式是“观察-对比-调整”的循环,这也是 K8S 声明式 API 的精髓。
3.2 工作节点:真正跑容器的地方
工作节点上有两个关键组件:
kubelet是节点上的“监工”,它负责跟 API Server 通信,接收分配给本节点的 Pod 任务,然后调用容器运行时去创建、启动、监控容器。如果容器挂了,kubelet 会根据重启策略决定是否拉起。
kube-proxy负责节点上的网络规则,实现 Service 的负载均衡。当你创建一个 Service 时,kube-proxy 会在每个节点上配置转发规则,让访问 Service 虚拟 IP 的流量能分发到后端真实的 Pod 上。
至于容器运行时,早期 K8S 默认用 Docker,后来因为 Docker 太重,社区推出了 CRI(容器运行时接口)标准,现在主流的选择是 containerd 或 CRI-O。这个变化对普通用户基本无感,你写的 YAML 还是一样的。
3.3 一个 Pod 的完整生命周期
理解 K8S,最好的方式是跟着一个 Pod 走一遍它的生命周期。假设你用 kubectl 提交了一个 Deployment,声明要 3 个副本:
- kubectl 把请求发给 API Server,API Server 校验后写入 etcd。
- Deployment 控制器发现期望 3 个副本、实际 0 个,于是创建 3 个 ReplicaSet 记录。
- ReplicaSet 控制器发现需要 3 个 Pod,于是创建 3 个 Pod 对象(此时还没调度)。
- Scheduler 发现有 3 个未调度的 Pod,根据资源情况给它们各选一个节点,更新 Pod 的 nodeName 字段。
- 目标节点的 kubelet 发现有自己的 Pod 要跑,调用容器运行时拉镜像、启动容器。
- kube-proxy 更新网络规则,让 Service 能把流量转发到这些新 Pod。
- 如果某个 Pod 挂了,kubelet 会重启它;如果整个节点挂了,Node 控制器会把上面的 Pod 标记为失联,ReplicaSet 控制器会在其他节点重建。
这一整套流程全是自动的,你只需要声明“我要 3 个副本”,剩下的交给系统。这就是声明式 API 的威力——你描述目标,系统负责达成。
4. 从零搭建一个可用的 K8S 环境:实操步骤与参数解析
4.1 环境规划与工具选型
在动手之前,先想清楚你要搭什么规模的集群。如果是学习用,单节点就够了;如果是测试环境,建议至少 3 个节点,这样才能体验调度和故障转移。我下面以 3 台机器为例,规划如下:
| 角色 | 数量 | 配置建议 | 说明 |
|---|---|---|---|
| 控制平面 | 1 | 2 核 4G,SSD | 学习环境单控制平面即可 |
| 工作节点 | 2 | 4 核 8G,SSD | 跑业务负载 |
| 容器运行时 | - | containerd | 比 Docker 更轻量 |
| 网络插件 | - | Calico | 支持网络策略,生产常用 |
操作系统建议用 Ubuntu 20.04 或 22.04,内核版本 4.15 以上。每台机器需要做的基础配置包括:关闭 swap(K8S 要求)、设置主机名、配置 hosts 解析、开启内核转发模块。这些步骤网上教程很多,我就不逐条列命令了,重点说几个容易出错的点。
注意:关闭 swap 不是可选项,K8S 的调度器假设节点没有 swap,如果开着 swap,kubelet 启动会直接报错。临时关闭用
swapoff -a,永久关闭要注释掉 /etc/fstab 里的 swap 行。
4.2 安装容器运行时和 K8S 组件
容器运行时我选 containerd,安装步骤大致是:添加官方源、安装 containerd、生成默认配置、修改配置启用 SystemdCgroup、启动服务。这里有个关键参数:SystemdCgroup = true,如果不设置,kubelet 和 containerd 的 cgroup 驱动不一致,会导致 Pod 启动失败。这个坑我见过太多人踩。
K8S 组件的安装,官方推荐用 kubeadm。流程是:添加 K8S 源、安装 kubeadm/kubelet/kubectl、初始化控制平面、加入工作节点。初始化命令类似这样:
kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.0--pod-network-cidr这个参数必须和后面网络插件的配置一致,否则 Pod 之间无法通信。Calico 默认用 192.168.0.0/16,如果你这里写了 10.244.0.0/16,就要去改 Calico 的配置。我建议直接用 Calico 默认的网段,省得改。
初始化完成后,会输出一个 join 命令,复制到工作节点执行即可加入集群。然后在控制平面安装网络插件:
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml等几分钟,用kubectl get nodes看到所有节点状态是 Ready,集群就算搭好了。
4.3 部署第一个应用并暴露服务
集群搭好后,先跑一个简单的 Nginx 验证一下。创建一个 Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi这里我特意加了 resources 配置。requests 是调度依据,Scheduler 会根据它判断节点有没有足够资源;limits 是运行上限,超过会被限制或杀掉。不写这两个,Pod 可能被调度到资源不足的节点,或者某个 Pod 内存泄漏拖垮整个节点。生产环境这是必填项。
然后创建 Service 暴露服务:
apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - port: 80 targetPort: 80 type: ClusterIPClusterIP 只能在集群内部访问。如果要从外部访问,可以改成 NodePort 或配合 Ingress。NodePort 会在每个节点上开一个 30000-32767 的端口,简单但不好管理;Ingress 需要一个 Ingress Controller,更灵活,生产环境推荐。
5. 常见问题与排查技巧实录
5.1 Pod 一直 Pending 怎么办
这是新手最常遇到的问题。Pod 处于 Pending 状态,说明它没有被调度到任何节点。排查思路是:
- 用
kubectl describe pod <pod-name>看 Events 部分,通常会有明确原因。 - 如果是
Insufficient cpu或Insufficient memory,说明节点资源不够,要么加节点,要么调小 requests。 - 如果是
node(s) had taint,说明节点有污点,Pod 没有对应的容忍。 - 如果是
no nodes available,检查节点是否都 Ready。
我遇到过一次很隐蔽的情况:节点显示 Ready,但 Pod 就是调度不上去。后来发现是节点的磁盘压力触发了驱逐阈值,节点被打上了node.kubernetes.io/disk-pressure污点。清理磁盘后恢复正常。所以平时要监控节点的磁盘使用率。
5.2 Pod 反复重启怎么查
Pod 状态显示 CrashLoopBackOff,说明容器启动后很快退出,K8S 在反复重启它。排查步骤:
kubectl logs <pod-name> --previous看上一次崩溃的日志,这是最直接的线索。kubectl describe pod看退出码。退出码 0 通常是程序正常结束但没保持前台运行;退出码 1 是应用报错;退出码 137 是被 OOM Kill 了。- 如果是 137,检查 limits 是不是设太小,或者应用有内存泄漏。
有个经典坑:容器里跑的是后台进程,主进程启动后就退出了,容器跟着结束。解决办法是让主进程保持前台运行,比如用nginx -g "daemon off;"而不是直接nginx。
5.3 Service 访问不通的排查顺序
Service 不通的原因可能出在多个环节,我习惯按这个顺序排查:
| 排查点 | 命令 | 可能问题 |
|---|---|---|
| Pod 是否正常 | kubectl get pods | Pod 未 Running |
| Endpoints 是否有值 | kubectl get endpoints | selector 不匹配 |
| 端口是否正确 | kubectl describe svc | targetPort 写错 |
| 网络插件是否正常 | kubectl get pods -n kube-system | Calico 异常 |
| DNS 是否正常 | kubectl exec 进入 Pod 测试 | CoreDNS 故障 |
最常见的是 selector 不匹配。Service 的 selector 必须和 Pod 的 labels 完全对应,差一个字符都不行。我建议用kubectl get pods --show-labels确认一下。
5.4 几个我踩过的坑和独家技巧
坑一:镜像拉取失败。国内环境拉 Docker Hub 镜像经常超时。解决办法是配置镜像加速器,或者把镜像同步到私有仓库。containerd 的镜像配置在/etc/containerd/config.toml,改完要重启 containerd。
坑二:时区不对。容器默认用 UTC 时间,日志时间戳和本地差 8 小时。解决办法是在 Deployment 里挂载宿主机的/etc/localtime,或者设置TZ环境变量。
技巧一:用 kubectl 的 dry-run 验证 YAML。写完 YAML 不确定对不对,先跑kubectl apply --dry-run=client -f xxx.yaml,能提前发现语法错误。
技巧二:给 Pod 加就绪探针。没有就绪探针,Service 会在容器刚启动、还没准备好时就转发流量,导致请求失败。加上 readinessProbe 后,K8S 会等探针通过才把 Pod 加入 Endpoints。
技巧三:用 namespace 隔离环境。开发、测试、生产用不同的 namespace,配合 ResourceQuota 限制每个 namespace 的资源用量,避免测试环境把生产资源抢光。
6. 云原生落地的一些现实思考
6.1 不是所有团队都适合立刻上 K8S
我见过一些团队,业务规模不大,就十几个服务,也跟风上 K8S,结果运维成本反而更高。K8S 本身有学习曲线,控制平面要维护,网络插件要调优,监控日志要配套。如果团队没有专门的平台工程能力,用 Docker Compose 或者更简单的托管容器服务可能更划算。
判断标准其实很简单:当你的服务数量超过 20 个,或者机器数量超过 10 台,或者需要频繁扩缩容时,K8S 的收益才开始明显超过成本。在这之前,把精力放在代码质量和自动化测试上,回报率更高。
6.2 云原生带来的组织变化
技术选型从来不只是技术问题。上了 K8S 之后,开发和运维的边界会发生变化。传统模式下,开发写完代码交给运维部署;云原生模式下,开发要自己写 Dockerfile、自己定义 K8S 资源清单,运维则转向维护平台和制定规范。这个转变需要组织上的配合,否则很容易变成“开发觉得运维不干活,运维觉得开发瞎指挥”。
我的建议是循序渐进:先让开发学会写 Dockerfile 和基本的 Deployment,运维负责搭建集群和提供模板;等大家熟悉了,再逐步下放更多权限。一上来就搞“谁开发谁运维”,往往会因为能力没跟上而翻车。
6.3 监控和日志不能省
K8S 把容器变成了“用完即弃”的东西,这给监控和日志带来了新挑战。容器挂了可以重建,但日志如果没收集就永久丢失了。所以上 K8S 之前,一定要先把可观测性体系搭好。基础组合是 Prometheus 做指标采集,Grafana 做展示,Loki 或 ELK 做日志聚合。这三个东西的部署本身也可以跑在 K8S 上,但要注意它们自己也需要资源,别把集群压垮。
我在实际项目里的体会是:K8S 本身不难,难的是围绕它的一整套配套体系。集群搭起来可能只要半天,但要让整个研发流程顺畅运转,可能需要几个月。所以如果你正准备推动团队上云原生,先把配套的工具链和规范想清楚,比急着敲命令更重要。
最后分享一个我常用的排查思路:遇到任何 K8S 问题,先问自己三个问题——期望状态是什么?实际状态是什么?哪个控制器负责协调这两者?把这三个问题回答清楚,大部分问题都能定位到具体组件。这个思路比死记命令有用得多。