先说一个观点:Kubernetes 不是给所有项目准备的,但你一旦开始管理多台服务器上的几十个容器,它就会成为绕不开的东西。Kubernetes,简称 K8s,是一个开源的容器编排平台,核心目标其实就一句话:自动化部署、扩展、管理容器化应用。很多人学它的时候被一堆抽象概念劝退,比如 Pod、控制器、声明式 API、服务发现,但它的底层思路并不复杂——把你希望运行的容器状态描述清楚,集群负责保证现实情况无限接近这个描述。
这篇文章不打算写成官方文档的翻译稿,而是想以一个常年和 K8s 打交道的开发者视角,把这套系统的来龙去脉、核心组件、实战操作和那些文档里不会写的坑讲透。如果你已经会用 Docker,但不太清楚“为什么还需要 K8s”,这篇文章正好合适;如果你刚准备在团队里把应用迁到 K8s,这篇文章也能帮你提前避开几个最容易踩的雷。
1. 为什么单机上跑容器还不够:聊聊编排层解决的问题
1.1 从 docker compose 到多节点集群,中间差了什么
Docker 解决了“应用怎么随身携带”的问题,它用镜像把环境和代码打包在一起,任何安装了 Docker 的机器都能跑起来。但 Docker 解决不了另一个问题:当一个应用有几十个副本,分散在好几台服务器上,你打算怎么管理它们?
用 docker compose 管理多容器应用,在单机场景下非常舒适。一个 YAML 文件能定义好几个服务,一条命令全部启动。但一旦节点数超过两三台,问题就接踵而至:副本跑在哪些机器上?某台机器挂了,上面的容器怎么迁移到别的机器?流量怎么在多个副本之间分配?内存不够时是扩容还是迁移?新版本发布时怎么做到不中断服务?
这些问题不是几个脚本能解决的。容器编排的本质,是把“管理大量容器的复杂规则”从人的脑子里搬到一个自动化系统里。Kubernetes 做的就是这件事:它把一堆服务器抽象成一个资源池,你告诉它要跑什么样的应用、要多少个副本、需要多少资源,它自己决定放在哪台机器上,自己负责故障恢复和流量接入。
1.2 “声明式”和“自愈”到底是什么
很多人第一次接触 K8s 时,会看到一个高频词:声明式。与之相对的是“命令式”。命令式是你一步步告诉系统怎么做,比如“先启动这个容器,再把端口映射出来,最后复制文件进去”。声明式是你只告诉系统“我要什么结果”,例如“我要 3 个名为 nginx 的容器副本”,系统自己想办法完成,并且持续保证状态不变。
这个概念听起来简单,但它带来的是运维方式的根本转变。以前排查问题时,你关注的是“某条命令执行后发生了什么”;在 K8s 的世界里,你关注的是“某个对象期望的状态是什么,实际的状态偏离了多少”。系统里有一堆控制器在后台不断比对,一旦发现有偏差,马上纠正。
所谓的“自愈”也源于这套机制。节点挂了?控制器在别的地方重新拉起副本。容器崩溃了?kubelet 直接重启它。副本数不够了?控制器补到期望值。这些操作全部不需要人工干预。
1.3 Kubernetes 管理的不是容器,而是“状态”
还有一点值得放下误解:Kubernetes 并不直接管理容器,它管理的是 Pod——你可以把 Pod 理解成容器的运行外壳。一个 Pod 里通常装一个主容器,也可能装一个辅助容器,比如日志收集器、代理之类。它们共享网络命空间和存储卷,同生共死。
这套抽象是有代价的:你多了一层概念,排查问题时不能直接 ssh 进某台机器去看某个进程,得通过 kubectl 和 API 对象打交道。但收益更大:团队可以在不同公有云、私有云甚至混合环境里,用同一套工作流部署应用,底层的差异被隐藏掉了。
2. 集群是怎么运作的:控制平面与工作节点的分工
2.1 API Server 与 etcd:集群的“大脑”。
Kubernetes 集群从结构上看,分成两部分:控制平面和工作节点。控制平面是管理集群的一组进程,包括 kube-apiserver、etcd、kube-scheduler、kube-controller-manager,通常跑在独立的机器上。工作节点则负责真正跑用户的容器,上面运行着 kubelet、kube-proxy 和容器运行时。
第一次接触时最容易忽略的是 kube-apiserver。它是整个集群的唯一入口,你输入的每一条 kubectl 命令,最终都会变成对它的 API 请求。这个设计很关键:所有组件都不直接面对面通信,而是通过 API Server 间接交互。换句话说,API Server 像一个总接线员,谁要修改状态,都必须先登记。
而状态存在哪里?存在 etcd 里。etcd 是一个分布式键值数据库,集群的所有信息——哪个节点存活、哪个 Pod 跑在哪个节点上、配置了什么资源——全部记录在 etcd 中。如果 etcd 的数据丢了,等于整个集群失忆。所以生产环境里 etcd 的高可用和定期备份,永远应该排在优先级前列。
2.2 调度器与控制器:一个管分配,一个管纠偏
kube-scheduler 负责决定“一个新 Pod 应该落在哪个节点上”。它的工作不是随机分配,而是读一堆约束:节点有没有充足的内存和 CPU、是否匹配了亲和性规则、是否被污点屏蔽等。调度器找到满足条件的节点后,把这个决定写到 API Server 的 Pod 记录里,剩下的事交给对应节点的 kubelet。
kube-controller-manager 是一组控制器的集合,它们才是“自愈”的真正执行者。比如 ReplicaSet 控制器观察到副本数是 2,期望值是 3,就会通过 API Server 创建新的 Pod;Node 控制器发现某节点失联时间超过阈值,会把这个节点上的 Pod 标记为需要重新调度。控制器永远在后台循环工作,无限缩短“实际状态”和“期望状态”之间的差距。
2.3 工作节点上的三个关键角色
真正干活的模块在工作节点。kubelet 是节点上的代理,它从 API Server 接收 Pod 定义,负责启动容器、定期上报节点状态、执行探针检查。容器运行时现在大部分环境已经用 containerd 或者兼容 CRI 的运行时,不再直接依赖 Docker 作为默认运行时。
kube-proxy 做的是网络转发。它维护着节点上的网络规则,让 Pod 和 Pod、Pod 和 Service 之间的通信能够正常进行。它的实现方式一直在演进,早期依赖 iptables,后来有些环境切换到更高效的 eBPF 方案,但对用户来说,感知比较小——你只需要知道,集群内转发这件事,是 kube-proxy 这类组件托底。
2.4 一次部署背后究竟发生了什么
为了把整个链路讲清楚,可以想象你执行了一条kubectl apply -f deployment.yaml:
- kubectl 把 Deployment 的定义发送给 kube-apiserver。
- API Server 先做认证鉴权,然后把数据写入 etcd。
- Deployment 对应的控制器发现“没有对应的 ReplicaSet”,于是创建了一个 ReplicaSet。
- ReplicaSet 控制器发现副本数为 0,开始创建期望数量的 Pod 对象。
- 调度器为新 Pod 挑选合适的节点,并把结果写回 API Server。
- 对应节点的 kubelet 收到 Pod 定义,通过容器运行时把镜像拉下来并启动容器。
- kubelet 持续汇报状态;控制器确认副本数达到期望值,流程结束。
这套流水线在首次使用时看似繁琐,但它换来的是“发布一个应用”这件事从手工操作变成了可追溯、可回滚、可审计的标准化流程。
3. 绕不开的核心对象:Pod、Deployment、Service、Ingress
3.1 Pod 是最小调度单位,不是容器
很多初学者会问:既然 Docker 已经是容器了,为什么 K8s 还要再包一层 Pod?因为容器只是一个运行状态,K8s 还需要管理它的网络、存储、生命周期。Pod 既是这些资源的载体,也是调度的最小单元。
一个常见误区是以为 Pod 就是容器的别名。实际上,一个 Pod 可以包含多个容器。在什么场景下需要多个容器共用一个 Pod?最典型的是边车模式:主容器运行业务,边车容器负责收集日志或做本地代理,两者共享网络栈和存储卷,通信不需要走网络。这种模式比让外部组件介入更省资源,也更直观。
3.2 Deployment 负责发布与滚动更新
很少有人直接创建裸 Pod,因为裸 Pod 不会自愈,节点挂了它就没了。实际工程中最常用的是 Deployment。它管理 ReplicaSet,而 ReplicaSet 管理带副本的 Pod。Deployment 的核心价值体现在发布和回滚上。
当你修改 Deployment 中的镜像版本并执行 apply 时,它不会把旧 Pod 全杀光再启新的,而是采用滚动更新:先把新 Pod 启动起来,等新 Pod 就绪后再逐步剔除旧 Pod,始终保持服务在线。滚动更新还能设置失败阈值,如果新版本不断崩溃,它可能中途停下,方便你回滚。这点对线上系统的重要性,做过发布的人应该都有体会。
3.3 Service 是固定不变的访问入口
Pod 是会变化的,今天叫 demo-web-abcde,明天发布后就变成 demo-web-xyzab。客户端如果直接记住 Pod 的地址,会非常痛苦。Service 的作用就是提供一个稳定的访问入口,它通过标签选择器匹配一组 Pod,把流量负载均衡地转发给它们。
Service 的类型最常见的是 ClusterIP。它给 Service 分配一个集群内部虚拟 IP,集群里的应用通过这个 IP 或对应的 DNS 名称访问。跨节点访问、负载均衡都交给 kube-proxy 处理。对普通应用来说,理解了 ClusterIP,就理解了 K8s 内部服务发现的基本玩法。
3.4 Ingress 管的是“从外面进来”的流量
Service 的 ClusterIP 只能内部访问。要想从公网访问,最早的做法是改成 NodePort 或 LoadBalancer,但每个 Service 都要占用端口或独立负载均衡器,成本很高。Ingress 出现的目的是统一管理外部流量入口。
可以把 Ingress 理解为集群的“门卫”:所有外部 HTTP/HTTPS 流量先到 Ingress,再根据域名和路径转发到不同的 Service。更关键的是,Ingress 把 SSL 证书、路由规则这些基础设施配置和业务部署解耦开,团队改路由只需要更新 Ingress 规则,不用动 Service。
3.5 对象之间的关系用一句话讲清楚
Deployment 管 Pod 的生命周期,Service 管 Pod 的访问方式,Ingress 管外部流量如何到达 Service,ConfigMap 和 Secret 管配置和敏感信息,Namespace 管资源隔离,PV/PVC 管持久化存储。这套关系并不需要死记,用的时候查一下文档,多实践几次就熟练了。
4. 把应用跑起来:一套可直接照抄的实战流程
4.1 环境选择:本地练习和生产部署要分开看
如果你只是想快速理解 K8s,在 Windows 或 macOS 上装一个单节点的轻量环境很有必要。这类工具把控制平面和节点合并在一起,一条命令启动一个集群,资源占用相对可控,特别适合新手跑示例。网上教程很多,我这里不重点展开。
真正生产环境有几个选择:自建集群,或者使用云厂商提供的托管 Kubernetes 服务。自建集群的好处是透明可控,坏处是控制平面的 etcd、apiserver 等都要自己维护,升级和安全补丁得自己盯着。托管服务的好处是控制平面不用管,节点坏了可以自动替换,虽然贵一点,但省下的是大批运维人力。我的建议是:没有特殊合规要求的团队,直接使用托管服务,把精力放在业务容器化上。
4.2 最小示例:部署一个 Nginx 并开放访问
下面是一份最简单的 Deployment 定义,直接保存成 deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: demo-web spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: demo-web image: nginx:1.26 ports: - containerPort: 80这里有几个细节值得注意。selector.matchLabels必须和template.metadata.labels匹配,Deployment 才能管理到这些 Pod。replicas声明期望副本数。containerPort只是想告诉读者容器监听了 80 端口,并不是说 K8s 会自动做端口映射。镜像没有加最新标签,而是指定了具体版本,这是生产环境的基本修养。
创建 Deployment 后,执行kubectl apply -f deployment.yaml,再用kubectl get pods观察 Pod 状态。你会看到 Pod 名字带着一串随机字符,那是 ReplicaSet 自动生成的。
但 Pod 地址不稳定,所以还需要一个 Service。创建 service.yaml:
apiVersion: v1 kind: Service metadata: name: demo-web-svc spec: selector: app: demo-web ports: - port: 80 targetPort: 80selector 会选中所有带app: demo-web标签的 Pod。port是 Service 对外暴露的端口,targetPort是 Pod 里容器实际监听的端口。应用后执行kubectl get svc,可以看到系统给这个 Service 分配了一个 ClusterIP。在集群内部,其他应用可以直接用demo-web-svc这个名称访问这个服务。
如果你用的是本地轻量环境,可以用kubectl port-forward svc/demo-web-svc 8080:80把端口转发到宿主机,然后浏览器访问 localhost:8080 验证效果。
4.3 发布、扩容和回滚的几个高频操作
发布新版本,最常见的方式是修改镜像版本后再次 apply:
kubectl set image deployment/demo-web demo-web=nginx:1.27 kubectl rollout status deployment/demo-web扩容缩容也很简单:
kubectl scale deployment/demo-web --replicas=5如果想回滚到上一个版本:
kubectl rollout history deployment/demo-web kubectl rollout undo deployment/demo-web这些命令看着简单,但它们的意义在于:所有操作都会记录在集群状态里,谁在什么时间改了什么、从哪个版本切到哪个版本,都能溯源。这一点在多人协作的团队里非常重要,它避免了很多“明明昨天还好好的,今天怎么不行了”的争议。
5. 生产环境里最容易翻车的几个点
5.1 不写资源限制,节点迟早被拖垮
第一次部署应用时,很多人图省事,只在 YAML 里写镜像和端口,不写resources。开发环境无所谓,生产环境这么干很危险。想象一个节点上跑了十几个 Pod,其中几个代码里有内存泄漏,它们不断膨胀,最终可能触发系统 OOM,导致节点不稳定甚至宕机。
正确做法是在每个容器里声明requests和limits:
resources: requests: cpu: 250m memory: 512Mi limits: cpu: 500m memory: 1Girequests是调度器分配资源的依据,代表你期望至少拿到这么多资源;limits是硬上限,超过之后容器可能会被限制或被杀掉。这两个值不能拍脑袋填,最好先拿压测数据或历史监控数据来定。
这里还有一个很多人忽略的点:如果只写limits不写requests,某些情况下系统会默认把requests设成和limits一样大,导致调度器认为节点资源不够用,副本数量上不去。所以建议两者都写,而且要刻意让requests略小于limits。
5.2 探针缺失,发布时流量打到故障 Pod
很多人第一次接触探针时觉得多此一举。实际上,没有探针的发布会遇到一个经典事故:新版本 Pod 已经启动,但业务代码还在加载数据,端口已监听但接口返回 5xx,而负载均衡直接把流量转发过来,用户访问一片报错。
K8s 提供了三种探针:livenessProbe用来判断容器是否存活,失败超过阈值就会重启容器;readinessProbe用来判断容器是否具备接收流量的能力,失败时从 Service 端点里剔除;startupProbe主要针对启动慢的容器,给足首次启动时间,避免 liveness 反复误杀。
建议是在所有面向用户的 Deployment 上配置readinessProbe,在关键业务上配置livenessProbe。写探针时注意超时和容错:探针periodSeconds不宜设太短,否则一次慢查询可能导致连续失败;failureThreshold要容忍短暂的线程阻塞。我见过不少团队第一次加探针后反而频繁重启容器,就是因为超时时间太短。
5.3 镜像拉取策略和版本标签
镜像写latest标签在生产环境是大忌。latest是动态的,拉下来什么取决于拉取时刻的远端仓库状态,很难追溯当前部署的到底是哪个版本。更稳妥的做法是每次构建都打上不可变标签,比如 git commit hash,或者带时间戳的版本号。这样部署记录和镜像能一一对应,回滚时也能精确拉到旧版本。
镜像拉取策略在本地位于节点上时也有讲究。K8s 默认策略是IfNotPresent:本地没有镜像才拉取。如果所有节点都用相同标签的镜像,而你又想更新,需要手动加imagePullPolicy: Always,或者干脆打新标签,否则会出现“改了镜像,重启后还是老版本”的诡异问题。
5.4 日志和监控要在第一天就想好
容器是无状态的,Pod 被重新调度后,节点上的临时文件都会消失。如果日志只是写到容器内部磁盘,Pod 一换,日志全丢。K8s 里最常见的日志方案是让容器把日志写到标准输出,再由节点上的日志代理收集并送走。
监控方面,光看kubectl get pods是不够的。容器崩溃后 ReplicaSet 会立刻拉一个新的,旧 Pod 可能已经被清理,你连崩溃现场都看不到。所以从第一天起就应该把每个命名空间的资源使用率、Pod 重启次数、API Server 错误率这类指标收集起来。不一定要用最重的方案,至少要有一些基础的监控面板。否则线上出了故障,你只能靠猜。
6. 生态配套和“什么时候别硬上 K8s”
6.1 常用配套工具,按需引入
Kubernetes 只是一个底座,真正让团队生产力提升的是围绕它的一圈生态伴侣。
Helm 是很多团队部署应用时会用到的包管理工具。它把一组 K8s 资源打包成一个 chart,用一个配置文件就能覆盖不同环境。最直接的价值是:应用一共有 Deplloyment、Service、Ingress 三个对象,每次调整变量,不需要在多个 YAML 文件里跳来跳去,而是在 values 文件里改几行。
配置下发也是个常被忽略的点。ConfigMap 用来保存普通配置,Secret 用来保存敏感信息。它们的公共同步机制天然适合管理多套环境的差异。但注意 Secret 默认只是做了 base64 编码,不是真正的加密,如果集群没有开启加密,敏感信息最好接入外部密钥管理服务。
Service Mesh 这类工具适合服务较多且对可观测性要求高的场景,它通过给每个 Pod 注入边车代理来治理流量,实现灰度、熔断、链路追踪等能力。但它的运维成本同样不低,团队规模小或者服务数量少时,不建议一开始就上。
6.2 什么场景不一定要用 K8s
坦诚地讲,有些事情并不需要 Kubernetes 来兜底。如果你的项目只有三五台机器、十个以内的容器,并且没有频繁发布和弹性伸缩的需求,用 docker compose 加一个简单的监控脚本可能比 K8s 舒服得多——省掉控制平面的维护、网络插件的调试、存储和权限的配置,这些成本对一个小团队来说并不轻松。
如果你的应用是无状态且可以多次重启的,K8s 很适合;但如果你要跑的是一堆有状态应用,比如数据库集群,直接上 K8s 会把复杂度拉到一个很高的级别。倒不是说不能跑,而是自带存储、集群容灾这些工作都需要额外的设计和经验。先评估团队有没有能力维护这套体系,是更实际的问题。
6.3 关于学习路线,我个人实际经历过的一个建议
回过头看自己的学习路径,发现最快入门的顺序是:先理解 Docker,再学会用 docker compose 管理一组容器,然后逼着自己在一个轻量 K8s 环境里把同一个应用部署跑通——不跳步骤,不用托管服务帮你隐藏网络细节。整个过程会有很多“为什么这么搞”的疑问,但一旦把一次完整的容器从代码到 Pod 的这条链路走通,前面的迷雾就会散一层。之后再去啃那些高级功能,比如自动伸缩、自定义控制器、多集群管理,就有抓手了。
Kubernetes 的门槛不在于某个概念有多难,而在于一次要接触太多新概念。解决的办法不是背概念,而是反复验证一条最小链路:打镜像、建 Deployment、暴露 Service、发一次新版、做一次回滚。这条链路跑顺了,后续再碰到疑难问题,你就知道该从哪个对象入手排查了。