从一次生产事故说起。凌晨两点线上服务的旧版本突然开始疯狂报错新功能上线后的内存泄漏把整台机器拖到卡死。我登录服务器看了一眼进程又看了一眼时间——回滚需要重打JAR包、重新部署、重启服务保守估计四十分钟。但如果当时用的是容器整个操作只需要一条命令docker run换成上一个镜像标签等几十秒服务就恢复了。那次之后我把团队里所有的应用都容器化了。这篇文章不是讲某个单一工具的教程而是把一个完整的链讲透Docker、Docker Compose、Docker Swarm、Kubernetes这四个词经常同时出现很多人把它们混在一起觉得都是容器技术但其实它们是四个不同层级、解决不同问题的东西。我会从实际使用者的角度把这四者的定位、关系、坑以及从单机到集群的演进逻辑拆开来聊。如果你是一个刚开始接触容器技术的开发者或运维工程师或者已经在用 Docker 但还没搞明白为什么要上 Kubernetes这篇文章可以帮你把整条路线串起来。1. 容器技术解决的根本问题交付物不再是代码而是完整运行环境很多人第一次接触 Docker 时最大的困惑是我为什么要用这个我本地直接跑不行吗这个问题恰恰是容器技术存在的根本原因。1.1 从在我机器上是好的说起Java 应用本地跑得好好的部署到 Linux 服务器上就起不来Python 脚本在 Windows 上运行正常到了 CentOS 上报一堆依赖缺失前端构建产物在 Node 18 环境下没问题换到 Node 16 就编译报错。这些场景每个后端工程师都遇到过根子在于代码只是交付物的一部分运行时环境才是另一半。传统解决方式是写部署文档但文档永远是滞后且不完整的总有一行命令没记录到。镜像的出现改变了一切。镜像本质上是把操作系统的一个用户空间快照——包括基础文件、依赖库、运行时、应用代码、配置文件——全部打包在一个只读文件集合里。这意味着交付物从一个源码压缩包变成了一整个自包含的运行单位环境差异不再影响应用本身。你在笔记本上构建的镜像可以原封不动地跑到生产服务器的容器引擎上行为完全一致。1.2 镜像的层次化设计与不可变交付镜像并不是一个大大的黑盒子它由一层层的只读文件系统叠加而成。每一层都对应 Dockerfile 中的一条指令。这种设计带来两个关键收益一是共享同一台机器上多个镜像的基础层可以共用存储空间十几个 Spring Boot 应用共享同一个基础 Java 镜像层磁盘开销大幅降低二是增量传输推送或拉取镜像时只需要传输新增的层而不是整个镜像。生产环境中最重要的一点是镜像的不可变性。一旦构建完成镜像内容不再改变一个 SHA256 摘要就能唯一标识一个交付物部署什么就运行什么。相比从 Git 拉代码再编译的方式这种方式消除了一类常见的部署问题——代码版本相同但构建环境不同导致产物不同。1.3 容器与虚拟机资源开销不在一个量级虚拟机里跑应用也能解决环境隔离问题但它太重了。每台虚拟机需要完整的客户操作系统CPU、内存都被虚拟化层吃掉一部分一台 16G 内存的物理机跑三四个虚拟机就有些吃力。容器完全不同它不虚拟化硬件而是通过 Linux 内核的命名空间隔离资源和进程视图通过 cgroups 限制资源配额直接共享宿主机内核。比如说在虚拟机里启动一个 Ubuntu 环境大概需要几秒钟到半分钟而容器引擎里启动一个 Ubuntu 容器只需要几十毫秒——因为不需要启动一个完整的内核。这种开销差异直接影响开发迭代速度和组织方式虚拟机是一台小机器容器是一个进程两者的心智模型完全不同。2. Docker 入门镜像、容器、数据卷、网络一次讲清楚Docker 是最基础的容器运行时用法本身并不复杂但要真正用好它需要理解几个核心概念背后的运作机制。2.1 镜像与容器的关系类比程序与进程镜像Image和容器Container的关系可以类比为程序文件与运行中的进程。镜像是一个静态的、只读的文件集合容器是镜像运行时生成的隔离环境。同一个镜像可以启动多个容器它们之间互不影响因为每个容器都有自己独立的可写层和命名空间。拿我常用的命令来举例# 从 Docker Hub 拉取镜像到本地 docker pull nginx:1.25 # 基于镜像创建并启动一个容器 docker run -d --name web -p 8080:80 nginx:1.25 # 进入运行中容器的交互式 Shell docker exec -it web bash # 查看容器日志支持 -f 实时跟踪 docker logs -f webdocker run是使用频率最高的命令它有几个参数需要清楚其中的逻辑-d让容器在后台运行因为很多应用不是交互式进程如果不用-d前台会一直占据你的终端。-p 宿主机端口:容器端口做端口映射。容器有自己独立的网络命名空间宿主机不能直接访问容器 IP必须把容器内的端口映射出来。--name给容器起个名字后面操作就不需要记一长串容器 ID 了。--restartalways设置容器退出后自动重启策略。但注意重启只是重启容器进程不是修复服务问题。2.2 写 Dockerfile构建镜像的标准姿势生产环境几乎不会直接docker run一个已存在的镜像而是基于 Dockerfile 构建自己的应用镜像。一个典型的 Spring Boot 应用 Dockerfile 长这样FROM openjdk:17-jdk-slim WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]关键点在于多阶段构建 —— 编译和运行分离镜像里不保留编译工具和源码体积可以小好几个数量级。比如一个 Go 应用的最终镜像甚至可以只有 10MBFROM golang:1.22 AS builder WORKDIR /src COPY . . RUN CGO_ENABLED0 go build -o app . FROM alpine:3.20 COPY --frombuilder /src/app /usr/local/bin/app ENTRYPOINT [app]第一阶段的编译环境全部丢弃只复制可执行文件到轻量级的 Alpine 基础镜像中。这种做法在微服务架构下非常重要——服务数量多每个镜像小一点传输和启动都快一点。构建镜像时还有一个小技巧利用缓存分层机制把依赖安装放在代码复制之前。COPY go.mod和RUN go mod download放在最前面源码放在最后面。这样每次改代码之后重新构建只有最后一层需要重建前面几步都能命中缓存构建时间可以大幅缩短。2.3 数据卷与端口映射绕过容器文件系统的两大限制容器是隔离的同时意味着两点容器内写文件是临时的容器删除后数据也就没了容器内的端口外界访问不到。一个用于存数据的容器绝不能这样这就引出-v挂载和网络端口映射。# 把宿主机目录挂载到容器内 docker run -d --name mysql -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDyourpassword \ mysql:8.0MySQL 的数据库文件会直接写入宿主机的/data/mysql目录即使容器被删除重建数据依然在。-e环境变量是向容器内注入配置的标准方式之一MySQL 官方镜像就是通过环境变量来初始化 root 密码和数据库的。这里有个新手容易犯错的地方-v /data/mysql:/var/lib/mysql是目录挂载如果宿主机目录里已经存在文件挂载后会把容器目录里的内容遮盖掉。如果/data/mysql是空的容器启动时会拷贝镜像中该目录的内容到宿主机但如果宿主机目录里已有旧版本的数据容器使用的就是这份旧数据可能和镜像版本不兼容。所以做数据挂载前最好先确认宿主机目录是干净的或者用具名卷如-v mysql-data:/var/lib/mysql避免歧义。2.4 常见坑时区、日志和权限问题把一台服务器当成永久的临时环境跑容器会踩到几个高频坑。时区问题不少官方镜像是基于 Alpine 或者最小化系统默认时区是 UTC容器内的日志时间会和中国标准时间差 8 个小时。解决办法很简单在 Dockerfile 里显式设置ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone日志爆炸问题容器默认把 stdout/stderr 直接写到宿主机磁盘如果不作限制一个日志量大且没有轮转的容器能把整个磁盘写满导致宿主机不可用。必须在启动参数里加上日志轮转配置docker run -d --log-opt max-size50m --log-opt max-file5 ...更推荐的做法是用 JSON 文件格式的日志驱动配合 Docker 配置里的 Daemon 级默认限制。权限问题容器内部默认以 root 运行虽然容器内的 root 受到命名空间限制但如果磁盘挂载目录是宿主机上的普通目录就会出现容器内以 root 写入的文件宿主机普通用户删不掉的情况。在 Dockerfile 里显式指定运行用户是个好习惯RUN useradd -m appuser USER appuser这条命令加上之后容器内进程权限立即降级。另一个常见场景是 Docker DesktopWindows/Mac 上的虚拟化检查失败报错说 virtualization support not detected。这通常不是 Docker 的问题而是 Windows 的 Hyper-V / WSL2 功能没有启用需要在开机时进入 BIOS 开启 VT-x 虚拟化支持然后重新安装 WSL2 内核。这种问题在物理机上很常见虚拟机里更容易被忽略因为在虚拟机软件里嵌套虚拟化默认是关闭的。3. Docker Compose从手工敲命令升级为声明式编排如果系统只有一个容器docker run就够了。但实际业务里一个应用往往依赖 MySQL、Redis、消息队列等多个服务。如果每个服务都手工敲一长串启动命令不仅容易漏参数而且团队其他人无法一眼看清整个系统依赖了什么。Docker Compose 就是为了解决多容器应用的编排定义而出现的。3.1 为什么需要 Compose把环境定义为代码Compose 的核心价值是把一系列容器的启动方式、网络关系、依赖顺序写在一个 YAML 文件里这个文件就是整个应用环境的基础设施即代码。比如一个 Web 应用依赖数据库和缓存它的docker-compose.yml长这样version: 3.8 services: mysql: image: mysql:8.0 container_name: app-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: appdb volumes: - mysql-data:/var/lib/mysql ports: - 3306:3306 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine ports: - 6379:6379 app: build: . depends_on: mysql: condition: service_healthy redis: condition: service_started environment: DB_HOST: mysql REDIS_HOST: redis ports: - 8080:8080启动整个系统只需要一条命令docker compose up -d停止则对应docker compose down一个 YAML 文件描述整个环境一个命令启停所有服务新人加入团队后不需要挨个问数据库连接地址是什么看文件就都明白了。这种“环境即文件”的思想是团队成员协作的基础。3.2 compose 文件的核心结构services、networks、volumesCompose 文件顶层通常有三个主要部分services定义每个容器服务networks定义容器之间的通信网络volumes定义持久化存储。三者配合才能把多容器环境组织起来。默认情况下Compose 会为整个项目创建一个独立网络所有服务都在这个网络中通过服务名可以相互访问。比如上面的例子中app服务连接数据库时只需要将数据库地址写为mysql而不是localhost因为localhost在容器内部指向容器自身。一个实用的网络设置是控制端口暴露范围内部服务如数据库只需在 compose 网络内被 app 访问不需要直接暴露到宿主机的公网端口只需让 app 映射端口即可。如果确实需要从宿主机访问数据库做排查可以仅绑定到本机回环地址例如127.0.0.1:3306:3306避免把数据库端口暴露到公网。3.3 依赖与健康检查解决服务启动顺序问题初学到 Compose 时最常犯的错误是app 服务启动时数据库还没准备好导致连接失败直接退出。depends_on确实定义了顺序但默认的depends_on只保证「容器启动了」并不意味着「容器内的应用就绪了」。MySQL 容器起来后可能需要几秒才能接受连接而 app 容器已经跑起来了。正确做法是给被依赖的服务加健康检查并在depends_on中指定条件。我上面的例子已经演示了mysql 服务加healthcheck用mysqladmin ping探活app 服务中depends_on.mysql.condition: service_healthy这时 Compose 会等待 MySQL 真正就绪后才启动 app。这是 Compose 生产中最重要的细节之一。3.4 Compose 常用命令与两个易混淆的选项Compose 命令表面上不多但有几个参数的区别是高频问题。docker compose up -d里的-d表示后台运行与之相关的一个常见需求是我想让服务在宿主机开机时自动启动对应方案是在 compose 服务中加restart: unless-stopped加上后Docker 引擎启动时自动拉起容器解决了容器作为系统服务开机自启的问题。docker compose -f和docker compose -p这两者的区别经常有人问。-f指定的是 compose 文件路径用于在非当前目录下执行或者在多个 compose 文件叠加时使用docker compose -f /path/to/docker-compose.yml up -d-p指定的是项目名称。Compose 默认以当前目录名作为项目名网络、容器名前缀都由这个项目名生成。如果你在同一台机器上同时部署两套相同配置的环境比如一套开发、一套测试就需要通过-p区分docker compose -p dev up -d docker compose -p test up -d上面的命令会启动两套相互隔离的环境容器名分别是dev-app-1、test-app-1网络也分别命名为dev_default和test_default。如果网段冲突Compose 会自动避开。3.5 Compose 部署有状态应用的注意点很多人以为 Compose 里的 MySQL、Redis 和单机部署完全一样其实差别不小。Compose 适合单机编排单机上所有容器共享宿主机资源虽然可以通过deploy.resources.limits限制内存和 CPU但在单机模式下这个配置作用是有限的真正严格的资源限制需要靠 Docker 引擎配置或者后面提到的 orchestrator。我还踩过一个坑用 Compose 部署 RocketMQ 时RocketMQ 的 Broker 容器启动后一直连接 NameServer 失败。排查半天发现是 Broker 默认注册了容器内 IP而客户端在宿主机上访问不了容器网络。解决办法是在 RocketMQ 的启动脚本中把brokerIP1设置为宿主机 IP或者在 compose 文件中用network_mode: host让容器直接共享宿主机网络。不是所有镜像都适合用端口映射的方式访问遇到集群类应用多查镜像文档里的网络配置说明。4. Docker Swarm把集群当作一台巨型 Docker 主机Docker 解决了单机上的容器运行问题Compose 解决了单机上的多容器编排问题。但再往下走问题就来了单机能力有限CPU、内存、磁盘总有天花板单机也是单点宕机了服务就断了。这时候需要把多台机器组成集群让容器可以跨机器调度。Docker Swarm 就是 Docker 官方给出的第一种集群方案。4.1 Swarm 模式是怎么工作的Swarm 模式不是独立的软件而是 Docker Engine 内置的集群能力。把一个 Docker 节点初始化为 Swarm 管理节点再让其他节点加入集群就得到了一个由多台机器组成资源池。在管理节点上仍用类似 compose 的语法定义服务Swarm 负责把任务调度到合适的节点上运行。初始化一个 Swarm 集群# 在管理节点上初始化 docker swarm init --advertise-addr 192.168.1.10 # 查看工作节点加入令牌 docker swarm join-token worker # 在工作节点上执行返回的 join 命令 docker swarm join --token SWMTKN-1-xxxx 192.168.1.10:2377初始化后管理节点上运行docker node ls即可看到集群中的所有节点。4.2 服务、任务、副本理解 Swarm 的调度单元Swarm 中核心抽象是服务Service它类似 Compose 中的 service 定义但增加了副本数量。每个服务需要指定镜像、资源需求、端口映射、网络、环境变量和期望运行的副本数Swarm 调度器会保证集群中始终运行指定数量的副本。docker service create --name web \ --replicas 3 \ --publish 80:80 \ --update-order start-first \ nginx:1.25--replicas 3表示同时在集群中运行 3 个 nginx 容器调度器会把它们分布到不同节点上。某个节点宕机了Swarm 会在其他健康节点上新建容器重新补齐副本数。对于无状态服务来说这种机制非常有用。滚动更新也是 Swarm 的亮点之一。docker service update --image nginx:1.26 web可以逐步替换容器更新过程中服务保持可用如果新镜像启动失败--update-order start-first会先启动新容器、确认成功再停止旧容器。4.3 overlay 网络与内置服务发现跨节点的多副本容器如何互相通信Swarm 引入了 Overlay 网络。它在所有节点之间建立了一层虚拟网络每个容器分配一个虚拟 IP不管容器在哪个节点上都可以通过服务名稳定访问。拓扑如下用户请求到达任意节点上 Swarm 的 ingress 负载均衡端口。Ingress 网络将请求转发到某个运行该服务容器的节点。容器之间通过 Overlay 网络直接通信节点边界对应用透明。这解决了两个问题一是服务入口不需要记住每个容器所在节点统一通过负载均衡 IP 或服务名访问二是容器重启或迁移后虚拟 IP 不变对调用方完全无感。服务发现方面Swarm 内置了 DNS 解析。在一个 Overlay 网络中服务名可以直接解析到对应的容器地址。比如在 Swarm 环境中部署一个微服务 A另一个微服务 B 可以直接通过http://service-a:8080调用 A无需人工配置注册中心。这个特性在集群环境下非常方便。4.4 Swarm 的数据持久化和配置管理有状态服务数据库、缓存在 Swarm 中最麻烦的是数据持久化。Swarm 中容器可以漂移到任意节点如果数据存在本地磁盘容器到其他节点后数据就丢了。解决方案是使用 volume配合 NFS 这类共享存储services: mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql volumes: mysql-data: driver: local driver_opts: type: nfs o: addr192.168.1.100 device: :/srv/nfs/mysqlSwarm 调度容器时会把数据卷挂载到任意节点上共享存储保证不管容器跑到哪个节点都能挂载到同一份数据。NFS 有单点风险可以用 CephFS 或云厂商的共享文件存储替代思路是一样的。密钥管理方面Swarm 提供docker secret。密钥文件只下发到运行对应服务的节点以内存文件系统的形式挂载到容器管理节点上也不会出现明文。这种方式比环境变量传递密码安全得多printf s3cret | docker secret create db_password -创建服务时声明--secret db_password容器内读取/run/secrets/db_password即可。4.5 Swarm 的定位简单、够用但天花板很明显Swarm 的最大优势是简单。它是 Docker 内置的不需要额外安装任何组件一条命令就能把一堆机器变成集群。如果公司规模不大、服务数量有限用 Swarm 部署生产环境是完全可行的。但 Swarm 在天花板方面也很明显自动伸缩能力弱没有原生的 HPA 式自动扩缩容需要配合外部脚本或工具生态几乎为零没有 Service Mesh、没有成熟的 Operator 机制、没有像 Helm 那样的包管理滚动更新策略灵活度有限复杂发布流程金丝雀、蓝绿实现起来要自己做很多工作。Docker 官方后来也在事实上把更多精力转向了 Kubernetes 兼容层。从我的亲身经历来说Swarm 适合服务数量在二三十个以内、发布频率不高的中小团队。一旦服务数量过百你就会发现 Swarm 的监控、日志、安全、网络方面的工具链支撑远远不够这时就是向 Kubernetes 迁移的时机。5. Kubernetes从如何跑容器到如何运营容器平台Kubernetes简称 K8s是目前容器编排的事实标准。之所以它不是简单替代 Swarm而是重构了编排的整个思路——把运维规则从脚本式的调度指令提升为声明式的系统期望状态。5.1 从 Pod 到节点Kubernetes 的核心抽象在 Kubernetes 中最小的调度单元不是容器而是 Pod。Pod 是一组紧密协作的容器的集合它们共享网络命名空间、共享存储卷可以认为是一个逻辑主机。同一个 Pod 里的容器可以通过localhost互相访问比如一个日志采集 Sidecar 容器和应用主容器就是典型组合。对外Pod 有自己的虚拟 IP但它是临时的——Pod 被删除重建后 IP 会变化所以直接访问 Pod IP 很不可靠。为了稳定访问Kubernetes 引入了 Service。Service 是一个稳定的访问入口它通过 Label Selector 找到后端一组 Pod 的 IP并自动维护一个 VIP集群 IP或 DNS 记录。请求到达 Service 后再由 kube-proxy 转发到具体 Pod。这层抽象解决了两个问题Pod 的 IP 变化不影响客户端水平扩展时新增或删除 Pod 不需要修改客户端配置。从节点Node层面看Kubernetes 把一台台服务器抽象为资源池Master 控制面负责调度和状态管理Worker 节点负责运行 Pod。节点上的 kubelet 是 Kubernetes 在每台机器上的代理它负责与 Master 通信、管理和监控本节点上的 Pod。5.2 Kubernetes 如何调用 containerd从 kubelet 到 CRI这个问题搜索引擎上经常被搜到也是我们作为运维人员最需要搞清楚的链路Kubernetes 究竟是怎么调用 containerd 的链路顶端的组件是 kubelet。kubelet 是运行在每个 Worker 节点上的守门人它通过 API Server 监听应该运行在哪些 Pod 的指令。然后呢kubelet 并不是直接调ctr命令去操作 containerd 的而是通过 CRIContainer Runtime Interface容器运行时接口这个标准化协议来请求运行时完成操作。简要的调用链如下kubelet → CRI 插件containerd 内置 cri plugin → containerd → runc → 容器进程具体步骤是kubelet 通过 gRPC 接口向 containerd 的 CRI 插件发送RunPodSandbox请求创建 Pod 沙箱。containerd 收到请求后拉起一个pause容器。这个容器非常小唯一作用是持有 Pod 级别的命名空间。kubelet 继续发送CreateContainer请求创建业务容器。containerd 调用 runc或其它 OCI 兼容运行时来创建容器进程并把该进程加入 pause 容器持有的命名空间。容器启动后containerd 的 CRI 插件会把容器的运行时状态通过 CRI 返回给 kubelet。这条链路上有一个关键设计CRI 是 Kubernetes 层面的标准接口containerd 是实现了这个接口的运行时之一。过去 Docker Runtime 时代kubelet 是通过 dockershim 这个适配层去调用 Docker EngineDocker Engine 再调用 containerd。后来 Kubernetes 在 1.24 版移除了 dockershimcontainerd 直接成为最主流的运行时。这也解释了为什么现在部署 Kubernetes 一般不再安装 Docker Engine而是直接装 containerd。5.3 Deployment、控制器模式与声明式 APIDeployment 是 Kubernetes 最常用的工作负载类型。它描述的是期望状态需要多少个副本、使用什么镜像、更新策略如何。Deployment 控制器会实时检查当前运行状态一旦状态偏离期望立即启动修复。比如apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80提交这个文件后Kubernetes 会在集群内创建 3 个 nginx Pod。任何节点宕机、Pod 被删、镜像拉取失败Deployment 控制器都会自动创建新 Pod 补齐副本数。运维不需要手工干预Kubernetes 会一直努力让现实状态收敛到期望状态。声明式 API 的使用体验和命令式的区别是巨大的。命令式操作像点菜我要运行哪个容器把容器放到哪台机器。声明式操作像报菜名我要的最终宴席是这样无论怎么实现最终状态必须是这个。这让基础设施的变更有据可查、可审计、可回滚。5.4 故障排查的通用路径从 Pod 到集群网络上热词里经常有人搜Kubernetes 故障排查实战确实K8s 排障跟普通应用排障不太一样因为它增加了大量抽象层。我在实际工作中形成了一套固定的排查路径第一层看 Pod 状态kubectl get pods -n namespace重点关注CrashLoopBackOff、ImagePullBackOff和Pending三种状态。CrashLoopBackOff通常是容器启动即退出应用自身问题ImagePullBackOff是镜像拉取失败要么镜像仓库地址错误要么认证有问题Pending则一般是资源不足或调度不成功需要kubectl describe pod查看详细事件。describe是排障利器它会显示节点调度事件、镜像拉取结果、容器启动日志、卷挂载失败原因几乎所有问题都能在描述里看到线索。第二层看事件和容器日志kubectl describe pod pod-name -n namespace kubectl logs -f pod-name -n namespace如果 Pod 有多个容器kubectl logs -c container-name指定容器查看日志。配合--previous参数可以查看上一次容器实例的日志对 CrashLoop 尤其有用。第三层看 Service 到 Endpoint 的连通性Pod 正常不代表服务可用。Service 必须正确选择到后端 Pod 才能转发流量kubectl get endpoints service-name -n namespace kubectl describe svc service-name -n namespace如果 Endpoints 列表为空说明 Service 的 selector 没有匹配到任何 Pod。最常见的原因就是标签不匹配。加标签的命令kubectl label pod pod-name appnginx -n namespace第四层看节点状态和资源kubectl get nodes kubectl top nodes kubectl top pods -n namespace节点出现 NotReady、内存或磁盘压力会导致 Pod 无法调度或被杀掉。如果资源紧张即使describe pod显示调度成功Pod 也可能会因为 OOM 被反复杀掉需要在容器定义中设置合理的resources.requests和resources.limits。5.5 Kubernetes 在生产中的真实心态学习曲线虽陡回报是规模化的解放很多人问 Kubernetes 值不值得学。坦白讲如果你的业务只有几台服务器K8s 的复杂度是溢出的。但一旦服务数量超过上百个、团队超过十人、每周发布多次K8s 在自动化、自愈、滚动发布、弹性伸缩方面的收益会超过它带来的学习成本。最明显的变化是一次发布。在没有 K8s 之前发布意味着要登录多台服务器、逐个拉代码、逐个重启进程。在 K8s 之后发布就是改镜像标签加kubectl rollout restart回滚就是kubectl rollout undo。这背后是 Kubernetes 控制器的能力而不是某个人操作熟练度的提升。还有一点是生态。Helm 把这些常用应用定义为可复用的包Prometheus 提供了原生监控体系Ingress Controller 统一了入口流量管理Service Mesh 可以在服务间实施细粒度的流量治理。这些都建立在 Kubernetes 之上越往深处价值越大。6. 四者关系与选型从开发到生产的完整路线图聊完四个工具之后最后想系统地梳理一下它们之间的层级关系以及什么场景下该用哪个。6.1 四者不是竞争关系而是演进关系的不同阶段我经常把这条技术链拆成四个阶段来理解Docker单机上的容器运行时解决一个应用怎么打包和运行。Docker Compose单机上的多容器编排解决多个相关应用怎么一起跑。Docker Swarm多机上的容器编排解决一堆机器怎么组成集群跑容器。Kubernetes多机上的容器编排平台解决大规模容器集群如何自动化运营。需要注意的是Kubernetes 并不是 Swarm 的超集而是独立演进出来的一套体系。两者底层都依赖容器运行时Docker、containerd 等但 API、调度策略、网络模型、扩展方式完全不同。6.2 从场景出发的选型建议没有绝对的最好用只有最适合当前规模。以下是我的实际建议场景一本地开发 / 个人项目 / 单机测试选择Docker Docker Compose。开发时用 Docker Compose 一键启动 MySQL、Redis、本地依赖服务应用代码在宿主机跑通过配置连接到容器内的依赖。这种方式隔离性好、清理方便。单机场景下几乎不需要考虑 Swarm 和 K8s。场景二中小团队生产环境服务几十个以内选择Docker Compose 或 Docker Swarm 起步。服务数量不多、发布频率不高时Swarm 比 K8s 更容易运维。同样的服务用 K8s 编排可能需要多维护一堆组件用 Swarm 只是几行命令的事。如果团队里还没有人熟悉 K8s不要为了追技术热点强行上 K8s——复杂度是真实的成本。场景三中大规模生产环境服务上百、需要精细化治理选择Kubernetes Helm Prometheus。这个规模下Swarm 的保守运维方式和有限生态会成为瓶颈。K8s 才能提供预期的自动伸缩、金丝雀发布、多集群管理和丰富的监控告警体系。6.3 同时使用多个工具的情况这些工具不是非此即彼的现实中也经常组合使用。比如本地开发环境用 Compose上到测试环境用 Swarm生产环境用 K8s它们之间可以通过同一套 Dockerfile 构建的镜像无缝流通。Compose 文件也可以通过kompose这样的工具转换为 K8s 的 Deployment 和 Service 配置迁移成本比想象中低。我自己目前最常见的组合是本地用 Compose 起依赖环境测试环境用 Swarm 快速部署一套完整业务生产环境在 K8s 集群上跑。三个环境共用同一套镜像配置文件格式不同但内容有对应关系运维工具不必重复建设。6.4 最后几点经验之谈如果让我只传达三条经验下面这些是我在实际踩坑中获得的第一镜像仓库必须尽早做私有化。业务代码的镜像是公司资产不能长期依赖公共镜像仓库。用 Harbor 或 Nexus 搭一个内网镜像仓库复制中心仓库的关键基础镜像部署速度和安全性都会明显提升。公共镜像仓库在大规模拉取时容易限流内网镜像仓库能彻底避开这个问题。第二日志和监控要跟着容器化同步建设。容器一多登录每台服务器上看日志已经不现实了。ELK 或 Loki 收集日志Prometheus 加 Grafana 做指标监控这两套体系最好在容器化初期就规划进去而不是等故障爆发后再补。第三配置文件里一定少用环境变量传递密码和密钥。环境变量在进程运行期间可以被读到的风险很高生产环境应该用 Swarm 的 Secret 或 K8s 的 Secret 机制。这个习惯越早养成越好。容器化不是终点而是一条持续演进的路线。Docker 让你跑通第一步Compose 让你把多服务串起来Swarm 让你尝到集群的甜头Kubernetes 让你体会到真正的平台化运维。每一步都有它的边界和适用场景不用急着一步跳到最后按实际的业务规模逐步往上走才是最稳妥的路线。