Spring Batch Performance Monitoring – Licht in der Dunkelverarbeitung

#JCON2017 #SpringBatch #PerformanceMonitoring

Ein Spring-Batch-Job ist während seiner Laufzeit eine Black-Box: Einzelne Prozessschritte werden sukzessiv abgearbeitet, ohne dass dabei Benutzerinteraktionen nötig bzw. möglich sind. Im Falle einer Performance-Analyse muss die „Black-Box-Batch“ jedoch geöffnet werden. Im Folgenden wird eine Lösung vorgestellt, die eine detaillierte und übersichtliche Performance-Messung durchführt, ohne die Black-Box dabei manipulieren zu müssen.

Motivation

Neben dem Bugfixing ist es insbesondere die Performance-Optimierung, die eine detaillierte Analyse von einzelnen Verarbeitungsschritten eines Spring-Batch-Jobs erfordert, im Einzelfall sogar bis auf das Detailniveau der individuellen Datensätze. Um dies zu erreichen, werden beispielsweise Zeitmessungspunkte
oder andere Hooks in die Prozessschritte eingefügt. Doch dies stößt an Grenzen: Niemand möchte fachlichen Code dauerhaft mit Laufzeitmessungen verschmutzen oder in separaten Versionen pflegen, dazu noch individuell für jede einzelne Spring- Batch Anwendung.

Daher sollte die Aufgabe der Performance-Analyse als Cross-Cutting-Concern behandelt und generisch gelöst werden. Als Cross-Cutting-Concerns werden Belange bezeichnet, die innerhalb einer Software von querschnittlicher Bedeutung sind1. Sie sind eher technischer Natur und lassen sich nicht fachlichen Teilaspekten zuordnen. Entsprechend sollten die fachlich-geprägten Bereiche der Anwendung auch keine Kontrolle oder Kenntnis über die Cross-Cutting-Concerns haben. Bezogen auf Spring-Batch heißt das: Die Performance-Analyse braucht den einzelnen Job nicht zu interessieren.

Es gibt eine Fülle an Tools, die genau diesem Zweck dienen: Laufzeitverhalten von Java-Anwendungen zu analysieren, ohne sie dafür anpassen zu müssen. Für individuelle Lösungen wird häufig auf JMX bzw. MBeans zurückgegriffen. Basierend auf JSR-32 ist dies seit Java 1.5 Teil der Standard-API und ermöglicht eine Überwachung von Software-Komponenten über ein einfaches Management-Interface.

Allerdings haben solche Lösungsansätze alle den gleichen Nachteil: Sie ignorieren weitgehend den technischen und fachlichen Kontext in dem sie genutzt werden, kennen also die individuelle Beschaffenheit einer Anwendung nicht. So wird etwa ein Spring-Batch-Job in gleicher Form analysiert wie eine Spring-MVC-Anwendung. Spring-Batch-spezifische Fragestellungen wie „in welchem Chunk ging die meiste Zeit verloren“ sind nur sehr schwer bzw. oft gar nicht zu beantworten.

Da wir hier eine spezielle Gattung von Java-Anwendungen betrachten, suchen wir möglichst nach einer Lösung, die speziell für diese konzipiert wurde. So sollten z.B. die einzelnen Spring-Batch-Bausteine (z.B. Reader, Processor, Writer) im Ergebnis der Performance-Analyse wiederzufinden sein.

Des Weiteren sollte die Komponente, die für die Performance-Messung zu- ständig ist, nur lose an die Geschäftslogik gekoppelt sein und sich bei Bedarf leicht wieder entfernen lassen. Deshalb sollte sie sich schon während der Entwicklung unkompliziert ein- und ausschalten lassen (z.B. bei der Ausführung von
Unit-Tests). Eine einfache Einbindung von Performance-Messungen fördert das Bewusstsein für Performance-Fragen schon vor der ersten Produktivsetzung und vereinfacht die Durchführung von Lasttests.

Da einzelne Steps von Spring-Batch-Jobs gemäß JSR-3523 aus den Schritten read, process, write bestehen, beginnt und endet ein Lauf meist in einer Datenbank oder einem REST-Service. Doch im Lese- bzw. Schreibvorgang kann dabei durch individuelle Datenkonstellationen viel Zeit verloren gehen. Fachliche Informationen gewinnen an Relevanz, die von den JMX-Tools nicht geliefert werden: Wie ist die Beschaffenheit der einzelnen Datensätze (im Spring-Batch-Jargon: Items), die gelesen und weggeschrieben werden?

Spring-Batch-Bausteine (Abb. 1)

Spring-Batch-Bausteine (Abb. 1)

Nicht zu unterschätzen ist auch die Problematik bei der Integration eines Tools in die vorhandene Anwendungs-Landschaft eines Unternehmens. Im Regelfall gibt es bereits umfassende und zentrale Monitoring-Lösungen bzw. –Portale. Statt einer eigenen, unabhängigen graphischen Oberfläche sollten dann Standard-
Schnittstellen zur einfachen Integration vorhanden sein.

Lösungsansatz

Wir suchen für die Performance-Messung von Spring-Batch-Jobs also nach einer Lösung, die

  • den Cross-Cutting-Concern Performance als solchen behandelt,
  • dabei jedoch den fachlichen und technischen Kontext erhält,
  • einfach einzubinden und wieder zu entfernen ist,
  • den Entwicklungsprozess unterstützt,
  • das Laufzeitverhalten des betrachteten Jobs nicht wesentlich verändert,
  • ein Ergebnis liefert, das sich in Unternehmens-spezifische Reporting-Landschaften einbinden lässt.

Spring-Batch liefert bereits eine Lösung mit, die einen Teil der obigen Anforderungen erfüllt: Spring-Batch-Admin. Dabei handelt es sich um eine Web-Oberfläche, die neben einfachen Management-Funktionen (z.B. Job starten/stoppen) die Spring-Batch-Arbeitstabellen (welche Details zur Ausführung von
Jobs enthalten) ausliest und die Laufzeitinformationen in aggregierter Form darstellt. Dadurch lässt sich die Dauer der Ausführung eines Jobs bis auf die einzelnen Steps herunterbrechen. Allerdings fehlen Informationen zu Chunks, Reader/Processor/ Writer und einzelnen Items (Abb. 2).

Chunk-basierte Verarbeitung eines Steps innerhalb eines Spring-Batch-Jobs. (Abb. 3)

Chunk-basierte Verarbeitung eines Steps innerhalb eines Spring-Batch-Jobs. (Abb. 3)

Um diese Informationen zu ergänzen, Integration zu ermöglichen und die allgemeine Nutzung zu vereinfachen, wurde das Tool Spring-Batch-Performance-Monitoring (SBPM) entwickelt und als Open-Source-Software auf Github4 veröffentlicht. Im Folgenden wird dargestellt, wie das SBPM konzipiert ist und genutzt werden kann.

Zum besseren Verständnis wird in (Abb. 3) zunächst eine vereinfachte Übersicht zur Chunk-basierten Verarbeitung eines Steps dargestellt.

Spring-Batch-Bausteine Laufzeitsicht. (Abb. 2)

Spring-Batch-Bausteine Laufzeitsicht. (Abb. 2)

Für eine Chunk-Größe n werden im Rahmen eines Steps folgende Operationen durchgeführt:

  1. Transaktion für einen Chunk öffnen.
  2. Im Reader werden maximal n Items ausgelesen und in einem Iterator zwischengespeichert. Per read() Methode wird über die Items iteriert und sie werden einzeln herausgegeben. Wenn keine Items mehr zu verarbeiten sind, wird null zurückgegeben.
  3. Solange read() ein Item zurückgibt, wird es jeweils einzeln im Prozessor verarbeitet. Sobald die read() Methode null zurückgibt, wurden alle Items prozessiert.
  4. Daraufhin schreibt der Writer die prozessierten Items weg. Dies geschieht nicht einzeln, stattdessen werden die Items in einer Liste zusammengefasst und in einem Schritt weggeschrieben.
  5. Transaktion committen.
  6. Falls noch weitere Items zu verarbeiten sind: beginne für den nächsten Chunk wieder bei 1.

Wir benötigen also Laufzeitinformationen zum Job, Step, Chunk, Reader/Processor/Writer und allen einzelnen Items.

Aufzeichnung der Laufzeit von Job, Step und Chunk

Glücklicherweise bietet Spring-Batch eine Vielzahl von Möglichkeiten, Laufzeitinformationen zu diesen Bausteinen abzugreifen, ohne Implementierungsdetails
zu kennen:

  • Job: JobListener kennen den Beginn- und Endzeitpunkt des Jobs
  • Step: Für Steps werden StepListener genutzt
  • Chunk: Für Chunks werden ChunkListener genutzt

Die Listener werden an den jeweiligen Bausteinen registriert und ermitteln daraufhin die Laufzeitinformationen, indem sie den Start- und Endpunkt des betrachteten Prozesses aufzeichnen (für einen Chunk z.B. über die beforeChunk() und afterChunk() Methoden des ChunkListeners5) und die Differenz berechnen. Dabei sind jedoch die Anforderung der losen Kopplung und der generische Charakter (Cross-Cutting-Concern) zu berücksichtigen. Die Listener sollen nicht händisch für einzelne Jobs registriert werden müssen. Vielmehr muss es reichen, die Konfigurationsklasse @Configuration des SBPM in der Konfiguration des zu messenden Jobs zu via @Import zu importieren. Dies wird durch das BeanPostProcessor Interface6 ermöglicht. Für jede Bean, die Teil des betrachteten Spring-ApplicationContexts ist, wird in einer Klasse, welche die BeanPostProcessor implementiert, die Methode postProcessAfterInitialization() aufgerufen. Darin kann der Typ einer Bean ermittelt und, falls es sich um einen Job, Step oder Chunk handelt, ein entsprechender Listener ergänzt werden.

Aufzeichnung von Reader, Processor, Writer und einzelnen Items

Bei der Überwachung von Reader/Processor/Writer und den einzelnen Items stoßen die Bordmittel von Spring-Batch an ihre Grenzen. Es existieren auch hierfür spezielle Listener, die eine Aufzeichnung ermöglichen, z.B. der ItemReadListener. Sind jedoch mehrere Reader/Processoren/Writer im ApplicationContext vorhanden, ist eine genaue Zuordnung bei Verwendung des gleichen Listeners nicht möglich. Darüber hinaus arbeitet der ItemWriteListener auf einer Liste von Items. Wirbenötigen jedoch Laufzeit-Informationen zu jedem einzelnen Item.

Das Problem kann unter Verwendung von Spring-AOP (Aspect-Oriented-Programming) gelöst werden. Dabei werden mit Hilfe von JoinPoints zu überwachende Methoden identifiziert, z.B. org.springframework.batch.item.ItemWriter.write(..). Sobald diese gefunden sind, kann über einen sogenannten Around-Advice an der Stelle Code injiziert werden. Das ermöglicht die Aufzeichnung von Laufzeitinformationen, ohne die eigentliche Implementierung manipulieren zu müssen.

Für Reader und Processor wird vor und nach Ausführung der im JoinPoint deklarierten Methode read() und process() ein Zeitstempel gesetzt und so für jedes Item die Verarbeitungsdauer aufgezeichnet.

Da der Writer auf einer Liste von Items arbeitet, wird sie von einem Around-Advice abgefangen und in eine dekorierte7 Liste inkl. dekoriertem Iterator übertragen. Diese setzt die benötigten Zeitstempel innerhalb der next() und hasNext Methoden ihres Iterators.

In (Abb. 4) sind die von den Around-Advises im Reader/Processor/Writer gesetzten Zeitstempel rot markiert.

Around-Advices um Reader, Processor und Writer. (Abb. 4)

Around-Advices um Reader, Processor und Writer. (Abb. 4)

So können die einzelnen Laufzeiten aller Items aufgezeichnet und letztlich über ihre Verweildauer in Reader, Processor und Writer summiert werden. Dabei ist jedoch zu beachten, dass die Transaktion hier ignoriert wird; das Delta zwischen der Summe der Laufzeit von Reader/Processor/Writer und Chunk bzw. Step ist der Zeitaufwand des Commits. Informationen zur Transaktion sind nur auf Ebene eines Chunks vorhanden (Abb. 3). Dadurch sind alle eingangs geforderten Spring-Batch-Bausteine abgedeckt.

Falls noch detailliertere fachliche Informationen benötigt werden, können diese durch das Überschreiben der toString() Methode der betrachteten Item-Klasse aufgezeichnet werden, da der entsprechende Rückgabewert für jedes Item gespeichert wird.

Persistierung

Bleibt noch die Frage zu klären, wie die aufgezeichneten Informationen persistiert werden können. Dazu wird eine H2 In-Memory-Datenbank mitgeliefert, in welche das SBPM fortlaufend Daten wegschreibt. Um dabei die Laufzeit des Batchjobs nicht negativ zu beeinflussen, geschieht dies in einer Queue, die in einem eigenen Thread läuft. Wenn die gemessene Zeit aufgezeichnet ist, wird sie in die Queue eingereiht, welche die Daten daraufhin eigenständig, unabhängig vom überwachten Batchjob und in fest definierten Zeitintervallen in die H2 schreibt. Wurde der Batchjob  eendet, steht die Datenbank als Datei zur Verfügung, welche standardmäßig im Projektordner abgelegt ist (/target/database/monitoringDB – dies ist speziell konfigurierbar). Damit sich die Performance durch steigendes Datenvolumen bei wiederholten Ausführungen nicht verschlechtert, wird jedes Mal eine neue Datenbank angelegt. Zum Auslesen der Datenbank wird ein Tool mitgeliefert, welches den Inhalt der SBPM-Tabellen im Systembrowser darstellt. Daneben können aber selbstverständlich auch andere Tools (wie z.B. SQuirreL) oder programmatische Schnittstellen zum Export in vorhandene Tabellen genutzt werden.

Der Datenbank liegt ein einfaches Snowflake-Schema zugrunde mit den Item-Laufzeiten als Fakten (Abb. 5); Action ist in diesem Kontext der Überbegriff für Reader/Processor/Writer). Dieses ermöglicht eine einfache Integration in vorhandene Reporting-Landschaften. Bei Bedarf kann die H2 auch durch eine beliebige andere relationale Datenbank ausgetauscht werden.

Installation

Um das SBPM zu nutzen, müssen folgende Schritte durchgeführt werden:

  1. Das Maven-Artefakt in der POM des Batchjobs referenzieren:
    <dependency>
       <groupId>de.viadee</groupId>
       <artifactId>springBatchPerformanceMonitoring</artifactId>
       <version>1.0.2</version>
    </dependency>
    
  2. Die SBPM-Configuration importieren, z.B. in der Job-Configuration:
    @Configuration
    @Import( de.viadee.spring.batch.infrastructure.
    Configurator.class )
    public class EineSpringBatchJobConfig{
    
       @Bean
       public Job einSpringBatchJob() {
          ...
       }
    
       ...
    }
    

    Möglich ist dies aber auch in einem JUnit-Test, falls das SBPM nur zu Testzwecken genutzt werden soll:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { 
    EineSpringBatchJobConfig.class,de.viadee.spring.batch.infrastructure.
    Configurator.class })
    public class TestMonitoring {
    
       @Test   
       public void testMonitoring(){
             ...
          }
       ...
    }
    

    Daraufhin führt das SBPM bei jeder Ausführung des Batchjobs eine Performance-Messung durch.

Fazit

Für die eingangs formulierten Anforderungen an ein generisches Performance-Messungs-Tool existiert mit dem Spring-Batch-Performance-Monitoring-Tool nun eine Möglichkeit, die

  • den Cross-Cutting-Concern-Performance als solchen behandelt – da es mit Hilfe von Spring-Bordmitteln (Listener und AOP) generisch einsetzbar ist, ohne die Fachlogik ändern zu müssen,
  • dabei den fachlichen und technischen Kontext nicht ignoriert – da es die Spring-Batch-Bausteine bis zum Detailgrad einzelner Items berücksichtigt,
  • einfach einzubinden und wieder zu entfernen ist – da es durch einen einfachen @Import eingebunden und bei Bedarf ohne großen Aufwand wieder entfernt werden kann,
  • den Entwicklungsprozess unterstützt – da es schon vor der Produktivsetzung in Unit- und Integrationstests verwendet werden kann,
  • das Laufzeitverhalten des betrachteten Jobs nicht wesentlich verändert – da die Aufzeichnung in einem eigenen Thread abläuft,
  • ein Ergebnis liefert, das sich in Unternehmensspezifische Reporting-Landschaften einbinden lässt – da die Ergebnisse in einem einfachen  Datenbankschema festgehalten werden.

 

Autor – Christian Nockemann

Diplom-Wirtschaftsinformatiker Christian Nockemann arbeitet seit 2009 als IT-Berater und Softwareentwickler bei der viadee IT-Unternehmensberatung. Beim Einsatz in Kundenprojekten im Bereich Handel, Telekommunikation und Versicherung liegt sein Fokus auf Design und Entwicklung von Spring-basierten Enterprise-Anwendungen. Eine besondere Bedeutung gibt er dabei Qualitätskriterien des Softwareerstellungsprozesses wie bspw. der Anwendung des Domain-driven Designs, dem sinnvollen Einsatz von Entwurfsmustern und der Einhaltung von Clean-Code-Richtlinien.


Quellenachweise und Referenzen

  1. R. Miles: AspectJ Cookbook. Manning Publications Co., 2003, S. 2
  2. https://www.jcp.org/en/jsr/detail?id=3
  3. https://www.jcp.org/en/jsr/detail?id=352
  4. https://github.com/viadee/springBatchPerformanceMonitoring
  5. http://docs.spring.io/spring-batch/apidocs/org/springframe work/batch/core/ChunkListener.html
  6. https://docs.spring.io/spring/docs/current/javadoc-api/org/
  7. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. Addison-Wesley, 1996, S. 199.


Die JAVAPRO ist Deutschlands erstes kostenlose Magazin für professionelle Java-Entwickler. Die JAVAPRO ist redaktionell unabhängig. Das Magazin erscheint alle drei Monate, auch der Versand ist frei Haus.

Leave a Reply