1. 一次煮饺子,讲透容器与编排
先说结论:Docker 和 Kubernetes 的关系,本质上就是“煮饺子”和“开饺子馆”的关系。
把饺子丢进沸水,锅是运行环境,饺子是应用,水是操作系统,火是硬件资源。一个饺子能煮熟,靠的是锅、水、火配合得当。但开一家饺子馆,要面对的是几十桌客人、不同馅料的订单、后厨与前厅的协作、高峰期与淡季的流量波动。这时候,一口锅就远远不够了。你需要一口能按需增减灶台的大厨房,需要一个能自动记录“哪桌点了什么馅、煮到几成熟、该上哪一桌”的调度系统。Docker 解决了“单个饺子怎么煮”的问题,Kubernetes 解决的是“整家饺子馆怎么高效运转”的问题。
我见过太多人一上来就啃 Kubernetes,结果被 Pod、Service、Deployment 这些名词砸得晕头转向。其实只要先想明白“容器到底解决了什么”“编排又解决了什么”,后面的一切都会顺理成章。这篇文章就是我从零开始理解云原生时,用煮饺子这个类比打通思路的完整记录,顺带把 Docker 和 Kubernetes 的核心概念、常见坑、实操路径都串一遍。如果你是刚接触云原生的小白,或者已经用过 Docker 但始终没搞懂 Kubernetes 有什么用,这篇文章正合适。
整篇会分四个部分来聊:先拆解 Docker 和 Kubernetes 各自的核心设计,再手把手走一遍从“煮好一个饺子”到“开好一家饺子馆”的完整实操,然后整理我实际踩过的坑和排查思路,最后聊聊从容器到微服务这条路上还会遇到什么。每个部分我都会把概念映射到煮饺子的场景里,尽量做到让你合上文章就能跟别人讲明白。
2. 容器与编排的核心设计思路
2.1 为什么用容器:从“环境搬家”到“厨房标准化”
传统部署应用的方式,相当于每个厨师自带锅和灶,去别人家的厨房干活。你写好的 Java 应用,在本地跑得好好的,到了测试环境的 CentOS 7 上就内存溢出,到了生产环境的 Ubuntu 上又缺依赖库。每个环境都是一间陌生的厨房,你永远不知道灶台火力是否一致、调料摆放是否合理、锅的材质会不会影响口感。
容器把“应用 + 它需要的所有依赖”打包成一个标准化的“饺子”:面皮是基础镜像,馅料是你的代码和运行库,而锅里的水就是宿主机内核。无论这锅水是 Ubuntu 还是 CentOS,只要内核兼容,饺子就能以同样的方式煮熟。这就是 Docker 最核心的价值——交付的不仅是代码,而是整个运行环境。
有人会问:虚拟机也能隔离环境啊,为什么还要容器?打个比方,虚拟机是给每个客人单独配一间厨房,里面要有完整的灶台、水槽、冰箱,启动一台虚拟机就像重新装修一间厨房,动辄几十秒甚至几分钟,资源开销也大。容器则是在同一间大厨房里,用几个独立的锅同时煮不同馅料的饺子,锅与锅之间有隔断但共用同一个水源和火源,启动一个容器只需要几秒钟,因为不需要重复加载操作系统内核。这也是为什么容器能在同样一台物理机上跑出远比虚拟机更多的实例。
2.2 镜像与容器:菜谱与成品的关系
Docker 里有两个高频词:镜像(Image)和容器(Container)。我用饺子来类比:镜像就是菜谱加冷冻饺子的半成品,它把“面皮怎么和、馅料怎么调、包好后怎么保存”全部固化下来;容器就是按照这份菜谱实际煮出来的那一盘饺子,是镜像运行起来后的实例。
镜像有一个关键特性:分层。每一层 Dcokerfile 指令都会生成一个新的只读层,就像包饺子时先擀皮、再放馅、最后捏褶,每一道工序都在前一道工序的基础上叠加。这样的好处是,多个镜像可以共享底层基础层。比如你同时部署 nginx 和 mysql,它们都基于某个基础 Linux 镜像,那底下那几层就不用重复存储和传输,下载新镜像时只需要拉取差异层。这个设计大大加速了镜像分发,也是 Docker 能快速启动的重要因素。
容器的生命周期则和饺子一样:从镜像创建、启动运行、中途可能被停止,也可能被删除后按原镜像再煮一次。要注意的是,容器被删除后,容器内产生的数据也会消失,就像你把一盘饺子吃完了,那张菜谱依然还在,但盘子里的饺子没了。所以需要持久化存储的场景,必须用数据卷(Volume)把数据挂载到宿主机上,相当于把馅料单独存在冰箱里,饺子皮煮烂了,馅料还能拿出来重新包。
2.3 Kubernetes 的三大核心机制:声明式、控制器、调谐
Kubernetes 的复杂度,很大程度上是它的抽象概念带来的。但剥开外壳,核心只有三件事:你想要什么状态(Desired State)、当前是什么状态(Current State)、怎么把当前变成想要(Reconciliation)。
用一个具体的例子:你开了一家饺子馆,生意好的时候需要同时有 50 份饺子在煮,生意差的时候只需要 10 份。传统方式是你盯着后厨,人少了赶紧加锅,人多了赶紧撤锅。Kubernetes 的做法是,你写一份声明:请始终保持 30 份饺子正在煮(这个声明就是 Deployment 对象里的 replicas 字段,replicas = 30)。Kubernetes 里有个叫控制器(Controller)的组件,每分钟检查一次现状,发现锅里只有 20 份,就自动多煮 10 份;发现锅里煮了 35 份,就自动捞起来 5 份。整个过程不需要你动手,控制器会一直循环“检查差异 - 调整 - 再检查”,直到现状和期望完全一致。
这套机制在技术圈里叫“调谐循环”(Reconcile Loop),是 Kubernetes 最精髓的设计。它不像传统运维脚本那样“执行一段操作就结束”,而是永远处于“监控 + 纠正”的状态。这也是为什么 Kubernetes 特别适合处理线上不可预测的故障:某个容器崩溃了,控制器发现数量少于期望值,立刻自动拉起一个新的,全程不需要人工介入。
2.4 为什么不能用 Docker 直接跑所有容器
如果只是在一台服务器上跑几个应用,Docker Compose 其实就够了。Compose 相当于一份“家庭聚餐菜谱”,用 YAML 文件描述了这次聚餐要煮哪些饺子、每盘要几份、谁先下锅谁后下锅。单机场景下,Compose 的体验非常流畅。
但一旦应用多起来,Docker 本身的短板就暴露了。第一,没有自动恢复能力。Docker 本身没有内建的机制来监控 mysql 容器挂了然后自动拉一个新的起来,除非你写一堆 shell 脚本配合 cron。第二,没有弹性伸缩。活动大促时想让 nginx 从 2 个实例变成 20 个实例,Docker 单机无法自动感知流量并扩容。第三,没有跨节点调度。你有多台物理机,Docker 不知道把容器放在哪台机器上更合适。第四,没有服务发现和负载均衡。多个容器之间如何互相找到对方,端口变化了怎么把请求转发到正确位置,这些都需要额外搭一套系统。
Kubernetes 把这些能力全部内置了。它给你的不只是“能跑容器”的能力,而是一整套“管理容器生命周期”的框架。就像饺子馆不只是需要锅,还需要菜单(配置)、服务员(Service)、洗碗工(回收销毁容器)、店长(控制器)——这些角色,Kubernetes 里都有对应的组件。
3. 从煮好一个饺子到开好一家饺子馆:实操全流程
3.1 准备厨具:安装 Docker Desktop
这里以 Windows 和 macOS 用户最常用的 Docker Desktop 为例。安装过程本身不复杂,但有几个非常容易踩的坑:安装后提示“virtualization support not detected”或者“virtualisation support wasn't detected”,十有八九是因为 BIOS 里的虚拟化开关没打开,VT-x 或者 AMD-V 被禁用了。进 BIOS 找到 Intel Virtualization Technology 或 SVM Mode,设为 Enabled,然后重启。
装完 Docker Desktop 之后,先不用急着写 YAML,先在终端验证一下环境是否正常。
docker version docker info看到 Client 和 Server 版本都能显示出来,就说明 Docker 引擎已经在运行了。如果你的 Windows 上跑的是老版本系统,或者装的是 Docker Toolbox 那种古董,建议直接升级到 Docker Desktop,别再折腾 VirtualBox 的坑了。Linux 用户则直接装 docker.io 或者用官方脚本,过程相对简单,但注意 Ubuntu 老版本(比如 14.04)需要升级内核才能正常跑容器。
3.2 煮第一个饺子:运行 Nginx 容器
Nginx 是容器世界的“Hello World”,因为它轻量、无状态、跑起来立竿见影。在终端里执行这一行:
docker run -d -p 8080:80 --name my-nginx nginx:1.25这条命令的意思是:从 Docker Hub 拉取 nginx:1.25 镜像,以前台还是后台的方式运行?-d 表示后台运行;-p 8080:80 表示把宿主机的 8080 端口映射到容器内的 80 端口;--name 给容器起个名字叫 my-nginx。
映射端口这个动作,可以理解为:你把煮好的饺子从后厨端到前厅,客人从前厅的窗口(8080 端口)拿走饺子,而后厨里的锅(容器内 80 端口)只认自己的窗口。如果不开映射,容器里的 nginx 虽然在 80 端口监听,但外界根本访问不到——相当于饺子在后厨煮好了,但没人把它端出来。
启动成功后,打开浏览器访问 http://localhost:8080,看到 Nginx 的欢迎页,第一个饺子就算煮成功了。此时你可以在docker ps里看到容器状态,用docker logs my-nginx查看日志,用docker stop my-nginx把容器停下来。这些命令都是基本中的基本,必须形成肌肉记忆。
3.3 一次煮多个饺子:Docker Compose 编排多容器
单个饺子煮好了,接下来要同时煮 mysql 和 redis,还要让它们能互相通信。如果手动一条条 docker run,光是端口、网络、依赖关系就能把人绕晕。这时候用 Docker Compose 就舒服多了。
在项目目录下创建一个 docker-compose.yml,内容如下:
version: '3.8' services: mysql: image: mysql:8.0 container_name: blog-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: blog ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: blog-redis restart: always ports: - "6379:6379" volumes: mysql-data:这个文件声明了两个服务:mysql 和 redis。mysql 容器需要数据持久化,所以我声明了一个叫 mysql-data 的卷,挂载到容器内的 /var/lib/mysql。这样即使容器删了重建,数据库数据也不会丢。redis 则不需要持久化,因为它存的是缓存,丢了可以从数据库重新加载。
启动方式:
docker compose up -d-d依然是后台运行。执行后,Compose 会自动创建一个默认网络,让这两个容器在同一个网络里可以互相用服务名访问。比如你的后端应用可以用mysql:3306或者redis:6379来连接它们,而不需要关心它们的 IP 地址。这就是服务发现的基础雏形。
这里有个容易犯的错:mysql 容器启动后,立即用客户端去连接会报错“Access denied”或者“Can't connect”。这是因为 mysql 初始化需要时间,容器状态变成 running 不代表数据库已经 ready。解决办法是在 docker-compose.yml 里加 healthcheck,或者在自己的应用里加重试逻辑。Compose 本身不支持原生的 depends_on 等待健康检查的条件判断,但在 Kubernetes 里,这个功能是通过探针(Probe)实现的,后面会讲到。
3.4 把饺子馆开起来:Kubernetes 部署 Nginx 的最小配置
本机想体验 Kubernetes,最简单的方式是直接用 Docker Desktop 自带的 Kubernetes 集群。在 Docker Desktop 的设置里勾选 Enable Kubernetes,等一两分钟,集群就起来了。
然后你需要一个管理工具 kubectl。macOS 上brew install kubectl,Windows 上可以直接用 Docker Desktop 自带的 kubectl 命令。验证集群:
kubectl cluster-info kubectl get nodes看到节点状态是 Ready,饺子馆就开门了。接下来部署我们的第一个 Kubernetes 资源。创建一个 nginx-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80执行kubectl apply -f nginx-deployment.yaml。这行命令不是“启动容器”,而是告诉 Kubernetes:“我希望集群里始终有 3 个 nginx Pod(Pod 是一组容器的组合,通常一个 Pod 里就一个容器)。”Kubernetes 的控制器会创建 3 个 Pod,如果其中某个 Pod 崩溃了,控制器会立刻再创建一个新的,保证永远是 3 个。
查看状态:
kubectl get pods kubectl get deployment此时 Pending、ContainerCreating、Running……等你看到 3 个 Running,就说明饺子馆已经按声明煮出 3 盘饺子了。
3.5 给客人一个统一入口:Service 与负载均衡
如果直接访问这三个 Pod 的 IP,你会发现两个问题:Pod 的 IP 是随时变化的,而且你有 3 个后端,前端到底该访问哪个?这时需要引入 Service。Service 在 Kubernetes 里相当于饺子馆的“前厅领位员”,对外暴露一个稳定的访问地址,然后把客人均匀地引导到后厨的各个灶台。
再创建一个 service.yaml:
apiVersion: v1 kind: Service metadata: name: nginx-service spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80 nodePort: 30080这个 Service 会匹配带有app: nginx标签的所有 Pod,并给它们创建一个统一的入口。NodePort 类型意味着,集群中每一台节点都会监听 30080 端口,把流量转发到后端任意一个 Pod。浏览器访问http://localhost:30080,你会发现能正常打开 Nginx 页面。多刷新几次,Kubernetes 默认的负载均衡策略(轮询)会让请求轮流转到不同的 Pod,你可以在 Nginx 日志里看到不同 Pod 的访问记录。
3.6 从 3 盘饺子到 30 盘:弹性的真正价值
手动改 YAML 文件里的 replicas 再重新 apply 也能扩容。但真实场景里,我们希望 Kubernetes 能自动根据 CPU 或内存使用率来扩缩容。这就是 Horizontal Pod Autoscaler(HPA,横向 Pod 自动扩缩器)。
打个比方:你开饺子馆,平时后厨 3 口锅就够。到了饭点客人暴增,后厨的锅用得太满了(CPU 超过某个阈值),HPA 自动增加锅的数量;过了饭点客人少了,HPA 自动撤掉多余的锅,省燃气。
创建一个 HPA:
kubectl autoscale deployment nginx-deployment --cpu-percent=50 --min=1 --max=10这条命令的意思是:当平均 CPU 使用率超过 50% 时,自动将 nginx-deployment 的副本数扩展到最多 10 个;CPU 降下来后,再缩到最少 1 个。这正是我前面说的“调谐循环”的实战体现——你只声明了边界(1~10),剩下的交给控制器。
等待一分钟后查看:
kubectl get hpa在 REPLICAS 一列可以看到当前的副本数。如果你用压测工具比如 ab 或者 wrk 给 Service 发请求,一分钟后就能看到 HPA 自动扩容,副本数从 3 涨到 5、8 甚至 10。这个体验确实震撼,也是 Kubernetes 与 Docker 单机拉开差距的核心功能。
4. 常见的坑与排查技巧实录
4.1 安装阶段的三个典型故障
| 现象 | 原因 | 解决办法 |
|---|---|---|
| Docker Desktop 无法启动,提示 virtualization support not detected | BIOS 未开启硬件虚拟化 | 重启进 BIOS,开启 Intel VT-x 或 AMD-V |
| 启动 Docker 后 docker ps 报 permission denied | 当前用户不在 docker 用户组 | 执行sudo usermod -aG docker $USER,重新登录 |
| CentOS 7 升级 Docker 后服务起不来 | 旧版本配置文件不兼容 | 清理 /etc/docker/daemon.json 中废弃参数,用journalctl -u docker查看日志 |
第四个高频问题是镜像下载慢。Docker Hub 在国内访问确实不稳定,解决方案是配置镜像加速器。在 Docker Desktop 的 Docker Engine 配置里,加入"registry-mirrors": ["https://docker.m.daocloud.io"](这是最常用的公共镜像加速站,也可用其他稳定服务),然后 Apply & Restart。
4.2 容器网络不通的排查思路
容器内应用能跑,但外部访问不了,是新手期最常遇到的问题。先按这个顺序排查:
docker ps # 看容器是否 Running docker port my-nginx # 看端口映射是否生效 curl -v http://localhost:8080 # 看宿主机能否访问 docker exec -it my-nginx bash # 进入容器内 curl localhost:80 验证容器内服务 docker logs my-nginx # 看应用日志有没有报错一句口诀:一查容器状态,二查端口映射,三查容器内服务,四查防火墙。特别是云服务器,安全组没放行端口是常见坑,本机 curl 通了,但公网访问不了,基本都是云厂商安全组的问题,跟 Docker 没关系。
另一个常见的网络坑是多个容器之间互相访问不到。用 Docker Compose 启动的所有服务都在同一个默认网络里,可以用服务名互通。如果你手动 docker run 的容器,默认是各自独立的网络,互相之间无法用容器名解析。解决办法是 docker run 时指定--network my-network,或者把容器加入同一个已存在的网络docker network connect my-network 容器名。
4.3 Kubernetes 排障三板斧
Kubernetes 排障比 Docker 复杂,因为多了 Pod、ReplicaSet、Service 这些层。我总结了一句话流程:先看 Pod,再看事件,最后看日志。
kubectl get pods # Pod 是否 Running kubectl describe pod <name> # 查看 Pod 的事件,比如镜像拉取失败、探针失败 kubectl logs <name> # 看容器内应用的输出我遇到最多的一个问题是:镜像拉取策略导致 Pod 一直 ImagePullBackOff。比如你本地改了镜像但 tag 没变,Kubernetes 默认可能不会重新拉取。避免办法是给镜像打不同的 tag,比如nginx:1.25-myapp-20250101,或者设置imagePullPolicy: Always。
另一个坑是 Service 的 selector 写错,导致 Service 没有匹配到任何 Pod。这时kubectl get endpoints nginx-service会显示<none>,表示 Service 背后的 Pod 列表是空的。我因为 label 大小写写错排查了半小时,后来养成了先kubectl get pods --show-labels再写 selector 的习惯。
还有一次,Pod 一直处于 CrashLoopBackOff 状态,kubectl logs看不到任何输出。最后才发现是应用的配置文件里连接数据库的地址写成了 localhost,导致应用启动时连不上数据库,反复重启。在 Kubernetes 里,连接到同一个命名空间内的其他服务,应该用服务名比如mysql-service:3306,而不是 localhost。
4.4 数据持久化的经典误区
容器是“一次性”的,这是我从 Docker 迁移到 Kubernetes 后适应了很久才彻底内化的观念。很多人刚接触时,总觉得容器就像一台小虚拟机,可以在里面随意安装、修改、保存,重启后一切还在。实际上,容器被删除后,容器文件系统里的所有改动都随之消失。
我的经验是:凡是需要长期保存的数据(数据库文件、应用上传的图片、日志文件),必须走数据卷(Docker Volume)或持久卷(PV/PVC)。在 docker-compose 里我用volumes: - mysql-data:/var/lib/mysql;在 Kubernetes 里则用 PersistentVolumeClaim 声明一块存储,挂载到 Pod 里。千万别为了方便,把数据直接写在容器内部。
另外,迁移到 Kubernetes 时还要注意多副本的存储问题。如果 Deployment 有 3 个副本,并且它们都挂载了同一个可写数据卷,就需要确认这个数据卷是否支持多节点读写。比如 NFS 支持多节点读写,而大多数云盘只支持单节点读写。这类架构问题如果不在早期发现,上线后数据损坏的代价会非常高。
4.5 一个小技巧:用健康检查避免假活
容器处于 Running 状态不等于应用是可用的。比如 Java 应用在启动时可能还没完全加载完,或者某个依赖服务没就绪,但容器的进程已经在运行了。如果 Kubernetes 把流量转发到这个“假活”的 Pod,用户就会遇到连接被拒绝或超时。
解决办法是配置探针。在 Deployment 的配置里加 readinessProbe 和 livenessProbe:
readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 20readinessProbe 决定这个 Pod 什么时候可以开始接收流量,livenessProbe 决定这个 Pod 是否还活着,如果多次失败,Kubernetes 会杀掉容器并重新创建。这在煮饺子的场景里,就相当于后厨多了一个“试吃员”:饺子没煮熟之前,不允许端给客人;如果发现锅里的饺子煮烂了,直接倒掉重煮。
我在实际项目中养成的习惯是,每个应用都必须暴露一个 /health 接口,返回应用的依赖状态(数据库连接、缓存连接等)。没有健康检查的应用,在 Kubernetes 里就是一颗定时炸弹,表面上一切正常,实际上随时可能炸在关键时刻。
5. 从煮饺子到开中央厨房:云原生的下一步
跑通 Docker 和 Kubernetes 只是起点。当我把越来越多的应用容器化之后,会发现还有一堆新问题等着解决:配置怎么管理、密钥怎么保护、应用之间的调用怎么治理、流量怎么灰度发布。这些在云原生生态里都有对应的工具。
配置和密钥:应用需要数据库密码、API Key,总不能写死在镜像里。Kubernetes 提供了 ConfigMap 和 Secret 来解耦配置与镜像,相当于饺子馆的“调料间”——饺子的配方固定,但今天放多少盐、多少醋,是可以通过调料间动态调整的,不用重新包饺子。
服务治理:当应用拆分成几十个微服务后,服务之间的调用变得极其复杂,需要熔断、限流、超时控制。Service Mesh(比如 Istio)这时候就有用了,它相当于在后厨和餐桌之间加了一条自动化传送带,每条传送带都自带温度和湿度监控,任何一条链路出问题都能预警和容错。不过 Service Mesh 有一定学习成本,建议先把 Kubernetes 基础玩熟再上。
CI/CD:云原生时代讲究“不可变部署”——镜像一旦构建,就不允许在运行环境中打补丁,环境有什么问题就回滚镜像版本。配合 GitOps 工具(比如 Argo CD),每次修改代码都会自动构建新镜像、推送到镜像仓库、更新 Kubernetes 里的 Deployment 对象。这就好比饺子馆的中央厨房统一配送半成品饺子到各分店,任何一家分店都不会因为厨师心情不同导致饺子口味飘忽。
我个人在实际操作中最受益的一点是:不要把这三件事分开学。Docker、Kubernetes、云原生是一条线,单学 Docker 会觉得容器化部署很简单,单学 Kubernetes 会觉得概念抽象难以下手。但当你脑子里始终带着“煮饺子”和“开饺子馆”这两个画面,Docker 就是那个能让每个饺子都煮得一模一样的标准锅,Kubernetes 就是那个能让饺子馆自动化运转的店长。你写出的 YAML 不再是一堆陌生的字段,而是给店长下的一个个清晰指令。最后再分享一个经验:学容器编排,一定不要只看文章或视频,打开终端,把 Nginx、MySQL、Redis 这几个最基础的应用亲手用 Docker Compose 跑起来,再用 kubectl 部署一遍,多敲几次命令,多踩几次坑,比任何理论都管用。