Ускоряем приложения на Java и Spring Boot с CRaC

Ускоряем приложения на Java и Spring Boot с CRaC

Это вторая статья из цикла статей, посвящённых механизму Coordinated Restore at Checkpoint (CRaC).

Axiom JDK с поддержкой CRaC помогает сократить время запуска и прогрева Java до миллисекунд. Проект CRaC вышел только в конце 2023 года и ещё не получил достаточно обратной связи.

В этой статье вы попробуете CRaC в проектах Java и Spring Boot с конкретными примерами кода.

CRaC доступен только для Linux на машине x64 (Intel/AMD).

Содержание:

Используемые технологии

  • 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, хотя более сложные проекты могут потребовать дополнительного тестирования и устранения неполадок.

Если у вас возникнут вопросы по скорости старта приложений и другим вопросам производительности, то обращайтесь, и наши инженеры вам помогут!

Свяжитесь с нами

Author image

Сергей Лунегов

Директор по продуктам Axiom JDK

Axiom JDK info@axiomjdk.ru Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67