Skip to content

Java File Persistence I

Um Daten in Enterprise Systemen persistent zu machen gibt es mehrere Ansätze. Bei dauerlaufenden, missionskritischen Systemen muss man darauf achten, dass die Daten zuverlässig gespeichert und wiedergelesen werden. Herausforderungen sind hier:
  • konsistente Daten, auch nach einem Programmabsturz oder Stromausfall
  • Online Datensicherung
  • Eventuell eingebaute Replikation
  • Wiederanlauf
  • Transaktionen mit weiteren Systemen
Am einfachsten sind diese Probleme in den Griff zu bekommen, indem man sich (mittels JDBC oder generell EJB CMP) auf Datenbanken verläßt. Diese gliedern sich sauber in den Transaktionsmanager ein, bieten Datensicherungsschnittstellen, erlauben automatische Replikation (Log Shipping) im Betrieb und sorgen durch ausgeklügelte Strategien beim Filezugriff für ein Maximum an Verlässlichkeit. Jedoch gibt es bei EIS auch immer wieder Fileschnittstellen. Die sind so beliebt, weil diese für große Datenmengen geeignet sind, weil diese einfach zu implementieren und zu debuggen sind, und weil es eine Schnittstelle ist die alle Anwendungen anbieten. Filetransfer über Platformgrenzen ist auch nicht wirklich eine Herausforderung. Fileschnittstellen sind leichtgewichtig und haben so oft einen Performance Vorsprung gegenüber RDBMS. Es sind aber Schönwetterschnittstellen.

Wenn also der Einsatz von Files unbedingt notwendig ist, so muss man sich zuerst mal ansehen, welche Probleme mit Files im Allgemeinen, und bei bestimmten Betriebsystemen insbesondere auftauchen: Im Betrieb:

  • Online Backup mittels "Open File" Software zwar mögich, sicher dann aber keinen garantiert transaktionalen Stand
  • Große Anzahl von Dateien überfordern Filesysteme
  • Fragmentierung
Bei einem Crash:
  • Partielle Files
  • Datenblöcke gemischt aus alten und neuen Daten
  • File-Ende aufgefüllt mit 0-bytes
  • Keine Atomare Transaktion um File abzuschliessen und Anwendung zu benachrichtigen
In der Praxis haben sich hier ein paar Vorgehensweisen etabliert, um das Risiko zu minimieren. In dem Artikel möchte ich auf die Besonderheiten bei der Umsetzung mit Java eingehen: a) wenn es vermeidbar ist, Dateien niemals in-place modifizieren. Denn sonst muss man sich bei einem Crash Gedanken darum machen, wie man die bereits geschriebenen neuen Daten von den alten im File trennen kann. Mit Journalen und Header/Trailer auf Sektorgrenzen die Änderungsnummern enthalten ist dies zwar möglich (so machen es DBs auch), aber das setzt ein großes Hardware/Filesystem spezifisches Know How vorraus und ist so komplex, dass die Selbstimplementierung keinen Sinn macht. Hauptproblem: Festplatten garantieren in der Regel bei einem Stromausfall maximal die konsistente Speicherung eines einzelnen Sektors. Bei Schreibzugriffen die nicht Sektor-Aligned sind, oder bei Multi-Sektor Datenböcken können ganz unvorhergesehene Mischungen passieren. Ausserdem gibt es Laufwerke die sich das Recht nehmen Defekte Sektoren bei einem Stromausfall zu erzeugen, die erst bei der Wiederbeschreibung benutzbar werden. Hier empfehle ich mal einen Blick auf das Stress Tool SQLIOStress von Microsoft, das Hardware auf entsprechende Probleme testet. Der Artikel dazu enthält Details zum Thema Unordered Writes, etc: kb230785 b) Anhängen nur in Ausnahmefälle. Das Anhängen von Daten an Files ist etwas unkritischer, da die alten Daten immer noch zur Verfügung stehen. Allerdings muss man damit rechnen, dass die neuen Daten die angehängt wurden nicht vollständig sind. Zwei weitere Probleme kommen von den Filesystemen: regelmäßig anwachsende Files (z.B. Protokolle) sorgen gerne für eine starke Fragmentierung auf der Platte, da diese in kleinere Lücken "wachsen". Eine Möglichkeit ist es Protokolle mit fester Größe anzulegen und rollierend zu beschreben. Jedoch macht dies die Betrachtung etwas schwieriger und verstößt gegen den Punkt a). Beim Anhängen werden in der Regel Metadaten nicht nach jeder Schreib Operation auf die Platte geschrieben, was bei einem Filesystem Recovery dazu fuehrt, dass neue Datenblöcke gefunden werden, die genaue Dateigröße aber nicht bekannt ist. Als Effekt gibt es dann Files die mit 0-bytes bis zur nächsten Blockgröße aufgefüllt sind. Sowohl anhängen als auch überschreiben machen auch bei den typischen Übergabeschnittstellen (Übergabeverzeichnisse) Probleme. Hier sollte man sich auf keinen Fall auf das sperren der Files verlassen (zum einen unterstützen das nicht alle Betriebsysteme, und zum anderen werden die Locks freigegeben wenn die schreibende Anwendung abstützt). Besser sind hier lock/flag-files oder einfache rename Operationen. Daraus ergibt sich übrigens auch, dass Übergabeverzeichnisse mit Sammeldateien die immer wieder erweitert werden, bis die Zielanwendung das ungesperrte File abholt absolut nicht zu empfehlen sind. Meiner Erfahrung nach wird gerne vergessen dass eine Kopier-Funktion genau das gleiche Problem hat. Wird sie abgebrochen bleiben halb-fertige Files stehen. Selbst das wiederaufsetzen der Kopieraktion sollte nicht nur auf Zwischendateigröße beruhen (wegen dem 0-byte Problem oder der Tatsache dass die Quelldatei sich inzwischen geändert hat). Die Frage die sich geradezu aufdrängt ist nun, wie man Files dennoch mit halbwegs ruhigem Gewissen einsetzen kann. Ich empfehle hierzu: a) wenn Daten gespeichert werden sollen diese zuerst in eine Arbeitsdatei schreiben, dann diese abschliessen, danach erst umbenennen. Alle vernünftigen Filesysteme implementieren die Rename Operation so, dass die Zieldatei nach einem Stromausfall entweder nicht vorhanden ist, oder aber vollständig. (Die temporäre Arbeitsdatei sollte man nicht versuchen zu recovern, einfach löschen). Das saubere Beenden einer Datei-Schreib Operation erfordert übrigens mehr, als nur das Flushen der Anwendungspuffer und das Schliessen der Datei. Die meisten Betriebsysteme nehmen sich die Freiheit die Daten erst noch im Cache zu speichern und später auf Festplatte zu bannen. Von daher muss die OS Funktio fsync() verwendet werden. Im 2. Teil des Artikels gehe ich auf Java dazu ein. Ebenso sollte man sichergehen dass die temporäre Quelle und die zukünftige Zieldatei im gleichen Filesystem liegen (idealerweise iim gleichen Verzeichnis), sonst kann es passieren dass hier unbemerkt wieder umkopiert wird. (Die im Unix Lager übliche Vorgehensweise mit Hardlinks lässt sich in Java leider nicht sehr schön umsetzen). b) beim Wiedereinlesen aus Dateien, insbesondere wenn diese nicht nach obigem Verfahren erstellt wurden, sollte man Vorsicht walten lassen: Die Datei könnte nicht komplett geschrieben worden sein, sie könnte 0-byte Müll am Ende enthalten, oder gar eine unbeabsichtigte 0-Byte Größe haben. Ein Wiederaufsetzen muss mit diesen Problemen zurechtkommen. Eine Strategie ist es, diese Daten zu ignorieren. Das ist allerdings ein Risiko, insbesondere wenn die Daten nach der Methode a) geschrieben wurden (weil das Problem dann eher auf Fehlverhalten der Anwednung hinweist). Es bietet sich also an, mit einer klaren Fehlermeldung (welche Datei kann warum nicht gelesen werden, und was fuer Auswirkungen hat das) zu beenden um manuelles Recovery zu erlauben. Es ist keine Option vom Admin zu erwarten dass er in einem Verzeichnis mit tausenden Dateien den Übeltäter findet. Zur Erkennung von vollständig geschriebenen Dateien bieten sich Trailer mit Prüfsumme am Ende der Datei an. Alternativ eine bekannte Dateigröße mit zusätzlicher 0-byte Erkennung. Diese Methoden sind einfach zu implementieren und funktionieren für Files die nicht angehängt oder überschrieben wurden recht zuverlässig. Übrigens ist hier XML eine Option, dank dem schließenden, textuellen Root Tag kann eine komplett geschriebene XML Datei einfach erkannt werden. Allerdings sollte man tunlichst siherstellen dass eine korrupte Datei erkannt werden kann und nicht den Systemadministrator mit obskuren Parserfehlermeldungen ins Boxhorn jagd:
Datei abc.xml im Verzeichnis /bla konnte nicht gelesen werden.
Dieses Konsitenzproblem führt zu einem Abbruch der Verarbeitung der
Transation abc, aufgrund der XML-Parser Fehlermeldung: xxx"
DB Systeme verwenden in Trailern immer eine change number die auch im Header zu finden ist. Der Grund hierfür liegt darin, dass in DBs Blöcke überschrieben werden. Wenn Sie dies nicht tun, wie ich empfohlen habe ist das kein Problem. Wenn Sie sich aber vom überschreiben nicht abhalten lassen wollen, so genügt ein XML Ende Tag nicht mehr der Anforderung, es könnte ja noch aus der letzten Datengeneration stammen. Um Online Datensicherungen etwas einfacher zu machen hat es sich bewährt, neben der oben beschriebenen Methode des File-Renames auch noch sich an den Grundsatz zu halten, dass Dateien nicht verändert werden dürfen. D.h. jeder Dateiname sollte für einen bestimmten Inhalt stehen und nicht wieder verwendet werden. Die alleinige Existenz/nichtexistenz ist hier also das Konsistenzmerkmal. Hier bieten sich z.B. UUIDs als Dateinamen an. Der Fragmentierung von Dateisystemen kann man nicht ganz entgehen, einige Tricks sind aber möglich:
  1. Die Anwendung sollte die Möglichkeit haben Logfiles in getrennten Filesystemen zu speichern
  2. Möglicht Zeitnahe eine Datei füllen
  3. Die Maximalgröße von Dateien, insbesondere Logfiles auf z.b. 100MB begrenzen
Eine große Anzahl von Datein in Verzeichnissen sind meistens recht problematisch. Je nach Betriebsystem kann es dann noch zu Besonderheiten kommen. So ist z.B. NTFS empfindliche gegen viele Dateien mit gleichem Prefix (wenn 8.3 namen gebildet werden), einige Filesysteme die den Verzeichniszugriff mit Hash Funktionen optimieren können Probeme mit Hash Kollisionen haben. Von daher bieten sich Dateinamen wie "data" oder ".bla" weniger, an, als UUIDs an. Zur Vermeidung von zuvielen Einträgen in Verzeichnissen kann man ein "Hashing" vornehmen. D.h. man legt Unterverzeichnise an. Bei UUIDS bietet sich an nicht die ersten Stellen der UUID zu nehmen weil diese zum einen weniger oft wechseln, und zum anderen dann das typische NTFS Präfix Problem auftaucht. Im 2. Teil findet sich ein Verfahren zum grupieren von UUID Dateinamen. Dateinamen und Pfade sollte man dann auch nicht allzusehr aufblähen, das erlaubt einigen Dateisystemen deutliche Optimierungen. Fazit Meiner Erfahrung nach sind Datei basierende Verfahren deswegen sehr beliebt, weil sie auf den Fall hin optimiert sind, dass keine Probeme auftauchen. Dies ist in vielen Situationen natürlich eine gültige Annahme, bei Missionskritischen Anwendungen aber im Allgemeinen nicht akzeptabel. Der Aufwand ein System mit Dateipersistenz crashfest und betriebssicher zu machen ist enorm. Dabei werden sehr hohe Anforderungen an das Betriebsystem gestellt. Von daher sollten verantwortungsvolle Entwickler immer auch Alternativen wie DB Systeme in Betracht ziehen. Allerdings sollte man nicht vergessen: File Storage ist einfach zu benutzen, das ist auch ein Pro Argument, da unnötige Komplexität den Betrieb ebenfalls behindert. Wer kennt nicht das Probem von properitären Datenbanken die so inkonsitent geworden sind, dass nichts mehr zu retten ist (man denke nur an Outook PST Files). Dieses Problem haben Filesysteme wesentlich weniger. Die Datenbanken die eingesetzt werden müssen also einen vergleichbaren Reifegrad haben. (Das erklärt übrigens auch meine starke Skepsis gegenüber Java/Embedded Datenbanken im OO und XML Umfeld). Gerade Organiationsprogrammierer, die Unterhnehmensspezifische Anwendungen erstellen sollten deswegen um die Verwendung von einfachen Framework oder Containern bemüht sein, die einem die Arbeit abnehmen. (Aber vorsicht, so mancher Container verwendet eine sub-optimale File Implementierung, man stelle sich nur einen JMS Provider auf dieser Basis vor). Im 2. Teil geht es ans eingemachte, Java Codeschnippsel für den Coder.

Trackbacks

IT Blog on : Fragmentierung

Windows User kennen die vielen Beschwörungsformeln die es so gibt, um ein Windows Laufwerk benutzbar zu halten. Tatsächlich sind Filesystemfragmentierung neben dem Vermüllen der Registry ein großes Problem bei der Performance von Desktop Systemen. Auf

IT Blog on : Java File Persistence II

Wie schon im ersten Teil beschrieben, muss man trotz der Platform Unabhängikeit von Java etwas über die Zielsysteme wissen, um grobe Fehler zu vermeiden: /** save data as UTF-8 string to file. */ saveFile(String data, File file) throws IOException {

Comments

Display comments as Linear | Threaded

No comments

Add Comment

BBCode format allowed
Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
To leave a comment you must approve it via e-mail, which will be sent to your address after submission.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA