电商系统怎么做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"基本消失,团队就能把精力放回业务本身。