Docker & Kubernetes Java应用部署实战
·
引言
容器化和Kubernetes已成为现代应用部署的标准。作为Java开发者,掌握Docker和Kubernetes不仅能让应用部署更高效,还能大幅提升系统的可伸缩性和可靠性。本文将带你从Docker基础到Kubernetes高级特性,全面掌握Java应用的容器化部署。
Docker容器化深度实践
1.1 优化Java应用Docker镜像
# 多阶段构建优化Dockerfile
# 第一阶段:构建阶段
FROM maven:3.8.6-openjdk-17 AS builder
# 设置工作目录
WORKDIR /app
# 复制pom文件并下载依赖(利用Docker缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 复制源代码并构建
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行时阶段
FROM eclipse-temurin:17-jre-alpine
# 安装必要的工具
RUN apk add --no-cache tini curl
# 设置Java优化参数
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
ENV JAVA_TOOL_OPTIONS="-Xmx512m -Xms256m"
# 创建非root用户运行应用
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
# 设置工作目录
WORKDIR /app
# 从构建阶段复制jar文件
COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 使用tini作为init进程
ENTRYPOINT ["/sbin/tini", "--"]
# 启动应用
CMD java $JAVA_OPTS -jar app.jar
# 暴露端口
EXPOSE 8080
# 标签信息
LABEL maintainer="dev-team@company.com"
LABEL version="1.0.0"
LABEL description="Spring Boot Application"
# 针对不同环境的Dockerfile模板
# 开发环境Dockerfile
FROM eclipse-temurin:17-jdk-alpine as development
WORKDIR /app
COPY . .
RUN ./mvnw dependency:go-offline -B
CMD ["./mvnw", "spring-boot:run"]
# 生产环境Dockerfile
FROM eclipse-temurin:17-jre-alpine as production
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
WORKDIR /app
COPY --from=development /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
1.2 Docker Compose编排实战
# docker-compose.yml - 完整微服务栈
version: '3.8'
services:
# MySQL数据库
mysql:
image: mysql:8.0
container_name: app-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./config/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
# Redis缓存
redis:
image: redis:7-alpine
container_name: app-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# Nacos服务发现
nacos:
image: nacos/nacos-server:2.2.0
container_name: app-nacos
environment:
- MODE=standalone
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql
- MYSQL_SERVICE_DB_NAME=nacos_dev
- MYSQL_SERVICE_PORT=3306
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=${MYSQL_ROOT_PASSWORD}
ports:
- "8848:8848"
depends_on:
mysql:
condition: service_healthy
networks:
- app-network
# 用户服务
user-service:
build:
context: ./user-service
dockerfile: Dockerfile
target: production
container_name: user-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/user_db
- SPRING_REDIS_HOST=redis
- SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
ports:
- "8081:8080"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
nacos:
condition: service_started
networks:
- app-network
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
# 订单服务
order-service:
build:
context: ./order-service
dockerfile: Dockerfile
container_name: order-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- USER_SERVICE_URL=http://user-service:8080
ports:
- "8082:8080"
depends_on:
- user-service
networks:
- app-network
# API网关
api-gateway:
build:
context: ./api-gateway
dockerfile: Dockerfile
container_name: api-gateway
ports:
- "8080:8080"
depends_on:
- user-service
- order-service
networks:
- app-network
# 监控栈
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- app-network
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
networks:
- app-network
volumes:
mysql_data:
redis_data:
networks:
app-network:
driver: bridge
1.3 Java应用容器化最佳实践
// 容器感知的Spring Boot配置
@Configuration
@Slf4j
public class ContainerAwareConfiguration {
@Bean
@Profile("docker")
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
return factory -> {
// 在容器环境中优化Web服务器配置
if (factory instanceof TomcatServletWebServerFactory) {
customizeTomcat((TomcatServletWebServerFactory) factory);
}
};
}
private void customizeTomcat(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
// 优化Tomcat连接器配置
connector.setProperty("maxThreads", "200");
connector.setProperty("minSpareThreads", "20");
connector.setProperty("connectionTimeout", "5000");
});
log.info("Tomcat配置已针对容器环境优化");
}
@Bean
@Profile("docker")
public DataSource dataSource(Environment env) {
HikariConfig config = new HikariConfig();
// 容器环境下的数据库连接池优化
config.setJdbcUrl(env.getProperty("spring.datasource.url"));
config.setUsername(env.getProperty("spring.datasource.username"));
config.setPassword(env.getProperty("spring.datasource.password"));
config.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
// 连接池优化配置
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setLeakDetectionThreshold(60000);
return new HikariDataSource(config);
}
// 容器健康检查端点
@Component
@Endpoint(id = "container")
@Slf4j
public class ContainerHealthEndpoint {
@ReadOperation
public Map<String, Object> health() {
Map<String, Object> health = new HashMap<>();
// 检查内存使用
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
double memoryUsage = (double) usedMemory / maxMemory;
health.put("memoryUsage", String.format("%.2f%%", memoryUsage * 100));
health.put("maxMemory", formatMemory(maxMemory));
health.put("usedMemory", formatMemory(usedMemory));
// 检查CPU核心数
health.put("availableProcessors", runtime.availableProcessors());
// 检查文件系统
File root = new File("/");
health.put("diskFree", formatMemory(root.getFreeSpace()));
health.put("diskTotal", formatMemory(root.getTotalSpace()));
log.debug("容器健康检查: {}", health);
return health;
}
private String formatMemory(long bytes) {
return String.format("%.2f MB", bytes / 1024.0 / 1024.0);
}
}
}
// 容器环境检测工具
@Component
@Slf4j
public class ContainerEnvironmentDetector {
private final boolean runningInContainer;
public ContainerEnvironmentDetector() {
this.runningInContainer = detectContainerEnvironment();
log.info("运行环境: {}", runningInContainer ? "容器" : "物理机/虚拟机");
}
public boolean isRunningInContainer() {
return runningInContainer;
}
private boolean detectContainerEnvironment() {
// 检查常见的容器环境指标
return checkCGroup() || checkDockerEnv() || checkKubernetesEnv();
}
private boolean checkCGroup() {
try {
File cgroup = new File("/proc/1/cgroup");
if (cgroup.exists()) {
String content = Files.readString(cgroup.toPath());
return content.contains("docker") || content.contains("kubepods");
}
} catch (IOException e) {
log.debug("检查cgroup失败", e);
}
return false;
}
private boolean checkDockerEnv() {
return System.getenv("DOCKER_CONTAINER") != null ||
new File("/.dockerenv").exists();
}
private boolean checkKubernetesEnv() {
return System.getenv("KUBERNETES_SERVICE_HOST") != null ||
System.getenv("KUBERNETES_PORT") != null;
}
// 获取容器资源限制
public ContainerResources getContainerResources() {
return new ContainerResources(
getMemoryLimit(),
getCpuLimit(),
getCpuQuota()
);
}
private Long getMemoryLimit() {
try {
// 从cgroup读取内存限制
Path memoryLimitPath = Paths.get("/sys/fs/cgroup/memory/memory.limit_in_bytes");
if (Files.exists(memoryLimitPath)) {
String content = Files.readString(memoryLimitPath).trim();
return Long.parseLong(content);
}
} catch (Exception e) {
log.debug("读取内存限制失败", e);
}
return null;
}
private Long getCpuLimit() {
try {
// 从cgroup读取CPU限制
Path cpuPeriodPath = Paths.get("/sys/fs/cgroup/cpu/cpu.cfs_period_us");
Path cpuQuotaPath = Paths.get("/sys/fs/cgroup/cpu/cpu.cfs_quota_us");
if (Files.exists(cpuPeriodPath) && Files.exists(cpuQuotaPath)) {
long period = Long.parseLong(Files.readString(cpuPeriodPath).trim());
long quota = Long.parseLong(Files.readString(cpuQuotaPath).trim());
if (quota > 0 && period > 0) {
return Math.round((double) quota / period);
}
}
} catch (Exception e) {
log.debug("读取CPU限制失败", e);
}
return null;
}
private Long getCpuQuota() {
try {
Path cpuQuotaPath = Paths.get("/sys/fs/cgroup/cpu/cpu.cfs_quota_us");
if (Files.exists(cpuQuotaPath)) {
return Long.parseLong(Files.readString(cpuQuotaPath).trim());
}
} catch (Exception e) {
log.debug("读取CPU配额失败", e);
}
return null;
}
public static class ContainerResources {
private final Long memoryLimit;
private final Long cpuLimit;
private final Long cpuQuota;
public ContainerResources(Long memoryLimit, Long cpuLimit, Long cpuQuota) {
this.memoryLimit = memoryLimit;
this.cpuLimit = cpuLimit;
this.cpuQuota = cpuQuota;
}
// getters...
}
}
Kubernetes部署实战
2.1 Kubernetes基础资源配置
# namespace.yaml - 命名空间配置
apiVersion: v1
kind: Namespace
metadata:
name: java-app
labels:
name: java-app
environment: production
---
# configmap.yaml - 应用配置
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: java-app
data:
application.yml: |
server:
port: 8080
tomcat:
max-threads: 200
min-spare-threads: 20
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
show-sql: false
hibernate:
ddl-auto: validate
redis:
timeout: 2000
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
probes:
enabled: true
logback-spring.xml: |
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
---
# secret.yaml - 敏感信息配置
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: java-app
type: Opaque
data:
database-password: c3VwZXJzZWNyZXRwYXNzd29yZA== # base64编码
redis-password: cmVkaXNwYXNzd29yZA==
jwt-secret: anN0c2VjcmV0a2V5MTIzNDU2Nzg=
2.2 Deployment高级配置
# deployment.yaml - 用户服务部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: java-app
labels:
app: user-service
version: v1.0.0
spec:
replicas: 3
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
# 亲和性调度
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- user-service
topologyKey: kubernetes.io/hostname
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
# 资源限制
containers:
- name: user-service
image: registry.company.com/user-service:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
- name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE
value: "health,info,metrics,prometheus"
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
# 健康检查
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /actuator/health/startup
port: 8080
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
# 生命周期钩子
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"]
# 安全上下文
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
- name: tmp-volume
mountPath: /tmp
volumes:
- name: config-volume
configMap:
name: app-config
- name: tmp-volume
emptyDir: {}
# 服务账户
serviceAccountName: java-app-sa
# 镜像拉取密钥
imagePullSecrets:
- name: registry-credentials
---
# service.yaml - 服务暴露
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: java-app
labels:
app: user-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
selector:
app: user-service
ports:
- name: http
port: 8080
targetPort: 8080
protocol: TCP
- name: metrics
port: 8081
targetPort: 8081
protocol: TCP
type: ClusterIP
---
# ingress.yaml - 入口配置
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: user-service-ingress
namespace: java-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: 10m
spec:
ingressClassName: nginx
rules:
- host: users.api.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: user-service
port:
number: 8080
tls:
- hosts:
- users.api.company.com
secretName: tls-secret
2.3 Kubernetes Operator模式
// 自定义资源定义 - Java应用配置
public class JavaAppSpec {
private String version;
private int replicas;
private Resources resources;
private ProbeConfig livenessProbe;
private ProbeConfig readinessProbe;
private Map<String, String> env;
private List<String> jvmArgs;
// getters and setters...
public static class Resources {
private String memoryRequest;
private String cpuRequest;
private String memoryLimit;
private String cpuLimit;
// getters and setters...
}
public static class ProbeConfig {
private String path;
private int initialDelay;
private int period;
private int timeout;
// getters and setters...
}
}
// 自定义控制器
@Component
@Slf4j
public class JavaAppOperator {
@Autowired
private KubernetesClient kubernetesClient;
@EventListener
public void onJavaAppCreated(GenericKubernetesResource resource) {
String name = resource.getMetadata().getName();
String namespace = resource.getMetadata().getNamespace();
log.info("处理Java应用创建事件: {}/{}", namespace, name);
try {
// 解析自定义资源
JavaAppSpec spec = parseSpec(resource);
// 创建或更新Deployment
createOrUpdateDeployment(namespace, name, spec);
// 创建或更新Service
createOrUpdateService(namespace, name, spec);
// 创建或更新Ingress(如果需要)
if (spec.isExposePublic()) {
createOrUpdateIngress(namespace, name, spec);
}
log.info("Java应用部署完成: {}/{}", namespace, name);
} catch (Exception e) {
log.error("处理Java应用失败: {}/{}", namespace, name, e);
}
}
private JavaAppSpec parseSpec(GenericKubernetesResource resource) {
// 解析自定义资源规范
ObjectMapper mapper = new ObjectMapper();
return mapper.convertValue(resource.getAdditionalProperties().get("spec"), JavaAppSpec.class);
}
private void createOrUpdateDeployment(String namespace, String name, JavaAppSpec spec) {
Deployment deployment = new DeploymentBuilder()
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.withLabels(createLabels(name, spec.getVersion()))
.endMetadata()
.withNewSpec()
.withReplicas(spec.getReplicas())
.withNewSelector()
.withMatchLabels(createLabels(name, spec.getVersion()))
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(createLabels(name, spec.getVersion()))
.endMetadata()
.withNewSpec()
.withContainers(createContainer(name, spec))
.withSecurityContext(createSecurityContext())
.endSpec()
.endTemplate()
.withStrategy(createUpdateStrategy())
.endSpec()
.build();
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.resource(deployment)
.createOrReplace();
}
private Container createContainer(String name, JavaAppSpec spec) {
return new ContainerBuilder()
.withName(name)
.withImage("registry.company.com/" + name + ":" + spec.getVersion())
.withPorts(new ContainerPortBuilder()
.withContainerPort(8080)
.withProtocol("TCP")
.build())
.withEnv(createEnvVars(spec))
.withResources(createResourceRequirements(spec.getResources()))
.withLivenessProbe(createProbe(spec.getLivenessProbe()))
.withReadinessProbe(createProbe(spec.getReadinessProbe()))
.withSecurityContext(createContainerSecurityContext())
.build();
}
private List<EnvVar> createEnvVars(JavaAppSpec spec) {
List<EnvVar> envVars = new ArrayList<>();
// 添加JVM参数
if (spec.getJvmArgs() != null) {
envVars.add(new EnvVarBuilder()
.withName("JAVA_OPTS")
.withValue(String.join(" ", spec.getJvmArgs()))
.build());
}
// 添加自定义环境变量
if (spec.getEnv() != null) {
spec.getEnv().forEach((key, value) ->
envVars.add(new EnvVarBuilder()
.withName(key)
.withValue(value)
.build())
);
}
return envVars;
}
private Map<String, String> createLabels(String name, String version) {
Map<String, String> labels = new HashMap<>();
labels.put("app", name);
labels.put("version", version);
labels.put("managed-by", "java-app-operator");
return labels;
}
// 其他创建方法...
}
Helm Chart包管理
3.1 Helm Chart结构设计
# Chart.yaml
apiVersion: v2
name: java-springboot-app
description: A Helm chart for Spring Boot Java applications
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: mysql
version: 9.7.0
repository: https://charts.bitnami.com/bitnami
condition: mysql.enabled
- name: redis
version: 17.3.0
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
- name: prometheus
version: 15.0.0
repository: https://prometheus-community.github.io/helm-charts
condition: monitoring.enabled
# values.yaml - 配置默认值
# 应用配置
app:
name: "user-service"
version: "1.0.0"
replicaCount: 3
image:
repository: "registry.company.com/user-service"
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
metricsPort: 8081
ingress:
enabled: true
className: "nginx"
hosts:
- host: "users.api.company.com"
paths:
- path: "/"
pathType: Prefix
tls: []
# Java配置
java:
opts: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
jmx:
enabled: false
port: 1099
# 资源限制
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
# 健康检查
livenessProbe:
path: "/actuator/health/liveness"
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
path: "/actuator/health/readiness"
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
# 环境变量
env:
SPRING_PROFILES_ACTIVE: "kubernetes"
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: "health,info,metrics,prometheus"
# 数据库配置
mysql:
enabled: true
auth:
rootPassword: "mysql-root-password"
database: "user_db"
username: "app_user"
password: "app_password"
primary:
persistence:
enabled: true
size: "8Gi"
resources:
requests:
memory: "256Mi"
cpu: "100m"
# Redis配置
redis:
enabled: true
auth:
password: "redis-password"
master:
persistence:
enabled: true
size: "4Gi"
resources:
requests:
memory: "128Mi"
cpu: "100m"
# 监控配置
monitoring:
enabled: true
prometheus:
scrapeInterval: "30s"
grafana:
enabled: true
adminPassword: "admin"
# 自动伸缩配置
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "java-springboot-app.fullname" . }}
labels:
{{- include "java-springboot-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.app.replicaCount }}
{{- end }}
revisionHistoryLimit: {{ .Values.app.revisionHistoryLimit | default 3 }}
strategy:
{{- toYaml .Values.app.strategy | nindent 4 }}
selector:
matchLabels:
{{- include "java-springboot-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "java-springboot-app.selectorLabels" . | nindent 8 }}
{{- with .Values.app.podLabels }}
{{ toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.app.podAnnotations }}
{{ toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.app.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "java-springboot-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.app.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.app.securityContext | nindent 12 }}
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.app.service.port }}
protocol: TCP
- name: metrics
containerPort: {{ .Values.app.service.metricsPort }}
protocol: TCP
env:
{{- range $key, $value := .Values.app.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.app.java.jmx.enabled }}
- name: JAVA_TOOL_OPTIONS
value: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port={{ .Values.app.java.jmx.port }} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
{{- end }}
envFrom:
- configMapRef:
name: {{ include "java-springboot-app.fullname" . }}-config
- secretRef:
name: {{ include "java-springboot-app.fullname" . }}-secrets
livenessProbe:
httpGet:
path: {{ .Values.app.livenessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.app.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.app.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.app.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.app.livenessProbe.failureThreshold | default 3 }}
readinessProbe:
httpGet:
path: {{ .Values.app.readinessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.app.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.app.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.app.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.app.readinessProbe.failureThreshold | default 3 }}
resources:
{{- toYaml .Values.app.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
volumes:
- name: config
configMap:
name: {{ include "java-springboot-app.fullname" . }}-config
{{- with .Values.app.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.app.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.app.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
高级部署策略
4.1 蓝绿部署与金丝雀发布
# blue-green-deployment.yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: user-service
namespace: java-app
spec:
# 目标部署引用
targetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
# 服务引用
service:
port: 8080
targetPort: 8080
# 流量策略
trafficPolicy:
tls:
# mTLS配置
mode: ISTIO_MUTUAL
# 分析配置
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
threshold: 99
interval: 1m
- name: request-duration
threshold: 500
interval: 1m
- name: error-rate
threshold: 1
interval: 1m
# 自定义指标
webhooks:
- name: "load-test"
type: pre-rollout
url: http://flagger-loadtester.test/
timeout: 5m
metadata:
type: "cmd"
cmd: "hey -z 1m -q 10 -c 2 http://user-service-canary.java-app:8080/actuator/health"
- name: "analysis"
type: rollout
url: http://analyzer-service/
metadata:
query: "max_over_time(application_requests_total{job='user-service', status=~'2..'}[1m])"
// 部署策略管理器
@Component
@Slf4j
public class DeploymentStrategyManager {
@Autowired
private KubernetesClient kubernetesClient;
/**
* 执行蓝绿部署
*/
public void blueGreenDeployment(String deploymentName, String namespace,
String newImage, String newVersion) {
log.info("开始蓝绿部署: {}/{} -> {}", namespace, deploymentName, newImage);
try {
// 1. 创建绿色部署
String greenDeploymentName = deploymentName + "-green";
createGreenDeployment(greenDeploymentName, namespace, newImage, newVersion);
// 2. 等待绿色部署就绪
waitForDeploymentReady(greenDeploymentName, namespace);
// 3. 切换服务流量
switchServiceTraffic(deploymentName, greenDeploymentName, namespace);
// 4. 清理蓝色部署
cleanupBlueDeployment(deploymentName, namespace);
log.info("蓝绿部署完成: {}/{}", namespace, deploymentName);
} catch (Exception e) {
log.error("蓝绿部署失败: {}/{}", namespace, deploymentName, e);
rollbackBlueGreenDeployment(deploymentName, namespace);
}
}
/**
* 执行金丝雀发布
*/
public void canaryDeployment(String deploymentName, String namespace,
String newImage, String newVersion,
int initialTrafficPercent) {
log.info("开始金丝雀发布: {}/{} -> {} ({}%)",
namespace, deploymentName, newImage, initialTrafficPercent);
try {
// 1. 创建金丝雀部署
String canaryDeploymentName = deploymentName + "-canary";
createCanaryDeployment(canaryDeploymentName, namespace, newImage, newVersion);
// 2. 逐步增加流量
for (int trafficPercent = initialTrafficPercent; trafficPercent <= 100; trafficPercent += 10) {
adjustTrafficSplit(deploymentName, canaryDeploymentName, namespace, trafficPercent);
// 等待一段时间观察指标
Thread.sleep(300000); // 5分钟
// 检查应用健康状态
if (!isDeploymentHealthy(canaryDeploymentName, namespace)) {
log.warn("金丝雀部署不健康,回滚");
rollbackCanaryDeployment(deploymentName, namespace);
return;
}
log.info("金丝雀流量已调整到: {}%", trafficPercent);
}
// 3. 完成发布
completeCanaryDeployment(deploymentName, canaryDeploymentName, namespace);
log.info("金丝雀发布完成: {}/{}", namespace, deploymentName);
} catch (Exception e) {
log.error("金丝雀发布失败: {}/{}", namespace, deploymentName, e);
rollbackCanaryDeployment(deploymentName, namespace);
}
}
private void createGreenDeployment(String name, String namespace, String image, String version) {
// 基于现有部署创建绿色部署
Deployment existing = kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name.replace("-green", ""))
.get();
Deployment greenDeployment = new DeploymentBuilder(existing)
.editMetadata()
.withName(name)
.withResourceVersion(null)
.endMetadata()
.editSpec()
.editTemplate()
.editMetadata()
.withLabels(updateLabels(existing.getSpec().getTemplate().getMetadata().getLabels(), version))
.endMetadata()
.editSpec()
.editContainer(0)
.withImage(image)
.endContainer()
.endSpec()
.endTemplate()
.endSpec()
.build();
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.resource(greenDeployment)
.create();
}
private Map<String, String> updateLabels(Map<String, String> labels, String version) {
Map<String, String> newLabels = new HashMap<>(labels);
newLabels.put("version", version);
newLabels.put("deployment-color", "green");
return newLabels;
}
private void waitForDeploymentReady(String deploymentName, String namespace) throws InterruptedException {
int maxWaitTime = 600; // 10分钟
int waited = 0;
while (waited < maxWaitTime) {
Deployment deployment = kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(deploymentName)
.get();
if (deployment != null && deployment.getStatus() != null &&
deployment.getStatus().getReadyReplicas() != null &&
deployment.getStatus().getReadyReplicas().equals(deployment.getSpec().getReplicas())) {
log.info("部署就绪: {}/{}", namespace, deploymentName);
return;
}
Thread.sleep(10000); // 等待10秒
waited += 10;
log.info("等待部署就绪: {}/{} (已等待{}秒)", namespace, deploymentName, waited);
}
throw new RuntimeException("部署就绪超时: " + deploymentName);
}
// 其他方法实现...
}
监控与日志管理
5.1 应用监控配置
# service-monitor.yaml - Prometheus监控
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: user-service-monitor
namespace: java-app
labels:
app: user-service
release: prometheus
spec:
selector:
matchLabels:
app: user-service
endpoints:
- port: metrics
path: /actuator/prometheus
interval: 30s
scrapeTimeout: 10s
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
namespaceSelector:
matchNames:
- java-app
// 自定义应用监控
@Component
@Slf4j
public class ApplicationMetrics {
private final MeterRegistry meterRegistry;
private final Counter httpRequestsTotal;
private final Timer httpRequestDuration;
private final Gauge memoryUsage;
private final Counter businessEvents;
public ApplicationMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// HTTP请求指标
this.httpRequestsTotal = Counter.builder("http_requests_total")
.description("Total HTTP requests")
.tags("application", "user-service")
.register(meterRegistry);
this.httpRequestDuration = Timer.builder("http_request_duration_seconds")
.description("HTTP request duration in seconds")
.tags("application", "user-service")
.register(meterRegistry);
// 内存使用指标
this.memoryUsage = Gauge.builder("jvm_memory_usage_ratio")
.description("JVM memory usage ratio")
.tags("application", "user-service")
.register(meterRegistry, this, self -> self.calculateMemoryUsage());
// 业务指标
this.businessEvents = Counter.builder("business_events_total")
.description("Total business events")
.tags("application", "user-service")
.register(meterRegistry);
}
public void recordHttpRequest(String method, String path, int status, long duration) {
httpRequestsTotal.increment();
httpRequestDuration.record(duration, TimeUnit.MILLISECONDS);
// 记录带标签的指标
Counter.builder("http_requests_detailed")
.tags("method", method, "path", path, "status", String.valueOf(status))
.register(meterRegistry)
.increment();
}
public void recordBusinessEvent(String eventType) {
businessEvents.increment();
Counter.builder("business_events_detailed")
.tags("type", eventType)
.register(meterRegistry)
.increment();
}
private double calculateMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
return (double) usedMemory / maxMemory;
}
// 数据库连接池监控
@EventListener
public void monitorDataSource(DataSourcePoolCreatedEvent event) {
DataSource dataSource = event.getDataSource();
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
Gauge.builder("database_connections_active")
.description("Active database connections")
.tags("application", "user-service")
.register(meterRegistry, hikariDataSource, HikariDataSource::getHikariPoolMXBean)
.getActiveConnections();
Gauge.builder("database_connections_idle")
.description("Idle database connections")
.tags("application", "user-service")
.register(meterRegistry, hikariDataSource, HikariDataSource::getHikariPoolMXBean)
.getIdleConnections();
}
}
}
总结
🎯 Docker & Kubernetes部署核心要点
-
容器化最佳实践
- 多阶段构建优化镜像大小
- 非root用户运行增强安全
- 合理的资源限制和健康检查
-
Kubernetes部署策略
- 使用ConfigMap和Secret管理配置
- 合理的资源请求和限制
- 多维度健康检查确保应用可用性
-
高级部署模式
- 蓝绿部署实现零停机发布
- 金丝雀发布降低发布风险
- Helm标准化应用部署
-
监控与可观测性
- 应用指标暴露和收集
- 日志聚合和分析
- 分布式追踪
🚀 生产环境建议
-
安全加固
- 使用Pod安全策略
- 网络策略限制通信
- 镜像漏洞扫描
-
性能优化
- 合理的资源配额
- HPA自动伸缩
- 节点亲和性调度
-
灾难恢复
- 定期备份ETCD
- 应用数据备份
- 跨可用区部署
📋 部署检查表
- Docker镜像优化和安全扫描
- Kubernetes资源配置合理
- 健康检查配置正确
- 监控告警配置完善
- 日志收集配置正确
- 备份策略准备就绪
- 灾难恢复方案测试
- 安全策略配置到位
掌握Docker和Kubernetes的Java应用部署,能够让你的应用在云原生环境中获得更好的弹性、可观测性和可靠性。
下一篇预告:《响应式编程实战:WebFlux与RSocket深度解析》
如果觉得本文对你有帮助,请点赞、收藏、关注!欢迎在评论区分享你的容器化部署经验和问题。
更多推荐


所有评论(0)