容器化已成为 Java 应用部署的标准方式。但 Java 与容器的结合并非简单地将 *.jar 放进镜像——JVM 的内存管理、线程模型与容器的 cgroups 体系需要深度适配,否则极易出现容器内 OOM、CPU 限流失效、启动缓慢等问题。
1. Dockerfile 分层优化
1.1 经典 Dockerfile 的痛点
# ❌ 反模式:所有内容在一个层
FROM openjdk:17
COPY . /app
RUN ./mvnw package
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# 问题:
# 1. 构建上下文太大,每次拷贝所有源码
# 2. 依赖没有缓存层,微小改动也会重新下载依赖
# 3. 最终镜像包含 Maven、源码、.git 等无关内容
1.2 多阶段分层构建
# === 阶段一:依赖缓存层 ===
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /workspace
# 先只拷贝 pom.xml,利用 Docker 缓存
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
# 下载依赖(只要 pom.xml 不变,这层就缓存)
RUN ./mvnw dependency:go-offline -B
# 再拷贝源码并构建
COPY src src
RUN ./mvnw clean package -DskipTests -B && \
mkdir -p target/dependency && \
(cd target/dependency; jar -xf ../*.jar)
# === 阶段二:生产镜像(最小化) ===
FROM eclipse-temurin:17-jre-alpine
# 安全:非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# 分层拷贝,ClassPath 顺序优化
COPY --from=builder /workspace/target/dependency/BOOT-INF/lib /app/lib
COPY --from=builder /workspace/target/dependency/META-INF /app/META-INF
COPY --from=builder /workspace/target/dependency/BOOT-INF/classes /app/classes
# Spring Boot 2.3+ 支持分层 Jar
# java -Djarmode=layertools -jar app.jar extract
USER appuser:appgroup
EXPOSE 8080
# 容器感知 JVM 参数(见第2节)
ENTRYPOINT ["java", "-cp", "app:app/lib/*", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"org.springframework.boot.loader.launch.JarLauncher"]
1.3 Spring Boot 分层 Jar
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
FROM eclipse-temurin:17-jre AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
分层效果:
Dependencies ← 很少变化 → 缓存命中率高
Spring Boot Loader ← 极少变化
Snapshot Dependencies ← 偶尔变化
Application Code ← 频繁变化 → 最上层
2. JVM 容器感知
2.1 问题根源
容器设置: --memory=1g
JVM 感知: 看到宿主机全部内存(如 64GB)
JVM 默认: -Xmx = 1/4 物理内存 = 16GB
结果: JVM 尝试分配 16GB 堆,但容器只有 1GB → OOMKilled
2.2 JDK 8u191+ / JDK 10+ 的容器感知
# JDK 8u191+ 引入的实验性参数(JDK 10+ 默认开启)
-XX:+UseContainerSupport # 从 cgroup 读取容器限制
-XX:MaxRAMPercentage=75.0 # 容器内存的 75% 作为最大堆
-XX:InitialRAMPercentage=50.0 # 初始堆分配
# 替代旧的、不感知容器的参数:
# ❌ -Xmx1g # 硬编码,不灵活
# ✅ -XX:MaxRAMPercentage=75.0 # 随容器大小自适应
2.3 完整的生产 JVM 参数
ENTRYPOINT ["java",
# === 容器感知 ===
"-XX:+UseContainerSupport",
"-XX:MaxRAMPercentage=75.0",
"-XX:InitialRAMPercentage=50.0",
# === 内存优化 ===
"-XX:+UseG1GC",
"-XX:MaxGCPauseMillis=200",
"-XX:+UseStringDeduplication", # G1 字符串去重
# === OOM 处理 ===
"-XX:+HeapDumpOnOutOfMemoryError",
"-XX:HeapDumpPath=/tmp/heapdump.hprof",
"-XX:OnOutOfMemoryError=kill -9 %p",
# === 性能优化 ===
"-XX:+AlwaysPreTouch", # 启动时预分配,避免运行时停顿
"-Djava.security.egd=file:/dev/./urandom", # 加速启动
# === 调试 (可选) ===
"-XX:+PrintFlagsFinal",
"-jar", "/app.jar"]
2.4 手动验证容器感知
# 在容器中运行,验证 JVM 读取了正确的内存限制
docker run --memory=1g --rm myapp \
java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
# 期望输出约为 750MB (75% of 1GB)
3. Kubernetes 资源配置
3.1 完整的 Deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
# 调度策略
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["order-service"]
topologyKey: kubernetes.io/hostname
containers:
- name: order-service
image: registry/myapp/order-service:v1.2.0
resources:
requests:
memory: "512Mi" # 调度依据
cpu: "250m"
limits:
memory: "1Gi" # 硬限制,超过 OOMKill
cpu: "1000m" # CPU 限流(非硬性)
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: management
containerPort: 8081
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport"
- name: SPRING_PROFILES_ACTIVE
value: "production"
# === 探针配置 ===
startupProbe:
httpGet:
path: /actuator/health/liveness
port: management
failureThreshold: 30
periodSeconds: 10 # 最大等待 5 分钟启动
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 3
# 优雅关闭
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 15"] # 等待 Service 摘除
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
3.2 探针解释
| 探针 | 用途 | 失败行为 |
|---|---|---|
| startupProbe | 容器是否已启动 | Kill & Restart |
| livenessProbe | 容器是否存活 | Kill & Restart |
| readinessProbe | 容器是否就绪接受流量 | 从 Service Endpoints 摘除 |
Java 应用的探针设计:
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Autowired private DataSource dataSource;
@Autowired private RabbitTemplate rabbitTemplate;
@Override
public Health health() {
// 就绪: 依赖服务可访问
try (Connection conn = dataSource.getConnection()) {
if (!conn.isValid(3)) {
return Health.down()
.withDetail("database", "connection invalid")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("database", e.getMessage())
.build();
}
return Health.up().build();
}
}
# application.yml
management:
endpoint:
health:
probes:
enabled: true # 为 K8s 启用 liveness/readiness 分组
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
4. Sidecar 模式
4.1 常见 Sidecar 场景
# 日志采集 Sidecar
spec:
containers:
- name: order-service
image: order-service:v1
volumeMounts:
- name: logs
mountPath: /app/logs
- name: filebeat
image: docker.elastic.co/beats/filebeat:8.11
volumeMounts:
- name: logs
mountPath: /app/logs
readOnly: true
- name: filebeat-config
mountPath: /usr/share/filebeat/filebeat.yml
subPath: filebeat.yml
volumes:
- name: logs
emptyDir: {}
# 配置热重载 Sidecar (Consul / Nacos)
spec:
containers:
- name: app
image: myapp:v1
env:
- name: CONFIG_PATH
value: /config
- name: config-reloader
image: config-reloader:v1
volumeMounts:
- name: shared-config
mountPath: /config
4.2 Service Mesh (Istio Sidecar)
# Istio 自动注入 sidecar
metadata:
labels:
app: order-service
annotations:
sidecar.istio.io/inject: "true"
# Sidecar 资源限制
spec:
containers:
- name: istio-proxy
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
5. Helm Chart 结构化部署
# Chart.yaml
apiVersion: v2
name: order-service
description: Order Service Helm Chart
type: application
version: 1.2.0
appVersion: "1.2.0"
# values.yaml
replicaCount: 3
image:
repository: registry/myapp/order-service
tag: "v1.2.0"
pullPolicy: IfNotPresent
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 1000m
java:
opts: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
className: nginx
hosts:
- host: api.mycompany.com
paths:
- path: /orders
pathType: Prefix
# 安装/升级
helm upgrade --install order-service ./chart \
--namespace production \
--set image.tag=v1.2.1 \
--set replicaCount=5
6. 生产检查清单
| 检查项 | 要求 | 命令 |
|---|---|---|
| 非 root 运行 | UID > 0 | docker run --rm myapp id |
| 镜像最小化 | 无包管理器、无 shell(或 distroless) | dive myapp |
| 安全扫描 | 无 CRITICAL 漏洞 | trivy image myapp |
| JVM 容器感知 | MaxHeap ≈ limit * 75% | jcmd 1 VM.flags |
| 优雅关闭 | SIGTERM 后完成请求再退出 | kubectl delete pod 观测 |
| 资源限制 | requests ≤ limits | kubectl describe pod |
延伸阅读
- Spring Cloud 微服务架构全家桶 — 微服务在 K8s 中的服务发现
- Spring Native 与 GraalVM — Native Image 容器化
- 日志框架与分布式链路追踪 — 容器环境的日志采集
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。