CRaC в Java

Привет, Хабр!
Сегодня рассмотрим CRaC — это технология, позволяющая создать контрольную точку работающего Java‑приложения, сохранив его полное состояние: память, потоки, системные ресурсы и прочее. Иными словами, вы проводите полную инициализацию приложения один раз, делаете «снимок», а затем при повторном запуске восстанавливаете это состояние, обходя долгую процедуру холодного старта.
Техническая реализация CRaC
Состояние JVM и его сериализация
Вся JVM работает с огромным количеством данных: объекты в куче, состояния стеков, регистры, нативные потоки… Сохранить все это — задача не из легких. При вызове Crac.getRuntime().checkpoint()
происходит следующее:
Сбор данных: JVM проходит по внутренним структурам, включая метаданные классов и объекты, которые необходимо сохранить.
Преобразование в бинарный формат: состояние переводится в двоичный вид. Это требует учёта выравнивания памяти, ссылочной целостности и взаимосвязей между объектами.
Запись на диск: результат записывается в файл (или иной носитель), откуда его можно будет восстановить.
Сериализация здесь идёт на уровне нативных структур JVM, а не через стандартную Java‑сериализацию.
Обработка потоков и синхронизации
При checkpoint‑е нужно зафиксировать состояние всех потоков, их регистры, стеки вызовов и синхронизированные блокировки. Если поток находится в критической секции, то его состояние должно быть сохранено таким образом, чтобы после восстановления не возникло дедлоков или других проблем синхронизации.
Работа с внешними ресурсами
Как я уже упоминал, внешние ресурсы (сокеты, дескрипторы, файловые дескрипторы) не могут быть просто сериализованы. Поэтому есть два варианта:
Закрыть ресурс перед чекпоинтом. Например, разорвать соединение с базой данных, закрыть файлы.
Восстановить ресурс после восстановления. Здесь в методе
afterRestore()
нужно заново открыть соединение, пересоздать дескрипторы и т. п.
Это и есть причина, почему приходится явно прописывать логику в callback«ах.
Пример реализации
Класс с реализацией CracLifecycle
package com.example.crac;
import jdk.crac.Context;
import jdk.crac.Resource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class MyCracLifecycle implements Resource {
private static Connection connection;
@Override
public void beforeCheckpoint(Context context) throws Exception {
System.out.println("Перед чекпоинтом: закрываем нестандартные ресурсы...");
closeDatabaseConnection();
}
@Override
public void afterRestore(Context context) throws Exception {
System.out.println("После восстановления: восстанавливаем ресурсы...");
initializeDatabaseConnection();
}
private static void initializeDatabaseConnection() throws SQLException {
String url = "jdbc:postgresql://localhost:5432/mydb";
String username = "myuser";
String password = "mypassword";
connection = DriverManager.getConnection(url, username, password);
System.out.println("Соединение с БД восстановлено.");
}
private static void closeDatabaseConnection() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
System.out.println("Соединение с БД закрыто перед чекпоинтом.");
}
} catch (SQLException e) {
System.err.println("Ошибка при закрытии БД: " + e.getMessage());
}
}
}
ВbeforeCheckpoint()
подготавливаем приложение к чекпоинту. Это может включать закрытие соединений, сброс временных данных и т. п. Если не выполнить эти действия, то после восстановления вы можете столкнуться с некорректными состояниями, например, с «зависшими» транзакциями или утечками памяти. afterRestore():
после восстановления необходимо заново открыть ресурсы, которые не были сериализуемыми.
Основное приложение с CRaC
package com.example.crac;
import jdk.crac.Crac;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class CracDemoApplication {
private static Connection connection;
static {
Crac.getGlobalContext().register(new MyCracLifecycle());
}
public static void main(String[] args) {
try {
if (args.length > 0 && "restore".equalsIgnoreCase(args[0])) {
System.out.println("Восстанавливаемся из чекпоинта...");
} else {
System.out.println("Обычный запуск приложения...");
initializeDatabaseConnection();
}
Crac.getRuntime().checkpoint();
runApplicationLogic();
} catch (Exception e) {
System.err.println("Ошибка при запуске приложения: " + e.getMessage());
e.printStackTrace();
}
}
private static void initializeDatabaseConnection() throws SQLException {
String url = "jdbc:postgresql://localhost:5432/mydb";
String username = "myuser";
String password = "mypassword";
connection = DriverManager.getConnection(url, username, password);
System.out.println("Соединение с БД установлено.");
}
private static void runApplicationLogic() {
System.out.println("Приложение работает!");
try {
var stmt = connection.createStatement();
var rs = stmt.executeQuery("SELECT version()");
if (rs.next()) {
System.out.println("Версия БД: " + rs.getString(1));
}
} catch (SQLException e) {
System.err.println("Ошибка при выполнении запроса: " + e.getMessage());
}
}
}
Сразу в статическом блоке регистрируем MyCracLifecycle
. Это гарантирует, что перед чекпоинтом будут вызваны необходимые callback«и.
Проверяем аргументы командной строки: если приложение запущено с параметром «restore», то логика инициализации может быть сокращена, ведь состояние уже сохранено.
Вызов Crac.getRuntime().checkpoint()
инициирует процесс сохранения текущего состояния. Этот процесс блокирует выполнение до завершения всех callback«ов, что гарантирует целостность сохранённых данных.
После восстановления состояния приложение продолжает работу, выполняя запросы и обрабатывая данные. Здесь важно, чтобы все компоненты, особенно те, что не могут быть сериализованы, корректно восстановились в методе afterRestore()
.
Интеграция CRaC в веб-приложения
Не все приложения — консольные утилиты. Рассмотрим пример интеграции CRaC в веб‑сервис на базе Jetty.
package com.example.crac;
import jdk.crac.Crac;
import jdk.crac.Context;
import jdk.crac.Resource;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CracWebApplication {
private static Server server;
private static boolean wasStarted = false;
static {
Crac.getGlobalContext().register(new Resource() {
@Override
public void beforeCheckpoint(Context context) throws Exception {
System.out.println("Перед чекпоинтом: останавливаем Jetty сервер...");
try {
if (server != null && server.isStarted()) {
wasStarted = true;
server.stop();
}
} catch (Exception e) {
System.err.println("Ошибка при остановке сервера: " + e.getMessage());
}
}
@Override
public void afterRestore(Context context) throws Exception {
System.out.println("После восстановления: перезапускаем Jetty сервер...");
try {
if (server != null && wasStarted) {
server.start();
}
} catch (Exception e) {
System.err.println("Ошибка при запуске сервера: " + e.getMessage());
}
}
});
}
public static void main(String[] args) {
try {
if (args.length > 0 && "restore".equalsIgnoreCase(args[0])) {
System.out.println("Восстанавливаем веб-приложение из чекпоинта...");
} else {
System.out.println("Нормальный запуск веб-приложения...");
startJettyServer();
}
Crac.getRuntime().checkpoint();
server.join();
} catch (Exception e) {
System.err.println("Ошибка веб-приложения: " + e.getMessage());
e.printStackTrace();
}
}
private static void startJettyServer() throws Exception {
server = new Server(8080);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
server.setHandler(context);
context.addServlet(HelloServlet.class, "/hello");
server.start();
System.out.println("Jetty сервер запущен на порту 8080");
}
public static class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
resp.getWriter().write("Привет, мир!");
}
}
}
В методе beforeCheckpoint()
останавливаем сервер, чтобы корректно закрыть все открытые сокеты. Это необходимо, чтобы при восстановлении не возникли конфликты с уже занятыми портами или некорректным состоянием сервера. В методе afterRestore()
— наоборот, сервер перезапускается, что гарантирует готовность к обработке HTTP‑запросов.
Как и в консольном примере, используем аргумент «restore» для определения, что приложение восстанавливается, а не запускается с нуля.
Пользуясь случаем, напомню про открытый урок, который будет полезен для начинающих Java-разработчиков: «Создание приложения Блокнот на Java», урок пройдет 24 марта.
Вы научитесь работать с файлами и потоками ввода/вывода в Java, а также взаимодействовать с файловой системой. Кроме того, освоите разработку приложений с графическим интерфейсом, создавая удобный и функциональный UI. Записаться