Ускоряем приложения на Java и Spring Boot с CRaC
Это вторая статья из цикла статей, посвящённых механизму Coordinated Restore at Checkpoint (CRaC).
Axiom JDK с поддержкой CRaC помогает сократить время запуска и прогрева Java до миллисекунд. Проект CRaC вышел только в конце 2023 года и ещё не получил достаточно обратной связи.
В этой статье вы попробуете CRaC в проектах Java и Spring Boot с конкретными примерами кода.
CRaC доступен только для Linux на машине x64 (Intel/AMD).
Содержание:
- Используемые технологии
- Проверка прав доступа и корректности установки
- Пример с простым Java-приложением
- Возможные проблемы и их решение
- Пример с приложением на Spring Boot
Используемые технологии
- Axiom JDK 17 c поддержкой CRaC. CRaC поддерживается LTS-версиями Axiom JDK 17 и 21, но в большинстве компаний в продакшене используется JDK 17. Получите демо-версию JDK с поддержкой CRaC, связавшись с нами.
- Spring Boot 3.2 или более новая версия с поддержкой CRaC.
- (Необязательно) Интегрированная среда разработки (IDE).
- Ubuntu 22.04.3 LTS.
Проверка прав доступа и корректности установки
В deb- и rpm-пакетах axiomjdk-jdk-pro17.0.12+11-linux-amd64-crac.deb
Axiom JDK с поддержкой CRaC работает “из коробки”, поскольку эти пакеты предоставляют необходимые права доступа исполняемому файлу criu
. Для пакетов tar.gz
вам придётся предоставить эти права вручную.
CRIU (Checkpoint and Restore in Userspace) — это программа, которая входит в комплект CRaC JDK и обеспечивает использование необходимых функций ядра ОС. Механизм CRaC реализован в JDK через использование CRIU.
Если у вас файл tar.gz, выполните следующие команды для Axiom JDK 17 с CRaC:
$ sudo chown root:root jdk-17.0.12-crac/lib/criu
$ sudo chmod u+s jdk-17.0.12-crac/lib/criu
Проверьте владельца и права доступа к исполняемому файлу criu
:
$ ls -al jdk-17.0.12-crac/lib/criu
$ -rwsrwxr-x 1 root root 8478552 Oct 30 17:31 jdk-17.0.12-crac/lib/criu
Убедитесь, что вы корректно установили Axiom JDK с CRaC, проверив версию Java:
$ java --version
openjdk 17.0.12 2024-07-16 LTS
OpenJDK Runtime Environment (build 17.0.12+11-LTS)
OpenJDK 64-Bit Server VM (build 17.0.12+11-LTS, mixed mode, sharing)
Пример с простым Java-приложением
За основу возьмём простое Java-приложение — таймер, который считает секунды до 100 и выводит их на экран:
import java.util.stream.IntStream;
public class Example {
public static void main(String args[]) throws InterruptedException {
// This is a part of the saved state
long startTime = System.currentTimeMillis();
for (int counter: IntStream.range(1, 100).toArray()) {
Thread.sleep(1000);
long currentTime = System.currentTimeMillis();
System.out.println("Counter: " + counter + "(passed " + (currentTime-startTime) + " ms)");
startTime = currentTime;
}
}
}
Поскольку CRaC сохраняет точное состояние запущенного приложения, снапшот (snapshot) может содержать секреты (secrets) или другую персональную информацию. Учитывайте это при работе с CRaC, чтобы исключить возможные риски безопасности.
Шаг 1. Скомпилируйте код и запустите приложение с механизмом CRaC
При запуске приложения используйте опцию -XX:CRaCCheckpointTo=checkpoint-dir
, где checkpoint-dir
— каталог, в котором будут храниться данные JVM после создания контрольной точки.
$ javac Example.java
$ java -XX:CRaCCheckpointTo=checkpoint-dir Example
Шаг 2. Создайте контрольную точку
Пока приложение работает, создайте контрольную точку с помощью jcmd
:
$ jcmd Example JDK.checkpoint
86221:
CR: Checkpoint ...
Вывод консоли будет следующим:
Counter: 1(passed 1007 ms)
Counter: 2(passed 1010 ms)
Counter: 3(passed 1001 ms)
Counter: 4(passed 1000 ms)
Counter: 5(passed 1000 ms)
Counter: 6(passed 1000 ms)
Nov 01, 2023 5:05:36 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Killed
Если вы посмотрите содержимое каталога checkpoint-dir
, то увидите что-то подобное:
$ ls checkpoint-dir
core-42361.img core-42369.img core-42377.img core-56815.img core-56823.img core-56831.img core-56914.img core-56922.img core-56950.img fs-56911.img pagemap-42361.img tty-info.img
core-42362.img core-42370.img core-42378.img core-56816.img core-56824.img core-56832.img core-56915.img core-56923.img core-56951.img ids-42361.img pagemap-56813.img
core-42363.img core-42371.img core-42379.img core-56817.img core-56825.img core-56852.img core-56916.img core-56924.img core-56952.img ids-56813.img pagemap-56911.img
core-42364.img core-42372.img core-42468.img core-56818.img core-56826.img core-56853.img core-56917.img core-56925.img dump4.log ids-56911.img pages-1.img
core-42365.img core-42373.img core-42469.img core-56819.img core-56827.img core-56854.img core-56918.img core-56926.img fdinfo-2.img inventory.img pstree.img
core-42366.img core-42374.img core-42470.img core-56820.img core-56828.img core-56911.img core-56919.img core-56927.img files.img mm-42361.img seccomp.img
core-42367.img core-42375.img core-56813.img core-56821.img core-56829.img core-56912.img core-56920.img core-56928.img fs-42361.img mm-56813.img stats-dump
core-42368.img core-42376.img core-56814.img core-56822.img core-56830.img core-56913.img core-56921.img core-56929.img fs-56813.img mm-56911.img timens-0.img
Шаг 3. Восстановите состояние приложения
Передайте java
в качестве аргумента только параметр -XX:CRaCRestoreFrom=checkpoint-dir
, где checkpoint-dir
— каталог, в котором хранятся данные JVM после создания контрольной точки.
$ java -XX:CRaCRestoreFrom=checkpoint-dir
Вывод команды будет следующим:
$ java -XX:CRaCRestoreFrom=checkpoint-dir
Output:
Counter: 7(passed 91124 ms)
Counter: 8(passed 1000 ms)
Counter: 9(passed 1001 ms)
Counter: 10(passed 1000 ms)
Counter: 11(passed 1000 ms)
Counter: 12(passed 1001 ms)
Counter: 13(passed 1000 ms)
Counter: 14(passed 1000 ms)
...
Время в миллисекундах Counter: 7(passed 91124 ms)
показывает, что приложение было прервано и затем восстановлено.
Возможные проблемы и их решение
Если вы попытались восстановить состояние приложения, но оно неожиданно завершилось, то измените код Java-приложения и обработайте событие контрольной точки.
Сымитируем такое поведение программы на следующем примере:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExampleWithCRaC {
private ScheduledExecutorService executor;
private long startTime = System.currentTimeMillis();
private int counter = 0;
public static void main(String args[]) throws InterruptedException {
new ExampleWithCRaC().startTask();
}
private void startTask() throws InterruptedException {
executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
long currentTime = System.currentTimeMillis();
System.out.println("Counter: " + counter + "(passed " + (currentTime-startTime) + " ms)");
startTime = currentTime;
counter++;
}, 1, 1, TimeUnit.SECONDS);
Thread.sleep(1000*30);
executor.shutdown();
}
}
Скопируйте этот пример и попробуйте создать контрольную точку. Для этого повторите шаги из предыдущего примера:
1. Скомпилируйте программу и запустите её с опцией -XX:CRaCCheckpointTo=checkpoint-dir, где checkpoint-dir — каталог, в котором будут храниться данные JVM после создания контрольной точки.
$ javac ExampleWithCRaC.java
$ java -XX:CRaCCheckpointTo=checkpoint-dir ExampleWithCRaC
2. Пока приложение работает, создайте контрольную точку с помощью jcmd
:
$ jcmd ExampleWithCRaC JDK.checkpoint
56813:
CR: Checkpoint ...
Вывод будет следующим:
Counter: 0(passed 1007 ms)
Counter: 1(passed 999 ms)
Counter: 2(passed 1000 ms)
Counter: 3(passed 1000 ms)
Counter: 4(passed 1000 ms)
Counter: 5(passed 1000 ms)
Counter: 6(passed 1000 ms)
Counter: 7(passed 1000 ms)
Counter: 8(passed 1000 ms)
Counter: 9(passed 1000 ms)
Counter: 10(passed 1000 ms)
Sep 11, 2024 9:26:08 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Killed
3. Восстановите состояние приложения из этой контрольной точки, передав java в качестве аргумента только параметр -XX:CRaCRestoreFrom=checkpoint-dir, где checkpoint-dir — каталог, в котором хранятся данные JVM после создания контрольной точки.
$ java -XX:CRaCRestoreFrom=checkpoint-dir
Counter: 11(passed 64673 ms)
Приложение запустилось, а затем неожиданно завершилось. Мы должны были получить несколько итераций счётчика, но вместо этого приложение завершилось по прошествии одного шага. В нашем случае — Counter: 11(passed 64673 ms)
.
Чтобы избежать такого поведения, приложение должно обрабатывать событие контрольной точки. Это можно сделать с помощью классов из пакета jdk.crac
, поставляемого с Axiom JDK с поддержкой CRaC. Интерфейс jdk.crac.Resource
позволяет обрабатывать события контрольной точки и восстановления с помощью методов beforeCheckpoint()
и afterRestore()
.
На основе полученной информации изменим пример и перезапустим потоки счётчика.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import jdk.crac.Context;
import jdk.crac.Core;
import jdk.crac.Resource;
public class ExampleWithCRaCRestore {
private ScheduledExecutorService executor;
private long startTime = System.currentTimeMillis();
private int counter = 0;
class ExampleWithCRaCRestoreResource implements Resource {
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
executor.shutdown();
System.out.println("Handle checkpoint");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
System.out.println(this.getClass().getName() + " restore.");
ExampleWithCRaCRestore.this.startTask();
}
}
public static void main(String args[]) throws InterruptedException {
ExampleWithCRaCRestore exampleWithCRaC = new ExampleWithCRaCRestore();
Core.getGlobalContext().register(exampleWithCRaC.new ExampleWithCRaCRestoreResource());
exampleWithCRaC.startTask();
}
private void startTask() throws InterruptedException {
executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
long currentTime = System.currentTimeMillis();
System.out.println("Counter: " + counter + "(passed " + (currentTime-startTime) + " ms)");
startTime = currentTime;
counter++;
}, 1, 1, TimeUnit.SECONDS);
Thread.sleep(1000*30);
executor.shutdown();
}
}
Попробуйте снова создать контрольную точку и восстановить состояние приложения.
1. Скомпилируйте приложение и укажите каталог для сохранения контрольной точки:
$ javac ExampleWithCRaCRestore.java
$ java -XX:CRaCCheckpointTo=checkpoint-dir ExampleWithCRaCRestore
Counter: 0(passed 1007 ms)
Counter: 1(passed 999 ms)
Counter: 2(passed 1000 ms)
Counter: 3(passed 1000 ms)
Counter: 4(passed 1000 ms)
Counter: 5(passed 1000 ms)
Counter: 6(passed 1000 ms)
Sep 11, 2024 9:36:13 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Handle checkpoint
Killed
2. Создайте контрольную точку:
$ jcmd ExampleWithCRaCRestore JDK.checkpoint
56911:
CR: Checkpoint ...
3. Восстановите приложение из контрольной точки:
$ java -XX:CRaCRestoreFrom=checkpoint-dir
ExampleWithCRaCRestore$ExampleWithCRaCRestoreResource restore.
Counter: 7(passed 61407 ms)
Counter: 8(passed 1000 ms)
Counter: 9(passed 1000 ms)
Counter: 10(passed 1000 ms)
Counter: 11(passed 1000 ms)
Теперь всё работает как ожидается.
Пример с приложением на Spring Boot
Попробуем использовать механизм CRaC с приложением на Spring Boot. В качестве примера возьмём приложение Spring Boot PetClinic, исходный код которого доступен на Github. Для сборки PetClinic в нашем примере используются Spring Boot 3.3.3 и Spring Framework 6.1.12.
Шаг 1. Создайте локальную копию исходного кода PetClinic
Создайте локальную копию исходного кода PetClinic:
git clone https://github.com/spring-projects/spring-petclinic.git
Шаг 2. Добавьте зависимость от пакета org.crac/crac
Чтобы использовать CRaC с Spring Boot и Spring Framework, добавьте зависимость от пакета org.crac/crac
в файл pom.xml
:
<dependency>
<groupId>org.crac</groupId>
<artifactId>crac</artifactId>
<version>1.4.0</version>
</dependency>
Сравните изменения с обновленным pom.xml:
$ git diff
diff --git a/pom.xml b/pom.xml
index 287a08a..f403155 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,11 @@
</properties>
<dependencies>
+ <dependency>
+ <groupId>org.crac</groupId>
+ <artifactId>crac</artifactId>
+ <version>1.4.0</version>
+ </dependency>
<!-- Spring and Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
Проверьте зависимость и используемую версию Spring Framework с помощью команды mvn dependency
:
$ mvn dependency:tree | grep "spring-boot:jar"
[INFO] | +- org.springframework.boot:spring-boot:jar:3.3.3:compile
$ mvn dependency:tree | grep "spring-core:jar"
[INFO] | +- org.springframework:spring-core:jar:6.1.12:compile
Шаг 3. Соберите jar-файл
В каталоге проекта spring-petclinic
соберите jar-файл с помощью mvn package
:
$ ./mvnw package
Шаг 4. Запустите приложение с механизмом CRaC
Запустите приложение с опцией -XX:CRaCCheckpointTo=cr
, где cr
— каталог, в котором будут храниться данные JVM после создания контрольной точки.
$ java -XX:CRaCCheckpointTo=cr -jar ./target/spring-petclinic-3.2.0-SNAPSHOT.jar
|\ _,,,--,,_
/,`.-'`' ._ \-;;,_
_______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______
| | '---''(_/._)-'(_\_) | | | | | | | | |
| _ | ___|_ _| | | | | |_| | | | __ _ _
| |_| | |___ | | | | | | | | | | \ \ \ \
| ___| ___| | | | _| |___| | _ | | _| \ \ \ \
| | | |___ | | | |_| | | | | | | |_ ) ) ) )
|___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / /
==================================================================/_/_/_/
:: Built with Spring Boot :: 3.2.0
2023-11-24T02:28:52.989-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v3.2.0-SNAPSHOT using Java 21.0.1 with PID 17325 (.../target/spring-petclinic-3.2.0-SNAPSHOT.jar started ..)
2023-11-24T02:28:52.995-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to 1 default profile: "default"
2023-11-24T02:28:53.948-08:00 INFO 17325 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-11-24T02:28:53.992-08:00 INFO 17325 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 37 ms. Found 2 JPA repository interfaces.
2023-11-24T02:28:54.678-08:00 INFO 17325 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2023-11-24T02:28:54.686-08:00 INFO 17325 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-11-24T02:28:54.686-08:00 INFO 17325 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.16]
2023-11-24T02:28:54.718-08:00 INFO 17325 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-11-24T02:28:54.719-08:00 INFO 17325 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1658 ms
2023-11-24T02:28:55.024-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-11-24T02:28:55.184-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:be9e0d83-4345-497c-84ce-2df90b5740bb user=SA
2023-11-24T02:28:55.185-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-11-24T02:28:55.330-08:00 INFO 17325 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-11-24T02:28:55.374-08:00 INFO 17325 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.3.1.Final
2023-11-24T02:28:55.400-08:00 INFO 17325 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2023-11-24T02:28:55.550-08:00 INFO 17325 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2023-11-24T02:28:56.297-08:00 INFO 17325 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2023-11-24T02:28:56.299-08:00 INFO 17325 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-11-24T02:28:56.544-08:00 INFO 17325 --- [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used.
2023-11-24T02:28:57.751-08:00 INFO 17325 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/actuator'
2023-11-24T02:28:57.837-08:00 INFO 17325 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2023-11-24T02:28:57.850-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 5.204 seconds (process running for 5.568)
Шаг 5. Создайте контрольную точку
Пока приложение работает, создайте контрольную точку с помощью jcmd
:
$ jcmd spring-petclinic JDK.checkpoint
17325:
CR: Checkpoint ...
Если у вас не создалась контрольная точка из-за ошибки jdk.crac.CheckpointException(jdk.crac.impl.CheckpointOpenSocketException), то вы не добавили зависимость от пакета org.crac/crac (шаг 2).
Вывод будет следующим:
2023-11-24T02:29:39.847-08:00 INFO 17325 --- [Attach Listener] jdk.crac : Starting checkpoint
2023-11-24T02:29:39.876-08:00 INFO 17325 --- [Attach Listener] o.s.b.j.HikariCheckpointRestoreLifecycle : Evicting Hikari connections
Killed
Вы увидите, что создался каталог cr
, в котором содержатся файлы:
$ ls cr
... core-17415.img core-17419.img files.img mm-17325.img pstree.img tty-info.img
... core-17416.img cppath fs-17325.img pagemap-17325.img seccomp.img
... core-17417.img dump4.log ids-17325.img pages-1.img stats-dump
... core-17418.img fdinfo-2.img inventory.img perfdata timens-0.img
Эти файлы — выгруженная память HotSpot JVM со всей необходимой информацией о восстановлении состояния приложения.
Шаг 6. Восстановите состояние приложения
Восстановите состояние приложения из этой контрольной точки, передав java в качестве аргумента только параметр -XX:CRaCRestoreFrom=cr
, где cr
— каталог, в котором хранятся данные JVM после создания контрольной точки.
$ java -XX:CRaCRestoreFrom=cr
2023-11-24T02:30:59.012-08:00 WARN 17325 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m33s725ms577µs950ns).
2023-11-24T02:30:59.015-08:00 INFO 17325 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Restarting Spring-managed lifecycle beans after JVM restore
2023-11-24T02:30:59.019-08:00 INFO 17325 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2023-11-24T02:30:59.020-08:00 INFO 17325 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Spring-managed lifecycle restart completed (restored JVM running for 50 ms)
Состояние приложения восстановилось. Убедитесь, что приложение успешно запускается на localhost:8080
.
Время запуска (восстановления) составляет всего 50 мс, что в 100 раз быстрее, чем время запуска без CRaC — 5,2 с. Заключение CRaC удобно интегрируется с приложениями Spring Boot, хотя более сложные проекты могут потребовать дополнительного тестирования и устранения неполадок.
Если у вас возникнут вопросы по скорости старта приложений и другим вопросам производительности, то обращайтесь, и наши инженеры вам помогут!