Java 服务的冷启动不只花在创建 JVM 上。类加载、字节码验证、JIT 编译、依赖初始化、连接池建立和缓存预热都会消耗时间。Project CRaC(Coordinated Restore at Checkpoint)探索的思路,是让应用先完成启动和预热,再把运行中进程保存为快照;后续启动时恢复该进程,跳过已经完成的工作。
CRaC 不是 Java 对象序列化,也不是把业务对象写到磁盘后另起一个全新 JVM。它面向整个进程状态:堆、线程、映射和运行时状态都在操作系统级别的进程快照中恢复。对文件描述符、套接字、时钟和外部服务连接等资源,应用必须在检查点前配合释放或协调,并在恢复后重新建立有效状态。
这是一条独立于 Java SE 标准启动流程的工程路线。OpenJDK CRaC 项目维护相关运行时与接口;可用性取决于采用的 JDK 构建、平台支持和系统权限。不能假设任意发行版的 java 命令都能创建或恢复 CRaC 检查点。
运行时快照解决的是什么问题
用传统启动方式启动服务,流程大致是:创建操作系统进程、初始化 JVM、加载类、运行应用初始化代码、预热热点代码和缓存,最后进入稳定服务阶段。若每个新副本都从头重复这些步骤,弹性扩容和短生命周期任务会承担明显启动成本。
CRaC 在初次运行时让应用走到适合服务流量的状态,再请求检查点。快照中保留此刻的进程执行状态。后续部署恢复快照后,应用从保存的位置继续运行,执行恢复回调并修复那些不能跨越暂停时间或机器边界的资源。
检查点可缩短从进程启动到可服务的时间,但不会消除基础设施初始化、网络注册、负载均衡器健康检查、容器拉起和外部依赖准备。系统最终需要优化的是“新实例何时能安全接流量”,而不是只看 JVM 进程从 main 到恢复完成的时间。
谁负责保存和恢复进程状态
CRaC 使用可插拔的进程快照机制。OpenJDK CRaC 项目当前的 Linux 实现使用 CRIU(Checkpoint/Restore In Userspace)完成进程级检查点与恢复。项目 README 要求使用较新的 CRIU(4.0 或以上),并说明创建或恢复检查点通常需要由 root 持有的系统能力。部署平台、内核和发行版会影响支持程度。
在保存检查点时,JVM 与应用先执行协调逻辑,然后由外部快照机制记录进程状态。恢复时,进程状态重新出现,JVM 继续执行,并在允许业务接入之前通知应用进行恢复处理。应用钩子与操作系统快照各自承担不同职责:CRIU 恢复进程,org.crac API 让应用能处理检查点边界上的资源生命周期。
该模型保存的是运行时执行现场,不是可在任意系统、任意 JDK 或不同 CPU 架构之间移植的通用文件格式。制作快照的运行环境与恢复环境必须满足兼容要求;镜像、内核、JDK 构建、CPU 特性和权限策略都要纳入部署约束。
Resource 回调:给外部资源一个检查点边界
org.crac.Resource 提供 beforeCheckpoint 和 afterRestore 两个生命周期回调。组件可以注册到全局 Context 或合适的资源上下文,在检查点前清理不可安全保存的状态,并在恢复后重建状态。
例如,一个管理 socket 的组件可以按以下思路注册:
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
final class ConnectionResource implements Resource {
ConnectionResource() {
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(Context<? extends Resource> context) {
closeConnection();
}
@Override
public void afterRestore(Context<? extends Resource> context) {
reopenConnection();
}
private void closeConnection() { /* release process-local connection */ }
private void reopenConnection() { /* establish a fresh connection */ }
}示例只展示回调形状。具体 CRaC JDK 版本的依赖坐标和 API 细节应以对应发行版文档为准;如果系统没有 org.crac API,这段代码不能直接在标准 JDK 上编译。
需要审查的资源不止数据库连接:
- TCP/Unix 套接字、HTTP client、数据库连接池和消息代理会话;
- 文件、锁、临时目录与本地库持有的文件描述符;
- 线程池中的定时任务、选举或租约状态;
- TLS 会话、身份凭据、令牌、随机数源和硬件句柄;
- 依赖当前时间、进程 ID、主机名、IP 地址或可用 CPU 数的缓存。
简单地在检查点前关闭连接并不够。组件还需确保旧异步回调不会在恢复后继续操作过期连接,恢复完成前没有请求使用未重建的客户端,并且失败时能报告清晰的恢复错误。
把应用带到一个可以快照的边界
检查点前的资源回调不能代替应用协调。服务通常还需要一个明确的状态转换:先停止接收新请求,再决定排空还是拒绝在途工作,之后处理后台任务与外部资源,然后才进入可检查点状态。否则快照可能把“请求已从队列取走、业务事务尚未提交”的中间态保存下来。
恢复后也需要一个“尚不可接流量”的窗口。应用先重建凭据、数据库连接、消息订阅、临时目录和实例身份,验证依赖可用后才报告 readiness。仅仅看到进程恢复成功,不代表业务组件已经恢复到能够安全处理请求的状态。
对计划任务要决定恢复语义:过期的任务是跳过、补跑还是重排?若几十个副本从同一快照恢复并立即执行同一周期任务,可能形成重复工作或流量尖峰。可在恢复后刷新 leader/lease 状态,为周期任务重新加入随机抖动,并将业务操作设计为幂等。
时间、随机数与实例状态会跨越快照边界
快照会保留大量 JVM 内存状态,但不能让已经经过的真实时间消失。过期缓存、证书、令牌、数据库租约和本地计时器,恢复后都可能看起来仍处于保存时刻。涉及有效期的状态应在恢复时重新校验,而不只是沿用快照中的布尔标志。
随机数生成器状态同样需要认真处理。若同一快照复制成多个副本,它们可能从相同内部状态继续生成序列;密码学用途应按安全库的要求在恢复后重新播种或重建生成器。不要将安全令牌、实例 ID 和唯一性随机数当作可以复制的普通缓存。
需要依赖单调时间测量期限的组件也应确认目标平台和运行时恢复语义,必要时在 afterRestore 中重建 deadline。一个安全策略是保存绝对业务截止时间或外部状态版本,并在恢复时重新计算剩余有效期,而不是只保存 JVM 内部的计时器对象。
检查点时机与应用一致性
检查点应该发生在应用处于可恢复的状态,而不是仅仅“启动了一段时间之后”。应确认没有半写入文件、未提交的事务、不可重放的消息确认或正在变更的外部状态。如果应用将业务状态写入外部数据库,快照捕获的 JVM 堆可能处于某一刻,而数据库已经前进;恢复后应用必须重新校验租约、事务和版本号,避免重复或陈旧操作。
预热也要有边界。快照越晚,可能包含更多已加载类和优化后的代码,但也可能保存更多过时缓存、用户数据或租户状态。由真实工作负载触发过热代码路径,有助于预热 JIT;然而执行生产级预热流量时,要避免造成真实副作用或把敏感数据写入快照。
恢复钩子通常应满足幂等性:如果恢复失败后重试,资源重建不能重复提交不可逆操作。多个资源之间若有依赖关系,还要明确注册和回调顺序,避免数据库客户端在凭据提供者准备好之前启动。
和 Project Leyden 的区别
Leyden 关注在构建或部署过程中预先计算、缓存部分 Java 启动工作,例如类加载相关数据、链接或代码缓存等;它保留正常 JVM 进程启动与初始化边界。CRaC 则保存已经运行中的整个进程状态,在恢复时继续执行。
| 维度 | Project Leyden | Project CRaC |
|---|---|---|
| 保存内容 | 可重用的预计算数据或缓存 | 运行中进程的执行状态快照 |
| 应用生命周期 | 启动时创建 JVM 并消费预计算结果 | 恢复先前创建的 JVM 进程状态 |
| 环境耦合 | 需要与构建产物、JDK 和配置匹配 | 对 OS、内核、CPU、JDK、权限与外部资源更敏感 |
| 应用配合 | 多数优化可以由构建与运行时承接 | 必须处理检查点和恢复边界上的资源状态 |
二者可以解决相近的启动体验问题,却有不同的状态模型和部署限制。某些系统可以分别评估两种路线;具体组合能力要由所用 JDK 实现验证,不能假设 Leyden 产物可以自动变成 CRaC 快照。
CRaC 也不同于 CDS(Class Data Sharing)。CDS 共享或映射类元数据相关数据,帮助 JVM 更快加载类;它不会恢复一个已经预热的应用进程。容器镜像缓存减少镜像拉取和文件系统开销,也不会恢复 Java 堆、线程与 JIT 状态。这些优化可以叠加在相同部署链路中,但作用阶段不同。
生产部署需要验证的条件
快照兼容性
检查点通常与制作时的操作系统和运行环境密切相关。需固定镜像基础层、JDK 发行版与构建、内核能力和 CPU 特性,说明哪些快照可以在哪些节点恢复。滚动升级期间,不同版本的运行时不能共享未经验证的快照。
安全边界
堆和进程快照可能包含访问令牌、密钥、用户数据、TLS 会话信息和数据库凭据。快照文件应像内存转储一样分级保护,控制写入位置、权限、加密、复制、备份和销毁周期。检查点工具所需的系统权限也会影响容器隔离模型,需要与平台团队一起评估。
并发扩容与唯一身份
同一快照可以被多个副本恢复,但快照中若含有启动时生成的节点 ID、租约、随机数状态或周期任务,就可能出现副本身份冲突。恢复后应生成新的实例身份,并重新参与服务发现、租约获取和分布式选主。周期任务还需抖动或协调,避免恢复风暴。
可观测性与回退
应分开记录冷启动耗时、快照制作耗时、快照大小、恢复耗时、回调耗时和健康检查耗时。运行时的 JIT 状态、GC 堆状态与业务缓存也会随快照恢复,压测要覆盖恢复后的首批请求和长期稳态。部署系统必须保留不使用快照的回退路径,以应对快照失效、CRIU 不兼容或恢复钩子异常。
快照版本与失效策略
快照是运行时产物,应该有机器可读的来源信息:应用构建号、JDK build、基础镜像 digest、内核/架构约束、配置版本以及生成时间。若应用部署版本、序列化 schema、资源配置或依赖库变化,旧快照可能继续恢复,却已经不满足业务兼容条件。
部署平台应将快照与其构建版本绑定,并定义何时强制重建、何时允许恢复旧版本、失败时如何回到冷启动。共享快照还要审查构建期预热是否含特定租户或环境的数据。只要快照复用了进程内存,它就需要像二进制构件一样被标识、校验和撤销,而不能只当作一个临时文件。
评估 CRaC 的方法
先选择一个有代表性的服务实例,绘出从进程启动到接流量的耗时构成,区分 JVM 初始化、应用初始化、预热、平台注册与健康检查。只有知道主要耗时在哪一段,才能确定保存一个已预热进程能带来多大收益。
随后建立检查点资源清单,逐个组件测试:在空闲、活跃流量和扩容并发下制作快照,恢复后验证数据库、消息系统、定时器、日志、指标和网络连接。测量成功率、恢复 p99、内存峰值、磁盘占用和外部依赖重连压力。
最后把恢复路径作为正式的部署类型运行,而不只在开发机做一次演示。故障注入应覆盖检查点中断、恢复失败、依赖不可用、凭据轮换和快照过期。CRaC 是否值得采用,取决于启动时间收益能否抵消资源审查、快照管理、安全存储和运行环境约束。
适用边界
CRaC 对高频弹性扩缩容、短任务启动敏感、运行环境可固定且资源生命周期可控的服务较有吸引力。它对资源不断变化、必须跨异构主机恢复或无法提供检查点所需权限的平台,集成难度更高。
CRaC 快照也不能替代应用级的持久化与恢复设计:数据库事务、消息幂等、租约续期和外部状态一致性仍需应用自身负责。把进程恢复成功等同于业务状态正确,会遗漏最重要的故障模式。