Java Crac 几乎正式可用

查看 34|回复 2
作者: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.
*
* 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.
*/
public final class CracApplicationContextResource implements Resource {
    private static volatile CracApplicationContextResource instance;
    private final List checkpointRedissonClients = new ArrayList();
    private final List 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 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 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 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 findDruidDataSources(ConfigurableApplicationContext context) {
        Set 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 感觉会更受益,三秒容器上线

crac, checkpoint, restore

pigeon2049
OP
  
实际的 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   
老哥,开源了吗?
您需要登录后才可以回帖 登录 | 立即注册

返回顶部