之前在业务迭代中,单机用 Docker 部署应用时一切正常,但一旦涉及多实例扩展、滚动更新、故障恢复,手工docker run就会变得非常吃力。于是很自然地开始接触 Kubernetes,却在学习初期被一堆概念卡住:Docker 和 Kubernetes 到底是不是替代关系?容器和 Pod 有什么区别?有了 Docker 为什么还需要编排?本文基于实际使用经验,将 Docker 与 Kubernetes 的分工、边界、协作方式逐步拆解,并给出一套从单机容器到集群调度的完整实操示例,帮助新手建立整体认知,也让有基础的同学快速对照排查。
1. 容器与编排:先把两个概念放在同一个坐标系里
1.1 容器到底是什么
容器本质上是一个操作系统层面的虚拟化技术。它利用 Linux 内核的 Namespace 实现资源视图隔离,通过 Cgroup 实现 CPU、内存等资源限制,再借助 UnionFS 实现分层镜像的文件系统复用。说的直白一点:容器就是一个被隔离出来的“进程组”,这个进程组拥有自己独立的文件系统、网络栈、进程列表,但它共享宿主机的操作系统内核。
与虚拟机相比,容器不需要模拟完整硬件,也不需要单独安装 Guest OS,所以它的启动速度更快、资源占用更小、单机密度更高。这也是容器在微服务架构中迅速流行的根本原因。
从部署层面来看,容器最大的价值是“一致性”:开发环境、测试环境、生产环境共用同一份镜像,避免了“在我机器上是好的”这种经典问题。
1.2 Docker 解决的是“单机容器”问题
Docker 是当前最流行的容器运行时之一。它提供了一整套工具链,让我们可以:
- 通过 Dockerfile 构建镜像;
- 通过镜像仓库分发镜像;
- 通过 docker run 启动容器;
- 通过 docker stop、docker restart 管理容器生命周期;
- 通过 docker compose 在单机上编排多个容器。
注意,Docker 本身主要作用于一台宿主机。虽然 Docker 也有 Swarm 模式,但在实际生产环境中,Swarm 的使用率远不如 Kubernetes,目前大多数团队已经把 Kubernetes 作为容器编排的事实标准。
1.3 Kubernetes 解决的是“集群容器调度”问题
Kubernetes 是一个开源的容器编排平台,常简称为 K8s。它运行在多台服务器组成的集群之上,核心职责可以概括为几个方面:
- 调度:把一个应用副本调度到集群中合适的节点上;
- 伸缩:根据 CPU、内存或自定义指标自动增减副本数;
- 自愈:副本挂掉后自动重建,节点故障后自动迁移应用;
- 服务发现与负载均衡:通过 Service 为多个副本提供稳定的访问入口;
- 滚动更新:发布新版本时逐个替换旧副本,期间服务不中断。
也就是说,Kubernetes 并不关心你的应用是用 Docker 构建还是用 containerd 构建,它关心的是“集群中有多少副本在运行、副本的健康状态如何、如何把流量分发到健康副本上”。
1.4 Docker 与 Kubernetes 的分工边界
用一句话来概括分工:
Docker 负责“把应用装进标准化的容器里”,Kubernetes 负责“把容器安排到集群里并维持运行状态”。
两者不是二选一的关系,而是上下游关系。Kubernetes 本身早期默认使用 Docker 作为容器运行时,后来逐步支持 containerd、CRI-O 等其他运行时。当前主流 K8s 集群中,很多已经默认使用 containerd,但 Docker 镜像依然是通用格式,开发者日常构建镜像时使用 Docker,集群运行时负责拉取并运行这套镜像。
为了更好地理解,可以从下面几个维度划分:
| 维度 | Docker | Kubernetes |
|---|---|---|
| 主要作用 | 构建镜像、运行单机容器 | 管理跨节点容器集群 |
| 最小管理单元 | 容器 | Pod |
| 伸缩能力 | 手动/脚本实现 | 自动伸缩(HPA) |
| 故障恢复 | 单机层面有限 | 集群层面自动重建 |
| 网络方案 | 单机端口映射 | 集群内 DNS、Service、Ingress |
| 适用场景 | 开发、测试、单机部署 | 生产集群、微服务架构 |
2. 环境准备与核心术语
2.1 实验环境说明
本文的实操示例涉及 Docker 与 Kubernetes 两部分。版本方面需要说明:不同发行版和云环境的版本差异比较大,请以你自己的实际环境为准。本文示例以常见环境为例,重点演示配置思路。
建议准备以下环境:
- 一台可运行 Docker 的服务器或使用 Docker Desktop(Windows/Mac);
- 可选一个轻量 Kubernetes 环境,例如 kind、minikube,或者一台已部署好的 K8s 集群;
- 命令行工具:docker CLI、kubectl CLI;
- 一个简单的 Web 应用项目,用于构建镜像。
如果你的机器上还没有安装 Docker,安装时常见问题包括:Windows 下 Docker Desktop 提示 virtualization support 未开启,Linux 下出现permission denied while trying to connect to the Docker daemon socket。这类问题在后面的常见问题章节会专门说明。
2.2 镜像、容器、Pod、Deployment 等术语对照
初学者经常把下面几个术语混在一起,这里先统一梳理。
- 镜像(Image):一个只读模板,包含应用代码、运行时环境、系统依赖和配置。镜像类似“安装包”。
- 容器(Container):镜像运行时的实例,有自己的文件系统、进程、网络隔离。
- Pod:Kubernetes 的最小调度单元,一个 Pod 内可以包含一个或多个容器,它们共享网络命名空间和存储卷。日常使用中,一个 Pod 通常只跑一个主容器。
- Deployment:一种 Kubernetes 工作负载资源,负责管理一组无状态应用的副本,支持滚动更新和回滚。
- Service:为 Pod 提供稳定访问入口的抽象,它通过标签选择器(Selector)匹配一组 Pod,并做负载均衡。
- Namespace:Kubernetes 中的逻辑隔离空间,用来划分环境,例如 dev、test、prod。
把这些概念串起来就是一条链路:
Dockerfile → 镜像 → 容器; 镜像上传到仓库后,Kubernetes 通过 Deployment 声明“我要跑 3 个副本”,调度器创建多个 Pod; Service 再给这组 Pod 提供一个统一入口; 外部流量通过 Ingress 或 NodePort 进入 Service,最终转发到某个 Pod 内运行的容器。
3. Docker 核心能力拆解
3.1 镜像构建:用 Dockerfile 描述应用
Dockerfile 是一个文本文件,每一行指令都会生成一个镜像层。下面是一个最简单的 Python Flask 应用的 Dockerfile。
# 文件路径:demo-app/Dockerfile FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 5000 CMD ["python", "app.py"]对应需要两个文件:
# demo-app/requirements.txt flask==3.0.0# demo-app/app.py from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello from Docker + Kubernetes" if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)构建命令:
cd demo-app docker build -t demo-app:v1 .这里有几个关键点需要解释:
WORKDIR /app是给容器内部设置工作目录,避免后续指令路径混乱;COPY指令把宿主机文件复制到镜像内;RUN pip install在构建阶段安装依赖,依赖会被固化进镜像;EXPOSE只是声明容器内监听端口,并不会自动映射到宿主机;CMD是容器启动时执行的默认命令。
为什么要这样分层写?因为 Docker 构建时会尽量复用未变化的镜像层。当我们只修改 app.py 时,前面的基础镜像层和依赖安装层都会命中缓存,构建速度会明显加快。
3.2 容器运行与资源隔离
镜像构建完成后,启动容器:
docker run -d --name demo-1 -p 8080:5000 demo-app:v1参数说明:
-d:后台运行;--name demo-1:给容器命名;-p 8080:5000:把宿主机 8080 端口映射到容器内 5000 端口。
启动后访问http://localhost:8080就能看到页面内容。
如果希望限制容器资源,可以增加参数:
docker run -d --name demo-2 -p 8081:5000 --cpus 0.5 --memory 256m demo-app:v1--cpus 0.5表示容器最多使用 0.5 个 CPU 核心,--memory 256m表示内存上限为 256 MB。这里体现的正是 Cgroup 的隔离能力。资源限制在实际生产环境中很重要,因为一个容器如果无限占用内存,可能导致宿主机内存耗尽,进而影响其他容器。
查看容器状态:
docker ps docker statsdocker stats可以看到容器的实时 CPU、内存、网络和磁盘 IO 使用情况,是排查资源问题时的常用命令。
3.3 网络与数据卷
容器默认使用 Bridge 网络,多个容器可以通过容器名称互相访问。但要注意,容器是短暂资源,一旦被删除,容器内的数据也会丢失。所以需要持久化数据时,必须使用数据卷(Volume)或绑定挂载(Bind Mount)。
docker run -d --name nginx-demo -p 80:80 \ -v /opt/html:/usr/share/nginx/html \ nginx:latest上面的-v /opt/html:/usr/share/nginx/html表示把宿主机的/opt/html目录挂载到容器内的 Nginx 静态目录,宿主机上的文件变更会即时反映到容器中。这个特性在本地开发时非常方便。
3.4 Docker Compose 的“轻量编排”
Docker Compose 可以用一个 YAML 文件定义多个服务,适合在单机上启动一整套应用栈,例如前端 + 后端 + 数据库。
下面是一个示例:
# docker-compose.yml version: "3.8" services: web: build: . ports: - "8080:5000" environment: - DB_HOST=db depends_on: - db db: image: postgres:15-alpine environment: POSTGRES_USER: demo POSTGRES_PASSWORD: demo123 POSTGRES_DB: demo volumes: - db-data:/var/lib/postgresql/data volumes: db-data:启动命令:
docker compose up -d查看服务状态:
docker compose psCompose 解决的问题是“单机多容器编排”。它通过depends_on控制启动顺序,通过自定义网络让服务之间通过服务名互相访问。但它在跨多台服务器的场景下无能为力,这时就需要 Kubernetes 出场。
4. Kubernetes 核心能力拆解
4.1 Pod:最小调度单元
在 Kubernetes 中,我们通常不会直接运行一个“孤零零的容器”,而是把一个或多个容器封装进 Pod。Pod 里的所有容器共享同一个网络命名空间,也就是说它们可以通过 localhost 互相访问,同时共享存储卷。
为什么 Kubernetes 要引入 Pod 而不是直接操作容器?原因是有一些应用场景需要多个进程紧密协作,例如:
- 一个 sidecar 容器负责日志收集,主容器负责业务请求;
- 一个容器负责启动前初始化数据,完成后退出;
- 主容器和辅助容器需要共享同一份文件目录。
如果直接调度容器,这种“多进程协作”的语义会非常难管理。Pod 作为调度单元,既保留了容器的隔离性,又提供了协作能力。
4.2 Deployment:声明式副本管理
Deployment 是 Kubernetes 中最常用的工作负载类型。它声明了期望状态,例如“我要运行 3 个副本,镜像为 demo-app:v2”,Kubernetes 会持续努力让实际状态向期望状态靠拢。
下面是一个 Deployment 示例:
# k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: demo-app:v1 ports: - containerPort: 5000 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 300m memory: 256Mi readinessProbe: httpGet: path: / port: 5000 initialDelaySeconds: 3 periodSeconds: 5几个关键点:
selector.matchLabels用来匹配模板中的标签,Deployment 通过标签控制下辖的 Pod;resources.requests是调度时的资源请求,集群调度器会依据它决定 Pod 放在哪个节点;resources.limits是运行时的资源上限;readinessProbe是就绪探针,只有探针成功后,Pod 才会被标记为就绪并接收 Service 流量。
创建 Deployment:
kubectl apply -f k8s/deployment.yaml查看状态:
kubectl get deployment kubectl get pods4.3 Service:稳定的访问入口
Pod 的 IP 地址会随着重建而变化,不能直接作为对外访问地址。Service 解决了这个问题:它通过 Label Selector 匹配一组 Pod,并提供一个固定的 ClusterIP 和 DNS 名称。
# k8s/service.yaml apiVersion: v1 kind: Service metadata: name: demo-app-service spec: selector: app: demo-app ports: - protocol: TCP port: 80 targetPort: 5000 type: ClusterIP这里port: 80是 Service 的端口,targetPort: 5000是后端 Pod 的容器端口。外部流量进入 Service 后,会被转发到某个健康的 Pod 上。
如果想从集群外部访问,可以把 type 改为 NodePort,或使用 Ingress 进行域名和路径路由。
kubectl apply -f k8s/service.yaml kubectl get service4.4 自动伸缩与自愈
Kubernetes 的独特优势在于“自动化运维能力”。横向自动伸缩(HPA)可以根据 CPU 使用率等指标自动增减副本数。下面是一个简单的 HPA 配置:
# k8s/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: demo-app-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: demo-app minReplicas: 2 maxReplicas: 5 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60创建 HPA 后,当 Pod 的平均 CPU 使用率超过 60% 时,副本数会自动向最大值扩展;当负载降低后,会逐步缩回最小值。
自愈能力同样重要。如果你手动删除一个 Pod:
kubectl delete pod <pod-name>Deployment 会立即发现实际副本数小于期望副本数,然后自动创建一个新的 Pod 顶上。这是 Deployment 这种“控制循环”机制的核心表现。
5. 从 Docker 到 Kubernetes 的完整实战
这一节把前面两个工具串起来,完成一次完整部署演示。
5.1 使用 Docker 构建并运行示例应用
首先准备应用文件,内容如第 3 章所示。构建镜像:
cd demo-app docker build -t demo-app:v1 .然后本地运行验证:
docker run -d --name demo-1 -p 8080:5000 demo-app:v1 curl http://localhost:8080预期输出:
Hello from Docker + Kubernetes5.2 使用 Docker Compose 完成本地多容器编排
在项目根目录创建 docker-compose.yml,内容如前文所示。启动:
docker compose up -d docker compose ps这一步演示的是“单机编排”:Web 服务和数据库服务可以一键启动、一键停止。它适合开发环境,但不具备跨节点伸缩能力。
5.3 使用 Kubernetes 部署同一套应用
接下来把同一个镜像部署到 Kubernetes 集群。这里假设你已经有可用集群,并且可以通过 kubectl 访问。
先创建命名空间:
kubectl create namespace demo将镜像导入集群节点,或者推送到镜像仓库。如果是单节点 kind 或 minikube,可以通过kind load docker-image demo-app:v1这类方式加载本地镜像。
创建 Deployment:
kubectl apply -f k8s/deployment.yaml -n demo kubectl apply -f k8s/service.yaml -n demo查看资源状态:
kubectl get deployment -n demo kubectl get pods -n demo kubectl get service -n demo如果使用 minikube,可以用下面命令访问服务:
minikube service demo-app-service -n demo如果使用 kind,可以通过端口映射方式访问 NodePort 或配置 Ingress 访问。
5.4 验证滚动更新与故障恢复
修改容器镜像版本,例如将 app.py 中的返回文本改为Hello from Docker + Kubernetes v2,重新构建镜像并打新标签:
docker build -t demo-app:v2 .然后更新 Deployment 的镜像:
kubectl set image deployment/demo-app demo-app=demo-app:v2 -n demo滚动更新过程查看:
kubectl rollout status deployment/demo-app -n demo这条命令会阻塞等待,直到滚动更新完成。期间老的 Pod 会逐步下线,新的 Pod 会逐步上线。
接着做一次故障恢复测试。找出当前运行的 Pod 并删除:
kubectl get pods -n demo kubectl delete pod <pod-name> -n demo再次查看 Pod 列表,可以发现名字不同的新 Pod 自动出现,这说明 Deployment 的自愈机制已经生效。
6. 常见问题与排查思路
6.1 Docker 相关高频问题
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
permission denied while trying to connect to the Docker daemon socket | 当前用户未加入 docker 用户组 | 执行sudo usermod -aG docker $USER后重新登录;生产环境谨慎使用 sudo |
| Docker Desktop 提示 virtualisation support wasn't detected | 宿主机未开启虚拟化 | 进入 BIOS 开启 VT-x/AMD-V,并检查 Windows Hypervisor 是否启用 |
| 镜像下载慢 | 默认仓库网络不稳定 | 配置国内镜像加速器或使用内网镜像仓库,注意不要使用来源不明的加速地址 |
| 启动容器后立即退出 | 应用启动命令错误或依赖缺失 | 执行docker logs <container>查看日志,确认 CMD 是否合法 |
| Windows 下 Docker Desktop 报“应用程序特定权限设置”类错误 | Docker 相关服务权限异常 | 检查服务状态,必要时重置 Docker Desktop 数据目录,数据需提前备份 |
排查时的一个实用顺序是:先docker ps看容器是否在运行,再docker logs看应用日志,最后docker inspect检查挂载、网络和环境变量。
6.2 Kubernetes 相关高频问题
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Pod 一直处于 Pending | 节点资源不足或调度器无法安排 | 执行kubectl describe pod <pod>查看调度事件,检查资源请求是否超过集群容量 |
| Pod 反复 CrashLoopBackOff | 应用启动失败或探针配置不正确 | 查看kubectl logs,重点检查就绪探针路径和端口是否匹配 |
| Service 无法访问 | Selector 和 Pod 标签不匹配 | 执行kubectl get pods --show-labels核对标签;再检查 targetPort 是否正确 |
| 更新镜像后没有新版本效果 | imagePullPolicy 为 IfNotPresent 且本地有同名旧镜像 | 显式修改镜像标签或设置 imagePullPolicy: Always,注意生产环境版本管理 |
| 删除 Pod 后一直重建 | Deployment 的期望副本数大于当前数 | 使用kubectl rollout restart deployment/<name>或直接删除 Deployment |
使用kubectl describe是最有效的排查方式,它会把调度事件、探针失败原因、挂载错误等展示得非常清楚。
7. 最佳实践与工程建议
7.1 镜像层面的建议
- 尽量使用明确版本号的基础镜像,避免使用 latest 标签,因为 latest 在时间维度上是不可控的。
- 合并 RUN 指令,减少镜像层数,同时避免把敏感文件写入镜像。
- 多阶段构建是减少镜像体积的有效方式。例如 Java 应用可以在构建阶段使用 maven 镜像,在运行阶段只用 slim 版 JRE。
- 镜像仓库建议开启私有仓库权限管理,并定期扫描漏洞。镜像安全和容器安全是生产环境必须关注的两个维度。
7.2 容器与 Kubernetes 资源管理
- 所有容器都应该设置
resources.requests和resources.limits。只设置 limits 可能导致调度时无法判断节点容量,只设置 requests 则可能让某个 Pod 无限占用资源拖垮节点。 - 对于 Java 类应用,要特别注意容器内存限制与 JVM 堆内存的关系,否则可能出现容器内进程已被限制,但 JVM 认为可用内存更大的问题。
- 配置探针很关键:readinessProbe 决定流量是否进入 Pod,livenessProbe 决定容器是否重启。建议先配置 readinessProbe,再逐步引入 livenessProbe。
7.3 配置管理和权限控制
- Kubernetes 中的配置建议使用 ConfigMap 和 Secret,而不是把环境变量硬编码进 Deployment。Secret 默认只是 base64 编码,并不提供加密,生产环境需要结合可用的加密方案或外部密钥管理工具。
- 遵循最小权限原则,无论是 Docker 守护进程权限还是 Kubernetes RBAC 权限,都应该只授予完成任务所需的最小权限。开发环境使用
docker用户组已经足够,生产环境要严格控制节点访问权。 - 操作生产环境前先检查当前上下文。kubectl 默认连接的是当前 kubeconfig 对应的集群,执行
kubectl config get-contexts确认不会操作错误环境。
7.4 发布与回滚策略
- 使用 Deployment 的滚动更新参数控制发布节奏,例如:
spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxSurge表示更新过程中最多允许超过期望副本数的 Pod 数量,maxUnavailable表示最多允许不可用的副本数。通过这两个参数可以做到发布期间始终保持服务可用。
- 发布失败时回滚:
kubectl rollout undo deployment/demo-app -n demo- 数据库变更和代码发布要分开执行,不推荐在应用容器中执行破坏性的数据库 DDL。
8. 深入学习建议
到这里,Docker 与 Kubernetes 的分工已经比较清晰了:
- Docker 帮助你标准化应用的打包和运行,解决单机环境下的容器管理问题;
- Kubernetes 帮助你管理集群中成千上万个容器,解决调度、伸缩、自愈、服务发现和滚动更新问题;
- 两者协作关系是:Docker 构建镜像 → 镜像进入仓库 → Kubernetes 拉取镜像 → 调度器创建 Pod → Service 暴露服务 → 探针和 HPA 维持稳定性。
接下来可以关注以下几个方向:
- 深入学习 Kubernetes 的工作负载类型,除了 Deployment,还有 StatefulSet、DaemonSet、Job、CronJob,它们分别适合有状态应用、守护进程、批处理任务等不同场景;
- 研究 Ingress 与 Gateway API,理解集群外部流量如何进入集群内部;
- 学习容器网络原理,包括 CNI 插件的工作方式,例如 Flannel、Calico 的差异;
- 关注云原生生态其他组件,例如 Prometheus 监控、Grafana 可视化、Loki 日志采集;
- 认真读一遍 Kubernetes 官方文档中关于 kubelet、kube-scheduler、controller-manager 的设计说明,能帮助你理解为什么 Deployment 具有自愈能力。
如果本文对你有帮助,可以收藏备用。还有哪些在实际部署中卡住你的容器或编排问题,欢迎在评论区一起讨论。