CRaC steht für Coordinated Restore at Checkpoint. Damit ist es möglich Javaprogramme zu einem beliebigen Zeitpunkt anzuhalten, den aktuellen Zustand festzuhalten und zu einem späteren Zeitpunkt wieder fortzusetzen. Dabei ist es nicht zwingend erforderlich wieder auf dem gleichen System fortzufahren. Zum jetzigen Zeitpunkt prüft die CRaC Implementierung auf offene Dateien und Sockets. Die Erstellung eines Checkpoints wird mit abgebrochen und eine Exception geworfen, wenn das der Fall ist. Derzeit wird CRaC nur unter Linux unterstützt.
Bitte beachten das mit deb und rpm Paketen CRaC automatisch funktioniert da diese Pakete die notwendigen Rechte für die Datei criu automatisch setzen. Für den Fall einer Installation aus einem tar.gz Paket müssen die Rechte selber nachträglich gesetzt werden. Dies ist notwendig da dies als Wrapper um den eigentlichen JVM-Prozess verwendet wird.
sudo chown root:root jdk-21.0.1-crac/lib/criu
sudo chmod u+s jdk-21.0.1-crac/lib/criu
Zunächst erstellen wir ein einfaches Javaprogramm
public class CracTest {
public class CracTest {
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
for (int counter = 0; counter <100; counter++) {
Thread.sleep(1000);
final long currentTime = System.currentTimeMillis();
System.out.println("Schritt: "+counter+" (Zeit "+ (currentTime-startTime)+")");
startTime=currentTime;
}
}
}
}
Bitte beachten Sie das CRaC den exakten Zustand der ausgeführten Anwendung speichert. Dieses Abbild kann Passwörter oder andere sensitiven Informationen enthalten.
Kompilieren und Starten des Beispiels
javac CracTest.java
java -XX:CRaCCheckpointTo=checkpoint-verzeichnis CracTest
Die Option -XX:CRaCCheckpointTo=checkpoint-verzeichnis
zeigt auf das Verzeichnis in dem die Daten der JVM zum Zeitpunkt der Erstellung des Checkpoints gespeichert werden.
Ein Checkpoint wird mit dem Kommando jcmd
ertstellt werden
jcmd CracTest JDK.checkpoint
442512:
CR: Checkpoint ...
Die Ausgabe des Programms sieht folgendermaßen aus
Schritt: 0 (Zeit 1000)
Schritt: 1 (Zeit 1002)
Schritt: 2 (Zeit 1000)
Schritt: 3 (Zeit 1001)
Dec 29, 2023 3:16:47 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Die Anwendung wurde durch das jcmd
-Kommando beendet und der aktuelle Zustand in das angegebene Verzeichnis geschrieben.
Und jetzt kommen wir zum interessanten Teil dieser Übung. Wir starten die Anwendung wieder und setzen auf dem abgebrochenen Zustandt wieder auf.
java -XX:CRaCRestoreFrom=checkpoint-verzeichnis
Die Ausgabe sieht folgendermaßen aus:
java -XX:CRaCRestoreFrom=checkpoint-verzeichnis
Schritt: 4 (Zeit 45209)
Schritt: 5 (Zeit 1033)
Schritt: 6 (Zeit 1000)
Schritt: 7 (Zeit 1002)
Die vergangene Zeit in Millisekunden in der Zeile Schritt: 4 (Zeit 45209)
zeigt, dass die Anwendung gestoppt und wieder gestartet wurde.
Um die möglichen Probleme zu verdeutlichen, schreiben wir eine weitere Anwendung:
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CRaCTestMitThread {
private ScheduledExecutorService executor;
private long startTime = System.currentTimeMillis();
private int counter = 0;
public static void main(String args[]) throws InterruptedException {
CRaCTestMitThread exampleWithCRaC = new CRaCTestMitThread();
exampleWithCRaC.startTask();
}
private void startTask() throws InterruptedException {
executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
long currentTimeMillis = System.currentTimeMillis();
System.out.println("Anzahl: " + counter + " (Zeit " + (currentTimeMillis-startTime) + ")");
startTime = currentTimeMillis;
counter++;
}, 1, 1, TimeUnit.SECONDS);
Thread.sleep(1000*30);
executor.shutdown();
}
}
Wir starten die Beispielanwendung und erstellen einen Checkpoint
java -XX:CRaCCheckpointTo=checkpoint-verzeichnis CRaCTestMitThread
jcmd CRaCTestMitThread JDK.checkpoint
442948:
CR: Checkpoint ...
Die Ausgabe sieht folgendermaßen aus:
Anzahl: 0 (Zeit 1019)
Anzahl: 1 (Zeit 999)
Anzahl: 2 (Zeit 1000)
Anzahl: 3 (Zeit 1000)
Dec 29, 2023 3:34:27 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Killed
Jetzt starten wir die Anwendung wieder:
java -XX:CRaCRestoreFrom=checkpoint-verzeichnis
Die Anwendung beendet sich sofort wieder. Wir haben ein paar Iterationen erwartet stattdessen sind 30 Sekunden sind und die Anwendung hat sich beendet.
So die Anwendung sollte den Checkpoint-Event behandeln und entsprechend reagieren. Das kann mithilfe von Klassen aus dem Package jdk.crac
erreicht werden.
Wir können nun unsere Anwendung entsprechend modifizieren. Zunächst fügen wir eine neue Dependency zu unserem Projekt hinzu
<dependency>
<groupId>org.crac</groupId>
<artifactId>crac</artifactId>
<version>1.4.0</version>
</dependency>
Damit stehen uns die Klasse aus `org.crac zur Verfügung um unsere Klasse anzupassen
import org.crac.Context;
import org.crac.Core;
import org.crac.Resource;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CRaCTestMitThread {
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("Behandle Checkpoint");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
System.out.println(this.getClass().getName() + " restore.");
CRaCTestMitThread.this.startTask();
}
}
public static void main(String args[]) throws InterruptedException {
CRaCTestMitThread exampleWithCRaC = new CRaCTestMitThread();
Core.getGlobalContext().register(exampleWithCRaC.new ExampleWithCRaCRestoreResource());
exampleWithCRaC.startTask();
}
private void startTask() throws InterruptedException {
executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
long currentTimeMillis = System.currentTimeMillis();
System.out.println("Anzahl: " + counter + " (Zeit " + (currentTimeMillis-startTime) + ")");
startTime = currentTimeMillis;
counter++;
}, 1, 1, TimeUnit.SECONDS);
Thread.sleep(1000*30);
executor.shutdown();
}
}
Die neue Version probieren wir gleich mal aus
java -XX:CRaCCheckpointTo=checkpoint-dir -cp crac-1.4.0.jar:. CRaCTestMitThread
Anzahl: 0 (Zeit 1057)
Anzahl: 1 (Zeit 999)
Anzahl: 2 (Zeit 1000)
Anzahl: 3 (Zeit 1000)
Dec 29, 2023 3:52:35 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
Behandle Checkpoint
Dec 29, 2023 3:52:35 PM jdk.internal.crac.LoggerContainer info
INFO: /home/wkl/javatest/crac-1.4.0.jar is recorded as always available on restore
Killed
Anhalten der Anwendung
cmd CRaCTestMitThread JDK.checkpoint
445088:
CR: Checkpoint ...
Und jetzt versuchen wir die Anwendung wieder zu starten
java -XX:CRaCRestoreFrom=checkpoint-dir
CRaCTestMitThread$ExampleWithCRaCRestoreResource restore.
Anzahl: 4 (Zeit 68228)
Anzahl: 5 (Zeit 1000)
Anzahl: 6 (Zeit 1000)
Anzahl: 7 (Zeit 1000)
Anzahl: 8 (Zeit 1000)
Das Problem ist gelöst. Wir können programmatisch auf die Erstellung und Wiederherstellung eines Checkpoints reagieren.
Ich hoffe, mit dieser kurzen Einführung konnte ich einen Überblick geben, was es mit dem neuen Feature auf sich hat und wie man es benutzt. In einem folgenden Artikel werde ich zeigen wie eine Spring Boot Anwendung angehalten und wieder gestartet werden kann. Spring Boot bietet seit der Version 3.2 eine entsprechende Unterstützung an.