引言:为什么 Docker 熟练工也会在 K8s 面前感到困惑?
在当今的云原生时代,Docker 已经成为了容器技术的代名词。很多开发者通过 Docker 入门容器世界:学会了编写 Dockerfile 将应用打包成镜像,掌握了 docker run命令启动容器,能够熟练部署 Redis、MySQL 等中间件,也理解了 --restart参数如何让容器在退出后自动重启。从单机应用的角度看,这些技能已经足够让应用在本地或单台服务器上可靠运行。
然而,当这些技能被带入真实的线上环境、面对生产系统的复杂性时,一个令人困惑的现象出现了:明明 Docker 用得滚瓜烂熟,为什么一到线上就处处碰壁?
这个问题的答案并不在于个人技术能力,而在于技术本身的定位边界。就像精通驾驶汽车并不意味着能管理整个城市的交通系统一样,精通 Docker 容器也不等于能管理分布式的容器化应用系统。这种认知的跃迁,正是从 Docker 到 Kubernetes 必须跨越的关键鸿沟。
一、Docker 的设计边界:它从未承诺解决"系统级"问题
1.1 Docker 的核心价值:单机容器化
Docker 在技术史上的革命性贡献可以用一句话概括:"它标准化了应用的打包、分发和运行方式"。在 Docker 之前,开发与运维之间最大的矛盾之一就是环境不一致问题------"在我本地是好的,怎么到服务器上就不行了?" Docker 通过容器镜像技术,将应用及其所有依赖(包括运行时、系统工具、库文件等)打包在一起,形成了一个可移植、自包含的交付单元。
从工程实现角度看,Docker 主要解决了以下几个关键问题:
-
环境一致性:镜像包含了应用运行所需的一切,消除了"环境差异"导致的问题
-
资源隔离:通过 Linux 内核的命名空间和控制组(cgroups)技术,实现进程、网络、文件系统等层面的隔离
-
便携性:一次构建,到处运行(前提是内核兼容)
-
轻量快速:相比虚拟机,容器启动更快、资源开销更小
这些特性让 Docker 在开发、测试和单机部署场景中表现卓越。然而,当我们把这些优势放到多机、多服务的生产环境中审视时,就会发现 Docker 的能力边界。
1.2 当单机遇到集群:Docker 无法回答的系统级问题
在实际的生产环境中,你会很快遇到一系列 Docker 自身无法回答的问题:
问题一:镜像的分发与同步
在一台机器上,docker pull可以拉取镜像。但当你需要在一百台机器上部署同一个服务时,你需要手动或编写脚本在每台机器上执行拉取命令。更复杂的是镜像版本管理:如何确保所有机器上的镜像版本一致?如何高效地分发大型镜像?如何避免因网络问题导致的部分节点镜像拉取失败?
问题二:故障域与高可用
Docker 的 --restart策略确实可以在容器异常退出时自动重启,但这只解决了"容器级"的故障恢复。如果容器所在的宿主机(物理机或虚拟机)本身宕机了呢?容器会随着宿主机的宕机而全部消失,此时再多的重启策略也无济于事。这就是典型的"单点故障"问题。
问题三:弹性伸缩的人工成本
当流量高峰来临时,你需要快速扩容服务实例。使用 Docker,这意味着需要在更多机器上手动启动容器,配置网络,挂载存储。高峰过后,又需要手动清理这些资源。这个过程不仅耗时耗力,而且极易出错。
问题四:服务发现的脆弱性
在微服务架构中,服务之间需要相互调用。使用 Docker 时,常见的做法是通过 IP 地址和端口进行通信。但容器的 IP 地址是动态分配的,每次重启都可能变化。即使使用链接(link)功能,也仅限于单机环境。跨主机的服务发现成为了一个需要自行解决的难题。
问题五:发布更新的服务中断
更新应用版本时,通常需要停止旧容器,启动新容器。这个过程中服务会出现短暂不可用。虽然可以通过一些技巧实现蓝绿部署,但这些都需要额外的工具和复杂的脚本,而且缺乏统一的管理和监控。
1.3 问题的本质:Docker 的设计哲学边界
这些问题的共同特征是:它们都发生在"多容器、多机器"的系统层面,而不是单个容器的运行层面。
Docker 的官方定位是"容器运行时和打包工具",它的设计边界非常清晰:单机 + 单容器生命周期管理。Docker 从未承诺,也从未试图解决分布式系统中的调度、编排、服务发现、弹性伸缩等问题。
用一个类比来理解:Docker 就像是一个优秀的"集装箱"技术,它标准化了货物的包装和装卸方式。但管理一个港口,需要的是集装箱的调度系统、吊车的协调、堆场规划、船只安排等更高级别的系统。Kubernetes 正是这个"港口管理系统"。
二、Kubernetes 的本质:集群级的 Docker 管理与调度系统
2.1 一句话定义 Kubernetes
如果要用一句话概括 Kubernetes 是什么,最准确的描述是:Kubernetes 是一个"集群级的 Docker 管理与调度系统"。
这个定义包含了几个关键点:
-
集群级:Kubernetes 的视野是整个集群(多台机器),而不是单台机器
-
管理:包括部署、更新、回滚、扩缩容等全生命周期管理
-
调度:决定容器应该在集群的哪台机器上运行
-
系统:它是一个完整的、自动化的系统,而不仅仅是一个工具
Kubernetes 不是 Docker 的替代品,而是 Docker 的"管理者"。在 Kubernetes 中,Docker 仍然是实际的容器运行时,负责在单个节点上创建和运行容器。Kubernetes 则站在更高维度,决定哪些容器应该在哪些节点上运行,如何管理它们之间的网络通信,如何确保它们的高可用性。
2.2 核心理念:从"人肉运维"到"系统自治"
Kubernetes 最根本的设计哲学是:把需要人工决策的运维操作,转化为系统自动执行的过程。
在传统的运维模式中,当服务需要扩容时,工程师需要:
-
查看监控,判断是否需要扩容
-
选择在哪些机器上启动新实例
-
登录这些机器,执行启动命令
-
配置负载均衡,将流量导入新实例
-
验证新实例是否正常工作
在 Kubernetes 中,你只需要声明:"我希望这个服务始终保持5个运行实例"。系统会自动监控当前状态,如果发现只有3个实例,会自动创建2个新实例;如果发现太多负载,会自动调整实例分布。这种转变是从"怎么做"(命令式)到"要什么"(声明式)的根本性转变。
三、声明式 vs 命令式:两种完全不同的工程思维
3.1 命令式思维:关注过程与控制
Docker 的使用方式是典型的命令式(Imperative)思维。命令式思维关注"如何做"(How),你需要给出具体的执行步骤:
bash
# 启动一个 Redis 容器
docker run -d --name redis -p 6379:6379 redis:6.0
# 停止这个容器
docker stop redis
# 重启这个容器
docker restart redis
在这种模式下,你是系统的"操作者",直接对系统发出具体指令。系统执行你的指令,但不知道你的最终目标是什么。如果中间任何一步出错,你需要手动处理异常情况。
3.2 声明式思维:关注目标与状态
Kubernetes 采用的是声明式(Declarative)思维。声明式思维关注"要什么"(What),你只需要描述期望的最终状态:
bash
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-deployment
spec:
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:6.0
ports:
- containerPort: 6379
在这份配置中,你声明了:
-
我需要一个名为 redis-deployment 的部署
-
它应该始终保持3个副本
-
使用 redis:6.0 镜像
-
容器暴露6379端口
你不需要告诉 Kubernetes:
-
在哪些机器上启动容器
-
如何拉取镜像
-
如何配置容器网络
-
如何监控容器状态
-
容器挂了怎么恢复
3.3 控制论视角下的 Kubernetes
Kubernetes 的声明式模型深受控制论(Cybernetic)思想影响。在控制论中,一个典型的控制系统包括:
-
期望状态(Desired State):你希望系统达到的状态
-
当前状态(Current State):系统当前的实际状态
-
控制回路(Control Loop):不断比较期望状态和当前状态,并采取措施减少两者差异
Kubernetes 的各个控制器就是这样的控制回路。以 Deployment 控制器为例,它持续运行一个循环:
-
观察:读取 Deployment 对象中声明的期望状态(如副本数=3)
-
比较:检查当前集群中匹配的 Pod 数量
-
执行:如果当前只有2个 Pod,就创建1个新的;如果有4个,就删除1个
-
等待:短暂等待后,回到第1步
这个过程无限循环,确保系统状态始终向期望状态收敛。这种设计使得系统具有自我修复(Self-healing)能力,能够自动应对各种异常情况。
四、Node:重新理解 Kubernetes 中的"机器"
4.1 Node 的本质:资源的提供者与故障边界
在 Kubernetes 中,Node 的概念看似简单,实则包含深刻的设计考量。Node 的本质是:提供计算资源的实体,通常是物理机或虚拟机。
但"机器"这个概念在 Kubernetes 中被重新定义和抽象。Node 的核心职责不是"管理",而是:
-
提供资源:向集群提供 CPU、内存、存储和网络资源
-
运行容器:为 Pod 提供实际的运行环境
-
定义故障域:Node 的故障意味着运行其上的所有 Pod 都需要迁移
-
资源边界:Node 的物理限制(如 CPU 核心数、内存大小)定义了资源分配的上限
4.2 澄清常见误解:Node 不是最小资源单位
一个常见的误解是将 Node 视为 Kubernetes 中的"最小资源单位",认为调度器总是以整个 Node 为单位进行调度。这种理解是不准确的。
正确的理解是:Node 是资源的提供边界,Pod 是资源的请求单位,而容器是资源的使用实体。
当你在 Pod 中定义:
resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m"
你是在向集群请求资源,而不是向特定 Node 请求。Kubernetes 调度器会查看所有 Node 的剩余可用资源,选择能够满足这个请求的 Node 来运行 Pod。Node 只是资源的"提供者",而不是"分配单位"。
4.3 Node 作为故障边界的设计含义
将 Node 设计为故障边界,是 Kubernetes 高可用设计的基础。这意味着:
-
故障隔离:一个 Node 的故障不应该影响其他 Node
-
故障恢复:Node 故障时,其上的 Pod 会被重新调度到其他健康 Node
-
滚动更新:可以通过逐个更新 Node 来实现集群的无中断升级
这种设计使得集群可以容忍单点(甚至多点)故障,实现真正的高可用性。
五、Pod:为什么 Kubernetes 不直接调度容器?
5.1 Pod 的设计动机:解决容器间的"亲密关系"问题
这是理解 Kubernetes 架构的第一道门槛,也是最重要的概念之一。一个很自然的问题是:既然最终运行的是容器,为什么 Kubernetes 不直接调度容器,而要引入 Pod 这个额外的抽象层?
答案是:因为现实世界中的应用很少是单一容器独立运行的。
考虑以下常见场景:
-
日志收集:应用容器需要与日志收集容器(如 Fluentd)共享日志文件
-
服务网格:应用容器需要与边车容器(Sidecar,如 Envoy)共享网络栈
-
文件同步:Web 服务器容器需要与内容同步容器共享网站文件
-
本地缓存:多个容器需要访问同一个内存缓存
这些场景中的容器对具有以下特征:
-
需要部署在同一台机器上
-
需要共享本地存储
-
需要通过 localhost 或共享内存通信
-
具有紧密耦合的生命周期
Docker 虽然提供了容器间通信的机制(如 --link、共享卷),但这些机制是命令式的、临时性的,缺乏统一的管理抽象。Pod 正是为了解决这个问题而设计的。
5.2 Pod 的工程定义:共享命运的一组容器
从工程角度,Pod 可以定义为:一组生命周期强耦合、必须运行在同一 Node 上、共享部分命名空间的容器集合。
理解 Pod 的关键在于理解它的共享机制:
-
共享网络命名空间:Pod 中的所有容器共享同一个 IP 地址和端口空间,它们可以通过 localhost 相互访问
-
共享存储卷:Pod 可以定义存储卷(Volume),这些卷可以被挂载到多个容器中
-
共享 IPC 命名空间:容器可以通过 System V IPC 或 POSIX 消息队列通信
-
共享 UTS 命名空间:容器共享同一个主机名和域名
-
共享 PID 命名空间(可选):容器可以相互看到进程
这些共享机制使得 Pod 内的容器可以像同一台机器上的进程一样紧密协作。
5.3 Pod 作为最小调度单位的意义
将 Pod 作为最小调度单位,而不是单个容器,具有重要的工程意义:
-
原子性调度:确保紧密耦合的容器总是被一起调度,避免了分散部署导致的协调问题
-
资源保证:Kubernetes 可以保证 Pod 所需的所有资源在同一个 Node 上得到满足
-
简化网络:Pod 有唯一的 IP 地址,外部通过这个 IP 访问 Pod,不需要关心内部有多少容器
-
统一生命周期:Pod 的启动、停止、重启是原子操作,内部容器一起被管理
这种设计体现了计算机科学中一个经典原则:将经常一起变化的东西放在一起(Common Closure Principle)。Pod 内的容器因为紧密耦合,所以一起调度、一起管理;而 Pod 之间相对独立,可以独立调度和管理。
六、Node 与 Pod 的约束关系:为什么 Pod 不能跨 Node?
6.1 一个基本原则:Pod 的边界就是单台 Node
这是 Kubernetes 设计中一个基本但至关重要的约束:一个 Pod 的所有容器必须运行在同一个 Node 上,不能跨 Node 分布。
这个约束不是技术限制(理论上可以通过分布式技术实现容器跨节点协同),而是经过深思熟虑的设计选择。理解这个选择背后的原因,是理解 Kubernetes 调度和故障模型的关键。
6.2 约束背后的工程考量
网络层面的原因:localhost 语义的崩溃
Pod 内的容器共享网络命名空间,它们通过 localhost 相互访问。localhost 的语义是"本机",如果容器分布在不同的物理机器上,localhost 的语义就崩溃了。虽然可以通过虚拟网络技术模拟 localhost,但这会引入巨大的复杂性和性能开销。
存储层面的原因:本地存储的语义问题
Pod 内的容器可以共享存储卷(Volume)。当这些卷是本地存储(如 hostPath、emptyDir)时,它们实际上对应着 Node 上的目录或文件。如果容器跨 Node 分布,共享本地存储就变得不可能,或者需要引入复杂的分布式文件系统。
调度层面的原因:资源协调的复杂性
Kubernetes 调度器需要为 Pod 找到满足其所有资源需求的 Node。如果 Pod 可以跨 Node,调度器需要同时找到多个 Node,这些 Node 的组合需要满足 Pod 的总需求。这会将调度问题从一维(单个 Node 的选择)提升到多维(多个 Node 的组合选择),大大增加调度算法的复杂性。
故障处理层面的原因:部分故障难以定义和处理
如果 Pod 跨 Node 运行,当其中一个 Node 故障时,Pod 处于"部分存活"状态。这种状态难以定义和处理:Pod 是算存活还是故障?存活的容器应该继续运行还是终止?如何重新调度?
6.3 这个约束带来的设计简洁性
Pod 不能跨 Node 的约束实际上带来了系统设计的简洁性:
-
清晰的故障边界:Node 故障意味着其上的所有 Pod 都故障,故障处理逻辑简单清晰
-
简单的调度模型:调度器只需要为 Pod 找到一台合适的 Node,而不是多台
-
直观的网络模型:Pod IP 对应实际 Node 上的网络端点
-
高效的本地通信:容器间通过本地 IPC 或共享内存通信,无需经过网络
这个约束也促使开发者合理设计应用架构:紧密耦合的组件放在同一个 Pod 中,通过本地通信;相对独立的组件放在不同 Pod 中,通过服务发现和网络通信。这正是微服务架构的实践原则。
七、ReplicaSet:单一职责的数量守护者
7.1 ReplicaSet 的精准定位
在 Kubernetes 的架构中,每个组件都有明确的单一职责。ReplicaSet 的职责可以用一句话概括:确保指定标签选择器匹配的 Pod 副本数量始终符合预期。
这个定义包含了几个关键点:
-
指定标签选择器:ReplicaSet 通过标签选择器(Label Selector)识别它要管理的 Pod
-
副本数量:ReplicaSet 只关心数量,不关心 Pod 的具体内容
-
始终符合预期:这是一个持续的过程,不是一次性的动作
7.2 ReplicaSet 的工作机制:持续的控制回路
ReplicaSet 控制器的工作机制是一个典型的控制回路:
-
观察:查询 API Server,获取当前集群中匹配其标签选择器的所有 Pod
-
比较:将实际 Pod 数量与期望副本数(spec.replicas)比较
-
行动:
-
如果实际数量少于期望数量,创建新的 Pod
-
如果实际数量多于期望数量,删除多余的 Pod
-
-
等待:短暂间隔后,重新开始观察
这个过程无限循环,确保系统状态始终向期望状态收敛。这种机制使得 ReplicaSet 具有自我修复能力:如果它管理的 Pod 被意外删除(如 Node 故障、人为误操作),ReplicaSet 会自动创建新的 Pod 来替代。
7.3 为什么需要 ReplicaSet?而不让 Deployment 直接管理 Pod?
这是一个很好的设计问题。从表面看,Deployment 可以直接创建和管理 Pod,为什么要引入 ReplicaSet 这个中间层?
答案是:为了职责分离和版本管理。
考虑一个场景:应用从 v1 版本升级到 v2 版本。如果没有 ReplicaSet:
-
Deployment 需要直接删除 v1 的 Pod,创建 v2 的 Pod
-
如果升级过程中出现问题,需要回滚
-
回滚时,Deployment 需要删除 v2 的 Pod,重新创建 v1 的 Pod
-
Deployment 需要自己记录历史版本信息
这种设计会让 Deployment 变得复杂,因为它需要同时处理:
-
期望状态的定义
-
Pod 的生命周期管理
-
版本历史管理
-
滚动升级逻辑
引入 ReplicaSet 后,职责被清晰分离:
-
Deployment 只关心"期望状态":使用哪个镜像、需要多少副本、如何升级
-
ReplicaSet 只关心"数量保证":确保特定版本的 Pod 数量符合预期
-
Pod 只关心"运行应用":实际运行容器化应用
这种分层设计使得每个组件都简单、专注,易于理解和维护。
八、Deployment:期望状态的描述者
8.1 Deployment 的误解与正解
一个常见的误解是:"Deployment 直接管理 Pod"。这个误解源于我们通常通过 Deployment 来创建和管理应用,但实际上,Deployment 并不直接与 Pod 交互。
正确的理解是:Deployment 是应用期望状态的描述者,而不是 Pod 的直接管理者。
Deployment 的主要职责包括:
-
描述期望状态:应用应该使用哪个镜像、运行多少副本、使用什么配置
-
管理发布策略:如何从当前版本升级到新版本(滚动更新、蓝绿部署等)
-
管理版本历史:保留历史版本信息,支持快速回滚
-
协调 ReplicaSet:创建和管理代表不同版本的 ReplicaSet
8.2 Deployment 与 ReplicaSet 的协作关系
Deployment 和 ReplicaSet 的协作关系可以用以下流程理解:
bash
Deployment (描述期望状态)
|
| 创建并管理
v
ReplicaSet-v1 (保证 v1 版本有3个副本)
|
| 创建和管理
v
Pod-v1-xxxxx (实际运行 v1 版本应用)
Pod-v1-yyyyy
Pod-v1-zzzzz
当需要升级到 v2 版本时:
bash
Deployment (更新期望状态:使用 v2 镜像)
|
| 创建新的 ReplicaSet
v
ReplicaSet-v2 (逐步创建 v2 版本的 Pod)
| |
| 逐步扩缩容 | 逐步缩容
v v
Pod-v2-aaaaa Pod-v1-xxxxx (逐步减少)
Pod-v2-bbbbb Pod-v1-yyyyy
Pod-v2-ccccc Pod-v1-zzzzz
最终,v1 版本的 Pod 全部被 v2 版本替代,但 ReplicaSet-v1 仍然被保留(用于可能的回滚)。
8.3 Deployment 的声明式升级策略
Deployment 的核心价值之一是为应用升级提供了声明式的策略。你不需要编写复杂的脚本来控制升级过程,只需要声明升级策略,Deployment 会自动执行。
以滚动更新(RollingUpdate)为例,你只需要在 Deployment 中声明:
bash
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
这表示:
-
升级策略是滚动更新
-
最多可以比期望副本数多1个 Pod(maxSurge)
-
升级过程中始终有100%的 Pod 可用(maxUnavailable: 0)
基于这个声明,Deployment 会自动执行以下步骤:
-
创建新的 ReplicaSet(对应新版本)
-
逐步创建新版本的 Pod(每次最多新增1个)
-
等待新 Pod 就绪
-
逐步删除旧版本的 Pod
-
重复2-4,直到所有 Pod 都替换为新版本
-
如果新 Pod 就绪检查失败,自动暂停升级
整个过程完全自动化,无需人工干预。这体现了 Kubernetes 声明式模型的强大之处:你只需要告诉系统"要什么",系统自动解决"怎么做"。
九、为什么需要分层:从单一组件到清晰职责
9.1 分层的设计哲学
Kubernetes 的分层设计(Deployment -> ReplicaSet -> Pod -> Container)体现了软件工程中的重要原则:单一职责原则 和 关注点分离。
每个层级都有明确的职责边界:
-
Deployment 层:应用生命周期管理
-
关注:应用的整体状态、发布策略、版本历史
-
不关注:具体的 Pod 实例、调度细节
-
-
ReplicaSet 层:副本数量管理
-
关注:确保指定版本的 Pod 数量符合预期
-
不关注:Pod 的具体内容、升级策略
-
-
Pod 层:容器编排单元
-
关注:一组容器的协同运行
-
不关注:副本数量、版本管理
-
-
Container 层:应用运行环境
-
关注:单个应用的运行
-
不关注:与其他容器的关系、资源调度
-
这种分层设计使得系统具有很好的可扩展性和可维护性。每层只需要关注自己的职责,通过清晰的接口与上下层交互。
9.2 分层的实际价值
价值一:支持复杂的发布策略
因为有 ReplicaSet 这一层,Deployment 可以同时管理多个版本的 ReplicaSet。这使得复杂的发布策略(如金丝雀发布、蓝绿部署)成为可能。Deployment 可以控制新旧版本的比例,逐步将流量切换到新版本。
价值二:简化回滚操作
当新版本有问题时,回滚只需要将 Deployment 指向旧版本的 ReplicaSet。因为旧版本的 ReplicaSet 仍然存在(只是副本数为0),回滚可以瞬间完成。如果没有 ReplicaSet 这一层,Deployment 需要从历史记录中重新创建旧版本的 Pod,过程会更复杂、更耗时。
价值三:清晰的版本管理
每个 ReplicaSet 对应一个具体的应用版本(通过镜像标签识别)。通过查看集群中的 ReplicaSet,可以清楚地知道当前和历史版本。这种清晰的版本管理是系统可观测性的重要部分。
价值四:独立的伸缩能力
Horizontal Pod Autoscaler(HPA)可以直接与 ReplicaSet 交互,根据指标自动调整副本数。因为 ReplicaSet 的职责单一(只关心数量),所以与 HPA 的集成非常简单直接。
十、一次完整的发布过程:K8s 内部的视角
10.1 发布前的状态
假设我们有一个运行中的应用,通过以下 Deployment 管理:
bash
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:v1.0.0
此时,Kubernetes 内部的状态是:
-
一个名为
myapp的 Deployment 对象,期望状态是3个副本,使用myapp:v1.0.0镜像 -
一个由 Deployment 创建的 ReplicaSet,假设名为
myapp-7fd5c5f5b4,确保有3个 Pod -
3个运行中的 Pod,每个 Pod 运行一个
myapp:v1.0.0的容器
10.2 触发发布:更新 Deployment
当我们更新 Deployment,将镜像版本改为 v1.0.1:
bash
kubectl set image deployment/myapp myapp=myapp:v1.0.1
或者通过更新 YAML 文件。这个操作修改了 Deployment 对象的期望状态。
10.3 Kubernetes 的响应:控制回路开始工作
-
Deployment 控制器检测到变化
Deployment 控制器定期检查 Deployment 对象的当前状态与期望状态。当发现镜像版本变化时,它知道需要进行一次更新。
-
创建新的 ReplicaSet
Deployment 控制器创建一个新的 ReplicaSet,命名为类似
myapp-7c6b5c4d3b的名称。这个新的 ReplicaSet 的 Pod 模板使用新镜像myapp:v1.0.1,但初始副本数可能为0。 -
执行滚动更新策略
根据 Deployment 中定义的策略(默认是 RollingUpdate),开始逐步替换 Pod:
-
新的 ReplicaSet 逐步增加副本数(比如从0增加到1)
-
旧的 ReplicaSet 逐步减少副本数(比如从3减少到2)
-
等待新 Pod 就绪(通过就绪探针检查)
-
继续逐步替换,直到新 ReplicaSet 有3个副本,旧 ReplicaSet 有0个副本
-
-
Pod 的创建与调度
当新的 ReplicaSet 需要创建 Pod 时:
-
向 API Server 提交 Pod 创建请求
-
调度器(Scheduler)监听到新的 Pod,开始调度决策:
a. 过滤(Filtering):排除不满足要求的 Node(资源不足、不满足节点选择器等)
b. 打分(Scoring):对满足要求的 Node 进行评分
c. 绑定(Binding):将 Pod 绑定到得分最高的 Node
-
目标 Node 上的 kubelet 监听到绑定事件:
a. 拉取镜像
myapp:v1.0.1b. 创建容器运行时(如 Docker)容器
c. 启动容器
d. 执行启动后钩子和就绪探针
-
-
流量切换
如果使用了 Service,流量会自动切换到新 Pod。因为 Service 通过标签选择器选择 Pod,而新旧 Pod 都有
app: myapp标签,所以 Service 会将流量同时路由到新旧 Pod。随着旧 Pod 被逐步终止,流量自然迁移到新 Pod。 -
更新完成
当新 ReplicaSet 的副本数达到3,旧 ReplicaSet 的副本数降到0时,更新完成。但旧 ReplicaSet 仍然被保留(用于可能的回滚)。
10.4 关键洞察:Deployment 不知道具体 Pod
在整个过程中,一个关键点是:Deployment 从不直接管理或知道具体的 Pod 实例。
Deployment 只与 ReplicaSet 交互,告诉每个 ReplicaSet 应该有多少副本。ReplicaSet 负责创建和管理具体的 Pod。这种间接性提供了重要的抽象和灵活性。
十一、Kubernetes 的核心思想浓缩
11.1 重要的设计理念
理念一:声明式优于命令式
不要告诉系统"怎么做",告诉系统"要什么"。系统会自动计算当前状态与期望状态的差异,并采取措施缩小差异。这种控制回路模式使得系统具有自我修复能力和最终一致性。
理念二:控制器模式
Kubernetes 的核心是控制器模式。每个控制器都是一个独立的过程,监视一类资源的状态,并采取措施使实际状态向期望状态收敛。这种模式使得系统易于理解和扩展。
理念三:面向API的设计
Kubernetes 的所有功能都通过 API 暴露。无论是 kubectl 命令行工具,还是 Dashboard 界面,或是其他客户端,都通过相同的 API 与系统交互。这种一致性简化了工具开发和系统集成。
理念四:松耦合的架构
组件之间通过清晰的 API 接口交互,每个组件有明确的职责边界。这种松耦合设计使得系统易于理解、调试和扩展。
11.2 对开发者的启示
启示一:从"操作者"到"声明者"的思维转变
使用 Kubernetes 时,你需要从"如何部署应用"的命令式思维,转变为"应用应该是什么状态"的声明式思维。编写 YAML 文件不是编写执行脚本,而是描述期望状态。
启示二:理解抽象的价值
Pod、Service、Deployment 等都是抽象。这些抽象隐藏了底层复杂性,提供了统一的接口。理解每个抽象的职责和边界,是有效使用 Kubernetes 的关键。
启示三:拥抱最终一致性
Kubernetes 是最终一致的系统。当你修改一个配置时,系统不会立即达到新状态,而是逐步向新状态收敛。这种模式可能需要一些适应,但它提供了更好的弹性和可靠性。
启示四:关注分离的重要性
Kubernetes 的分层设计体现了关注分离的原则。作为开发者,你的应用也应该遵循这个原则:容器只关注应用运行,Pod 关注容器组合,Deployment 关注应用生命周期。每层只关注自己的职责。
结语:从 Docker 到 Kubernetes 的思维升级
从 Docker 到 Kubernetes 的过渡,本质上是思维模式的升级:
-
从单机思维到集群思维:不再关注单个容器如何运行,而是关注整个应用系统如何在集群中运行
-
从命令式思维到声明式思维:不再编写具体的操作步骤,而是描述期望的最终状态
-
从手动操作到自动运维:将重复的运维决策交给系统自动执行
-
从关注实例到关注模式:不再关心具体的容器实例,而是关心应用部署和管理的模式
这种思维升级是云原生时代的核心要求。Kubernetes 不仅仅是一个工具,更是一种新的应用管理和运维范式。掌握这种范式,意味着你不再仅仅是应用的开发者,更是系统的设计者。
Kubernetes 的学习曲线确实陡峭,但一旦理解了其核心思想和设计哲学,你会发现它提供了一种优雅、统一的方式来管理复杂的分布式系统。这种理解不是通过记忆命令和参数获得的,而是通过理解系统背后的设计原则和工程考量获得的。
希望这篇从工程视角的梳理,能够帮助你跨越从 Docker 到 Kubernetes 的理解鸿沟,不仅在工具使用层面,更在系统设计层面,理解 Kubernetes 为什么这样设计,以及如何利用这些设计构建更可靠、更易管理的应用系统。