The Twelve-Factor App

Softwareentwicklung ist ein komplexes Pflaster. Da ist es praktisch, wenn der geneigte Entwickler eine Methodologie an die Hand bekommt, welche das Ergebnis der Entwicklung verbessert. Eine solche Methodologie ist die The Twelve-Factor App. Diese ursprünglich 2011 vorgestellte Methodologie kommt mit besagten zwölf Punkten aus. Diese führen von der Codebasis über die Abhängigkeiten bis hin zum Deployment. Zielgruppe der Methodologie sind Applikationen, welche als Service laufen.

12factor.net

Neben dem englischen Original existieren eine Reihe von Übersetzungen; unter anderem in die deutsche Sprache. Gelesen werden kann die Dokumentation der Methodologie unter 12factor.net. Lizenziert ist die Webseite unter der MIT-Lizenz und damit freie Software. Der Quelltext ist auf GitHub zu finden.

Services unter systemd einrichten

Das Init-System systemd ist mittlerweile in vielen Linux-Distributionen angekommen. Auch unter Ubuntu ist es seit Version 15.04 integriert. Ein Vorteil des neuen Systems ist, das Services, im Gegensatz zum alten Init-System, relativ unkompliziert erstellt werden können. Dazu muss eine sogenannte Unit erstellt werden. Bei einer Unit handelt es sich um eine Textdatei mit einer entsprechenden Konfiguration. Neben den Units für die Konfiguration eines Services existieren weitere Unit-Typen wie z.B. die Typen mit der Endung .path oder .network. In diesem Artikel soll es allerdings nur um die Units vom Typ Service gehen.

Die Units liegen in unterschiedlichen Orten im Dateisystem. Die vom System vorinstallierten Units sind im Ordner /lib/systemd/system/ zu finden. Eigene Services werden im Ordner /etc/systemd/system/ hinterlegt. Hier können allerdings nur Nutzer mit administrativen Rechten entsprechende Service hinterlegen. Soll ein nicht privilegierter Nutzer eine Unit angelegen, so muss er diese im Verzeichnis ~/.config/systemd/user/ hinterlegen. In den meisten Fällen werden Units für Services im Dateisystem unter dem Pfad /etc/systemd/system/ angelegt. In diesem Ordner soll nun eine Datei für die Unit angelegt werden:

nano languagetool.service

Beispielhaft könnte eine solche Unit wie folgt aussehen:

[Unit]
Description=LanguageTool
After=syslog.target
After=network.target

[Service]
Type=simple
User=languagetool
Group=languagetool
WorkingDirectory=/home/languagetool/server
ExecStart=/usr/bin/java -cp /home/languagetool/server/languagetool-server.jar org.languagetool.server.HTTPServer --config languagetool.cfg --port 3001 --allow-origin "*"
Restart=always
Environment=USER=git HOME=/home/languagetool

[Install]
WantedBy=multi-user.target

Die Unit unterteilt sich in unterschiedliche Bereiche, in diesem Beispiel sind dies Unit, Service und Install. Der Bereich Service ist hierbei nur in Units vom Typ Service zu finden. Im Bereich Unit sind eine kurze Beschreibung des Services und die Abhängigkeiten der Unit hinterlegt. Dabei wird definiert, welche Systeme bereits gestartet sein müssen, bevor der Service gestartet wird. Ein Target entspricht dabei einer Gruppe von Diensten, meist mit einer bestimmten Bedeutung, wie z.B. das Target für die Herstellung der Netzwerkkonnektivität (network-online.target).

In der Service-Gruppe wird der Typ des Services und der Nutzer und die Gruppe definiert, mit welchem bzw. welcher er starten soll. Daneben wird das Arbeitsverzeichnis und die Kommandozeile zur Ausführung des Services definiert. Weiterhin kann das Verhalten des Service, über den Parameter Restart, weiter definiert werden. So kann angegeben werden, dass der Service nach seiner Beendigung wieder neugestartet wird.

In der Install-Sektion wird angeben, wann der Service gestartet werden soll. Das multi-user.target entspricht dem klassischen Start eines normalen Linux-Systems. Ist die Unit definiert, kann sie aktiviert und der Service gestartet werden:

systemctl enable languagetool
systemctl start languagetool

Die Option enable sorgt dafür das die entsprechende Unit aktiviert wird. Dies führt dazu das sie je nach Konfiguration z.B. automatisch beim Systemstart oder bei dem Anschluss bestimmter Hardware gestartet wird. Unter bestimmten Systemen wie Ubuntu, kann der Service auch über das service-Kommando gestartet werden:

service languagetool start

Neben den Optionen enable und start für systemctl, existieren weitere Optionen zur Steuerung der Units. Dies sind unter anderem stop, zum Stoppen des Services und disable zur Deaktivierung der Unit. Mit der Option status, kann der Status eines Service erfragt werden:

systemctl status languagetool

Anschließend erhält der Nutzer den aktuellen Status des Service. Eine weitere wichtige Option ist restart um einen Service manuell neuzustarten. Mithilfe der systemd-Units lassen sich somit schnell Services in ein Linux-System einbinden.

Benchmarking mit dem JMH-Framework

Benchmarking unter Java hat mit einigen Problemen zu kämpfen. So optimiert die Java-Laufzeitumgebung den Quellcode, je nachdem wie oft er benutzt wird. Wenn nun kleinere Dinge getestet werden sollen, wie z.B. ob Methode A oder B besser funktioniert so wird dies problematisch. Am Anfang würde besagte Methoden nur interpretiert werden, anschließend würden sie bei häufiger Benutzung kompiliert werden und bei noch häufigerer Benutzung weiter optimiert werden. Daneben kann es passieren dass der Benchmark komplett wegoptimiert wird, da die JVM unter Umständen erkennt, dass die genutzten Werte bzw. die Ergebnisse des Benchmarks später nicht mehr genutzt werden.

Die offizielle Seite des Java Microbenchmark Harness-Framework

Damit sich ein Entwickler nicht immer und immer wieder mit solchen Problemen beim Benchmarking herumschlagen muss, wird seit einigen Jahren im Rahmen des OpenJDK-Projektes an dem Java Microbenchmark Harness-Framework gearbeitet. Das JMH-Framework nimmt dem Entwickler viele Aufgaben ab, um diese Probleme zu lösen. So kann bzw. wird die JVM durch JMH aufgewärmt werden und auch die Messung der Zeiten übernimmt JMH. Um JMH zu nutzen, müssen die entsprechenden Abhängigkeiten (in diesem Fall über die Maven-POM-Datei) dem Projekt hinzugefügt werden:

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>1.21</version>
</dependency>
<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>1.21</version>
</dependency>

Sind die Abhängigkeiten eingebunden, kann mit dem eigentlichen Aufbau des Benchmarks begonnen werden. Jede Methode, welche ein Benchmark durchführen möchte, muss mit der @Benchmark-Annotation gekennzeichnet werden:

@Benchmark
public int addInts() {
    return aInt + bInt;
}

Die Klasse, in welcher die Benchmarks definiert sind, muss mit einigen weiteren Annotationen ausgestattet werden:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class AddingBenchmark {
...

Die @State(Scope.Benchmark)-Annotation in diesem Beispiel sorgt dafür das die Felder im Rahmen des Benchmarks für die Benchmark-Methoden zur Verfügung stehen. Mit der @BenchmarkMode-Annotation wird definiert, welche Art von Messung vorgenommen werden soll. In diesem Fall wird die durchschnittliche Zeit für eine einzelne Operation gemessen und ausgegeben. Über die @OutputTimeUnit-Annotation wird definiert wie die Zeiten für die Messungen ausgegeben werden. Werden für den Benchmark Daten benötigt so können sie über eine Methode, welche mit der @SetupAnnotation versehen wird, erzeugt werden:

@Setup
public void setup() {
    ...
}

Die setup-Methode wird damit vor dem eigentlichen Benchmark ausgeführt und somit können benötigte Daten dort erzeugt werden. Damit der Benchmark nun ausgeführt werden kann muss das Java Microbenchmark Harness-Framework angestartet werden. Dafür wird eine Klasse geschrieben und mit einer main-Methode versehen:

package net.seeseekey.JavaBenchmark;

import org.openjdk.jmh.runner.RunnerException;

import java.io.IOException;

public class Runner {

    public static void main(String[] args) throws IOException, RunnerException {
        
        org.openjdk.jmh.Main.main(args);
    }
}

Über die main-Methode wird die main-Methode des Frameworks gestartet und damit wird das Benchmark durchgeführt. Alternativ kann die Methode org.openjdk.jmh.Main.main auch direkt in der Maven-POM-Datei als Startklasse definiert werden:

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>org.openjdk.jmh.Main.main</mainClass>
            </manifest>

Nachdem der Benchmark implementiert ist, kann dieser natürlich über die IDE ausgeführt werden. Das würde allerdings nicht ganz der Intention von JMH entsprechen. Stattdessen sollte eine JAR aus dem Projekt erzeugt werden und diese, auf einem möglichst unbelastetem System, über die Konsole ausgeführt werden:

java -jar benchmark.jar

Damit wird der Benchmark über JMH ausgeführt. JMH beginnt mit einer Warmup-Phase und führt anschließend den eigentlichen Benchmark durch. Nach einigen Minuten erhält der Entwickler die Auswertung des Benchmark:

Benchmark                                 Mode  Cnt   Score    Error  Units
JavaBenchmark.AddingBenchmark.addDoubles  avgt   25  ≈ 10⁻⁵           ms/op
JavaBenchmark.AddingBenchmark.addInts     avgt   25  ≈ 10⁻⁵           ms/op

In diesem Fall sieht der Entwickler bei der Ausgabe, dass die Ausgabeeinheit für die Zeit mit Millisekunden zu grob gewählt wurde und stattdessen besser Nanosekunden genutzt werden sollten.

LoadingCache der Guava-Bibliothek nutzen

Für Java existiert eine Bibliothek mit dem Namen Guava. Diese, von Google entwickelte, Bibliothek bietet gewisse Basisfunktionalitäten, welche vom Java-Framework nicht abgedeckt werden. Eine dieser Funktionalitäten ist der LoadingCache. Bei diesem handelt es sich um einen Cache, welcher dazu genutzt werden kann die Ergebnisse zeitintensiver Operationen zwischenzuspeichern. Daneben verfügt er über die Informationen wie er die entsprechenden Daten bezogen werden. Um Guava zu nutzen muss die Bibliothek z.B. mittels Maven eingebunden werden. Dazu muss die pom.xml-Datei um eine Abhängigkeit erweitert werden:

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>27.0-jre</version>
</dependency>

Anschließend kann Guava in dem betreffenden Projekt genutzt werden. In diesem Beispiel soll der Cache dazu dienen Anfragen an einen Webservice zu cachen. Der Webservice liefert Daten zurück, welche in einem Objekt vom Typ BookData gespeichert werden:

class BookData {

	public final String title;
	public final String description;
	public final double price;

	public BookData(String title, String description, double price) {
		this.title = title;
		this.description = description;
		this.price = price;
	}
}

Innerhalb der Anwendung übernimmt die Methode getBookData die Aufgabe, diese Daten vom Webservice zu beziehen:

public BookData getBookData(String isbn) {
	return bookdataService.getData(isbn);
}

Der LoadingCache von Guava ist technisch betrachtet eine Map. Bei der Initialisierung ist dies ersichtlich:

LoadingCache<String, BookData> books = CacheBuilder.newBuilder()
		.maximumSize(1000)
		.expireAfterWrite(7, TimeUnit.DAYS)
		.build(
				new CacheLoader<String, BookData>() {
					public BookData load(String isbn) {
						return getBookData(isbn);
					}
				});

Beim Anlegen des Cache können unterschiedliche Parameter wie die maximale Größe, die Zeit ab der ein Eintrag erneuert wird und der CacheLoader definiert werden. Der CacheLoader implementiert die Methode load, welche die entsprechenden Daten vom Webservice bezieht. Nachdem der LoadingCache definiert ist, kann die Map normal genutzt werden:

BookData bookData = books.get("9783736889842");

Im Hintergrund wird der Wert, wenn vorhanden, aus dem Cache bezogen. Ist dies nicht der Fall wird stattdessen der CacheLoader genutzt, welcher in diesem Beispiel einen Request an den Webservice sendet und das Ergebnis wieder im Cache hinterlegt. Der LoadingCache selber beschränkt sich auf die Laufzeit der Applikation und wird nicht persistiert.

Probleme beim MediaWiki Update auf Version 1.28.1

Beim Update einer MediaWiki-Installation auf die Version 1.28.1 über das update.php-Skript erhielt ich folgende Fehlermeldung:

Error: your composer.lock file is not up to date. Run "composer update" to install newer dependencies

Hintergrund sind unerfüllte Abhängigkeiten, welche wohl nur für die Entwicklung benötigt werden. Um das Update trotzdem durchzuführen sollte folgendes Kommando im Terminal genutzt werden:

php ./update.php --skip-external-dependencies

Damit wird die Prüfung der externen Abhängigkeiten deaktiviert und die Aktualisierung kann erfolgreich durchgeführt werden.