第34课:微服务Docker容器化部署 & 生产环境上线

文章目录

适配版本 :Spring Boot 4.0.8、Spring Cloud 2025.1.3(Oakwood)、Spring Cloud Alibaba 2025.1.0.0、Docker 27.x、Docker Compose 2.x、JDK 21

课程定位:实战阶段部署课,从Dockerfile编写到Compose编排,从环境变量注入到生产启动脚本,完成微服务项目的容器化上线

一、开篇:从"开发环境能跑"到"生产环境上线"

第33课我们完成了全链路整合调试。在开发环境中,ecommerce-platform的5个微服务、Nacos、Seata、RocketMQ、Sentinel、Prometheus、Grafana、SkyWalking、ELK全部正常运行,下单请求可以从网关一路贯通到数据库,熔断限流和分布式事务也验证通过。

但"开发环境能跑"和"生产环境上线"之间,隔着一条巨大的鸿沟:

  • 环境差异:开发环境的JDK路径、配置文件位置、依赖服务地址,与生产环境完全不同。手工部署时,运维人员需要逐台服务器安装JDK、配置环境变量、上传JAR包、修改配置文件------这个过程极易出错。
  • 依赖管理:每个微服务都依赖Nacos、MySQL、Redis、RocketMQ等中间件。手工部署时,中间件的版本、配置、网络互通全靠人工保证,一旦某个中间件地址变更,所有服务都要重新配置。
  • 扩展困难:大促时需要将订单服务从2个实例扩展到10个实例。手工部署时,需要逐台服务器复制JAR包、启动进程、注册到Nacos------耗时且容易遗漏。
  • 回滚缓慢:新版本上线后发现严重Bug,需要紧急回滚。手工部署时,需要重新上传旧版本JAR包、重启所有实例------故障恢复时间(MTTR)可能长达数十分钟。

Docker容器化 正是解决这些问题的标准方案。它将每个微服务及其运行环境打包为一个不可变的镜像 ,在任何支持Docker的机器上都能以完全相同的方式运行。配合Docker Compose,可以一键启动所有服务及其依赖,实现"基础设施即代码"。

本课将完成ecommerce-platform的完整容器化部署。你将学会:为Spring Boot微服务编写生产级Dockerfile、使用Maven插件自动化构建镜像、用Docker Compose编排全部服务与中间件、通过环境变量注入配置、挂载日志到宿主机、编写生产启动脚本、以及线上运维的关键配置。

二、Dockerfile生产级编写

2.1 为什么需要多阶段构建

传统的Dockerfile将编译和运行放在同一个镜像中,导致两个问题:

  • 镜像体积巨大:包含了Maven、JDK完整版、源码等,镜像可能超过800MB。
  • 安全风险:构建工具和源码留在生产镜像中,增加了攻击面。

多阶段构建(Multi-stage Build) 将Dockerfile分为两个阶段:

  • 构建阶段(Builder) :使用完整的JDK和Maven,编译源码、打包JAR。
  • 运行阶段(Runtime) :只复制构建好的JAR和精简的JRE,生成最终镜像。

这样最终镜像只包含运行所需的最小内容,体积可缩小到200MB以内。

2.2 生产级Dockerfile模板

在service-order模块的根目录下创建Dockerfile:

dockerfile 复制代码
# ========== 阶段一:构建 ==========
FROM maven:3.9.9-eclipse-temurin-21 AS builder

WORKDIR /build

# 先复制POM文件,利用Docker缓存层加速依赖下载
COPY pom.xml .
COPY platform-dependencies/pom.xml platform-dependencies/
COPY common-core/pom.xml common-core/
COPY common-api/pom.xml common-api/
COPY common-web/pom.xml common-web/
COPY service-order/pom.xml service-order/

# 下载依赖(如果POM未变化,这一层会被缓存)
RUN mvn dependency:go-offline -B

# 复制源码并编译打包
COPY . .
RUN mvn clean package -pl service-order -am -DskipTests -B

# ========== 阶段二:运行 ==========
FROM eclipse-temurin:21-jre-jammy

# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 创建非root用户运行应用
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# 从构建阶段复制JAR包
COPY --from=builder /build/service-order/target/*.jar app.jar

# 创建日志目录并授权
RUN mkdir -p /app/logs && chown -R appuser:appuser /app

USER appuser

# 暴露端口
EXPOSE 8083

# JVM 参数(可通过环境变量覆盖)
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 健康检查(依赖 Actuator)
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8083/actuator/health/liveness || exit 1

# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar app.jar"]

关键优化点:

优化项 作用
mvn dependency:go-offline 单独一层下载依赖,源码变化时不会重新下载
-pl service-order -am 只构建service-order及其依赖模块,减少构建时间
eclipse-temurin:21-jre-jammy 只包含JRE,比完整JDK小很多
非root用户 安全加固,避免容器内root权限
HEALTHCHECK 容器健康检查,配合Compose的依赖管理

2.3 为每个服务编写Dockerfile

按照相同模式,为service-user、service-product、service-payment、gateway-server编写Dockerfile。各服务的差异在于:

  • EXPOSE端口不同:8081、8082、8084、9000
  • HEALTHCHECK中的端口和路径不同
  • JAVA_OPTS可根据服务特点调整(网关需要更大的堆内存)

三、Maven插件自动化镜像构建

3.1 使用spring-boot-maven-plugin构建分层JAR

Spring Boot 4.0支持分层JAR(Layered JAR),将依赖、资源、应用代码分开存放,配合Docker的分层缓存可以进一步加速构建。在父POM中配置:

xml 复制代码
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>

3.2 使用jib-maven-plugin构建镜像

Google的Jib插件可以直接从Maven构建Docker镜像,无需安装Docker守护进程,且自动优化镜像分层。在父POM中添加:

xml 复制代码
<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>3.4.3</version>
    <configuration>
        <from>
            <image>eclipse-temurin:21-jre-jammy</image>
        </from>
        <to>
            <image>registry.example.com/ecommerce/${project.artifactId}</image>
            <tags>
                <tag>${project.version}</tag>
                <tag>latest</tag>
            </tags>
        </to>
        <container>
            <jvmFlags>
                <jvmFlag>-Xms512m</jvmFlag>
                <jvmFlag>-Xmx1024m</jvmFlag>
            </jvmFlags>
            <environment>
                <TZ>Asia/Shanghai</TZ>
            </environment>
            <user>appuser</user>
        </container>
    </configuration>
</plugin>

构建命令:

bash 复制代码
# 构建并推送到镜像仓库
mvn compile jib:build -pl service-order

# 构建到本地Docker守护进程
mvn compile jib:dockerBuild -pl service-order

Jib的优势:无需编写Dockerfile,自动分层,构建速度快,且支持直接推送到镜像仓库。

3.3 镜像版本管理策略

策略 示例 适用场景
语义化版本 1.0.0、1.0.1 正式发布
Git Commit SHA a1b2c3d CI/CD自动构建
日期时间 20260913-1030 每日构建
latest latest 开发环境,生产环境禁用

生产建议 :使用语义化版本 + Git SHA 的组合,例如1.0.0-a1b2c3d,既方便识别版本,又精确对应代码提交。

四、Docker Compose全栈编排

4.1 编排架构设计

ecommerce-platform的完整部署包含基础设施 和微服务两大部分。基础设施包括MySQL、Nacos、Seata、RocketMQ、Sentinel、Prometheus、Grafana、SkyWalking、ELK。微服务包括网关和4个业务服务。

Docker Compose文件结构:

复制代码
ecommerce-platform/
├── docker/
│   ├── docker-compose-infra.yml      # 基础设施
│   ├── docker-compose-app.yml        # 微服务
│   ├── .env                          # 环境变量
│   ├── mysql/
│   │   └── init.sql                  # 数据库初始化脚本
│   ├── nacos/
│   │   └── custom.env                # Nacos 环境变量
│   ├── seata/
│   │   └── application.yml           # Seata 配置
│   ├── prometheus/
│   │   └── prometheus.yml            # Prometheus 配置
│   └── logstash/
│       └── logstash.conf             # Logstash 管道配置

4.2 环境变量文件

创建docker/.env:

properties 复制代码
# ===== 镜像版本 =====
IMAGE_TAG=1.0.0
REGISTRY=registry.example.com/ecommerce

# ===== 数据库 =====
MYSQL_ROOT_PASSWORD=Root@2026!
MYSQL_HOST=mysql
MYSQL_PORT=3306

# ===== Nacos =====
NACOS_HOST=nacos
NACOS_PORT=8848
NACOS_USERNAME=nacos
NACOS_PASSWORD=Nacos@2026!

# ===== Seata =====
SEATA_HOST=seata-server
SEATA_PORT=8091

# ===== RocketMQ =====
ROCKETMQ_NAMESRV=rocketmq-namesrv:9876

# ===== 服务端口 =====
GATEWAY_PORT=9000
USER_PORT=8081
PRODUCT_PORT=8082
ORDER_PORT=8083
PAYMENT_PORT=8084

4.3 基础设施编排文件

创建docker/docker-compose-infra.yml:

yaml 复制代码
version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: ecommerce-mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - ecommerce-net

  nacos:
    image: nacos/nacos-server:v3.1.1
    container_name: ecommerce-nacos
    environment:
      MODE: standalone
      SPRING_DATASOURCE_PLATFORM: mysql
      MYSQL_SERVICE_HOST: mysql
      MYSQL_SERVICE_PORT: 3306
      MYSQL_SERVICE_DB_NAME: nacos_config
      MYSQL_SERVICE_USER: root
      MYSQL_SERVICE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      NACOS_AUTH_ENABLE: "true"
      NACOS_AUTH_TOKEN: "SecretKey012345678901234567890123456789012345678901234567890123456789"
      NACOS_AUTH_IDENTITY_KEY: "nacos"
      NACOS_AUTH_IDENTITY_VALUE: "nacos"
    ports:
      - "8848:8848"
      - "9848:9848"
      - "8080:8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - ecommerce-net

  seata-server:
    image: seataio/seata-server:2.5.0
    container_name: ecommerce-seata
    environment:
      SEATA_PORT: 8091
      STORE_MODE: db
      DB_HOST: mysql
      DB_PORT: 3306
      DB_USER: root
      DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      DB_NAME: seata
      SEATA_IP: seata-server
    ports:
      - "8091:8091"
      - "7091:7091"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - ecommerce-net

  rocketmq-namesrv:
    image: apache/rocketmq:5.3.1
    container_name: ecommerce-rocketmq-namesrv
    command: sh mqnamesrv
    ports:
      - "9876:9876"
    networks:
      - ecommerce-net

  rocketmq-broker:
    image: apache/rocketmq:5.3.1
    container_name: ecommerce-rocketmq-broker
    command: sh mqbroker -n rocketmq-namesrv:9876 -c /home/rocketmq/conf/broker.conf
    environment:
      NAMESRV_ADDR: rocketmq-namesrv:9876
    ports:
      - "10909:10909"
      - "10911:10911"
    volumes:
      - ./rocketmq/broker.conf:/home/rocketmq/conf/broker.conf
    depends_on:
      - rocketmq-namesrv
    networks:
      - ecommerce-net

  sentinel-dashboard:
    image: bladex/sentinel-dashboard:1.8.9
    container_name: ecommerce-sentinel
    ports:
      - "8858:8858"
    networks:
      - ecommerce-net

  prometheus:
    image: prom/prometheus:v3.2.0
    container_name: ecommerce-prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - ecommerce-net

  grafana:
    image: grafana/grafana:11.5.2
    container_name: ecommerce-grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin123
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    networks:
      - ecommerce-net

  skywalking-oap:
    image: apache/skywalking-oap-server:10.4.0
    container_name: ecommerce-skywalking-oap
    environment:
      SW_STORAGE: elasticsearch
      SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
    ports:
      - "11800:11800"
      - "12800:12800"
    depends_on:
      - elasticsearch
    networks:
      - ecommerce-net

  skywalking-ui:
    image: apache/skywalking-ui:10.4.0
    container_name: ecommerce-skywalking-ui
    environment:
      SW_OAP_ADDRESS: http://skywalking-oap:12800
    ports:
      - "8088:8080"
    depends_on:
      - skywalking-oap
    networks:
      - ecommerce-net

  elasticsearch:
    image: elasticsearch:8.15.0
    container_name: ecommerce-elasticsearch
    environment:
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms2g -Xmx2g
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - ecommerce-net

  logstash:
    image: logstash:8.15.0
    container_name: ecommerce-logstash
    volumes:
      - ./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    ports:
      - "5044:5044"
    depends_on:
      - elasticsearch
    networks:
      - ecommerce-net

  kibana:
    image: kibana:8.15.0
    container_name: ecommerce-kibana
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
    networks:
      - ecommerce-net

volumes:
  mysql-data:
  es-data:

networks:
  ecommerce-net:
    driver: bridge

4.4 微服务编排文件

创建docker/docker-compose-app.yml:

yaml 复制代码
version: '3.8'

services:
  gateway-server:
    image: ${REGISTRY}/gateway-server:${IMAGE_TAG}
    container_name: ecommerce-gateway
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - NACOS_HOST=${NACOS_HOST}
      - NACOS_PORT=${NACOS_PORT}
      - NACOS_USERNAME=${NACOS_USERNAME}
      - NACOS_PASSWORD=${NACOS_PASSWORD}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "${GATEWAY_PORT}:9000"
    volumes:
      - ./logs/gateway:/app/logs
    depends_on:
      - nacos
      - seata-server
    networks:
      - ecommerce-net

  service-user:
    image: ${REGISTRY}/service-user:${IMAGE_TAG}
    container_name: ecommerce-user
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - NACOS_HOST=${NACOS_HOST}
      - NACOS_PORT=${NACOS_PORT}
      - NACOS_USERNAME=${NACOS_USERNAME}
      - NACOS_PASSWORD=${NACOS_PASSWORD}
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - SEATA_HOST=${SEATA_HOST}
      - SEATA_PORT=${SEATA_PORT}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "${USER_PORT}:8081"
    volumes:
      - ./logs/user:/app/logs
    depends_on:
      - nacos
      - seata-server
      - mysql
    networks:
      - ecommerce-net

  service-product:
    image: ${REGISTRY}/service-product:${IMAGE_TAG}
    container_name: ecommerce-product
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - NACOS_HOST=${NACOS_HOST}
      - NACOS_PORT=${NACOS_PORT}
      - NACOS_USERNAME=${NACOS_USERNAME}
      - NACOS_PASSWORD=${NACOS_PASSWORD}
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - SEATA_HOST=${SEATA_HOST}
      - SEATA_PORT=${SEATA_PORT}
      - ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "${PRODUCT_PORT}:8082"
    volumes:
      - ./logs/product:/app/logs
    depends_on:
      - nacos
      - seata-server
      - mysql
      - rocketmq-namesrv
    networks:
      - ecommerce-net

  service-order:
    image: ${REGISTRY}/service-order:${IMAGE_TAG}
    container_name: ecommerce-order
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - NACOS_HOST=${NACOS_HOST}
      - NACOS_PORT=${NACOS_PORT}
      - NACOS_USERNAME=${NACOS_USERNAME}
      - NACOS_PASSWORD=${NACOS_PASSWORD}
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - SEATA_HOST=${SEATA_HOST}
      - SEATA_PORT=${SEATA_PORT}
      - ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "${ORDER_PORT}:8083"
    volumes:
      - ./logs/order:/app/logs
    depends_on:
      - nacos
      - seata-server
      - mysql
      - rocketmq-namesrv
    networks:
      - ecommerce-net

  service-payment:
    image: ${REGISTRY}/service-payment:${IMAGE_TAG}
    container_name: ecommerce-payment
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - NACOS_HOST=${NACOS_HOST}
      - NACOS_PORT=${NACOS_PORT}
      - NACOS_USERNAME=${NACOS_USERNAME}
      - NACOS_PASSWORD=${NACOS_PASSWORD}
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - SEATA_HOST=${SEATA_HOST}
      - SEATA_PORT=${SEATA_PORT}
      - ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "${PAYMENT_PORT}:8084"
    volumes:
      - ./logs/payment:/app/logs
    depends_on:
      - nacos
      - seata-server
      - mysql
    networks:
      - ecommerce-net

networks:
  ecommerce-net:
    external: true
    name: ecommerce-net

4.5 环境变量覆盖配置

在application-prod.yml中,所有依赖地址都通过环境变量注入:

yaml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
      config:
        server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
      username: ${NACOS_USERNAME:nacos}
      password: ${NACOS_PASSWORD:nacos}
  datasource:
    url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/order_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ${MYSQL_ROOT_PASSWORD:root}

seata:
  service:
    grouplist:
      default: ${SEATA_HOST:localhost}:${SEATA_PORT:8091}

rocketmq:
  name-server: ${ROCKETMQ_NAMESRV:localhost:9876}

环境变量优先级 :环境变量 > application-prod.yml中的默认值。这样既保证了本地开发的便利性,又实现了生产环境的灵活配置。

五、日志挂载与生产启动脚本

5.1 日志挂载策略

容器中的日志默认存储在容器内部,容器删除后日志丢失。生产环境需要将日志挂载到宿主机,便于ELK采集和问题排查:

yaml 复制代码
volumes:
  - ./logs/order:/app/logs

日志目录结构:

复制代码
docker/
├── logs/
│   ├── gateway/
│   │   └── gateway-server.log
│   ├── user/
│   │   └── service-user.log
│   ├── product/
│   │   └── service-product.log
│   ├── order/
│   │   └── service-order.log
│   └── payment/
│       └── service-payment.log

Filebeat采集配置(在宿主机上安装Filebeat):

yaml 复制代码
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /opt/ecommerce/docker/logs/*/*.log
    fields:
      env: prod
    multiline:
      pattern: '^\d{4}-\d{2}-\d{2}'
      negate: true
      match: after

output.logstash:
  hosts: ["localhost:5044"]

5.2 生产启动脚本

创建docker/start.sh,实现一键启动全部服务:

bash 复制代码
#!/bin/bash
set -e

COMPOSE_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$COMPOSE_DIR"

echo "=========================================="
echo "  ecommerce-platform 生产环境启动脚本"
echo "=========================================="

# 1. 创建网络(如果不存在)
if ! docker network inspect ecommerce-net >/dev/null 2>&1; then
    echo "[1/6] 创建 Docker 网络..."
    docker network create ecommerce-net
else
    echo "[1/6] Docker 网络已存在,跳过"
fi

# 2. 创建日志目录
echo "[2/6] 创建日志目录..."
mkdir -p logs/{gateway,user,product,order,payment}

# 3. 启动基础设施
echo "[3/6] 启动基础设施(MySQL、Nacos、Seata、RocketMQ 等)..."
docker compose -f docker-compose-infra.yml --env-file .env up -d

# 4. 等待基础设施就绪
echo "[4/6] 等待基础设施就绪(60秒)..."
sleep 60

# 5. 检查基础设施健康状态
echo "[5/6] 检查基础设施状态..."
docker compose -f docker-compose-infra.yml ps

# 6. 启动微服务
echo "[6/6] 启动微服务..."
docker compose -f docker-compose-app.yml --env-file .env up -d

echo "=========================================="
echo "  全部服务启动完成!"
echo "  网关地址: http://localhost:${GATEWAY_PORT:-9000}"
echo "  Nacos: http://localhost:8080"
echo "  Grafana: http://localhost:3000"
echo "  SkyWalking: http://localhost:8088"
echo "  Kibana: http://localhost:5601"
echo "=========================================="

停止脚本 docker/stop.sh:

bash 复制代码
#!/bin/bash
cd "$(dirname "$0")"

echo "停止微服务..."
docker compose -f docker-compose-app.yml --env-file .env down

echo "停止基础设施..."
docker compose -f docker-compose-infra.yml --env-file .env down

echo "全部服务已停止"

六、线上运维配置

6.1 资源限制与重启策略

在docker-compose-app.yml中为每个服务添加资源限制和重启策略:

yaml 复制代码
services:
  service-order:
    # ... 其他配置
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1536M
        reservations:
          cpus: '0.5'
          memory: 512M
    restart: unless-stopped

资源限制建议:

服务 CPU限制 内存限制 说明
gateway-server 2 1.5G 网关需要处理高并发
service-order 2 1.5G 核心业务,堆内存1G
service-user 1 1G 查询为主
service-product 2 1.5G 涉及库存扣减
service-payment 1 1G 支付回调

重启策略:

策略 含义 适用场景
no 不自动重启 开发环境
always 总是重启 关键服务
unless-stopped 除非手动停止,否则重启 生产环境推荐
on-failure 失败时重启 非关键服务

6.2 健康检查与依赖顺序

Docker Compose的depends_on只保证启动顺序 ,不保证依赖服务已就绪 。例如,service-order依赖MySQL,但MySQL启动后可能需要10秒才能接受连接。如果service-order在MySQL就绪前启动,会因连接失败而崩溃。

解决方案:使用healthcheck + condition: service_healthy:

yaml 复制代码
services:
  mysql:
    # ...
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  service-order:
    # ...
    depends_on:
      mysql:
        condition: service_healthy
      nacos:
        condition: service_started

6.3 滚动更新与回滚

滚动更新:在不中断服务的情况下,逐个替换旧版本实例。

bash 复制代码
# 1. 构建新版本镜像
mvn clean package jib:build -pl service-order -Dimage.tag=1.0.1

# 2. 修改 .env 中的 IMAGE_TAG=1.0.1

# 3. 滚动更新 service-order(只更新该服务)
docker compose -f docker-compose-app.yml --env-file .env up -d --no-deps service-order

回滚 :将IMAGE_TAG改回旧版本,重新执行up -d即可。

bash 复制代码
# 回滚到 1.0.0
sed -i 's/IMAGE_TAG=1.0.1/IMAGE_TAG=1.0.0/' .env
docker compose -f docker-compose-app.yml --env-file .env up -d --no-deps service-order

6.4 生产环境安全加固

加固项 措施
非root用户 Dockerfile中创建appuser并以非root运行
镜像扫描 使用Trivy扫描镜像漏洞:trivy image registry.example.com/ecommerce/service-order:1.0.0
网络隔离 微服务只在内部网络通信,仅网关暴露端口
敏感信息 数据库密码通过环境变量注入,不写入镜像
镜像仓库认证 使用私有镜像仓库,配置认证
Actuator安全 生产环境只暴露health、info、prometheus端点

七、踩坑指南

坑一:容器内无法解析localhost

现象 :服务启动时报Connection refused,配置的localhost:8848无法连接Nacos。

原因 :容器内的localhost指向容器自身,而非宿主机或其他容器。容器间通信必须使用服务名 (Docker Compose中定义的服务名)或自定义网络。

解决 :将配置中的localhost替换为Docker Compose中的服务名(如nacos、mysql、seata-server)。

坑二:MySQL初始化脚本未执行

现象:数据库表不存在,服务启动失败。

原因 :docker-entrypoint-initdb.d中的脚本只在数据卷为空时执行 。如果mysql-data卷已存在,脚本不会重新执行。

解决 :删除旧数据卷(docker volume rm ecommerce-platform_mysql-data)后重新启动;或手动进入容器执行初始化脚本。

坑三:时区不正确导致日志时间偏差8小时

现象:容器日志中的时间比北京时间早8小时。

原因:容器默认使用UTC时区。

解决 :在Dockerfile中设置ENV TZ=Asia/Shanghai并创建时区链接;或在Compose中挂载/etc/localtime:

yaml 复制代码
volumes:
  - /etc/localtime:/etc/localtime:ro

坑四:JVM内存未限制导致容器被OOM Killer杀死

现象 :容器突然退出,docker logs中显示Killed。

原因:JVM默认使用宿主机内存的1/4作为堆内存,容器内存限制为1G时,JVM可能尝试使用超过1G的内存,触发OOM Killer。

解决 :显式设置-Xmx小于容器内存限制(如容器1.5G,JVM堆设1G);添加-XX:+UseContainerSupport(JDK 21默认开启)让JVM感知容器限制。

坑五:健康检查失败导致容器反复重启

现象:容器启动后不断重启,日志显示健康检查失败。

原因 :HEALTHCHECK中的curl命令在容器中不存在(基础镜像未安装curl)。

解决 :使用wget替代curl,或安装curl;或使用Actuator的健康端点配合wget:

dockerfile 复制代码
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget -qO- http://localhost:8083/actuator/health/liveness || exit 1

坑六:镜像构建时Maven依赖下载慢

现象 :Docker构建时卡在mvn dependency:go-offline,下载依赖耗时过长。

原因:Maven默认从中央仓库下载,网络慢。

解决 :在Docker构建阶段配置阿里云镜像。在settings.xml中配置镜像,并在Dockerfile中复制:

dockerfile 复制代码
COPY settings.xml /root/.m2/settings.xml

八、课后作业

作业一 :为service-order编写生产级Dockerfile(多阶段构建),构建镜像并验证镜像体积小于300MB。

作业二:使用Docker Compose编排MySQL、Nacos、Seata三个基础设施,验证三者可以正常启动并互通。

作业三 :将ecommerce-platform的5个微服务打包为Docker镜像,使用Docker Compose编排全部服务。通过网关发起下单请求,验证全链路贯通。

作业四:配置日志挂载,将容器日志输出到宿主机。使用Filebeat采集日志到ELK,在Kibana中验证日志可检索。

作业五(进阶) :编写滚动更新脚本,实现service-order从1.0.0到1.0.1的滚动更新,验证更新过程中服务不中断。然后回滚到1.0.0,验证回滚流程。

九、下节预告

第35课将进入SpringCloud2025架构复盘、性能调优 & 面试核心总结。内容包括全套架构优缺点复盘、高频性能瓶颈优化、GC优化、接口调优、集群优化、SpringCloud高频面试题、生产避坑大全和进阶学习路线。本课完成了微服务项目的容器化部署和生产上线,第35课将对整个专栏的技术体系进行复盘,提炼面试核心考点,为你的微服务架构师之路画上句号。

🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航

去订阅

第一部分:微服务前置基础 & 新版环境搭建(第1-5课)

第二部分:注册中心核心(Nacos 最新版)(第6-9课)

第三部分:配置中心核心(Nacos配置中心)(第10-12课)

第四部分:服务通信核心(OpenFeign + LoadBalancer)(第13-16课)

第五部分:网关核心(SpringCloud Gateway 新版)(第17-20课)

第六部分:熔断、限流、降级(Sentinel 新版)(第21-24课)

第七部分:微服务监控、链路追踪、日志体系(第25-28课)

第八部分:微服务高阶特性 & 分布式核心能力(第29-31课)

第九部分:企业级完整项目实战 & 架构复盘(第32-35课)

相关推荐
对象存储与RustFS19 小时前
网盘限速逼你开会员?用 RustFS 自己搭一个不限速的“私人网盘
服务器·后端·docker
程序员老赵1 天前
Docker 部署 Filestash:轻松搭建浏览器文件管理平台
前端·docker·开源
艾伦_耶格宇1 天前
【DOCKER容器实战】-5 Docker容器镜像的构建
运维·docker·容器
wangjialelele1 天前
一文梳理 Spring Cloud 核心组件:Eureka、Nacos、OpenFeign、LoadBalancer 与 Gateway
java·spring boot·spring·spring cloud·微服务·nacos
草莓熊Lotso1 天前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
程序员小孔.1 天前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
银河技术1 天前
从脚本到 Operator:用 Go 构建可靠的微服务自动扩缩容控制器
开发语言·微服务·golang
草莓熊Lotso1 天前
【C++ 基于微服务的即时通讯系统】etcd + brpc 从安装到服务注册发现全链路实战
网络·数据库·c++·redis·微服务·etcd
我不会起名字3222 天前
Redis 从 0 到 1(九):主库宕机 30 秒自动切主——1 主 2 从 3 哨兵 + Go 客户端
redis·docker·sentinel·高可用·哨兵