☰
Docker部署微服务集群:一致性、可重复性与生产落地实践
2026/10/27 4:08:33 网站建设 项目流程

1. 项目概述:为什么微服务集群非得用 Docker 来部署?

“Docker:部署微服务集群”——这八个字背后,不是一句技术口号,而是一整套现代软件交付体系的落地切口。我带过三届某高校实验室的工程实践课,也帮两家某公司做过从单体架构向云原生过渡的技术咨询,最常被问到的问题就是:“我们拆了微服务,为啥反而更难上线了?”答案往往就卡在部署环节:一个服务改一行日志配置,要重新打 war 包、找运维配 Tomcat、手动改 Nginx 转发规则、再重启整个中间件栈……这种操作在 2024 年已经不是“效率低”,而是“不可持续”。

Docker 的核心价值,从来不是“让程序跑起来”,而是让同一份代码,在开发机、测试环境、预发集群、生产服务器上,以完全一致的形态运行。它把“操作系统级依赖 + 运行时环境 + 应用二进制 + 配置文件”这四层东西,打包成一个不可变的镜像(image)。你不需要告诉运维“这个 Java 服务要用 OpenJDK 17u2、Spring Boot 3.2.3、MySQL 8.0.33 客户端驱动、并挂载 /etc/app/config.yml”,你只需要说:“拉取 registry.example.com/order-service:v2.4.1,启动 3 个副本,暴露 8080 端口”。剩下的事,Docker 引擎自己搞定。

微服务集群天然具备三个刚性特征:服务数量多(动辄 20+)、版本迭代快(日均发布 5~10 次)、依赖关系复杂(A 依赖 B 的 v1.3,C 却要求 B 的 v1.5)。没有容器化,光靠人工协调这些组合爆炸式增长的变量,不出三个月,团队就会陷入“发布恐惧症”——每次上线前全员戒严,上线后盯着监控屏不敢眨眼。而 Docker 提供的镜像分层机制(base OS 层 → runtime 层 → app layer)、镜像内容寻址(SHA256 哈希值唯一标识)、以及docker-compose或Kubernetes的声明式编排能力,恰好把这种混沌状态,转化成可版本化、可审计、可回滚的确定性流程。

所以,“Docker 部署微服务集群”这件事,本质是解决“一致性”与“可重复性”的工程问题。它不替代 Spring Cloud 或 Istio,但它是所有上层治理能力的地基。就像盖楼不用钢筋水泥,光谈装修风格毫无意义。你不需要成为 Linux 内核专家,但必须理解:Docker 不是虚拟机,它不模拟硬件,而是通过 Linux namespace(隔离进程视图)和 cgroup(限制资源使用)这两个内核特性,在宿主机上划出一个个轻量、独立、受控的执行沙盒。一个 4 核 8G 的服务器,能轻松跑 50 个 Docker 容器,但可能连 3 个传统虚拟机都扛不住——这就是为什么它成了微服务落地的第一道必经关卡。

2. 整体架构设计与方案选型逻辑

2.1 为什么不用纯裸机部署?——从一次真实故障说起

去年某公司电商大促前夜,支付服务突然超时率飙升至 35%。排查三天,最终发现是测试环境用的 JDK 是 Amazon Corretto 17.0.6,而生产服务器装的是 Oracle JDK 17.0.1,两者在 GC 算法对 G1RegionSize 的默认值处理上存在微妙差异,导致高并发下内存碎片加剧。这个问题在单体时代几乎不会暴露,因为全系统共用一套 JDK;但在微服务场景下,每个服务由不同小组维护,JDK 版本、GC 参数、甚至 glibc 小版本都可能不同。裸机部署等于把“环境一致性”这个责任,甩给了人脑和文档——而人脑会记错,文档会过期。

Docker 的镜像机制,强制把“环境”变成代码的一部分。你写FROM openjdk:17-jre-slim,就锁死了基础运行时;你加COPY ./app.jar /app.jar,就锁死了应用二进制;你用ENV SPRING_PROFILES_ACTIVE=prod,就锁死了配置上下文。整个构建过程可被 Git 追踪,每一次docker build都生成一个带哈希值的镜像 ID,谁改了哪行 Dockerfile,改了什么依赖,一查 commit 记录全清清楚楚。这不是“多此一举”,而是把过去靠“人肉约定”维系的脆弱契约,升级为“机器可验证”的硬性约束。

2.2 为什么不直接上 Kubernetes?——中小团队的真实成本考量

看到“微服务集群”,很多人第一反应是“上 K8s”。但我要坦白讲:对于 5~15 人的中小型技术团队,Kubernetes 往往是“杀鸡用牛刀”,且刀还容易割到自己手。K8s 的核心价值在于跨数百节点的大规模调度、自动扩缩容、服务网格集成等企业级能力。但它的学习曲线陡峭:你需要理解 Pod、Service、Ingress、ConfigMap、StatefulSet、Helm Chart、Operator 等至少 10 个核心概念;你需要维护 etcd 集群、API Server、Controller Manager 等控制平面组件;你还要处理证书轮换、网络插件(Calico/Cilium)选型、存储类(StorageClass)配置等底层细节。

而绝大多数微服务项目,在起步阶段的真实需求只有三条:

  1. 本地能一键启停整套服务(含 MySQL、Redis、Eureka、Gateway、Order、User 等);
  2. 测试环境能稳定复现生产行为,避免“在我机器上是好的”这类扯皮;
  3. 生产环境能灰度发布、快速回滚,不牵连其他服务。

这三点,docker-compose就能完美覆盖。它用一个 YAML 文件(docker-compose.yml)声明所有服务的镜像、端口、环境变量、卷挂载、启动顺序、健康检查,执行docker-compose up -d一条命令,整套集群就活了。它不涉及复杂的调度算法,不依赖分布式存储,所有状态都保留在单机或小规模 Swarm 集群上。我帮某公司搭建的初版微服务演示环境,从零开始,包括编写 Dockerfile、调试 compose 文件、配置 Nginx 反向代理,总共只花了 1.5 天。如果强行上 K8s,光是搭建一个高可用控制平面,没一周搞不定,而且后续每次服务增减,都要写 Helm Chart 或 kubectl apply,对开发同学的认知负担太大。

当然,当你的服务数突破 30 个、日请求量超千万、需要跨 AZ 容灾时,K8s 就是必选项。但路线图应该是:Dockerfile → docker-compose(单机/小集群)→ Docker Swarm(轻量编排)→ Kubernetes(大规模生产)。跳过中间环节,等于没学会走路就想跑马拉松。

2.3 镜像构建策略:分层缓存与安全扫描的实操平衡

Docker 镜像不是简单地把代码塞进容器,它的构建过程本身就是一门工程艺术。关键在于利用好 Docker 的分层缓存(Layer Caching)机制。每一行Dockerfile指令都会生成一个新层,Docker 在构建时会逐行比对缓存:如果某一层的输入(上一层镜像 + 当前指令内容)没变,就直接复用缓存层,跳过执行。这意味着指令顺序直接影响构建速度。

错误示范:

FROM openjdk:17-jre-slim COPY . /app # 每次代码变更,这一行就失效,导致后续所有层(包括 apt update)全部重建! RUN apt-get update && apt-get install -y curl

正确做法(把变动少的放前面,变动多的放后面):

FROM openjdk:17-jre-slim # 先装系统依赖(极少变更) RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* # 再复制构建脚本(相对稳定) COPY mvnw /app/mvnw COPY .mvn /app/.mvn # 最后复制源码和构建(高频变更) COPY pom.xml /app/pom.xml RUN ./mvnw dependency:resolve COPY src /app/src RUN ./mvnw package -DskipTests # 最终镜像只保留运行时所需 FROM openjdk:17-jre-slim COPY --from=0 /app/target/*.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar"]

这个双阶段构建(multi-stage build)还有额外好处:第一阶段用完整 Maven 环境构建,第二阶段只用 JRE 运行,最终镜像体积从 800MB 直降到 120MB,启动更快,攻击面更小。另外,别忘了加入安全扫描环节。我习惯在 CI 流水线里加一步trivy image --severity HIGH,CRITICAL your-registry/order-service:v2.4.1,用 Aqua Trivy 扫描镜像中的已知 CVE 漏洞。它能直接报告出cve-2023-1234影响openssl 1.1.1f,并给出修复建议(如升级到1.1.1t)。这比等上线后被渗透测试团队揪出来,成本低一百倍。

3. 核心细节解析与实操要点

3.1 Dockerfile 编写:从“能跑”到“跑得好”的七条铁律

写一个能跑的 Dockerfile 很容易,但写一个生产可用的 Dockerfile,需要牢记七条经过血泪验证的铁律:

第一,永远用slim或alpine基础镜像,禁用latest标签
openjdk:17-jre-slim比openjdk:17-jre小 40%,比openjdk:latest(实际指向某个旧版)更可控。latest是魔鬼标签——今天构建成功,明天基础镜像更新,你的应用可能因 glibc 版本不兼容而崩溃。必须锁定具体版本:openjdk:17.0.6-jre-slim。

第二,WORKDIR必须显式声明,禁止用cd

# 错误:cd 不会改变后续指令的工作目录 RUN cd /app && ./build.sh # 正确:WORKDIR 持久生效 WORKDIR /app RUN ./build.sh

第三,COPY优于ADD,除非你要自动解压 tar 包
ADD有隐藏行为:遇到本地 tar 文件会自动解压,遇到 URL 会下载并解压。这种“魔法”在生产环境是灾难。COPY行为明确,语义清晰,应该成为默认选择。

第四,用--mount=type=cache加速 Maven/Gradle 构建
Maven 每次构建都要下载依赖,不加缓存,CI 构建时间翻倍。Docker BuildKit 支持原生缓存挂载:

# 启用 BuildKit:DOCKER_BUILDKIT=1 docker build ... RUN --mount=type=cache,target=/root/.m2 \ ./mvnw package -DskipTests

这会让/root/.m2目录在多次构建间持久化,依赖只下载一次。

第五,EXPOSE只是文档,不真正开放端口
EXPOSE 8080只是告诉别人“这个镜像默认监听 8080”,它不触发任何防火墙规则。真正开放端口靠docker run -p 8080:8080或docker-compose.yml中的ports字段。

第六,HEALTHCHECK是生命线,别让它形同虚设
很多团队写了HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1,但没设--interval=30s --timeout=3s --start-period=40s --retries=3。结果容器刚启动,健康检查就失败,被编排系统反复重启。start-period必须足够长,让 Spring Boot 完成初始化(特别是数据库连接池建立)。

第七,USER指令必须启用,禁用 root 运行

# 构建阶段可以用 root RUN groupadd -g 1001 -f app && useradd -r -u 1001 -g app app # 运行阶段必须切换非 root 用户 USER app

这是 CIS Docker Benchmark 的强制要求。root 进程一旦被攻破,整个宿主机就沦陷。用普通用户,攻击者连/etc/shadow都读不到。

提示:所有RUN指令尽量合并成一行,减少镜像层数。每层都是只读的,合并能减小最终体积,也避免中间产物残留(如apt-get clean忘删,垃圾文件留在某一层里)。

3.2 docker-compose.yml 关键字段详解:不只是“写个配置”

docker-compose.yml看似简单,但几个关键字段的设置,直接决定集群的健壮性。下面以一个典型的电商微服务为例,拆解真实项目中必须关注的字段:

version: '3.8' # 必须指定版本,3.8 支持 healthcheck、profiles 等新特性 services: # 注册中心 Eureka eureka-server: image: registry.example.com/eureka-server:v1.2.0 ports: - "8761:8761" # 宿主机:容器内 environment: - SPRING_PROFILES_ACTIVE=dev - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true # 健康检查:eureka 自带 /actuator/health healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"] interval: 30s timeout: 10s start_period: 60s # 给足 60 秒让 eureka 初始化注册表 retries: 5 # 网关服务 api-gateway: image: registry.example.com/api-gateway:v2.1.0 ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka/ # 依赖关系:gateway 必须等 eureka ready 后再启动 depends_on: eureka-server: condition: service_healthy # 关键!不是 service_started # 重启策略:避免因依赖未就绪而永久失败 restart: on-failure:5 # 订单服务 order-service: image: registry.example.com/order-service:v2.4.1 # 环境变量从文件加载,避免敏感信息硬编码 env_file: - .env.prod # 挂载配置文件,支持运行时热更新(需应用支持) volumes: - ./config/order-application.yml:/app/config/application.yml:ro # 资源限制:防止单个服务吃光整机内存 deploy: resources: limits: memory: 512M cpus: '0.5' # 日志驱动:避免日志占满磁盘 logging: driver: "json-file" options: max-size: "10m" max-file: "3" # MySQL 数据库 mysql: image: mysql:8.0.33 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: ecommerce volumes: - mysql-data:/var/lib/mysql # 健康检查:mysql 自带 wait-for-it.sh,但更推荐用 mysqladmin healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"] interval: 20s timeout: 10s retries: 10 volumes: mysql-data: # 声明命名卷,数据持久化

这里有几个极易被忽略的细节:

  • depends_on的condition: service_healthy比service_started严格得多。后者只等容器进程启动,前者必须等健康检查返回healthy状态。否则 gateway 可能连不上 eureka,报Cannot execute request on any known server。
  • deploy.resources.limits是 Docker Compose 的 Swarm 模式专属字段,但在单机模式下会被忽略。如果你用docker-compose up,这个限制不生效;必须用docker stack deploy才起作用。所以生产环境务必确认部署模式。
  • volumes的路径格式./config/xxx.yml:/app/config/xxx.yml:ro中的:ro表示只读挂载,防止容器内进程意外修改配置文件,这是安全底线。

注意:.env.prod文件应加入.gitignore,里面只放非敏感配置(如LOG_LEVEL=INFO),密码类信息必须用 Docker Secrets(Swarm 模式)或 Kubernetes Secret(K8s 模式)管理,绝不能进 Git。

3.3 网络模型与服务发现:容器间通信的底层逻辑

很多新手以为docker-compose启动的服务,只要在docker-compose.yml里写了depends_on,就能互相访问。这是巨大误解。depends_on只控制启动顺序,不解决网络连通性问题。真正让api-gateway能调用order-service:8080的,是 Docker 内置的user-defined bridge network。

当你执行docker-compose up,Docker 会自动创建一个名为yourproject_default的自定义网桥网络,并把所有服务容器接入其中。在这个网络内,容器名(order-service)就是 DNS 主机名,ping order-service能通,curl http://order-service:8080/health也能通。这是 Docker 的默认行为,无需额外配置。

但要注意三个陷阱:
第一,端口映射是单向的。order-service的8080端口在docker-compose.yml中没有ports字段,意味着它不对外暴露,只能被同网络内的其他容器(如api-gateway)访问。而api-gateway有ports: - "8080:8080",所以外部浏览器能通过http://localhost:8080访问它。这种“内部服务不暴露,网关统一出口”的设计,是微服务安全的基本原则。

第二,DNS 解析有缓存延迟。容器刚启动时,api-gateway可能因 DNS 缓存未更新,无法解析order-service。解决方案有两个:一是用healthcheck+depends_on ... condition: service_healthy确保依赖服务真正就绪;二是应用层加重试逻辑(如 Spring Cloud LoadBalancer 的retry配置)。

第三,跨 compose 项目通信需共享网络。如果你有两个独立的docker-compose.yml(比如infra.yml起 MySQL,app.yml起业务服务),它们默认在不同网络,无法互通。必须显式声明共享网络:

# infra.yml networks: common: driver: bridge # app.yml services: order-service: networks: - common

然后用docker-compose -f infra.yml -f app.yml up一起启动。否则order-service永远找不到mysql容器。

4. 实操过程与核心环节实现

4.1 从零搭建:一个可运行的微服务集群 Demo

现在,我们动手搭建一个最小可行的微服务集群:包含 Eureka 注册中心、API 网关、订单服务(Spring Boot)、MySQL 数据库。整个过程在一台 8G 内存的开发机上完成,耗时约 40 分钟。

第一步:准备基础环境
确保 Docker 和 Docker Compose 已安装。验证版本:

$ docker --version Docker version 24.0.5, build ced0996 $ docker-compose --version Docker Compose version v2.20.2

注意:Docker Compose v2 是内置在 Docker Desktop 中的,命令是docker compose(无横杠),不是旧版的docker-compose。新版命令更统一,且性能更好。

第二步:创建项目目录结构

mkdir ecommerce-demo && cd ecommerce-demo mkdir -p config eureka-server order-service mysql-init

第三步:编写 Eureka Server 的 Dockerfile
在eureka-server/目录下创建Dockerfile:

FROM openjdk:17-jre-slim VOLUME /tmp ARG JAR_FILE=target/eureka-server-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar EXPOSE 8761 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

这个 Dockerfile 极简,因为 Eureka Server 本身不依赖外部组件,重点是-Djava.security.egd参数,它加速 Java 的随机数生成(否则容器启动慢)。

第四步:编写 Order Service 的 Dockerfile(含多阶段构建)
在order-service/目录下:

# 构建阶段 FROM maven:3.8.6-openjdk-17-slim AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM openjdk:17-jre-slim VOLUME /tmp ARG DEPENDENCY=/app/target/dependency COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

关键点:mvn dependency:go-offline预先下载所有依赖,确保后续package阶段不联网,提升构建稳定性。

第五步:编写 docker-compose.yml
在项目根目录创建docker-compose.yml:

version: '3.8' services: eureka-server: build: ./eureka-server ports: - "8761:8761" environment: - SPRING_PROFILES_ACTIVE=default - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"] interval: 30s timeout: 10s start_period: 60s retries: 5 mysql: image: mysql:8.0.33 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: ecommerce MYSQL_USER: app MYSQL_PASSWORD: app123 volumes: - ./mysql-init:/docker-entrypoint-initdb.d - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"] interval: 20s timeout: 10s retries: 10 order-service: build: ./order-service environment: - SPRING_PROFILES_ACTIVE=prod - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka/ - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/ecommerce?useSSL=false&serverTimezone=UTC - SPRING_DATASOURCE_USERNAME=app - SPRING_DATASOURCE_PASSWORD=app123 depends_on: eureka-server: condition: service_healthy mysql: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 10s start_period: 90s retries: 5 api-gateway: image: springio/gs-spring-boot-docker:0.0.1-SNAPSHOT # 这里用现成镜像简化演示,实际项目需自己构建 ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka/ depends_on: eureka-server: condition: service_healthy volumes: mysql-data:

第六步:初始化 MySQL 数据库
在mysql-init/目录下创建init.sql,写入建表语句:

CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `product_name` varchar(255) NOT NULL, `amount` decimal(10,2) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Docker 启动时会自动执行这个 SQL。

第七步:一键启动集群

# 构建镜像(第一次较慢,后续增量构建快) docker compose build # 启动所有服务,后台运行 docker compose up -d # 查看服务状态 docker compose ps # 查看日志(实时跟踪启动过程) docker compose logs -f eureka-server

等待 2~3 分钟,访问http://localhost:8761,能看到 Eureka 控制台,ORDER-SERVICE和API-GATEWAY应显示为 UP 状态。再访问http://localhost:8080/actuator/health,应返回{"status":"UP"}。

实测心得:第一次启动慢,主要是 MySQL 初始化和 Eureka 自我保护机制(默认 90 秒内无心跳则剔除实例)。如果看到order-service在 Eureka 上显示为DOWN,别慌,等满 90 秒再刷页面,大概率就变UP了。这是 Eureka 的设计,不是 bug。

4.2 生产环境加固:从 Demo 到上线的五项关键改造

Demo 能跑不等于能上线。生产环境必须做五项加固,缺一不可:

加固一:镜像签名与可信仓库
禁用docker pull从任意仓库拉取镜像。在 Docker daemon 配置中启用内容信任(Content Trust):

// /etc/docker/daemon.json { "content-trust": { "mode": "enabled" } }

然后所有推送都需签名:DOCKER_CONTENT_TRUST=1 docker push registry.example.com/order-service:v2.4.1。这样,docker pull时会校验签名,防止镜像被篡改。

加固二:资源限制与 OOM Killer 防护
在docker-compose.yml的deploy.resources.limits下,不仅设memory,还要设mem_reservation(内存预留)和oom_kill_disable(禁用 OOM Kill):

deploy: resources: limits: memory: 512M cpus: '0.5' reservations: memory: 256M

reservations确保容器至少能拿到 256M 内存,避免因内存竞争导致 JVM GC 频繁;limits是硬上限,超了会被 kill。oom_kill_disable: true仅用于关键系统服务(如数据库),防止被误杀。

加固三:日志集中收集
单机docker logs查日志效率低下。接入 ELK(Elasticsearch + Logstash + Kibana)或 Loki + Grafana。在docker-compose.yml中配置日志驱动:

logging: driver: "fluentd" options: fluentd-address: "localhost:24224" tag: "ecommerce.order-service"

Fluentd 作为日志收集 agent,把所有容器日志统一发送到中心存储,支持按服务、时间、关键词检索。

加固四:配置中心外置
把application.yml从镜像里剥离,用 Spring Cloud Config Server 或 Apollo 管理。docker-compose.yml中只留SPRING_CLOUD_CONFIG_URI=http://config-server:8888,所有配置动态拉取。这样,改一个数据库密码,不用重新构建镜像,只需刷新 Config Server 即可。

加固五:健康检查与就绪探针分离
Kubernetes 有livenessProbe(存活)和readinessProbe(就绪)之分。Docker Compose 虽然没原生支持,但可通过两个端点模拟:

  • /actuator/health:判断进程是否存活(liveness)
  • /actuator/health/readiness:判断是否准备好接收流量(readiness),例如数据库连接池是否建满、缓存是否预热完成
    在application.yml中启用:
management: endpoint: health: show-details: always endpoints: web: exposure: include: health,info,metrics,prometheus

然后在docker-compose.yml的healthcheck.test里,根据场景切换 URL。

5. 常见问题与排查技巧实录

5.1 “Connection refused” 类问题:九成源于网络或启动顺序

这是微服务集群启动时最高频的报错。api-gateway启动时报Connection refused: connect to http://order-service:8080,新手第一反应是“端口没开”,其实 90% 的情况是以下三种之一:

场景一:依赖服务根本没启动
执行docker compose ps,发现order-service状态是exited或unhealthy。此时看日志:docker compose logs order-service。常见原因:

  • MySQL 还没启动好,order-service就急着连,报Communications link failure。解决方案:在order-service的healthcheck里,start_period设为 120s,并在depends_on中明确mysql: condition: service_healthy。
  • order-service的application.yml里eureka.client.service-url.defaultZone写错了,比如写成http://eureka:8761/eureka/(容器名是eureka-server,不是eureka)。

场景二:DNS 解析失败
docker exec -it <gateway-container-id> sh进入容器,执行nslookup order-service。如果返回server can't find order-service: NXDOMAIN,说明 DNS 没生效。原因通常是:

  • 用了docker run单独启动容器,没加入docker-compose创建的网络。解决方案:docker network connect ecommerce-demo_default <container-name>。
  • docker-compose.yml版本太低(< 3.0),不支持自动 DNS。升级到3.8。

场景三:端口被占用或映射错误
docker compose ps显示order-service的 PORTS 列为空,说明没暴露端口。但它本就不该暴露——order-service只需被api-gateway调用,调用走的是容器内网http://order-service:8080,不是宿主机localhost:8080。如果api-gateway报错Connection refused,却curl http://localhost:8080能通,那一定是api-gateway的代码里写死了http://localhost:8080,而不是用服务名http://order-service:8080。这是代码层面的硬编码错误,必须改。

排查口诀:先docker compose ps看状态,再docker compose logs <service>看日志,最后docker exec进容器ping和curl测试连通性。三步下来,95% 的网络问题都能定位。

5.2 “OutOfMemoryError”:容器内存限制与 JVM 参数的协同

Java 应用在容器里 OOM,往往不是内存真不够,而是 JVM 没感知到容器限制。JDK 8u191+ 和 JDK 10+ 默认开启UseContainerSupport,能自动读取 cgroup 内存限制。但很多老项目还在用 JDK 8u131,必须手动加参数:

ENTRYPOINT ["java","-XX:+UnlockExperimentalVMOptions","-XX:+UseCGroupMemoryLimitForHeap","-Xms256m","-Xmx512m","-jar","/app.jar"]

-XX:+UseCGroupMemoryLimitForHeap告诉 JVM 从 cgroup 读内存上限,-Xmx512m是堆上限,必须小于容器memory: 512M(因为还有元空间、直接内存等开销)。如果docker-compose.yml设memory: 512M,JVM 堆设512m,必然 OOM。

实测数据:一个 Spring Boot 应用,容器内存限制512M,JVM 堆设384m,元空间128m,直接内存64m,刚好卡在 512M 边界。用docker stats观察,MEM USAGE稳定在480MiB/512MiB,非常健康。

5.3 “镜像拉取失败”:私有仓库认证与网络代理

内网环境拉取私有仓库镜

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询