ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker Compose实战:从单体拆分到微服务部署全指南

Docker Compose实战:从单体拆分到微服务部署全指南 提到 Docker 和微服务我先说一句实在话这两项技术走到一起不是因为趋势炒作而是因为微服务架构天然就有环境隔离、独立部署、依赖不冲突的刚性需求而 Docker 恰好把这些痛点全部摁死了。我自己最近刚把一套单体后端拆成六个微服务再用 Docker Compose 一把拉起全部依赖整条链路跑通之后不得不说“容器化”这事越早学会越划算。这篇就把我从设计思路到踩坑排错的全过程捋一遍照着走一遍你也能把微服务部署搞定。不管是刚接触容器的入门读者还是正在写 Spring Cloud、想快速搭建本地微服务环境的同学这篇文章都适用。我会从“为什么微服务离不开容器”开始一直讲到 Dockerfile 怎么写、Compose 文件里每个配置项的真实作用、容器网络怎么联通最后再补充几个我实际撞上的坑和排查方法。内容偏实战不搞原理堆砌。1. 内容整体设计与思路拆解1.1 微服务与容器化为什么是天然组合微服务架构的核心出发点是把一个大单体拆成多个可以独立开发、独立测试、独立部署的小服务。听起来很美好但落地的第一道坎就是环境依赖。比如订单服务用 Java 17用户服务用 Node.js 20库存服务又要 Python 3.11这三个服务如果直接部署在同一台机器上光是 SDK 版本互相冲突就够你喝一壶。而且升级某个服务时要么把所有服务的依赖都重新装一遍要么担心某个包被误删导致另一个服务崩掉维护成本非常高。Docker 对这个问题给出的答案就是容器化隔离。每个容器像一个独立的“微型系统”里面有自己的文件系统、环境变量、运行时依赖但和宿主机共享内核。打个生活化的比方一台电脑上装了很多软件有的需要 .NET Framework 3.5有的需要.net 6两个版本共存时经常出怪问题。容器化等于给每个软件单独配了一个“运行文件夹”里面把你需要的运行库都装好互不干扰。Docker 里的每个容器对于里面运行的服务来说就是一台自己的机器外面发生什么都不影响。这种隔离带来的好处直接对应到微服务的三个诉求独立部署——改一个服务只需要重建对应的镜像不用重新发布整套系统环境一致性——“在我机器上能跑”这句话彻底变成伪命题因为镜像打包了完整运行时资源利用率——所有容器共享宿主机内核比一台服务配一台虚拟机的方案轻得多启动一个容器的开销基本可忽略几千个容器同时运行的场景也不算稀奇。1.2 部署方案选型单机 Compose 还是上编排平台微服务的部署方案绕不开“谁来管理这些容器”的问题。早期我身边有人一上来就装 Kubernetes结果光是集群搭建、节点初始化、网络插件配置就折腾了一周最后依然没跑通。这里我要提一个很现实的选型原则先看业务规模和团队能力再决定技术栈的复杂度。如果你的微服务项目属于中小规模比如服务数量在十个以内部署环境就是一台或者几台服务器团队里也没有专门研究和维护 Kubernetes 的工程师那我强烈建议直接用 Docker Compose。Compose 的定位是“在单台主机上定义和运行多容器应用”的工具。你可以用一个 YAML 文件描述所有服务、网络、依赖关系一行docker compose up -d就能把整套微服务拉起来。它不负责跨多台机器的调度但单机场景下绝对够用而且排障链路非常简单。相比之下Kubernetes 解决的问题主要是“跨多台机器的编排、弹性伸缩、自动恢复”。如果服务数量真的上到十几个、几十个流量波动大需要自动扩容那时候再上 K8s 也不迟。Docker Swarm 作为 Docker 原生集群方案理论上是过渡选择但生态和运维工具的丰富度都不如 K8s实际项目里我见得少。我自己的实操选择就是本地开发、测试环境、甚至小型生产环境一律 Compose真要上规模了再花时间研究 K8s 那套不至于一开始就陷入复杂度地狱。2. Docker 核心概念与实操要点2.1 镜像、容器、仓库三个概念必须分清楚很多初学者最容易混的就是镜像和容器。一句话总结镜像是静态的模板容器是模板运行起来的实例。类比一下镜像就像烧录系统的“安装盘”容器就是装好系统后正在运行的那台机器。你可以从一个镜像启动任意多个容器每个容器都是独立的运行环境互不干扰。仓库则是存放镜像的地方。国内默认连的是 Docker Hub 公共仓库需要拉镜像时会报错或超时我一般会配上国内可用的镜像加速器这个后面讲。日常操作里最高频的几条命令我用表格列一下方便对照命令作用常用场景docker pull 镜像名:标签从仓库拉取镜像安装 MySQL、Redis 等现成中间件docker run -d -p 8080:80 镜像创建并后台运行容器启动一个服务实例docker ps -a查看所有容器状态排查容器是否挂掉、看端口映射docker logs -f 容器名实时查看容器日志看服务启动报错、业务日志docker exec -it 容器名 bash进入容器内部执行命令手动检查容器内文件、网络docker build -t 镜像名 .根据 Dockerfile 构建镜像部署自己写的服务代码docker compose up -d根据 Compose 文件启动一套服务微服务整体启动2.2 Dockerfile 编写实战与常见坑Dockerfile 就是你给项目打造的“安装盘制作说明书”每一行指令都会生成一个镜像层。写 Dockerfile 最容易犯的第一个错是不管什么语言都用同一个套路。Java 项目的 Dockerfile 和 Node.js 项目差别很大Python 项目又有自己的一套依赖安装方式。以 Java Spring Boot 项目为例一个基础但规范的 Dockerfile 大概是这样的# 第一阶段构建 FROM maven:3.8-openjdk-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:17-jdk-slim WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这个文件把多阶段构建用得很典型。第一阶段用 Maven 镜像编译代码第二阶段只把编译出来的 jar 包拷贝到精简版 JDK 里最终镜像体积比把整个 Maven 和源码都装进去的做法小了非常多。很多刚上手的人图省事直接FROM openjdk:17然后复制 jar 启动虽然也能用但镜像大、构建慢、可维护性差。我踩过的具体坑是漏写.dockerignore文件导致把target/下几 GB 的构建产物和本地.git目录全部拷进镜像构建耗时从一分钟膨胀到十几分钟。所以项目根部一定要放.dockerignore内容和.gitignore保持同步把不需要进镜像的目录全部排除掉。关于CMD和ENTRYPOINT的区别也值得单独说。简单理解ENTRYPOINT是容器启动后一定执行的命令CMD是给ENTRYPOINT传默认参数或者在没有ENTRYPOINT时当成启动命令。docker run 镜像名 参数后面的参数会覆盖CMD但不会覆盖ENTRYPOINT。实战中我习惯固定用ENTRYPOINT写启动命令再用CMD提供可覆盖的默认参数这样兼顾灵活性和稳定性。2.3 Docker Compose 的关键配置项Compose 的核心就是那个docker-compose.yml文件它把你之前用一串docker run命令做的事情全部浓缩成声明式配置。刚开始写的时候我经常犯的错是想当然改配置结果服务起不来后来彻底理解了每个字段的作用才算真正掌握。首先是services下每个服务名。这个名字不只是写起来好看它同时是 Docker 内部网络的 DNS 主机名。比如你在 Compose 里定义了一个服务叫mysql其他容器里通过jdbc:mysql://mysql:3306/db就能直接访问它不需要知道实际 IP 地址。然后是depends_on这个字段只是控制服务启动的先后顺序并不能保证依赖服务“已经就绪”。比如用户服务依赖 MySQLdepends_on只能保证 MySQL 容器先启动并不能保证 MySQL 客户端端口已经能接受连接。真正要等就绪需要配合healthcheck一起用后面实操部分会写一个完整例子。ports和expose也有区别。ports是把容器端口暴露到宿主机外部网络可以直接访问格式是“宿主机端口:容器端口”。expose只是告诉 Docker 这个容器开放了哪些端口作用仅限于 Docker 内部网络外部无法访问。微服务内部之间的调用可以用expose但给前端或者外部访问的网关必须用ports。数据持久化方面Compose 里常用volumes配置。命名卷和绑定挂载两种方式我推荐持久化数据库这种关键数据时用命名卷比如mysql-data:/var/lib/mysql因为 Docker 会帮你管理目录位置。绑定挂载更适合配置文件的热更新比如把本机的nginx.conf挂载进容器改完配置重启即可生效。3. 实操过程从单体拆分成微服务并用 Docker 部署3.1 拆分思路按业务能力切别按技术层切拆分微服务最忌讳的是照着“Controller 层一个服务、Service 层一个服务、DAO 层一个服务”这种技术分层去拆。那种拆法会让服务之间调用链路又深又乱分布式事务处理起来噩梦级。正确的拆法是按业务领域切分每个服务包含这个业务领域的完整数据访问、业务逻辑和对外接口。我拿一个简化版电商系统举例单体应用里有用户管理、商品展示、订单处理、库存扣减这几块逻辑。我最终拆成了四个服务加一个网关外加两个基础中间件一共六个组件gateway统一入口负责路由转发、权限校验、限流。user-service用户注册、登录、资料管理。product-service商品列表、详情查询。order-service订单创建、查询、状态流转。mysqlMySQL 8.0存放四个服务的业务数据。redisRedis 主从模式缓存热点商品数据和用户会话。拆完之后user-service不直接连数据库操作订单表所有跨服务的业务都通过 HTTP 网关调用保证边界清晰。如果是微服务刚入门的项目不建议一上来就搞消息队列、分布式事务这些复杂概念先把“独立部署、独立数据库、通过网络通信”这三件事跑通后面再逐步加深。3.2 环境准备Windows 和 Linux 的 Docker 安装差别开发环境我用的是 Windows安装 Docker Desktop 时踩过一个大坑这里必须单拎出来讲。Docker Desktop 在 Windows 上依赖 WSL2 虚拟化平台。如果安装完成后启动报错提示 virtualisation support 或者 virtualization not detected八成是以下三个原因中的一个第一电脑 BIOS 里没有开启 CPU 硬件虚拟化功能。开机进 BIOS 设置找到 Intel VT-x 或 AMD SVM 相关选项把它设为 Enabled保存重启即可解决。第二Windows 功能里没有启用“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。以管理员身份打开 PowerShell执行dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完重启电脑。第三WSL2 内核没更新。到微软官网下载“WSL2 Linux 内核更新包”并安装然后在 PowerShell 里执行wsl --set-default-version 2。做完这三步Docker Desktop 基本就能正常启动。如果是 Linux 服务器部署流程反而简单很多。直接安装官方仓库的docker-ce和docker-compose-plugin即可我一般用阿里云镜像源加速安装。服务器上不需要装 Desktop纯命令行就够用了。3.3 多个服务的 Dockerfile 编写与构建拆分以后每个服务都有自己的代码仓库和 Dockerfile。我以user-service为例演示一个实际能跑的 Dockerfile 链路的完整构建过程。前提是代码仓库已经有标准结构入口类放在src/main/java对应包下pom.xml在根目录。构建命令如下cd user-service docker build -t mall/user-service:v1.0 .这里有个细节值得说构建前先确认你当前目录就是 Dockerfile 所在目录因为docker build的上下文就是当前目录执行路径不对的话会找不到文件。构建完成后用docker images看一下镜像大小。如果发现镜像体积超过了预期大概率是基础镜像选大了或者构建上下文没精简回到 2.2 节说的.dockerignore和多阶段构建再把镜像瘦身。Node.js 服务的 Dockerfile 就是另一套写法。我的gateway是用 Node.js 写的用官方node:20-alpine作为基础镜像能大幅减小体积alpine 版本比标准版本小一半还多FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmmirror.com COPY . . EXPOSE 80 CMD [node, server.js]npm install在镜像构建时执行还是挺合适的能保证最终镜像里包含全部依赖运行时不依赖宿主机的 node_modules。如果是频繁改代码的开发场景可以把npm install挪到构建后的挂载卷里做热更新但正式部署我还是推荐这种完整打包的方式稳定。3.4 编写 docker-compose.yml一键启动整套微服务最核心的一步来了。在项目根目录建一个docker-compose.yml把之前手工运行docker run的命令全部变成声明式配置。一个完整的微服务部署文件大致这样version: 3.8 services: mysql: image: mysql:8.0 container_name: mall-mysql ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: mall volumes: - mysql-data:/var/lib/mysql command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine container_name: mall-redis ports: - 6379:6379 volumes: - redis-data:/data command: redis-server --appendonly yes user-service: build: ./user-service container_name: mall-user ports: - 8081:8081 environment: DB_HOST: mysql DB_PORT: 3306 DB_USER: root DB_PASSWORD: root123 REDIS_HOST: redis depends_on: mysql: condition: service_healthy redis: condition: service_started product-service: build: ./product-service container_name: mall-product ports: - 8082:8082 environment: DB_HOST: mysql DB_PORT: 3306 DB_USER: root DB_PASSWORD: root123 REDIS_HOST: redis depends_on: mysql: condition: service_healthy order-service: build: ./order-service container_name: mall-order ports: - 8083:8083 environment: DB_HOST: mysql DB_PORT: 3306 DB_USER: root DB_PASSWORD: root123 depends_on: mysql: condition: service_healthy user-service: condition: service_started gateway: build: ./gateway container_name: mall-gateway ports: - 80:80 depends_on: - user-service - product-service - order-service volumes: mysql-data: redis-data:这个文件里有两个设计值得细说。第一是 MySQL 的healthcheck。从前面的分析你肯定已经意识到depends_on默认只保证“容器先启动”不能保证“服务已就绪”。所以我在 MySQL 上加了mysqladmin ping轮询检查只有当 MySQL 里能执行 ping 并且返回成功时user-service和product-service才会被拉起。没有这个健康检查业务服务很容易在 MySQL 还没准备好时就启动然后疯狂报连接失败很多新手在这里卡半天。第二是服务名的 DNS 解析。user-service里配置DB_HOST: mysql实际连的就是 Compose 网络里名为mysql的那个容器代码里不需要写死宿主机 IP。整个 Compose 默认会创建一个网络所有在此文件定义的服务都接入同一个网络服务间通过服务名互相访问非常方便。对于 Redis 主从模式可以在 Compose 里定义两个 Redis 服务一个主节点一个从节点从节点通过command: redis-server --slaveof redis-master 6379来区分。不过注意Redis 5.0 以后--slaveof改成了--replicaof写法上要留意。如果还想做哨兵模式那需要再加一个 sentinel 服务配置复杂度会上去不是必须的话不用强上。3.5 部署启动与连通性验证配置文件写好后启动整套系统的命令极简。在docker-compose.yml所在目录执行docker compose up -d-d代表后台运行不会占住当前终端。我第一次运行这条命令的时候没有指定--build导致代码改动后容器还是旧的镜像排障时一度怀疑人生。正确的做法是首次构建或代码有更新时用docker compose up -d --build这个命令会先根据 Dockerfile 重新构建所有镜像再启动容器适合部署更新。启动完第一时间用docker compose ps看状态正常情况下所有服务都会是 Up 状态。接着用docker compose logs -f跟踪日志看看有没有连接数据库失败、端口冲突之类的报错。我的习惯是把gateway的日志单独开一个窗口盯因为它作为入口任何下游服务的异常都会在这里留下调用失败的痕迹。网络连通性验证也很重要。进入user-service容器里尝试解析并连接 MySQL验证服务间网络是否正常docker exec -it mall-user sh # 容器内执行 ping mysql # 如果能 ping 通说明两个服务在同一个 Docker 网络里 mysql -h mysql -u root -proot123 -e SELECT 1;如果是 spring boot 服务的容器里面未必有 mysql 客户端那就用curl去请求服务接口验证。比如从gateway容器里请求user-service的接口docker exec -it mall-gateway sh curl http://user-service:8081/api/user/health通过这种从容器内部发起的请求来验证服务通信能快速区分是“网络配置问题”还是“业务代码问题”。如果容器内没有curl可以临时安装或者直接看业务日志里有没有连接成功的记录。4. 常见问题与排查技巧实录4.1 Docker Desktop 启动失败的完整排查路径这个问题我碰到过当时的报错是 Docker Desktop failed to start because virtualisation support wasn‘t detected。按照我前面说的三步排查法先检查 BIOS 虚拟化再看 Windows 功能最后排查 WSL2 版本。我当时的根因是 Windows 功能面板里“虚拟机平台”没有勾选启用并重启后问题解决。后来还有个隐藏很深的坑Windows 的 Hyper-V 和第三方虚拟机软件冲突。如果你的电脑上装了 VMware 或 VirtualBox并且开启了 VT-x 嵌套虚拟化可能会和 Docker Desktop 抢虚拟化资源。解决办法也不是删掉第三方虚拟机而是确认在同一时间只运行一个虚拟化软件。实际开发中我一边开 VMware 跑虚拟机一边用 Docker Desktop经常出现 Docker 启动不了的情况。4.2 容器间网络不通的排查方法论容器网络不通是最让人头大的问题之一因为没有统一报错可能表现为超时、连接被拒绝、DNS 解析失败等多种症状。我的排查方法是一层层往下剥。第一步确认容器确实在同一个 Docker 网络中docker network ls docker network inspect 网络名在Inspect输出的Containers字段里你能看到这个网络下所有已接入的容器。如果某个服务不在里面说明 Compose 文件里的服务名写错了或者容器启动时指定了其他网络。确认网络没问题后第二步是从一个容器 ping 另一个容器的服务名看能否解析到 IP。如果 ping 不通但容器确实同一网络多半是网络模式的问题比如有个服务用了network_mode: host那它就不走 Docker 的 DNS 解析了。第三步查服务监听的端口。有时候网络通但服务内部端口监听在127.0.0.1而不是0.0.0.0外部容器来访问就失败。这个检查需要进入容器内部执行ss -tlnp或netstat -anp查看监听地址。4.3 服务启动顺序与依赖就绪问题典型的尴尬场景是docker compose up -d后业务服务起来了但日志里疯狂报Communications link failure或Unable to connect to Redis。原因前面说过就是depends_on只能控制启动顺序不能控制就绪状态。我的解决方案是在 Compose 中使用healthcheckcondition: service_healthy这个模式比较稳妥。但对于第三方服务没有提供健康检查工具的情况比如某些轻量中间件我一般先sleep 10或者写一个启动脚本轮询端口。启动脚本的思路很简单循环尝试连接目标端口连上后再启动主进程。4.4 镜像拉取慢和拉取失败的加速配置国内直连 Docker Hub 拉镜像经常出现卡到超时的现象。我的做法是在 Docker 配置文件daemon.json里添加镜像加速地址。以 Linux 为例配置文件路径是/etc/docker/daemon.jsonWindows 下通过 Docker Desktop 的设置界面就能改{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com ] }改完重启 Docker拉取速度会正常。需要说明的是镜像加速的可用性经常变化有时候某个加速地址失效了就换一个。公司内部如果搭建了 Harbor 私有仓库也可以通过同样方式配置insecure-registries来访问。4.5 数据持久化失败与容器卷权限问题MySQL 容器挂载命名卷后启动失败并且日志里报权限错误这个坑很多新手都会遇到。原因是容器内的 MySQL 进程以mysql用户运行但挂载到宿主机上的数据目录权限不对。我当时的解决方法是先停掉容器然后用docker run临时挂载一个 busybox 去修正宿主机目录的所有权或者直接chown给对应 UID。如果用命名卷Docker 会自动处理权限所以这种问题更多出现在绑定挂载的场景。绑定挂载一个宿主机目录到容器时建议先保证目录权限是 999:999 或者与容器内用户 UID 一致。4.6 端口冲突和容器重建后的残留问题微服务多起来以后端口冲突特别常见。mall-user用 8081另一个项目也用 8081新容器就会启动失败报Bind for 0.0.0.0:8081 failed: port is already allocated。处理方式是找到占用端口的容器改成别的端口或者杀掉冲突容器。另一个容易忽略的坑是服务重新构建并部署时旧的容器和网络如果没有被清理新容器可能用了旧容器的名字导致冲突。我在更新服务时会习惯先docker compose down清理再docker compose up -d --build重建。如果只是更新其中一个服务可以用docker compose up -d --build 服务名单独重建但这样旧容器虽然被替换网络和卷都没问题也不会有残留冲突。4.7 排查技巧速查表我把上面所说的步骤和症状整理成一张速查表方便你直接对照症状可能原因排查命令 / 解决方案Docker Desktop 无法启动虚拟化未开启 / WSL2 未安装检查 BIOS、开启 Windows 虚拟化功能、安装 WSL2 更新包容器启动后立即退出启动命令错误 / 端口冲突查看docker logs 容器名确认启动命令异常信息服务间连接超时网络不同 / 服务名拼错docker network inspect、容器内 ping 服务名业务服务报数据库连接失败依赖未就绪加入healthcheckcondition: service_healthy镜像拉取卡住网络原因配置 registry mirror 加速地址MySQL 容器权限报错绑定挂载目录权限不对修正宿主机目录 UID/GID或改用命名卷端口绑定失败宿主机端口被占用docker ps找到占用端口的容器调整端口映射代码更新后没生效镜像没重新构建docker compose up -d --build强制重建镜像最后分享一个我自己养成的习惯每次大规模改动 Compose 配置之前都先复制一份docker-compose.yml备份然后用docker compose config命令验证配置文件的语法和最终渲染结果。这个命令会展开所有环境变量和默认值输出一份完整的配置能提前发现不少低级错误。很多人一上来就up报了一堆错才回头改文件反而更浪费时间。微服务部署这种事本质上就是“规则写清楚、依赖管明白、故障拆解得细”Docker Compose 把前两步帮你做掉大半剩下那部分就靠你排查时思路是否清晰了。
返回列表