pigeon2049
V2EX  ›  Java

Java Crac 几乎正式可用

  •  
  •   pigeon2049 · 1h 32m ago · 316 views

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

    2 replies    2026-08-24 11:49:52 +08:00
    pigeon2049
        1
    pigeon2049  
    OP
       1h 28m ago
    实际的 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
    lxxzml
        2
    lxxzml  
       24 mins ago
    老哥,开源了吗?
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3998 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 04:14 · PVG 12:14 · LAX 21:14 · JFK 00:14
    ♥ Do have faith in what you're doing.