• pigeon2049

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

    主楼
  • pigeon2049

    实际的 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

    #1
  • lxxzml

    老哥,开源了吗?

    #2
  • pigeon2049

    @lxxzml 扔给 codex 让它调( linux 上)

    #3

发表回复

登录后回复