Mehr Geschwindigkeit für Java EE Anwendung 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 Montaten gelöscht, die eigentlichen Ergebnisse dauerhauft 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 gleiche konnte ich beim Einspielen von Dumps beobachten. Anfangs arbeitet pg_restore hochgradig parallel auf vielen Tabellen, zum Ende lief aber nur noch ein Job, der die größte Tabelle 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 abgeleitet Kind-Tabellen. 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 dann das in dem Constraint überprüfte Feld in die Abfrage mit einbezogen, 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 mit einbeziehen 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 mit aufnehmen.

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 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 also 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.

Pauschalvergleiche helfen nicht

PayPal stellt auf JavaScript um und schon beginnen sich die Mühlen zu drehen und hitzige Diskussionen beginne. Na, PayPal, die sind doch so groß, die machen das doch nicht einfach so. Firmen machen das einfach so und Firmen sind nicht rational. Von diesem Irrglauben sollten wir uns bitte einmal verabschieden. Aber das ist ein anderes Thema.

About PayPal’s Node vs Java “fight”

Einen sehr guten Blogpost zur angeblichen Umstellung von PayPal auf JavaScript gibt es im Entwickler-Blog von CloudBees unter dem Titel About PayPal’s Node vs Java “fight”.

Der Post ist sehr gut geschrieben und zeigt auf, daß es oft in solchen Diskussionen darum geht zu zeigen, was man zeigen will. Sonst gäbe es mehr von solchen guten Analysen, in denen auf Zusammenhänge eingegangen wird.

Tip des Tages: Mit watch Shell-Programme beobachten

Wer die Ausgabe von eines Shell-Befehls kontiunierlich beobachten will, um beispielsweise Veränderungen während der Ausführung von Testläufen zu beobachten, kann das mit watch erledigen und muß dafür keine Endlosschleife benutzen.

Beispiel Festplattennutzung beobachten

Um beispielsweise die Festplattennutzung zu beobachten kann man ohne watch die unten stehende Schleife nutzen.

df -h mittels Shell-Loop beobachten
1
2
3
4
5
while true; do 
    clear;
    df -h;
    sleep 2;
done

Schöner und kürzer geht das mit watch.

df -h mittels watch beobachten
1
watch "df -h"

watch ruft jetzt alle zwei Sekunden df -h auf, löscht vorher die Konsole und gibt die Ausgabe von df -h aus. watch kann aber noch mehr, als nur einen simple Shell-Loop ersetzen:

  • Unterschiede zwischen Aufrufen farblich mittels -d bzw. --difference hervorheben.
  • Den übergebenen Befehl in einem durch -n sec bestimmten Interval ausführen.
  • Piepsen, wenn mit -b oder --beep aufgerufen, falls das Programm sich mit einem Fehlercode beendet.

watch installieren

Auf einem Mac kann watch beispielsweise über die Mac Ports durch sudo port install watch installiert werden. Baut das System auf Debian auf, sollte es durch sudo apt-get install procps oder sudo apt-get install watch installiert werden können.

Tip des Tages: SVG-Grafiken mit Google Docs online zeichen

Ich bin ein Freund von Webseiten, die den Grundgedanken des Responsive Webdesign folgen. Das heißt, daß der Inhalt einer Webseite sich optimal an das Format jedes Endgerätes anpaßt. Ein Grund, warum ich mich von Wordpress verabschiedet und Octopress zugewandt habe. Es ist also egal, ob ich dieses Blog auf meinem iPhone lese oder auf einem 24”-Bildschirm, der Inhalt wird immer optimal angepaßt auf das Endgerät dargestellt. Gut, daß hätte vor >10 Jahren auch schon mit den ganz einfachen Webseiten geklappt, nur gab es da noch keine Smart-Phones und ähnliche Engeräte, aber das ist jetzt mal ein ganz anderes Thema.

Die wohl größte Herausforderung bei der endgeräteunabhängigen Darstellung sind Grafiken. Pixelbasierte Formate wie JPEG oder PNG müssen in unterschiedlichen Größen für die unterschiedlichen Endgeräte bereitgestellt oder interpoliert werden. Das bedeutet entweder mehr Arbeitsaufwand oder Qualitätsverlust bei der Darstellung. Beides ist keine Option für mich. Passender für eine möglichst variable Darstellung ist ein vektorbasiertes Format wie SVG, daß ohne Qualitätsverlust beliebig skalierbar ist.

Die folgende Grafik wurde als SVG-Grafik erstellt und eingebunden. Bei jeder Größenänderung des Browser-Fensters paßt sie sich an und sieht sowohl auf Smartphone und Bildschirm gleich gut aus. Zugegeben, einen hohen künstlerischen oder qualitätiven Anspruch hat sie nicht.

Leider gibt es nicht viele preiswerte Programme, mit denen gut und bequem SVG-Grafiken erstellt werden können. Omnigraffle wäre eigentlich mein Favorit und wird auch in der Zukunft angeschafft werden. Wenn es aber nur um das Zeichnen von einfachen Grafiken geht, reicht auch Google Docs Drawings aus. Die Beispielgrafik in diesem Artikel und alle in den nächsten Artikeln folgenden Grafiken sind oder werden mit Google Docs Drawings erstellt worden sein.

P.S.: Eigentlich bin ich nicht so ein großer Freund von Google Docs (NSA, Google, Datenschutz, …). Für Grafiken aber, die später in einem öffentlichen Blog online gestellt werden, ist es aus meiner Sicht vollkommen in Ordnung.

Conway’s Law - How Do Committees Invent?

1967 schickte Melvin Conway einen Beitrag mit dem Titel “How Do Committees Invent?” an die renommierte Harvard Business Review. Havard Business Review lehnte den Artikel ab, da er nach Ansicht der Redaktion nicht hinreichend belegt sei. Ein Fehler, den Conway beschrieb in seinem Artikel, der später dann doch in Datamation erschien, wie wie (Software-)Systeme von der sie schaffenden Organisation abhängig sind. Das ist ein Zusammenhang, den noch immer zu wenige kennen, aber alle kennen sollten.

Tip des Tages: Zeit in Java stoppen

Manchmal müssen wir die Zeit messen, die eine bestimmter Abschnitt Code braucht. Das geht zum einen ganz einfach über den zweimaligen Aufrufen von System.currentTimeMillis() und das Ausrechnen der Differenz zwischen den beiden erhaltenen Zeitstempeln.

Wollen wir es etwas bequemer haben und soll es
vielleicht auch mehr Funktionalität sein, dann ist die von der Apache Commons Lang 3 bereitgestellte Klassen StopWatch besser geeignet.

Also flugs Apache Commons Lang in Maven einbinden und das weiter unten stehende Beispiel ausprobieren.

Maven-Dependency zu Commons Lang 3
1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.1</version>
</dependency>
Zeitmessen mit StopWatch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.commons.lang3.time.StopWatch;

public class StopTime
{
    public static void main(String[] arguments)
        throws InterruptedException
    {
        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        Thread.sleep(5000);
        stopWatch.stop();

        long ms = stopWatch.getTime();

        System.out.println("Time stopped: " + ms);
    }
}

Sublime Text 2 aus der Shell heraus aufrufen

Sublime Text 2 ist ein beliebter Texteditor für Linux, MacOS und Windows. Sublime kann unter MacOS nicht nur über den Desktop, sondern auch aus der Shell herausgestartet werden.

Dafür bringt Sublime den Befehlszeilenclient subl mit, der unter /Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl zu finden ist.

Wie dieser in die Befehlszeile integriert werden kann, ist in der Online-Dokumentation von Sublime Text 2 unter “OS X Command Line” beschrieben.

Kurzzusammenfassung

Sofern der Pfad ~/bin bereits in der PATH-Variable enthalten ist, reicht das Anlegen eines einfach Symlinks.

Link auf subl anlegen
1
2
ln -s "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl" \
  ~/bin/subl

Das ist bereits ausreichend, um mittels subl datei.txt die Datei datei.txt aus der Shell heraus zu öffnen.

Weitere nützlich Optionen für subl können in der Online-Dokumentation von Sublime Text 2” gefunden werden.

Java-Regex online testen

Auf RegexPlanet können reguläre Ausdrücke für Java und viele weitere Sprachen online getestet werden. Das ist wesentlich schneller und effektiver als ein Austesten mittels Unittests.

ElasticSearch innerhalb einer Minute installieren

Auf einem Ubuntu- und Debian-System kann ElasticSearch über ein Debian-Paket innerhalb von einer Minute, zuzüglich der Zeit, die für den Download der Pakete benötigt wird, installiert werden.

Intallationschritte für ElasticSearch
1
2
3
4
5
6
7
8
9
10
$ sudo -i
$ apt-get update
$ apt-get install openjdk-7-jre-headless -y
$ cd /tmp
$ wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.6.deb
$ dpkg -i elasticsearch-0.90.6.deb
$ service elasticsearch stop
$ echo "cluster.name = meintollercluster" >> /etc/elasticsearch/elasticsearch.yml
$ service elasticsearch start
$ reboot

Damit bleibt natürlich noch eine Menge Luft für Konfigurations- und Optimierungsmöglichkeiten, aber ElasticSearch läuft. Nur der eindeutige Name des ElasticSearch-Clusters sollte explizit gesetzt werden, um ungewollte Verbrüderungen zwischen vielleicht irgendwo anders laufenden ElasticSearch-Clustern zu vermeiden.

Warum der Reboot am Ende? Wenn ich alles richtig gemacht habe, laufen anschließend alle Service wieder. Und nur ein Reboot kann das sauber testen.