UUID in Shell-Skripten erzeugen

Eine UUID ist ein bequemer Weg schnell einen eindeutigen Schlüssel zu erzeugen und für jede Programmiersprache gibt es mehr oder weniger UUID-Implementierungen.

Um eine UUID auf der Shell oder in Shell-Skripten zu erzeugen, gibt es das Programm UUID, daß es unter anderem als Debian-Package für Debian-Linux sowie für MacOS über MacPorts gibt.

uuid unter Debian installieren
1
$ sudo apt-get install uuid
uuid unter MacOS X installieren
1
$ sudo port install ossp-uuid

Anschließend kann mittels uuid eine oder mehrere UUID erzeugt werden.

UUID mit uuid erzeugen
1
2
3
4
5
6
7
8
$ uuid
3295643c-0a9d-11e4-b4c2-3c15c2c77b1a
$ uuid -n 5
34a0412a-0a9d-11e4-8ad9-3c15c2c77b1a
34a04260-0a9d-11e4-8ada-3c15c2c77b1a
34a04288-0a9d-11e4-8adb-3c15c2c77b1a
34a042a6-0a9d-11e4-8adc-3c15c2c77b1a
34a042c4-0a9d-11e4-8add-3c15c2c77b1a

Leider wird dabei nicht die Man-Page von UUID mitinstalliert, die die unterstützten Optionen erläutert, denn uuid unterstützt auch die Erzeugung der unterschiedlichen UUD-Versionen als auch die Bestimmung welcher Version eine UUID angehört.

CQRS-Artikel im JavaMagazin August 2014

In der August-Ausgabe des JavaMagazins ist mein Artikel Weg vom Schichtenmodell – Command Query Responsibility Segregation als Einführungsartikel in das Command Query Responsibility Segregation-Architekturmuster erschienen.

Im klassischen Schichtenmodell erfolgt der Datenzugriff vertikal durch alle Schichten, klassischerweise drei. CQRS hingegen unterteilt ein System in ein Write-Datenmodell für die Ausführung von Befehlen (Kunde anlegen, Adresse ändern) und ein oder mehrere auf die schnelle Beantwortung von Leseanfragen hin optimierte Read-Datenmodelle.

Interessant ist der Artikel für alle die, deren System unterschiedliche Sichten auf ihren Datenbestand anbieten müssen und dabei auf eine saubere Architektur und einen schnellen Datenzugriff angewiesen sind.

jQAssistent zur Architekturvalidierung

jQAssistent ist ein auf Neo4j beruhendes Tool zur dynamischen Validierung von Architekturregeln in Java-Projekten.

Im Gegensatz zu statischen oder schwer erweiterbaren Tools wie CheckStyle oder Findbugs, kann der SourceCode dynamisch mit der von Neo4j angebotenen Abfragesprache untersucht werden.

Dirk Mahler von der Buschmais GbR hat jQAssistent unter anderem auf der JavaLand 2014 vorgestellt. Die zur Einführung lesenswerten Folien sind online abrufbar.

Früher habe ich bereits Tools mit einem vergleichbaren Ansatz gefunden, die jedoch aus meiner subjektiven Sicht nie das Potential und die Flexibilität von jQAssistent hatten.

Tip des Tages: Git-Repository säubern

Unbekannte und geänderte Dateien in einem Git-Repository können mit dem Git-Befehl git clean aus dem lokalen Repository gelöscht werden.

Git-Repository mit ungetrackten Änderungen
1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch master
Your branch is up-to-date with 'bb/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

  readme.md~
  universal-parent/0.1.0-SNAPSHOT/

nothing added to commit but untracked files present (use "git add" to track)
$

Wird jetzt git clean -f -d ausgeführt, werden alle nicht-getrackten Dateien und Verzeichnisse gelöscht. Die Option -f erzwingt die Ausführung von git clean, da das Löschen von Dateien standardmäßig deaktiviert ist, jedoch über die Git-Konfigurationsvariable clean.requireForce geändert werden kann. Die Option -d weist Git an, auch ungekannte Verzeichnisse zu löschen.

Repository säubern
1
2
3
4
5
6
7
8
9
$ git clean -f -d
Removing readme.md~
Removing universal-parent/0.1.0-SNAPSHOT/
$ git status
On branch master
Your branch is up-to-date with 'bb/master'.

nothing to commit, working directory clean
$

Alles auf Ausgang

Was ist aber, wenn ich mein Repository auf den letzten Stand zurücksetzen will, also in den Zustand des letzten Pulls oder Pushs bringen will. git clean kümmert sich ja nur um Git unbekannte Dateien.

Leider gibt es keinen einfachen Befehl, der sowohl vorgenommene und, egal ob gestagte oder nichtstagte Änderungen zurücknimmt.

Um das zu erreichen werden die beiden Befehle git reset und git checkout benötigt. Mittels git reset HEAD . müssen dafür als erstes alle gestagten Änderungen aus dem Staging-Bereich wieder entfernt werden. git checkout -- . können vorgenommene Änderungen wieder auf den letzten Stand von HEAD gebracht werden.

Für ein ‘sauberes lokales Repository’ werden also die folgenden drei Befehle gebraucht:

  1. git reset HEAD .
  2. git checkout -- .
  3. git clean -d -f

Ein Beispiel

Als Beispiel soll ein Repository dienen, in dem es unbekannte, geänderte und nicht gestagte sowie geänderte und gestagte Dateien gibt:

Repository mit diversen Änderungen
1
2
3
4
$ git status -s
 M bs/js/bootstrap.js        # geänderte, nicht gestagt
M  bs/js/bootstrap.min.js    # geändert, gestagt
?? newfile                   # nicht verfolgt

Mit den drei gezeigten Befehlen bringe ich jetzt das Repository wieder in den Stand von HEAD.

Alle Änderungen zurücknehmen
1
2
3
4
5
6
7
8
9
10
$ git reset HEAD . ; git checkout -- . ; git clean -d -f
Unstaged changes after reset:
M  bs/js/bootstrap.js
M  bs/js/bootstrap.min.js
Removing newfile
$ git status
On branch master
Your branch is up-to-date with 'bb/master'.

nothing to commit, working directory clean

So, wieder alles sauber.

Vorsicht, Datenverlust!

So bequem das ist, so leicht kann ich damit zufällig Daten löschen. Daher habe ich diese drei Befehle in ein Shell-Skript mit Sicherheitsabfrage verpackt.

Shell-Skript mit Sicherheitsabfrage
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

echo "Are you sure? (y/N)" ;
read ;

if [[ ${REPLY} == 'Y' ]];
then
    git reset HEAD . ;
    git checkout -- . ;
    git clean -d -f ;
fi

Rabatt-Code für GOTO Berlin 2014

In diesem Jahr findet die bekannte GOTO Conferenz zum zweiten Mal in Berlin als GOTO Berlin 2014 statt.

Mit dem Rabatt-Code GOTObln2014_OF gibt es 100,– Nachlass auf den regulären Ticketpreis. Bei Buchung bis zum 18. Juni kostet das Ticket 699,– Euro + Mwst. anstatt 799,– Euro + Mwst..

Gebucht werden kann unter https://secure.trifork.com/berlin-2014/registration/.

Gründe die GOTO Berlin zu besuchen

Als Sprecher für die GOTO Berlin werden in diesem Jahr Martin Fowler, Günther Dück, Martin Thompson (LMAX Disruptor), Tim Vox (vert.x), Angelika Langer, Jez Humble (Autor von Continous Delivery), Chad Fowler, Carlo Zapponi MAKINGUSE und viele mehr sein.

Die vollständige Liste der Vortragenden gibt es unter http://gotocon.com/berlin-2014/speakers/. Die Liste ist online noch nicht ganz vollzählig, da das Programm erst heute finalisiert wurde (Stand 5. Juni 2014).

Spaltenreihenfolge in Thunderbird ändern

Alle meine Mails lese ich mit Thunderbird im 3-Spalten-Layout, bei dem die Ordnerleiste, der Nachrichtenverlauf und der Nachrichtentext vertikal von links nach rechts angeordnet sind.

Um Platz auf dem Bildschirm zu sparen, habe ich mir Nostalgy installiert, mit dem ich durch drücken von l die Ordnerleiste ausblenden kann. Jetzt braucht Thunderbird weniger Platz auf dem Notebook und auf dem großen Display muß ich Thunderbird nicht maximieren, um alles richtig sehen zu können.

Mit diesem Layout lebe ich jetzt schon ein paar Jahre, wobei mich immer gestört hat, daß sich der eigentliche Text, den ich lesen will, auf der rechten Seite befindet. Rein nach meinem Empfinden sollte er links sein. Auch wenn ich mir sonst die Fenster auf meinem Bildschirm anordne, befindet sich das, was meine hauptsächliche Aufmerksamkeit beansprucht, entweder in der Mitte oder mittig-links. Sicherlich gibt es dafür eine gute und fundierte Erklärung, doch die kenne ich nicht. Jedenfalls wäre es mir lieber, wenn der Mail-Text links und die Nachrichtenübersicht rechts wäre.

Sowohl Thunderbird als auch Firefox benutzen Mozillas XUL (XML User Interface Language) zusammen mit CSS für die Beschreibung ihrer Oberfläche. Daher ist es leicht die Oberfläche an eigene Bedürfnisse anzuspassen, wenn man genügend Zeit und Geduld hat, sich eingehen mit XUL zu beschäftigen. Ich hatte weder das eine noch das andere, aber wie bei vielem anderen ist das ein Problem, das andere schon gelöst haben.

Mit den unten stehenden CSS-Anweisungen lassen sich beide Fenster vertauschen.

1
2
3
4
5
6
7
/*
 * Switch the order of the panes in Thunderbird from
 *   folders | messages | message text
 * to
 *   folders | message text | messages
 */
#threadPaneBox { -moz-box-direction: reverse; }

Leider weiß ich nicht mehr, wo ich diesen Trick gefunden habe.

Um beide Fenster zu vertauschen, muß dieses CSS-Schnipsel in die Datei `userChrome.css’ eingefügt und Thunderbird neugestartet werden.

Anschließend befindet sich das Nachrichtenfenster jetzt, wo ich es immer gerne gehabt hätte, nämlich links.

Unter MacOS X befindet sich userChrome.css im Ordner ~/Library/Thunderbird/Profiles/[profile]/chrome/. Falls die Datei noch nicht existiert, kann sie einfach angelegt werden.

Wo sich diese Datei auf anderen Systemen befindet, kann in der Knowledge Base von Mozilla nachgelesen werden.

Lernen durch Prototypen - Mehr Spielen, mehr Prototyping

Ich habe in der letzten Zeit angefangen wieder mehr zu Spielen. Nein, keine Computer-Spiele. Dazu fehlt mir die Zeit oder besser gesagt, ich will sie mir nicht nehmen. Vielmehr habe ich angefangen mehr kleine Prototypen zu bauen. Und das ist wie zu spielen.

Wir müssen mehr Prototypen bauen

Gefühlte neunzig Prozent meiner Arbeitszeit verbringe ich damit Frameworks zu integrieren, um deren Funktionalität nutzen zu können. Egal ob EclipseLink, CDI, REST via Jersey und Jackson, die wenigsten Dinge funktionierten auf Anhieb wie erwartet und oft hat es mich eine Menge Zeit gekostet herauszufinden, wie dieses oder jenes Feature funktioniert, und wenn nicht, ob es ein Bug ist oder ob das Problem durch eine andere Komponente in meiner Applikation verursacht wird. Allein durch die Größe des Projekts an sich dauert jeder Test, jede Konfigurationsveränderung ein wenig länger, da mehr Klassen kompiliert oder Komponenten initialisiert werden müssen. Ein zu hoher Aufwand für einen kleinen Wie-funktioniert-das-Test.

Aus diesem Grunde habe vor einigen Monaten begonnen, mir für alles mögliche kleine Testprojekte anzulegen, in denen ich kleine Prototypen ablege. Nichts weltbewegendes. Oft bestehen diese Prototypen nur aus ein paar wenigen Klassen und ein paar Unit-Tests. Mit ihnen kann ich genau das Testen und ausprobieren, was mich interessiert und ich gerade brauche, ehe ich es in meinem richtigen Projekt einbaue.

Ein Thema, ein Repository

Anfangen habe ich damit, als ich begonnen habe mich mit Arquillian intensiver zu beschäftigen und ich nicht die bestehende Test-Infrastruktur durch das Ausprobieren von Arquillian beschädigen wollte. Das hätte dazu geführt, daß ich keine verläßlichen Tests gehabt hätte und daher keine Releases schnell hätte erstellen können.

Kurzerhand habe ich damals ein Bitbucket Repository neu angelegt und angefangen, die einzelnen von mir benötigten Funktionen mit Hilfe von Unit-Tests durchzutesten.

Erst wenn ich ein Feature und dessen Nutzung richtig verstanden habe, fange ich an, es im jeweils aktuellen Projekt zu benutzen. Inzwischen habe ich mehrere solcher Spielprojekte auf Bitbucket, mit denen ich prototypisch Funktionen von Jersey und anderen Frameworks durchtesten kann. Dabei finde ich es nützlich, daß ich bei Bitbucket private Repositories anlegen kann, auf die nur ich Zugriff habe.

Die Vorteile auf einen Blick

Das ich das Ausprobieren von diversen Features jetzt fast immer erst mit einem kleinen Prototypen erforsche hat mehrere Vorteile:

  1. Die Codebasis meines eigentlichen Projekts ist stabiler, da ich neue Framework-Features vor dem Einbau besser verstehe und somit einfache Fehler, die hinterher teuer werden, vermeiden kann.
  2. Features verstehe ich schneller, da ich für meinen Prototypen eine saubere Test-Umgebung habe, die nicht durch anderen Code und andere Frameworks beeinflußt wird. So kann ich mich ausschließlich auf das Feature selbst konzentrieren.
  3. Meine Prototypen bilden eine Wissensdatenbank. Wenn ich heute wissen will, wie ich das eine oder das andere Probleme gelöst habe, kann ich auf meine Prototypen zurückgreifen und muß mich seltener durch existierende Projekte wühlen.

Einem solchen Prototypen-Ansatz zu verfolgen ist oft nicht leicht. Denn auch für einen recht einfachen Prototypen muß ich Code schreiben und eine irgendwie geartete Infrastruktur (Container, Build) bereitstellen. Und der langfristige Nutzen stellt sich nur ein, wenn ich meine Prototypen ohne lange Konfiguration austesten und erweitern kann. Und das ist nur gegeben, wenn ich mit git clone ... und mvn clean install alles an den Start bringen kann.

Fazit

Dafür die Disziplin aufzubringen ist manchmal schwer, besonders wenn das, was ich eigentlichen machen möchte auf den ersten Blick einfach aussieht. Aber spätestens beim ersten Problem reverte ich meine Änderungen und baue erst einmal einen neuen Prototypen.

Bleibt zum Ende die Feststellung, daß wir aufhören sollten neue Framework-Features direkt im aktuellen Projekt auszuprobieren und zu erlernen. Wir sollten das Erlernen vom Nutzen trennen. Wir sollten Features erst einsetzen, wenn wir sie verstanden haben.

Kleine profane Prototypen auf der grünen Wiese helfen dabei.

Mehr Geschwindigkeit für Java EE-Anwendungen durch Partitionierung von PostgreSQL-Tabellen

Aktuell entwickle und betreue ich eine Anwendung zur Leistungsabrechnung, die tagesaktuell und minutengenau den Leistungsverbrauch von Kunden berechnet und die Ergebnisse in einer PostgreSQL-Datenbank speichert. Mit der Zeit degradierte die Performance des Systems immer stärker und Handlungsbedarf bestand, da sonst das System die an es gestellten Anforderungen im Laufe dieses Jahres nicht mehr hätte erfüllen können.

Meine Anfangsvermutung war, daß aufgrund der zunehmenden Datenmenge die Berechnung immer mehr Zeit in Anspruch nahm, ich dies nicht hinreichend berücksichtigt hatte und sich jetzt mit zunehmender Datenmenge die Schwächen des Systems zeigten. Gegen diese Vermutung sprach, daß jede einzelne Berechnung immer noch innerhalb von wenigen Millisekunden abgeschlossen war.

Nadelöhr Datenbank

Auffällig waren aber die Pausen zwischen jeder einzelnen Berechnung. Das System schien einfach für eine Sekunde stehen zu bleiben, ehe die nächste Berechnung begonnen wurde. Entgegen meiner ersten Vermutung, stellte sich die Datenbank als Nadelöhr bei der Berechnung heraus.

Bei einer Berechnung lade ich sämtliche Daten aus einem Cache, der zuvor aus der Datenbank befüllt wird. So wird sichergestellt, daß bei paralleler Berechnung nicht mehrfach auf die Datenbank zum Laden zugegriffen werden muß und sich Berechnungen dadurch Schreibe- und Lesevorgänge gegenseitig ausbremsen. Nach der Berechnung werden alle Ergebnisse und Zwischenergebnisse in die Datenbank geschrieben. Daher wird während der Berechnung nur in die Datenbank geschrieben. Pro Berechnung können das jedoch mehrere tausend Datensätze sein. Die Zwischenergebnisse werden nach ein paar Monaten gelöscht, die eigentlichen Ergebnisse jedoch dauerhaft aufbewahrt.

Nach einigen Monaten im Betrieb dauerte das Einfügen eines Ergebnisses wesentlich länger als ursprünglich, da die Datenbank-Tabellen inzwischen mehrere Gigabyte groß geworden waren und PostgreSQL damit mehr Zeit zum finden von freiem Platz und für die Aktualisierung der Indexe brauchte. Zudem degradiert ohne hinreichendes `vakuumen` die Performance von Tabellen und Indexen in PostgreSQL-Datenbanken. Details hierzu gibt es im PostgreSQL-Wiki im Abschnitt Introduction to VACUUM, ANALYZE, EXPLAIN, and COUNT.

Andere Probleme

Die zunehmende Größe der Datenbank bereitete auch bei Sicherung und Restore Probleme. PostgreSQLs pg_dump (Doku auf postgresql.org) und pg_restore (Doku auf postgresql.org) können zwar eine Sicherung und eine Wiederherstellung mit über die Option -j mit mehreren sogenannten Jobs parallel ausführen, jedoch arbeitet ein Job jeweils auf einer Datenbanktabelle. Das heißt, daß die größte Tabelle bestimmt die Dauer von Sicherung und Wiederherstellung. Das konnte ich beim Einspielen von Dumps beobachten: Anfangs arbeitet pg_restore hochgradig parallel auf vielen Tabellen, zum Ende lief aber nur noch ein einziger Job, der die größte der Tabellen wieder einspielte. Ein kurzer Überschlag ergab, daß ich für eine vollständige Wiederherstellung mehr als zwei Tage benötigen würde. Unakzeptabel.

Geschwindigkeit durch Tabellen-Partitionierung

Die Überlegung war also, daß wenn große Tabellen mit vielen Änderungen nicht optimal für die Anwendung sind, muß ich mit kleinen Tabellen arbeiten.

PostgreSQL erlaubt es mit Hilfe von Vererbung von Tabellen, einem von mir früher unterschätztem Feature, und Triggern Datenbanken-Tabellen zu partitionieren. Das funktioniert im Grunde recht einfach. Zuerst wird eine Master-Tabelle angelegt und anschließend beliebig viele Kind-Tabellen abgeleitet. Für die Master-Tabelle wird ein Trigger angelegt, der alle INSERT-Anweisungen abfängt und die einzufügenden Daten anhand eines selbstgewählten Partitionierungkriteriums auf die Kind-Tabellen verteilt. Damit erfolgt die Partitionierung der Daten transparent beim Hinzufügen von Daten.

Wie sieht es aber beim Lesen aus? Werden Daten via SQL aus der Master-Tabelle abgefragt, bezieht PostgreSQL auch sämtliche Kind-Tabellen in die Abfrage mit ein. Damit kann auch das Abfragen von Daten aus Klientensicht transparent gestaltet werden.

Und wo kommt die Performance her?

Mit Hilfe von Partitionierung können Daten aus Klientensicht also transparent auf beliebig viele Tabellen verteilt werden. Und wo kommt jetzt die Performance her? Wenn bei einer Anfrage, wie oben geschrieben, auch alle Kind-Tabellen abgefragt werden müssen, werden doch auch alle Daten bei der Abfrage mit einbezogen. Was ist jetzt also besser also vorher?

Bis jetzt hilft die Partitionierung nur bei einer Verbesserung der Performance von pg_dump und pg_restore, da sich die Daten jetzt auf mehrere kleine Tabellen verteilen.

PostgreSQL kann aber auch bei der Abfrage von Daten von der Partionierung profitieren, indem es nur die Tabellen abfragt, die für die Abfrage relevante Daten enthalten. Hierfür muß das gewählte Partionierungskriterium nicht nur über den INSERT-Trigger ausgedrückt werden, sondern auch explizit über Constraints auf den Kind-Tabellen. Wird das in dem Constraint überprüfte Feld in die Abfrage miteinbezogen, kann der Optimizer genau bestimmen, welche Tabellen überhaupt durchsucht werden müssen. Um dieses Feature von PostgreSQL zu nutzen, muß die Planner-Option constraint_exclusion richtig gesetzt sein.

Butter bei die Fische

Einen guten Einstieg in die Partitionierung bietet die offizielle PostgreSQL-Dokumentation im Abschnitt Partitioning, die ich mit einem kleinen Beispiel illustrieren möchte.

Als erstes wird die Master-Tabelle erstellt, von der anschließend mehrere Kind-Tabellen, also die eigentlichen Daten-Partitionen, abgeleitet werden. In den Tabellen sollen die Ergebnisse meiner Berechnungen abgelegt werden, wobei ich für jedes Ergebnis den Zeitpunkt seiner Berechnung speichere. Diesen Wert wird als Partitionierungskriterium für die Kind-Tabellen gewählt werden.

Master-Tabelle anlegen
1
2
3
4
5
6
7
8
9
10
CREATE TABLE computation
(
  c_id                  UUID                        NOT NULL,
  c_computation_date    TIMESTAMP WITH TIME ZONE    NOT NULL,
  c_customer            BIGINT                      NOT NULL
);

ALTER TABLE computation
  ADD CONSTRAINT computation_cs_primary_key
  PRIMARY KEY (c_id);

Im nächsten Schritt werden die Kind-Tabellen angelegt. Die erste Tabelle soll alle Ergebnisse für das Jahr 2013 und die zweite alle Ergebnisse für das Jahr 2014 aufnehmen. Dieses Partitionierungskriterium wird noch über Datenbank-Constraints abgesichert, sodaß später der Query-Planer von PostgreSQL dies bei der Optimierung von Abfragen miteinbeziehen kann.

Kind-Tabellen anlegen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE computation_2013
(
  CONSTRAINT computation_2013_cs_compution_date_in_range
    CHECK (c_computation_date >= '2013-01-01 00:00:00+00:00' AND
           c_computation_date < '2014-01-01 00:00:00+00:00')
)
  INHERITS (computation);

ALTER TABLE  computation_2013
  ADD CONSTRAINT computation_2013_cs_primary_key
  PRIMARY KEY (c_id);

CREATE TABLE computation_2014
(
  CONSTRAINT computation_2014_cs_compution_date_in_range
    CHECK (c_computation_date >= '2014-01-01 00:00:00+00:00' AND
           c_computation_date < '2015-01-01 00:00:00+00:00')
)
  INHERITS (computation);

ALTER TABLE computation_2014
  ADD CONSTRAINT computation_2014_cs_primary_key
  PRIMARY KEY (c_id);

Mit Hilfe eines Triggers auf die Master-Tabelle computation werden alle INSERT-Anweisungen auf die Master-Tabelle abgefangen und die Partionierung realisiert.

INSERT-Trigger auf der Master-Tabelle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE OR REPLACE FUNCTION computation_insert_trigger()
  RETURNS TRIGGER AS
  $$
  BEGIN
    IF (NEW.c_computation_date >= '2013-01-01' AND
        NEW.c_computation_date <  '2014-01-01')
    THEN
      INSERT INTO computation_2013 VALUES (NEW.*);
    ELSEIF (NEW.c_computation_date >= '2014-01-01' AND
            NEW.c_computation_date <  '2015-10-01')
    THEN
      INSERT INTO computation_2014 VALUES (NEW.*);
    ELSE
      RAISE EXCEPTION 'Date out of range.';
    END IF;
    RETURN NULL;
  END;
  $$ LANGUAGE plpgsql;

CREATE TRIGGER computation_trigger_insert
  BEFORE INSERT ON computation
  FOR EACH ROW EXECUTE PROCEDURE computation_insert_trigger();

Um das ganze jetzt noch testen zu können, werden noch zwei Testdatensätze eingefügt.

Testdaten einfügen
1
2
3
4
5
6
7
INSERT INTO computation (c_id, c_computation_date, c_customer)
  VALUES ('48fcd6b4-8c04-11e3-9caa-02013b56ffb6',
          '2013-02-02 05:06:07+00:00', 1000);

INSERT INTO computation (c_id, c_computation_date, c_customer)
  VALUES ('48fcd6b4-8c04-11e3-9caa-02013b56ffb7',
          '2014-09-02 05:06:07+00:00', 2000);

Testen der Partionierung

Ob die Partionierung richtig funktioniert hat, sollen ein paar Abfragen prüfen. Zuerst suchen wir nach allen Datensätzen. Wir haben zwei eingefügt, daher sollen auch zwei bei der Suche über die Master-Tabelle gefunden werden.

Abfrage über Mastertabelle
1
2
3
4
5
db=# SELECT * FROM COMPUTATION;
                 c_id                 |   c_computation_date   | c_customer
--------------------------------------+------------------------+------------
 48fcd6b4-8c04-11e3-9caa-02013b56ffb6 | 2013-02-02 06:06:07+01 |       1000
 48fcd6b4-8c04-11e3-9caa-02013b56ffb7 | 2014-09-02 07:06:07+02 |       2000

Die Master-Tabelle selber soll jedoch leer sein. Um nur die Master-Tabelle ohne die abgeleiteten Tabellen abfragen zu können, muß dem Tabellennamen in der Abfrage das Schlüsselwort ONLY hinzugefügt werden.

Abfrage der Mastertabelle alleine
1
2
3
4
db=# SELECT * FROM ONLY COMPUTATION;
 c_id | c_computation_date | c_customer
------+--------------------+------------
(0 rows)

Ok, auch das hat geklappt.

Optimierung der Abfrage

Wie bereits geschrieben, kann PostgreSQL die Abfrage von partionitionierten Tabellen optimieren, wenn die Planner-Option constraint_exclusion richtig gesetzt ist und das Partionierungskriterium in der Abfrage enthalten ist und auch vom Query-Planner auch ausgewertet werden kann.

Sehen wir uns dafür den Query-Plan an, wenn ich naiv nach der letzten Berechnung für den Kunden 1.000 suche.

Naive Suche nach Kunden
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
db# SET constraint_exlcusion TO on;
SET

db# EXPLAIN SELECT * FROM COMPUTATION
WHERE c_customer = 1000
ORDER BY c_computation_date DESC
LIMIT 1;

                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Limit  (cost=52.83..52.83 rows=1 width=32)
   ->  Sort  (cost=52.83..52.86 rows=15 width=32)
         Sort Key: public.computation.c_computation_date
         ->  Result  (cost=0.00..52.75 rows=15 width=32)
               ->  Append  (cost=0.00..52.75 rows=15 width=32)
                     ->  Seq Scan on computation  (cost=0.00..0.00 rows=1 width=32)
                           Filter: (c_customer = 1000)
                     ->  Seq Scan on computation_2013 computation  (cost=0.00..26.38 rows=7 width=32)
                           Filter: (c_customer = 1000)
                     ->  Seq Scan on computation_2014 computation  (cost=0.00..26.38 rows=7 width=32)
                           Filter: (c_customer = 1000)
(11 rows)

Es ist deutlich zu sehen, daß, um die letzte Berechnung zu finden, PostgreSQL alle Tabellen durchsuchen muß.

Jetzt wissen wir aber, daß der Kunde erst seit 2014 unser Kunde ist. Daher kann auch die erste Berechnung erst 2014 stattgefunden haben und wir können diese Information auch die Abfrage mitaufnehmen.

Optimierte Suche nach Kunden
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
db# EXPLAIN SELECT * FROM COMPUTATION
WHERE c_customer = 1000 AND
      c_computation_date >= '2014-01-01 00:00:00+00:00'
ORDER BY c_computation_date DESC
LIMIT 1;

             QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=29.66..29.67 rows=1 width=32)
   ->  Sort  (cost=29.66..29.67 rows=3 width=32)
         Sort Key: public.computation.c_computation_date
         ->  Result  (cost=0.00..29.65 rows=3 width=32)
               ->  Append  (cost=0.00..29.65 rows=3 width=32)
                     ->  Seq Scan on computation  (cost=0.00..0.00 rows=1 width=32)
                           Filter: ((c_computation_date >= '2014-01-01 01:00:00+01'::timestamp with time zone) AND (c_customer = 1000))
                     ->  Seq Scan on computation_2014 computation  (cost=0.00..29.65 rows=2 width=32)
                           Filter: ((c_computation_date >= '2014-01-01 01:00:00+01'::timestamp with time zone) AND (c_customer = 1000))
(9 rows)

Da c_computation_date auch das Partitionskriterium darstellt und auch in den Tabellen-Constraints abgebildet ist, kann der Planner jetzt die Tabelle computation_2013 ausschließen.

Bei zwei oder besser gesagt drei Tabellen bringt das natürlich noch nicht viel, aber bei einem Dutzend oder mehreren hundert Tabellen ist der Performance-Unterschied dramatisch.

Partitionierungskriterium in die Abfragen mit einbauen

Letztendlich kann das aber auch bedeuten, daß eventuell bestehende Abfragen umgeschrieben werden müssen, um dem Planner die für den effektiven Ausschluß von Partitionstabellen notwendigen Information übergeben zu können.

Was noch bei der Partitionierung zu beachten ist

Bei der Partitionierung über Ableitung von Tabellen ist zu beachten, daß unter anderem Fremdschlüssel und Indexe nicht vererbt werden. Diese müssen sofern notwendig, für die Kind-Tabellen separat angelegt werden.

Nach welchem Schlüssel partitionieren

Nach welchem Kriterium Tabellen partitioniert werden, hängt wie viele andere Dinge vom Anwendungsfall ab. Grundsätzlich kann Aufgrund von Teilmengen oder fortschreibend partitioniert werden.

Ein Beispiel für die Partitionierung aufgrund von Teilmengen wäre die Speicherung von Benutzerdaten in unterschiedlichen Tabellen in Abhängigkeit vom Wohnort.

Partitionierung nach Teilmengenprinzip
1
CHECK ( c_country IN ( 'DE', 'AT', 'CH' ))

In dem oben gegeben Beispiel handelt es sich um ein Beispiel für eine fortschreibende Partitionierung. Neue Daten werden auf andere, neue Tabellen verteilt. Der Vorteil dieser Methode ist, daß ab einem bestimmten Zeitpunkt es keine Änderungen mehr an den alten Tabellen gibt und diese daher auch besser über VACUUM und CLUSTER für Abfragen optimiert werden können.

Änderungen an der Java EE Anwendung

Mittels Tabellen-Partitionierung und -Vererbung können Daten in einer PostgreSQL-Datenbank relativ transparent für die sich darüber befindende Anwendung partitioniert und das Antwortverhalten der Datenbank aus Anwendungssicht verbessert werden.

Ganz ohne Änderungen bin an meiner Anwendung bin ich nicht ausgekommen. So mußte ich das Feld mit dem für die Partitionierung verwendeten Wert in einige Entities zusätzlich mitaufnehmen, obwohl es bereits über ein anderes Entity in einer referenzierten Tabelle abgespeichert wurde. Mit dieser technisch bedingten und eigentlich nicht so schönen Redundanz kann ich aber leben.

Vorteile und Fazit

Nach dem Umbau meiner Tabellenstruktur und der Partitionierung der Daten bleiben folgende Vorteile:

  • Steigerung der Schreibperformance auf den partitionierten Tabellen durch Verkleinerung der jeweils effektiv geschriebenen Tabelle und zu aktualisierenden Indexe.
  • Steigerung der Leseperformance, da nur noch die effektiv notwendig zu lesenden und jetzt kleineren Tabelle gelesen werden müssen.
  • Schellere Sicherung und Wiederherstellung der Datenbank, da jetzt viele kleine Tabellen parallel anstatt einiger sehr großer Tabellen gesichert beziehungsweise wieder hergestellt werden müssen.

Der wesentliche Punkt für mich bleibt im Rückblick aber, daß ich nur relativ kleine Änderungen an meiner Java EE-Anwendung vornehmen mußte, die sich auf einige wenige Properties und JPA-Abfragen beschränkten und den kleinsten Teil der Arbeit ausmachten.

Dafür bin ich mit einer ca. um 90% schnelleren Anwendung belohnt worden. Ich glaube nicht, daß ich die selbe Verbesserung in vergleichbarer Zeit durch andere Änderungen an meinem System hätten erreichen können, ohne wesentliche Punkte umzuschreiben.

Nur den Code zu betrachten reicht nicht!

Es hat sich mal wieder gezeigt, daß Performance sich aus dem Zusammenspiel aller Komponenten ergibt. Als Java-Entwickler nur auf seinen Code zu achten und die doch real existierende Welt drumherum nicht zu beachten, reicht nicht.

5.000,- Dollar kostet Java Mission Control mindestens im Einsatz

Als Oracle im letzten September sein Monitoring-Werkzeug Java Misson Control (JMC) mit Java 7 Update 40 zusammen ausgeliefert hat, wurde dieses neue Werkzeug in der Java-Welt regelrecht gehypt und in vielen Magazinen und Blogs vorgestellt.

Als Werkzeug ist JMC dem bisher zur Verfügung JVisualVM auch übergelegen, besonders aufgrund des Flight Recorders, der es ermöglicht wichtige Metriken zu CPU- und Speichernutzung zur Laufzeit aufzuzeichnen und anschließend zu analysieren.

Die Oracle Binary Code License for Java

Oracle stellt Java Mission Control zwar mit Java 7 Standard Edition zusammen zur Verfügung, jedoch unter der Oracle Binary Code License for Java. Nach der darf Java Mission Control zwar zum Entwickeln und Testen kostenlos genutzt werden, aber nicht zur Analyse und Untersuchung von Anwendungen in Produktionsumgebungen.

5.000,– Dollar pro Prozessor

Wer nicht gegen die Lizenzbestimmungen von Oracle verstoßen, Java Mission Control jedoch trotzdem zur Analyse von Produktionssystemen einsetzen will, muß eines von Oracles kommerziellen Java-Angeboten wie Java SE Advanced oder Java SE Suite lizenzieren. Wie bei vielen Oracle-Produkten, erfolgt hier die Lizenzierung auf Basis der Prozessor-Anzahl. Dabei werden für Java SE Advanced pro Prozessor 5.000,– Dollar und für Java SE Suite 15.000,– Dollar fällig. Die Java SE Suite enthält dafür dann jedoch auch JRockit Real Time.

Wenn das auch ein Wermutstropfen ist, bleibt unter dem Strich eine positive Bilanz. Selbst wenn Java Misson Control nur für Entwicklungs- und Testzwecke unentgeldlich eingesetzt werden darf, wird es hier sehr gute Dienste leisten und vielleicht helfen das eine oder andere System zu verbessern.