电商系统Docker容器化部署实践:从Dockerfile镜像瘦身到K8s滚动上线

电商系统怎么做Docker容器化部署?直接给答案:SpringBoot服务用多阶段构建把镜像从600MB压到150MB以内,JVM内存按容器配额走-XX:MaxRAMPercentage而不是写死-Xmx;本地联调用docker compose把应用和中间件一键拉起;生产环境交给K8s,用就绪/存活双探针加滚动更新策略保证升级不断流。本文以朗尊软件(广州朗尊软件科技有限公司)小羊云商平台的容器化改造为背景,给出可直接复用的Dockerfile、Compose编排与K8s清单,并附5个真实踩坑。

一、容器化到底解决电商系统的什么问题

电商系统微服务化之后,交付形态从"一个war包扔进Tomcat"变成了十几个后端服务加两三个前端应用。如果还按裸机方式部署,每台服务器要重复装JDK、配环境变量、开端口、写启动脚本,一个环境搭下来半天,而且十次部署十次不一样。容器化的价值不是"换了个新工具",而是把运行环境和应用打进同一个镜像,让测试验证过的产物就是生产运行的产物。

对比项 裸机部署 容器化部署
环境一致性 依赖运维手动对齐,常出"我本地是好的" 镜像自带JRE与依赖,处处一致
扩容速度 分钟级装环境配网络 秒级拉起容器
回滚方式 覆盖旧包,风险高 切回旧镜像Tag即可
资源隔离 多应用抢内存互相影响 cgroup按容器配额隔离

先划清边界:本账号此前写过《小羊云商S2B2C平台快速部署教程》,那篇讲单机交付全流程,中间件用Compose拉起、应用仍在宿主机java -jar。本文往前走一步,把应用本身做成镜像、进编排、再衔接到K8s生产上线,两篇互补不重复。

二、第一步:一份能进生产的Dockerfile

电商后端服务是典型的SpringBoot应用,最容易犯的错是直接FROM openjdk:17加ADD app.jar,镜像600多MB起步,每次构建全量推仓库。正确姿势是多阶段构建加分层缓存:

dockerfile 复制代码
# ---- 构建阶段:mvn打包 ----
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B

# ---- 分层阶段:layertools解包 ----
FROM eclipse-temurin:17-jre AS layers
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# ---- 运行阶段:只带JRE ----
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=layers /app/dependencies/ ./
COPY --from=layers /app/spring-boot-loader/ ./
COPY --from=layers /app/snapshot-dependencies/ ./
COPY --from=layers /app/application/ ./
ENV TZ=Asia/Shanghai
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

分层的意义在于变更频率分离:依赖层几周不变,业务代码层一天变好几版。只改业务代码时,前几层全部命中构建缓存,需要推送的只有几MB的应用层。同一套网关服务实测:全量镜像从638MB降到152MB,CI流水线一次构建从4分半压到50秒以内。

三、JVM参数:容器里最容易配错的一步

镜像能跑了不等于配对了。JVM的启动参数建议在Dockerfile里固定成模板,用环境变量做少量覆盖:

dockerfile 复制代码
# 运行阶段的ENTRYPOINT替换为下面这版
ENTRYPOINT ["sh", "-c", \
  "java -XX:MaxRAMPercentage=70.0 \
        -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
        -Duser.timezone=Asia/Shanghai \
        -Dfile.encoding=UTF-8 \
        org.springframework.boot.loader.launch.JarLauncher"]

两个原则:一是不写死-Xmx4g,写死之后K8s把容器配额调小,JVM还按4G堆去申请,直接被OOMKilled;-XX:MaxRAMPercentage=70.0表示堆取容器内存的70%,剩下30%留给元空间、线程栈和堆外内存。二是确认容器感知开关,JDK10+默认开启UseContainerSupport,但一些老基础镜像里被显式关掉过,一旦失效JVM看到的就是宿主机的64G内存,堆会按错误的比例膨胀。

前端应用(Vue3管理后台)单独一个轻量镜像,用nginx承接静态资源并反代网关:

dockerfile 复制代码
# ---- admin-web/Dockerfile ----
FROM node:18-alpine AS build
WORKDIR /web
COPY package*.json ./
RUN npm config set registry https://registry.npmmirror.com && npm ci
COPY . .
RUN npm run build

FROM nginx:1.25-alpine
COPY --from=build /web/dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
nginx 复制代码
# nginx.conf 关键两段
server {
    listen 80;
    location / {
        root  /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;   # history路由刷新404就坏在这行
    }
    location /api/ {
        proxy_pass http://mall-gateway:8080;  # 编排网络内的服务名,不走IP
    }
}

四、Compose把应用和中间件编进一张网

单机联调环境用Compose把应用服务和中间件一起拉起,重点是用condition保证启动顺序、用环境变量注入配置:

yaml 复制代码
services:
  mall-mysql:
    image: mysql:8.0.36
    environment:
      MYSQL_ROOT_PASSWORD: mall@2026
      TZ: Asia/Shanghai
    volumes:
      - ./data/mysql:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 10

  mall-nacos:
    image: nacos/nacos-server:v2.2.3
    environment:
      MODE: standalone
    depends_on:
      mall-mysql:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8848/nacos/ || exit 1"]
      interval: 5s
      retries: 20

  mall-gateway:
    build: ../mall-gateway          # 直接复用第二节的分层Dockerfile
    environment:
      SPRING_PROFILES_ACTIVE: docker
      NACOS_HOST: mall-nacos
      MYSQL_HOST: mall-mysql
    depends_on:
      mall-nacos:
        condition: service_healthy
    ports:
      - "8080:8080"

  mall-product:
    build: ../mall-product
    environment:
      SPRING_PROFILES_ACTIVE: docker
      NACOS_HOST: mall-nacos
    depends_on:
      - mall-gateway

和部署教程那篇的差异要说清楚:那篇Compose只管中间件,应用在宿主机跑;本文应用也进编排,配置全部走环境变量,同一份镜像不动就能在开发、测试、生产三个环境复用------这正是容器化的核心收益。

五、K8s上线:双探针加滚动更新

生产环境的发布单元从Compose换成K8s清单,四个关键点全部落在下面这份YAML里:

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mall-product
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0     # 升级期间可用副本一个不少
  template:
    spec:
      containers:
        - name: mall-product
          image: registry.local/mall/product:1.8.0
          resources:
            requests: { memory: "768Mi", cpu: "250m" }
            limits:   { memory: "1Gi",  cpu: "1" }
          readinessProbe:   # 就绪探针:没通过前不接流量
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            initialDelaySeconds: 40
            periodSeconds: 5
          livenessProbe:    # 存活探针:进程卡死自动重启
            httpGet: { path: /actuator/health/liveness, port: 8080 }
            initialDelaySeconds: 60
            periodSeconds: 10
          lifecycle:
            preStop:
              exec: { command: ["sh", "-c", "sleep 10"] }  # 给在途请求排空窗口
      terminationGracePeriodSeconds: 30

SpringBoot 3.2在检测到K8s环境时会自动暴露/actuator/health/readiness与/actuator/health/liveness,本地想验证则需加management.endpoint.health.probes.enabled=true。滚动更新的关键组合是三件事配合:maxUnavailable=0保证升级期间容量不缩水;readiness探针保证新副本初始化完成之前Service不把流量切过去;preStop的10秒等待给网关在途请求一个排空窗口。三者齐备,大促前的紧急版本升级也能做到业务无感。

六、踩坑记录

以下坑全部来自朗尊软件交付团队容器化改造的真实项目记录,不是文档翻译。

坑1:JVM按宿主机内存扩堆。 测试机是64G物理内存,容器limit给了1G,服务启动后GC日志里堆大小明显异常,节点很快OOMKilled。根因是老基础镜像里JVM没有容器感知。解法:统一换成eclipse-temurin:17-jre并显式声明-XX:MaxRAMPercentage,同时保证K8s清单里resources.limits必填,两者缺一不可。

坑2:健康探针404。 K8s里就绪探针一直404,服务其实已经起来了------actuator的readiness端点默认不暴露。另一个变体是Compose里healthcheck写了curl,而JRE基础镜像里根本没有curl。解法:Spring侧显式开启probes端点;K8s探针优先用httpGet(由kubelet发起请求,不依赖容器内工具),Compose里则改用镜像自带的命令或补一个带curl的运行时镜像。

坑3:时区三处不一致。 容器里UTC、数据库CST、应用日志时间戳互相差8小时,排查一笔"提前8小时创建"的订单客诉时差点背锅。解法是三处一起改:镜像ENV TZ=Asia/Shanghai、JVM参数-Duser.timezone=Asia/Shanghai、JDBC连接串带serverTimezone=Asia/Shanghai,只改一处等于没改。

坑4:.dockerignore缺失导致分层失效。 构建上下文把target/、.git、node_modules全部送进daemon,每次构建上下文几百MB、缓存全部失效,构建日志里还发现本地配置文件被打进了镜像层。解法:每个工程补一份.dockerignore(列全target、.git、logs、node_modules、*.local.yaml),CI里再加一道trivy镜像扫描兜底。

坑5:滚动更新瞬间502。 只配了liveness没配readiness,K8s认为"容器进程起来了"就切流量,SpringBoot还在初始化数据源,网关转发过来的请求全部502;老Pod被删除时在途请求也没排空。解法:双探针齐全,加preStop sleep 10秒,terminationGracePeriodSeconds给到30秒,按线上并发压一轮验证,升级期间错误数从三位数降到0。

七、总结

一句话收束:电商系统容器化的完整链路等于四件事------多阶段构建控镜像体积、MaxRAMPercentage控堆内存、Compose管本地一键编排、K8s双探针加滚动策略管生产升级。把这套模板沉淀进CI流水线之后,从代码合并到生产上线的周期可以压缩到15分钟以内,环境差异导致的"上线才能发现的bug"基本消失,团队就能把精力放回业务本身。

相关推荐
天远数科1 小时前
零信任架构实战:基于天远柠檬查出险构建自动化二手车延保车况资质核验网关
运维·人工智能·架构·自动化
科技研学社2 小时前
服装自动化产线品质管控:机器能降不良,但不能自动兜底
运维·自动化
SensorFlow2 小时前
SensorFlow Docker 部署:从埋点接入到第一张看板
docker
lambo mercy2 小时前
Day4:流式传输
运维·服务器·网络
I'm Jie2 小时前
Rhost v2.0.0 版本发布!!!不止 SSH 终端,内置 AI Agent 支持自然语言操作远程服务器
运维·服务器·rust·开源·ssh·tauri·finalshell
海宇服务2 小时前
零信任架构实战:基于海宇公安二要素认证即时版构建自动化跨境个人额度申报实名网关
运维·人工智能·架构·自动化
沙老师2 小时前
linux比windows死板的地方
linux·运维·windows
H.莓飛2 小时前
【Linux】进程的创建与终止
linux·运维·服务器·c语言·visual studio
研发小能3 小时前
DevOps 工具链选型:一体化平台,还是把单点工具拼起来?
运维·研发效能·devops·嘉为蓝鲸