宿主机负责正式运行 Java 服务最终仍由 systemd 管理,监听固定的 48080 ,不改变现有 Nginx 配置。
Docker 只负责制作 CRaC 快照 新 JAR 在容器内冷启动、完成健康检查和预热,然后创建 checkpoint 。容器内部仍使用 48080 ,宿主机映射随机端口,所以不会和线上服务冲突。
容器和宿主机使用相同路径 统一挂载: /opt/app-server/app-server.jar /opt/app-server/zulu25-crac /opt/app-server/crac/checkpoint 避免恢复时出现 JDK 、JAR 或快照路径不一致。
外部服务使用统一可达地址 MySQL 、Redis 、TDengine 等从 127.0.0.1 改成 172.17.0.1 ,保证容器制作快照和宿主机恢复后都能连接。
快照成功后再切换 Docker checkpoint 成功之前,旧服务继续运行。成功后才停止旧进程、替换 JAR 和快照,再由 systemd 从 checkpoint 快速恢复。
实测启动时间从 2 分钟压到了三秒 需要注意的是 1.crac 特性当前仅 linux 端可用 2.集成 crac,需要告诉它关闭连接的顺序,重启连接的顺序
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.ConfigurableApplicationContext;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
/**
* Coordinates external clients that are not managed by Spring's CRaC lifecycle support.
*
* <p>This resource is registered before the Spring application starts. Spring's resource
* is therefore called first before a checkpoint, pausing web, scheduling and messaging
* lifecycle beans. This resource then closes database and Redis connections. On restore,
* the connections are recreated before Spring restarts its lifecycle beans.</p>
*/
public final class CracApplicationContextResource implements Resource {
private static volatile CracApplicationContextResource instance;
private final List<RestartableRedissonClient> checkpointRedissonClients = new ArrayList<>();
private final List<DruidDataSource> checkpointDataSources = new ArrayList<>();
private volatile ConfigurableApplicationContext applicationContext;
private CracApplicationContextResource() {
}
public static synchronized CracApplicationContextResource register() {
if (instance != null) {
throw new IllegalStateException("CRaC application resource is already registered");
}
CracApplicationContextResource resource = new CracApplicationContextResource();
Core.getGlobalContext().register(resource);
instance = resource;
return resource;
}
public void attach(ConfigurableApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public synchronized void beforeCheckpoint(Context<? extends Resource> context) {
ConfigurableApplicationContext currentContext = this.applicationContext;
if (currentContext == null) {
throw new IllegalStateException("Spring application context is not attached");
}
System.out.println("[CRaC] Suspending external clients before checkpoint");
this.checkpointRedissonClients.clear();
Set<RedissonClient> redissonClients = Collections.newSetFromMap(new IdentityHashMap<>());
redissonClients.addAll(currentContext.getBeansOfType(RedissonClient.class).values());
for (RedissonClient redissonClient : redissonClients) {
RestartableRedissonClient restartable = new RestartableRedissonClient(redissonClient);
this.checkpointRedissonClients.add(restartable);
}
for (RestartableRedissonClient redissonClient : this.checkpointRedissonClients) {
redissonClient.suspend();
}
this.checkpointDataSources.clear();
this.checkpointDataSources.addAll(findDruidDataSources(currentContext));
for (DruidDataSource dataSource : this.checkpointDataSources) {
dataSource.close();
}
System.out.println("[CRaC] External clients suspended");
}
@Override
public synchronized void afterRestore(Context<? extends Resource> context) throws SQLException {
System.out.println("[CRaC] Resuming external clients after restore");
for (DruidDataSource dataSource : this.checkpointDataSources) {
dataSource.restart();
dataSource.init();
}
for (RestartableRedissonClient redissonClient : this.checkpointRedissonClients) {
redissonClient.resume();
}
System.out.println("[CRaC] External clients resumed");
}
private static Set<DruidDataSource> findDruidDataSources(ConfigurableApplicationContext context) {
Set<DruidDataSource> result = Collections.newSetFromMap(new IdentityHashMap<>());
for (DynamicRoutingDataSource routingDataSource
: context.getBeansOfType(DynamicRoutingDataSource.class).values()) {
for (DataSource dataSource : routingDataSource.getDataSources().values()) {
DataSource candidate = dataSource;
if (candidate instanceof ItemDataSource itemDataSource) {
candidate = itemDataSource.getRealDataSource();
}
if (candidate instanceof DruidDataSource druidDataSource) {
result.add(druidDataSource);
}
}
}
return result;
}
private static final class RestartableRedissonClient {
private final RedissonClient client;
private final Config config;
private RestartableRedissonClient(RedissonClient redissonClient) {
if (!(redissonClient instanceof Redisson)) {
throw new IllegalStateException("Unsupported RedissonClient implementation: "
+ redissonClient.getClass().getName());
}
this.client = redissonClient;
this.config = new Config(redissonClient.getConfig());
}
private void suspend() {
this.client.shutdown();
}
private void resume() {
RedissonClient replacement = Redisson.create(new Config(this.config));
replaceRedissonState((Redisson) this.client, (Redisson) replacement);
}
private static void replaceRedissonState(Redisson target, Redisson source) {
try {
for (Field field : Redisson.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
field.set(target, field.get(source));
}
} catch (ReflectiveOperationException exception) {
source.shutdown();
throw new IllegalStateException("Unable to restore Redisson client state", exception);
}
}
}
}
3.需要指定可能引用的文件位置 crac-resource-policies.yaml
root@base:/opt/app-server# cat crac-resource-policies.yaml
# Logback keeps the active application log open across checkpoint/restore.
type: file
path: /root/logs/app-server.log
action: reopen
---
# RocketMQ uses its own file logger outside the Spring logging lifecycle.
type: file
path: /root/logs/rocketmqlogs/rocketmq_client.log
action: reopen
---
# Java2D fonts loaded from streams are backed by persistent temporary files.
type: file
path: /opt/app-server/crac/tmp/+~JF*.tmp
action: reopen
root@base:/opt/app-server#
启动命令类似
root@base:/opt/app-server# cat /opt/app-server/run-app-server.sh
#!/usr/bin/env bash
set -Eeuo pipefail
APP_DIR="${APP_DIR:-/opt/app-server}"
APP_JAR="${APP_DIR}/app-server.jar"
CRAC_JAVA_HOME="${CRAC_JAVA_HOME:-${APP_DIR}/zulu25-crac}"
JAVA_BIN="${CRAC_JAVA_HOME}/bin/java"
IMAGE_DIR="${APP_DIR}/crac/checkpoint"
TMP_DIR="${APP_DIR}/crac/tmp"
POLICY_FILE="${APP_DIR}/crac-resource-policies.yaml"
PROFILE="${APP_PROFILE:-prod}"
APP_TIMEZONE="${APP_TIMEZONE:-Asia/Shanghai}"
install -d -m 700 "${IMAGE_DIR}" "${TMP_DIR}"
echo "[launcher] Trying CRaC restore from ${IMAGE_DIR}; falling back to a cold start when unavailable"
exec "${JAVA_BIN}" \
-server \
-Xms512m \
-Xmx1024m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath="${APP_DIR}/heapdump.hprof" \
-Duser.timezone="${APP_TIMEZONE}" \
-Djava.io.tmpdir="${TMP_DIR}" \
-Djdk.crac.resource-policies="${POLICY_FILE}" \
-XX:CRaCEngine=warp \
-XX:CRaCRestoreFrom="${IMAGE_DIR}" \
-XX:+CRaCIgnoreRestoreIfUnavailable \
-XX:CRaCCheckpointTo="${IMAGE_DIR}" \
-jar "${APP_JAR}" \
--spring.profiles.active="${PROFILE}"
如果检查不到 checkpoint ,会保底从 jar 冷启动
去年没有 ai,折腾这一套没成功 今年有 codex 的帮助,crac 这一套快速启动的便捷性成功实现了 我的这一套只针对单体 springboot 程序 如果是 spring cloud 感觉会更受益,三秒容器上线