Input/Output FileChannel writes / werden diese gecached vom OS und dann erst geschrieben?

miasma

Aktives Mitglied
Hallo,

mir stellt sich die Frage, ob es ausreicht FileChannel.write(ByteBuffer) mit mehreren "kleinen" ByteBuffern aufzurufen, bspw. wenn sagen wir mal 500* FileChannel::write aufgerufen wird mit zwischen 500 und 55_000 Bytes im ByteBuffer und am Ende FileChannel::force aufzurufen. Wird das von den gängigen Betriebssystemen gecached und dann erst geschrieben oder sollte ich eher schauen, dass ich einen großen ByteBuffer akkumuliere und dann erst in den FileChannel schreibe? Letzteres ist irgendwie grade etwas tricky in meinem Fall, aber würde es hoffentlich schon hinbekommen. Es geht hier auch Code, der richtig performant sein sollte...

Beste Grüße
Johannes
 
Ich bin auf dem Gebiet kein Experte, aber ich will mal meine Gedanken dazu weitergeben. Aber da kann ich durchaus auch falsch mit liegen.

Erst einmal ist hier das Betriebssystem und die Einstellungen wichtig. Die gängigen Betriebssysteme haben alle einen Cache gegenüber den Filesystemen. Bei Schreibvorgängen ist aber immer die Frage, ob dies gewollt ist: Bei kritischen Daten möchte die Applikation, dass Daten auch wirklich geschrieben sind, wenn z.B. das flush() erfolgreich war. Ein "write-back" Cache kann böse sein: Die zB. Transaktionsdaten einer geschlossenen Transaktion sind noch im Speicher und nicht auf der Platte und dann kommt der Stromausfall. Dann sind die Daten evtl. inkonsistent.

Daher wäre hier wirklich auch bei jedem System die Konfiguration des Caches wichtig. Bei Microsoft ist dies aber in der Regel aktiviert. Über die Policy "Enable write caching on disk" kann dies aber z.B. gesetzt werden.
Bei Linux kann dies z.B. mit hdparm gesetzt werden.

Unabhängig davon ist tatsächlich die Frage, ob es etwas verändert, wenn man unterschiedliche Blockgrößen schreibt (in Java). Daher wäre hier meine Herangehensweise, dass ich dies einfach testen würde: Also eine Applikation erstellen, die dann entsprechend Daten in diversen Größen sendet. Dann kann man ausprobieren, wie es sich verhält bei unterschiedlichen Größen.
Hier kann es aber auch durchaus zu Unterschieden kommen je nach Betriebssystem, Konfiguration, verwendeter Java Runtime. Die Tests wären also entsprechend groß. Aber man würde dann Erfahrungen sammeln und könnte dann Ratschläge geben bezüglich Software-Auswahl als auch Konfiguration (So man relevante Unterschiede findet).
Hier sehe ich auch zusätzlich das Risiko von Software Updates. Die Erkenntnisse können mit jedem Update hinfällig sein.

Aber hier ist auch die Frage nach dem Aufwand: Ist das heutzutage gerechtfertigt? Spielt das eine Rolle zu Zeiten von schnellen SSDs ... Die Zugriffszeiten und auch Übertragungsraten sind ja extrem bei modernen NVMe und da sind die Kosten ja nicht mal mehr so hoch. Daher kann es durchaus sein, dass da diese Aufwände evtl. gar nicht getrieben werden sondern einfach entsprechende Hardware verwendet wird.
 
Auf meinem Notebook scheint es sogar schneller zu sein, FileChannel::write direkt aufzurufen:


Was ich momentan per YourKit direkt sehe ist, dass der Shenandoah GC m.E. zu lange braucht, aber zumindest noch eine schöne Zickzack-"Kurve", so dass kein memory leak vorhanden sein sollte.

Ich hab mal die neue Klasse des `FileChannelWriter`s mit angehängt. Eventuell fällt euch direkt was auf -- bin eigentlich der Meinung, dass Shenandoah bei einer 3,8 Gb JSON-Testdatei als Import lediglich 10ms peak gebraucht hat.

Was jetzt auch nicht so ideal ist, dass vermutlich 0-bytes geschrieben werden, wenn ich die Schreib-Position von Chronicle Bytes (Bytes<ByteBuffer>) erhöhe (und zwischendrin Löcher lasse). Bisher hatte ich immer word-aligned bzw. so dass eine Datenbank-Seite bei muliplen von 64 byte anfängt und die RevisionRootPages nochmal auf 256 byte Granularität. Jetzt steigt die file-size dahingehend auch bei meinem kleinen Test von 2,4 auf knapp 3 Gb, was auch nicht so toll ist.

Aber irgendwie finde ich die Architektur mit dem Bytes akkumulieren eigentlich ganz gut, naja. Mittlerweile überprüfe ich aber doch lieber alles mit Profiler und wenn ich das nicht schneller bekomme muss ichs lokal wohl reverten oder mal in einen eigenen branch committen.

Eventuell lohnt es sich auch das mal auf einer Oracle/JDK Mailingliste zu fragen!?
 

Anhänge

  • Screenshot from 2022-07-22 21-42-01.png
    Screenshot from 2022-07-22 21-42-01.png
    92,9 KB · Aufrufe: 1
  • FileChannelWriter.java
    FileChannelWriter.java
    12 KB · Aufrufe: 0
Ok, scheint als ob ein FileChannel::force nach den 16 Mb, wenn ich in den FileChannel direkt davor schreibe etwas bessere Performance zu haben. Ich glaube ungefähr wie im Repo ohne das akkumulieren oder sogar minimal schneller. Die GC-Pause Zeiten sind allerdings immernoch um die 200ms oft, was recht komisch ist mit Shenandoah.
 
Ich hab mal einen Screenshot von den "Garbage Objects" angehängt.

IndexLogKey wird hier halt als Key einer Map verwendet um den korrekten Subbaum usw. zu traversieren.

Das kommt aus folgender Methode: https://github.com/sirixdb/sirix/bl...g/sirix/access/trx/page/NodePageTrx.java#L499 Die Funktion scheint hier recht viel Garbage zu erzeugen, kann ich aber ja leider auch nicht static machen. Habt ihr irgendwelche Tipps?

Ansonsten sind möglicherweise noch die 2 Sets und das Array in

Da hilfts aber auch nicht, die in einer clear-Methode zu clearen bzw. die Array-indexes auf null zu setzen.

Ein weiteres Problem dürfte diese Methode sein: https://github.com/sirixdb/sirix/bl...cess/trx/node/json/JsonNodeTrxImpl.java#L1314

Scheinbar erzeugt supplyLocked garbage oder ist das was anderes?

Code:
protected <T> T supplyLocked(final Supplier<T> supplier) {
  if (lock != null) {
    lock.lock();
  }
  try {
    return supplier.get();
  } finally {
    if (lock != null) {
      lock.unlock();
    }
  }
}
 

Anhänge

  • Screenshot from 2022-07-23 16-50-41.png
    Screenshot from 2022-07-23 16-50-41.png
    228,3 KB · Aufrufe: 2
Zuletzt bearbeitet:
Also supplyLocked könnte ich wieder rausnehmen und halt jeder Methode wieder das eventuelle Lock aquiren an den Anfang stellen und freigeben an den Schluss, wäre aber recht hässlich irgendwie. Mir gehts jetzt aber wirklich um gute Performance in erster Linie, da das ja ein data store ist und m.E. beim Einlesen eines JSON files schon an MongoDB mindestens rankommen sollte, auch wenn GSON wahrscheinlich als JSON parser recht langsam ist. Aber ansonsten auch single writer und append-only usw. und halt in-process statt über Netzwerk, wenn man die REST-API nicht nutzt, also wie SQLite oder DuckDB.

Somit bin ich für jeden Performance-Tipp mehr als dankbar 🙂
 

Zurück
Oben