MongoDB GridFS oder selbst machen

Info

Datum: 27. 11. 2017 um 22:19:09

Schlagworte:

Kategorie: MongoDB-POJO Mapper morphium

erstellt von Stephan Bösebeck

logged in

ADMIN


MongoDB GridFS oder selbst machen

Hinweis: dieser Post ist von 2017, ich habe nur einige aktuelle Zahlen zur Effizienz hinzugefügt. Mongodb 3.0 war zum Zeitpunkt des Post noch nicht erschienen.

You don't own it till you make it

Das ist ja der bekannte Do It Yourself - Spruch. Die Frage ist, gilt das auch in diesem Fall: Das Ablegen einer riesigen Anzahl binärere Daten / Dateien.

Grundsätzliches

Normalerweise würde man diese Binären Daten sicher nicht in einer Datenbank ablegen, dafür sind Filesystem im Normalfall wesentlich effizienter. Komplizier wird die Sache, wenn man die Dateien auch wieder finden will... denn dann ist man sehr schnell dabei, sehr viele verschiedene Verzeichnisstrukutren für die selben Dateien zu erzeugen. Und damit das nicht in Speicherplatzverschwendung ausartet, benutzt man links...

Das allein ist schon mal ein grund, die DAten struturierter abzulegen. Also, speichern wir diese Metadaten in eine Datenbank und verweisen (via Pfadangabe) auf das Dateisystem.

Das ist auch der vorzuziehende Weg, wenn es nicht um eine zu große Anzahl an Dateien geht. Denn dann stößt man auch schnell an die Grenzen dessen. Wer schon mal versucht hat, eine Datei in einem Verzeichnis mit einer Million Dateien drin zu finden, weiß wovon ich rede.

Das Dateisystem kommt damit im normalfall klar, die Tools haben so ihre Probleme damit. Das ist etwas unpraktisch aber ok.

Aber irgendwann ist es nicht mehr sinnvoll: die iNode-Dichte müsste raufgesetzt werden, um genügend dateien anlegen zu können. Damit die Filesystem nicht zu groß werden (was sie wiederum langsamer machen würde), splittet man das evtl. auf mehrere Datenträger (virtuell) auf... Und dann hat man das problem mit der Skalierbarkeit...

Alles in Allem nicht so doll

Ab in die Mongo, aber wie?

GridFS

Heureka, es gibt was, das ein "standard" ist. Naja... das mag sein, der ist nur leider irgendwie... Mist.

warum nicht?

Der Verschnitt ist hier beträchtlich. Ich kann da ein ganz konkretes Beispiel nennen, bei dem einfache Bilddaten abgelegt wurde. Mehrere Millionen an der Zahl. Gerade bei MongoDB-Versionen vor 3.0 ist die BlockGröße von GridFS 255KB - auch der letzte Block! Das bedeutet, wenn die Datei 257 KB groß ist, hat man 2 Blöcke zu belegen. Aber in dem letzen block sind nur 1KB Nutzdaten... nicht sehr effizient.

Angeblich ist das ab mongodb 3.0 besser. Aber nach unseren Tests kann ich das nicht zu 100% bestätigen. Es scheint so zu sein, dass - insbesondere wenn häufig gelesen und geschrieben wird, die binärdaten sich auch mal ändern - der Verschnitt auch recht groß ausfällt. Eigentlich konnten wir zw. der 3.2 und der 2.4 keinen signifikanten Unterschied im Verschnitt feststellen. Und ja, es waren gesonderte Datenbanken, die gleichzeitig laufen.

Der Verschnitt lag in beiden Fällen bei gut und gerne mal 80-100%! Das ist natürlich stark abhängig von der Art der daten, die abgelegt werden.

Warum kleine Blockgrößen

Die kleineren Blockgrößen haben natürlich auch ihre Vorteile. Insbesondere, wenn man innerhalb der binärdaten hin- under herspringen will, ist das natürlich sinnvoll. Auch wenn man die Daten Streamen will, und damit weniger Daten auf ein Mal im Speicher halten muss. Das ist für unsere Zwecke aber nicht hilfreich.

GridFS selbst machen

Was spricht dagegen, die Blockgröße stark zu vergrößern, wenn man die Dateien eh nur "im Ganzen" speichern und finden will.

Im Endeffekt erzeugt man 2 Dokumente:

  1. Das Dokument, welches die File-Metadaten beinhaltet, also im Normalfall so was wie Dateiname, Berechtigungen oder was man eben sonst noch gespeichert haben möchte. Insbesondere eine Liste von IDs auf Datenblöcke
  2. Die Datenblöcke. Jeder Datenblock hat nur 3 Felder: Die Binärdaten, eine ID und einen Hashwert. Mit diesem Hash kann man eine Deduplizierung auf Block-Ebene realisieren!
  3. Auch in den File-Metadaten haben wir einen Hash abgelegt, um gleiche Dateien zu erkennen. Sehr häufig wird die selbe Datei unter verschiedenen Namen / Metadaten gefunden. Dadurch kann beim ablegen der Datei die Gleichheit erkannt werden (vergleiche HardLink im Unix Filesystem). Deduplizierung auf Dateiebene.

Die Maximalgröße für ein Dokument liegt in der MongoDB bei 64MB, für ein einzelnes Feld bei 16MB. Deswegen haben wir die Blockgröße auf 15MB festgelegt.

Da wir auch nicht-kompimierte Datenfiles ablegen, werden die Binärdaten noch gezippt abgelegt.

Mit all diesen Features, Deduplizierung auf Datei und Blockebene, Daten gezippt ablegben etc., konnten wir den benötigten Speicherplatz drastisch reduzieren. Aktuell mit ~50 Millionen Dateien, welche in Summe ca. 11 TB Daten belegen müssten, belegen wir aktuell gerade mal 8TB Platz auf dem Storage.

@Entity
@CreationTime
public class FileMetaData{
    @Id
    private MorphiumId id;
    private String filename;
    private String mimeType;
    private long size;
    private String description;
    @CreationTime
    private long createdAt;
    @LastChange
    private long lastChange;
    private List<MorphiumId> dataBlocks;
    @Index
    private String fileChecksum;
    //Checksum needs to be updated before storing
    //usually something like
    morphium.createQueryFor(FileMetaData.class).f("file_checksum").eq(checksum)

    //if that is null => new file. If not, just update metadata
    //we use SHA as checksum algorithm

    //add getters and setters here
}




@Entity
@Lifecycle
public class DataBlock{
    private MorphiumId id;
    private byte[] data;

    @Index(options = {"sparse:true"})
    private String checkSumHex;

    @PreStore
    public void calcChecksum() {
        if (data == null || data.length == 0) {
            checkSumHex = "";
            return;
        }
        try {
            SHA3 sha = new SHA3();
            sha.engineUpdate(data, 0, data.length);
            checkSumHex = bytesToHex(sha.engineDigest());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Creating checksum failed: ", e);
        }
    }

    //Before storing a new block, the caller should look for
    //existing blocks with the same checksum for deduplication

    //getters and setters
}