Hallo,
Post by Florian WeimerDu benötigtst eine Art LineNumberReader, der die aktuelle
Dateiposition in Bytes ausgegeben kann, und ein paar Heuristiken zum
Erkennen einer ersetzten Logdatei. Extrem effizient wird das nie, aber
das quadratische Laufzeitverhalten ist wenigstens weg.
das geht hocheffizient. Wir machen das seit Jahren und das nicht nur in
Java, sondern auch etlichen anderen Sprachen, sogar mit rollierenden
Logfiles. Der Parser ist komplett O(n), praktisch unabhängig von der
Updaterate. Und irgendeine Heuristik kommt da auch nicht zum Einsatz,
das Ding funktioniert sogar 100% sicher, wenn einer meinte eine 1MB
Binärdatei in den Logtext schreibt.
Die Kunst beginnt allerdings schon beim Schreiben der Logs:
- Man sollte sinnvollerweise einzelne Log-Einträge atomar schreiben. Das
gilt vor allem dann, wenn mehrere Java-VMs in dieselben Files schreiben
sollen. Im java.nio gibt es dafür geeignete Sperrmechanismen, die auch
noch nicht geschriebene Bereiche sperren können.
Dummerweise gibt es kein passendes Semaphor dazu in Java, was auf die
Entsperrung warten würde, weshalb man auf ein Spin-Lock mit Sleeps
dazwischen ausweichen muss. Die meisten Betriebssysteme oder Java-VMs
warten aber von sich aus einige Millisekunden, bevor sie einen Fehler
melden. Das kommt also in der Praxis fast nie vor.
Das Schreiben sollte tunlichst in einem write-Aufruf erfolgen, also
vorher schon den Log-Eintrag vorbereiten, um ein unnötig langes Halten
der Sperre zu vermeiden. Zudem sollte die Sperre exceptionsicher
freigegeben werden (finally).
- Dann sollte das CSV-Format Erdbebensicher sein. Also alle geeigneten
Escapes Nutzen. Texte in Gänsefüßchen, Gänsefüßchen in Texten
verdoppeln. Das funktioniert sogar, wenn Zeilensprünge im Text stehen,
da selbige zwischen den Gänsefüßchen eben /keine/ Satztrenner sind.
- Falls man mit größenbegrenzten Logs arbeiten möchte (immer dringend zu
Empfehlen), sollte man NIEMALS das Verfahren des Umbenennens wählen (wie
Linux syslogd, log4j). Dies würde eine vollständige Synchronisation
aller schreibenden und lesenden Prozessese voraussetzen. Stattdessen
verpasst man den Logfiles /streng monoton aufsteigende/ Nummern. Das
kann einfach ein sortierbarer Zeitstempel sein. Also z.B.
MyLog_2011-07-17_23-12-25.
- Wenn man das genommen hat, kann man einen Parser schreiben, der sich
einfach immer die Byteposition hinter dem zuletzt erfolgreich gelesenen
Ereignis merkt. Das ist sein Wiederaufsetzpunkt. Den Parser kann man so
schreiben, dass er entweder ebenfalls vom Sperrmechanismus der
schreibenden Prozesse Gebrauch macht oder einfach schaut, ob die
CSV-Zeile vollständig ist. Wenn nein, bleibt es beim letzten
Wiederaufsetzpunkt und der halbe Logeintrag muss halt beim nächsten mal
erneut gelesen werden. Ich empfehle letzteres, weil dabei ein
fehlerhaftes Programm, das die Logs liest, nicht den produktiven Prozess
beeinträchtigen kann (Halten der Sperre).
- Wenn man, wie oben beschrieben, mit größenbegrenzten Logs arbeitet,
muss man sich zusätzlich den Dateinamen des Wiederaufsetzpunktes merken.
Noch nicht gelesene Log-Einträge liegen dann notwendigerweise in dem
genannten Dateinamen hinter der gespeicherten Position oder in
lexikalisch größeren Dateinamen ab Position 0. Die Existenz eines
größeren Dateinamens nach dem Erreichen des Endes der aktuellen Datei
ist ein hinreichendes Kriterium dafür, dass die letzte Datei nicht mehr
angefasst werden muss. Man muss sich also immer nur eine Datei und
Position merken.
Ein Pferdefuß bleibt: Das Auffinden aller Dateien mit passendem Namen
ist bei allen mir bekannten Dateisystemen außer HPFS ein vollständiger
Scan des gesamten Verzeichnisinhalts. Kein Hexenwerk wenn man nicht mehr
als 30 Dateien hält. Aber das Ablegen von tausenden von Logs, auch mit
komplett anderen Namen, in einem Verzeichnis ist natürlich ein absolutes
no-go. Man sollte also tunlichst für jeden Satz Logfiles ein eigenes
Verzeichnis nehmen.
- Um Ereignisgesteuert auf neue Log-Einträge zu reagieren, könnte man
sich zwar an den Modifikationszeitstempel des Dateisystems hängen, aber
das hat verschiedene Nachteile. Zum einen aktualisieren manche
Dateisysteme diesen nur, wenn eine schreibende Anwendung die Datei
schließt. Das könnte theoretisch überhaupt nicht passieren, wenn die
schreibende Anwendung ein Serverdienst ist, und die Datei immer offen
hält. Zum anderen hat dieser Zeitstempel nur eine endliche Genauigkeit
und ist zwar monoton, aber nicht streng monoton. Sprich ein in einer
Sekunde geschriebenes /und/ bereits gelesenes Ereignis könnte in
derselben Sekunde um ein weiteres und letztes Ereignis ergänzt werden,
ohne das dies jemals am Zeitstempel auffallen würde.
Es bleibt also faktisch nur Polling, also regelmäßig nachgucken, ob es
etwas neues gibt. Das wiederum ist aber ob der oben genannten Position
kaum mehr aufwändig als das Lesen des Zeitstempels, denn man öffnet die
Datei, macht ein Seek zur Position und holt sich die End-of-File
Bedingung ab (oder eben auch nicht). Das alles fackelt jeder Disk-Cache ab.
Das Verwenden der Dateigröße aus dem Verzeichniseintrag ich aus
denselben Gründen problematisch, wie beim Zeitstempel. Nicht alle
Dateisysteme aktualisieren den Verzeichniseintrag bevor die Datei
geschlossen wird. Es kann also durchaus vorkommen, dass man mehr Bytes
aus einer Datei lesen kann, als der Verzeichniseintrag suggeriert. Wenn
das nicht stört kann man das aber auch dazu verwenden, um mit nahezu
niemals halb geschriebene Logeinträge präsentiert zu bekommen, denn
keine mir bekannte Plattform aktualisiert die Länge mehrmals während
eines Systemaufrufs zum Schreiben.
Marcel