GoForum🌐 V2EX

Java Crac 几乎正式可用

pigeon2049 · 2026-08-24 10:48 · 0 次点赞 · 1 条回复

宿主机负责正式运行 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 感觉会更受益,三秒容器上线

1 条回复
pigeon2049 · 2026-08-24 10:48
#1

实际的 checkpoint 就是一个 800M 大小的 core.img 另外有一个小的 engine 记录了使用的是什么引擎 -rw——- 1 root root 834M 8 月 24 日 08:53 core.img -rw-r–r– 1 root root 4 8 月 24 日 08:53 engine

添加回复
你还需要 登录 后发表回复

登录后可发帖和回复

登录 注册
主题信息
作者: pigeon2049
发布: 2026-08-24
点赞: 0
回复: 0