Die Idee für den Artikel entstand spontan aus einer Diskussion in den Kommentaren zu einem Artikel .

Das Problem liegt darin, dass die interne Spezifikation unserer Dienste die Speicherung einer enormen Anzahl kleiner Dateien umfasst. Aktuell haben wir etwa mehrere Hundert Terabyte solcher Daten. Und wir sind auf einige offensichtliche und weniger offensichtliche Stolpersteine gestoßen und haben sie erfolgreich überwunden.
Deshalb teile ich unsere Erfahrungen, vielleicht kann ihnen jemand nützlich sein.
Erstes Problem: „Kein Speicherplatz mehr auf dem Gerät“
Wie im oben genannten Artikel erwähnt, besteht das Problem darin, dass es freie Blöcke im Dateisystem gibt, aber die inodes aufgebraucht sind.
Die Zahl der verwendeten und freien inodes kann mit dem Befehl überprüft werden df -ih:

Ich werde den Artikel nicht zusammenfassen, kurz gesagt, auf der Festplatte gibt es sowohl Blöcke direkt für Daten als auch Blöcke für Metainformationen, die inodes (Indexknoten) sind. Ihre Anzahl wird bei der Initialisierung des Dateisystems festgelegt (hier geht es um ext2 und seine Nachfolger) und ändert sich danach nicht. Das Verhältnis von Datenblöcken zu inodes wird aus statistischen Daten berechnet, in unserem Fall, bei vielen kleinen Dateien, sollte sich das Verhältnis zugunsten der Anzahl der inodes verschieben – es sollten mehr von ihnen vorhanden sein.
In Linux gibt es bereits Optionen mit unterschiedlichem Verhältnis, und all diese vordefinierten Konfigurationen befinden sich in der Datei /etc/mke2fs.conf.
Daher kann beim ersten Initialisieren des Dateisystems über mke2fs das entsprechende Profil angegeben werden.
Hier sind einige Beispiele aus der Datei:
small = {
blocksize = 1024
inode_size = 128
inode_ratio = 4096
}
big = {
inode_ratio = 32768
}
largefile = {
inode_ratio = 1048576
blocksize = -1
}
Die gewünschte Nutzungsoption kann mit der Option „-T“ beim Aufruf von mke2fs ausgewählt werden. Zudem können die erforderlichen Parameter manuell festgelegt werden, wenn es keine fertige Lösung gibt.
Weitere Einzelheiten sind in den Handbüchern für mke2fs.conf und mke2fs.
Ein Punkt, der im oben genannten Artikel nicht behandelt wird, ist die Möglichkeit, die Blockgröße für Daten festzulegen. Offensichtlich macht es für große Dateien Sinn, eine größere Blockgröße zu wählen, während es für kleine Dateien sinnvoll ist, eine kleinere zu wählen.
Es ist jedoch eine interessante Eigenschaft zu beachten, wie die Architektur des Prozessors.
Ich habe mir irgendwann einmal überlegt, dass ich für große Bilddateien eine größere Blockgröße benötige. Das Ganze geschah unter häuslichen Bedingungen, auf einer Heimdateispeicherlösung von WD auf ARM-Architektur. Ohne lange nachzudenken stellte ich die Blockgröße auf 8K oder 16K anstelle der standardmäßigen 4K ein, nachdem ich die Einsparungen gemessen hatte. Und alles war wunderbar, bis damaliger der Speicher ausfiel, während die Festplatte intakt war. Steckte ich die Festplatte in einen normalen Computer mit einem gewöhnlichen Intel-Prozessor, erhielt ich die Überraschung: unsupported block size. Es war unschön. Die Daten sind vorhanden, alles gut, aber sie konnten nicht gelesen werden. Prozessoren wie i386 und ähnliche können nicht mit Blockgrößen arbeiten, die nicht der Seitenformatgröße des Speichers entsprechen, und diese beträgt genau 4K. Letztlich endete es damit, dass wir Programme aus dem User-Space verwendeten, alles war langsam und trübselig, aber wir haben die Daten gerettet. Wer interessiert ist, kann nach dem Namen des Programms googeln. fuseext2. Moral der Geschichte: Entweder man denkt alle Fälle im Voraus durch, oder man gibt sich nicht als Superheld aus und verwendet die Standardkonfigurationen für Haushaltsbenutzer.
UPD. Laut der Anmerkung eines Nutzers stelle ich klar, dass die Blockgröße für i386 4K nicht überschreiten sollte, aber sie muss nicht unbedingt genau 4K betragen, d.h. 1K und 2K sind zulässig.
Also, so haben wir die Probleme gelöst.
Zunächst sind wir auf das Problem gestoßen, als die mehrterabyte große Festplatte mit Daten überfüllt war und wir die Konfiguration des Dateisystems nicht ändern konnten.
Zweitens musste eine schnelle Lösung her.
Schließlich kamen wir zu dem Schluss, dass wir das Gleichgewicht ändern müssen, indem wir die Anzahl der Dateien reduzieren.
Um die Anzahl der Dateien zu verringern, wurde beschlossen, die Dateien in ein gemeinsames Archiv zu packen. Angesichts unserer Spezifikationen haben wir alle Dateien über einen bestimmten Zeitraum in ein Archiv gepackt und die Archivierung wurde täglich nachts mit einem Cron-Job durchgeführt.
Wir haben das Zip-Archiv ausgewählt. In den Kommentaren zum vorherigen Artikel wurde Tar vorgeschlagen, aber damit gibt es eine Schwierigkeit: Es hat kein Inhaltsverzeichnis, und die Dateien liegen hintereinander (nicht von ungefähr, dass „tar“ eine Abkürzung für „Tape Archive“ ist, ein Erbe der Bandlaufwerke), d.h. wenn man eine Datei am Ende des Archivs lesen möchte, muss man das gesamte Archiv lesen, da es keine Verschiebungen für jede Datei relativ zum Anfang des Archivs gibt. Das ist also ein langwieriger Vorgang. Bei Zip ist alles viel besser: Es hat eben dieses Inhaltsverzeichnis und die Verschiebungen der Dateien innerhalb des Archivs, und die Zugriffszeit für jede Datei hängt nicht von ihrer Position ab. Außerdem konnte in unserem Fall die Komprimierungsoption „0“ festgelegt werden, da alle Dateien bereits im Voraus in gzip komprimiert waren.
Die Clients holen die Dateien über Nginx ab, und laut der alten API wird einfach der Dateiname angegeben, zum Beispiel so:
http://www.server.com/hydra/20170416/0453/3bd24ae7-1df4-4d76-9d28-5b7fcb7fd8e5
Um die Dateien on-the-fly zu entpacken, haben wir das Modul nginx-unzip-module gefunden und eingebunden (), und zwei Upstreams konfiguriert.
Das Ergebnis war diese Konfiguration:

Die zwei Hosts in den Einstellungen sahen so aus:
server {
listen *:8081;
location / {
root /home/filestorage;
}
}server {
listen *:8082;
location ~ ^/hydra/(\d+)/(\d+)/(.*)$ {
root /home/filestorage;
file_in_unzip_archivefile "/home/filestorage/hydra/$1/$2.zip";
file_in_unzip_extract "$2/$3";
file_in_unzip;
}
}
Und die Konfiguration der Upstreams auf dem übergeordneten Nginx:
upstream storage {
server server.com:8081;
server server.com:8082;
}
So funktioniert es:
- Der Client geht zu front nginx
- Front nginx versucht, die Datei vom ersten Upstream zu liefern, d.h. direkt vom Dateisystem
- Wenn die Datei nicht vorhanden ist, versucht er es mit dem zweiten Upstream, der versucht, die Datei im Archiv zu finden
Das zweite Problem: wieder „No space left on device“
Das ist das zweite Problem, mit dem wir konfrontiert wurden, wenn im Verzeichnis viele Dateien sind.
Wir versuchen, eine Datei zu erstellen, das System beschwert sich, dass kein Platz vorhanden ist. Wir ändern den Dateinamen und versuchen es erneut.
Es funktioniert.
Es sieht ungefähr so aus:

Die Überprüfung der Inodes hat nichts ergeben – es sind viele frei.
Die Überprüfung des Platzes – das Gleiche.
Wir dachten, dass vielleicht zu viele Dateien im Verzeichnis sind und es dafür eine Begrenzung gibt, aber das ist nicht der Fall: Maximale Anzahl der Dateien pro Verzeichnis: ~1.3 × 10^20
Und eine Datei kann erstellt werden, wenn man den Namen ändert.
Fazit – das Problem liegt im Dateinamen.
Weitere Nachforschungen ergaben, dass das Problem im Hashierungsalgorithmus beim Erstellen des Verzeichnisindexes liegt. Bei einer großen Anzahl von Dateien treten Kollisionen auf, mit allen daraus resultierenden Folgen. Weiterführende Informationen findet man hier:
Diese Option kann deaktiviert werden, aber… die Dateisuche nach Namen kann unvorhersehbar lange dauern, wenn alle Dateien durchgesehen werden.
tune2fs -O "^dir_index" /dev/sdb3
Im Großen und Ganzen kann es als vorübergehende Lösung funktionieren.
Moral: Viele Dateien in einem Verzeichnis – das ist normalerweise schlecht. So sollte man es nicht machen.
In solchen Fällen werden normalerweise verschachtelte Verzeichnisse erstellt, entweder nach den ersten Buchstaben des Dateinamens oder nach anderen Parametern, zum Beispiel nach Daten, das rettet in den meisten Fällen.
Aber die Gesamtanzahl kleiner Dateien ist immer noch schlecht, selbst wenn sie in Verzeichnisse aufgeteilt werden – dann siehe das erste Problem.
Drittes Problem: Wie sieht man die Liste der Dateien, wenn es viele sind
In unserer Situation, in der wir viele Dateien haben, hatten wir irgendwie das Problem, wie man den Inhalt des Verzeichnisses anzeigen kann.
Die standardmäßige Lösung ist der Befehl ls.
Okay, schauen wir mal, was mit 4772098 Dateien passiert:
$ time ls /home/app/express.repository/offercache/ >/dev/null
real 0m30.203s
user 0m28.327s
sys 0m1.876s
30 Sekunden… das ist zu viel. Dabei wird die meiste Zeit mit der Verarbeitung der Dateien im Benutzerspeicher verbracht und nicht mit der Arbeit des Kernels.
Aber es gibt eine Lösung:
$ time find /home/app/express.repository/offercache/ >/dev/null
real 0m3.714s
user 0m1.998s
sys 0m1.717s
3 Sekunden. Zehnmal schneller.
Hurra!
UPD.
Eine noch schnellere Lösung vom Benutzer – Deaktivierung der Sortierung bei ls
time ls -U /home/app/express.repository/offercache/ >/dev/null
real 0m2.985s
user 0m1.377s
sys 0m1.608s
Viertes Problem: hoher LA beim Arbeiten mit Dateien
Es kommt gelegentlich vor, dass man eine Menge Dateien von einer Maschine auf eine andere kopieren muss. Dabei steigt oft der LA enorm, da alles von der Leistung der Festplatten abhängt.
Am sinnvollsten wäre es, SSDs zu verwenden. Echt cool. Die Frage ist nur der Preis für mehrere Terabyte SSDs.
Aber wenn die Festplatten gewöhnlich sind, müssen Dateien kopiert werden, und dies ist auch noch ein Produktionssystem, wo Überlastungen zu unzufriedenen Kundenreaktionen führen? Es gibt mindestens zwei nützliche Werkzeuge: nice und ionice.
nice – verringert die Priorität des Prozesses, sodass der Scheduler mehr Zeitschlitze an anderen, höherpriorisierten Prozessen vergibt.
In unserer Praxis hat es geholfen, den nice-Wert auf maximal zu setzen (19 – das ist die minimale Priorität, -20 (minus 20) – die maximale).
ionice – korrigiert entsprechend die Eingabe-/Ausgabepriorität (I/O Scheduling)
Wenn Sie RAID verwenden und es plötzlich synchronisiert werden muss (nach einem fehlgeschlagenen Neustart oder wenn das RAID-Array nach dem Austausch einer Festplatte wiederhergestellt werden muss), macht es in bestimmten Situationen Sinn, die Synchronisationsgeschwindigkeit zu reduzieren, damit die anderen Prozesse mehr oder weniger angemessen arbeiten können. Dabei hilft der folgende Befehl:
echo 1000 > /proc/sys/dev/raid/speed_limit_max
Problem fünf: Wie synchronisiert man Dateien in Echtzeit
Wir haben all die gleichen riesigen Mengen an Dateien, die auf einen zweiten Server gesichert werden müssen, um… Dateien werden ständig geschrieben, daher muss man sie möglichst schnell kopieren, um mindestens Verluste zu vermeiden.
Standardlösung: Rsync über SSH.
Es ist eine gute Option, es sei denn, es muss alle paar Sekunden erfolgen. Und es gibt viele Dateien. Selbst wenn sie nicht kopiert werden, muss man irgendwie verstehen, was sich geändert hat, und das Vergleichen von mehreren Millionen Dateien ist zeitaufwändig und belastet die Festplatten.
Das heißt, wir müssen sofort wissen, was kopiert werden muss, ohne jedes Mal einen Vergleich zu starten.
Die Rettung ist — lsyncd. Lsyncd — . Es funktioniert ebenfalls über rsync, überwacht aber zusätzlich das Dateisystem auf Änderungen mithilfe von inotify und fsevents und startet die Kopie nur für die Dateien, die hinzugekommen oder verändert wurden.
Problem sechs: Wie versteht man, wer die Festplatten belastet
Das wissen wahrscheinlich alle, trotzdem zur Vollständigkeit: Für die Überwachung des Datenträger-Subsystems gibt es den Befehl iotop — ähnlich wie top, aber er zeigt die Prozesse an, die die Festplatten am aktivsten nutzen.

Übrigens, das gute alte top hilft auch zu verstehen, ob es Probleme mit den Festplatten gibt oder nicht. Dafür gibt es zwei am besten geeignete Parameter: Load Average und IOwait.

Der erste zeigt, wie viele Prozesse in der Warteschlange auf Bearbeitung warten; üblicherweise ist alles über 2 schon ein Zeichen dafür, dass etwas nicht stimmt. Bei aktivem Kopieren auf Backup-Server erlauben wir bis zu 6-8, danach wird die Situation als abnormal angesehen.
Der zweite zeigt, wie beschäftigt die CPU mit Festplattenoperationen ist. IOwait >10% ist ein Grund zur Besorgnis, obwohl wir auf Servern mit spezifischem Belastungsprofil stabil 40-50% haben, und das ist tatsächlich normal.
Damit schließe ich, obwohl es sicherlich viele Aspekte gibt, mit denen wir nicht konfrontiert wurden; ich freue mich auf Kommentare und interessante praktische Fälle.
Quelle: habr.com
