Die Idee zu diesem Artikel entstand spontan aus einer Diskussion in den Kommentaren zu einem vorherigen Artikel .

Das Besondere an der internen Funktionsweise unserer Dienste ist die Speicherung einer enormen Anzahl kleiner Dateien. Aktuell haben wir etwa mehrere hundert Terabyte solcher Daten. Dabei sind wir auf einige offensichtliche sowie weniger offensichtliche Probleme gestoßen und haben erfolgreich deren Herausforderungen gemeistert.
Daher teile ich unsere Erfahrungen, die vielleicht für andere nützlich sein könnten.
Problem eins: „Kein Speicherplatz mehr auf dem Gerät“
Wie in dem oben genannten Artikel erwähnt, besteht das Problem darin, dass zwar freie Blöcke im Dateisystem vorhanden sind, die Inodes jedoch erschöpft sind.
Die Anzahl der verwendeten und freien Inodes kann mit dem Befehl überprüft werden df -ih:

Ich werde den Artikel nicht erneut wiederholen, aber kurz gesagt gibt es auf der Festplatte sowohl Blöcke für die Daten selbst als auch Blöcke für die Metainformationen, die als inodes (Indexknoten) bekannt sind. Ihre Anzahl wird bei der Initialisierung des Dateisystems festgelegt (wir sprechen hier von ext2 und seinen Nachfolgern) und ändert sich danach nicht. Das Gleichgewicht zwischen Datenblöcken und inodes wird aus durchschnittlichen Daten berechnet; in unserem Fall, wo es viele kleine Dateien gibt, sollte das Gleichgewicht zugunsten der Anzahl der inodes verschoben werden – es sollten mehr inodes vorhanden sein.
In Linux wurden bereits Optionen mit unterschiedlichem Gleichgewicht vorgesehen, und all diese vorab berechneten Konfigurationen befinden sich in der Datei /etc/mke2fs.conf.
Daher kann beim anfänglichen Initialisieren des Dateisystems mit mke2fs das gewünschte 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 Nutzung kann durch die Option „-T“ beim Aufruf von mke2fs ausgewählt werden. Außerdem können die benötigten Parameter manuell festgelegt werden, falls keine fertige Lösung vorhanden ist.
Weitere Einzelheiten sind in den Handbüchern für mke2fs.conf und mke2fs.
Eine in dem oben genannten Artikel nicht behandelte Besonderheit ist die Möglichkeit, die Blockgröße festzulegen. Es liegt auf der Hand, dass für große Dateien eine größere Blockgröße sinnvoll ist, während für kleinere Dateien eine kleinere Blockgröße vorteilhaft ist.
Es ist jedoch wichtig, eine interessante Eigenschaft zu berücksichtigen: die Architektur des Prozessors.
Ich habe irgendwann einmal darüber nachgedacht, dass ich für große Fotodateien eine größere Blockgröße benötige. Das Ganze fand unter Heimbedingungen auf meiner WD-Festplatte mit ARM-Architektur statt. Nach kurzer Überlegung stellte ich die Blockgröße auf entweder 8k oder 16k statt des Standardwerts von 4k ein, nachdem ich die Einsparungen gemessen hatte. Alles lief großartig, bis die Festplatte ausfiel, während die Daten noch intakt waren. Als ich die Festplatte in einen normalen Computer mit einem klassischen Intel-Prozessor einbaute, erhielt ich die Überraschung: Nicht unterstützte Blockgröße. Tja. Die Daten waren da, alles gut, aber ich konnte sie nicht lesen. Prozessoren wie i386 und ähnliche Modelle können mit Blockgrößen nicht umgehen, die nicht mit der Seitengröße des Speichers übereinstimmen, und diese beträgt genau 4k. Im Großen und Ganzen endete es damit, dass ich Tools aus dem Benutzerraum verwendete; alles war langsam und traurig, aber ich konnte die Daten retten. Wer interessiert ist, kann nach dem Namen des Tools googeln. fuseext2. Moral: Entweder alle Fälle im Voraus durchdenken oder sich nicht als Superheld zu fühlen und die Standardoptionen für den Hausgebrauch nutzen.
UPD. Auf den Hinweis des Benutzers Ich möchte klarstellen, dass die Blockgröße für i386 4 k nicht überschreiten darf, aber sie muss nicht unbedingt genau 4 k betragen; d.h., 1 k und 2 k sind zulässig.
So haben wir die Probleme gelöst.
Zunächst standen wir vor dem Problem, dass die mehrterabyte große Festplatte mit Daten voll war und wir die Konfiguration des Dateisystems nicht ändern konnten.
Zweitens war eine schnelle Lösung erforderlich.
Am Ende kamen wir zu dem Schluss, dass wir das Verhältnis verändern müssen, indem wir die Anzahl der Dateien reduzieren.
Um die Anzahl der Dateien zu verringern, haben wir beschlossen, die Dateien in einem gemeinsamen Archiv abzulegen. Angesichts unserer Besonderheiten haben wir alle Dateien über einen bestimmten Zeitraum in ein Archiv gepackt und die Archivierung mit einem Cron-Job täglich nachts durchgeführt.
Sie haben ein 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 sequenziell darin (nicht umsonst ist „tar“ eine Abkürzung für „Tape Archive“, ein Erbe von Bandlaufwerken). Das bedeutet, wenn Sie eine Datei am Ende des Archivs lesen müssen, müssen Sie das gesamte Archiv durchlesen, da es keine Offset-Informationen für jede Datei relativ zum Anfang des Archivs gibt. Das ist eine langwierige Operation. Bei zip sieht es viel besser aus: Es hat das genannte Inhaltsverzeichnis und Offsets für die Dateien innerhalb des Archivs, und die Zugriffszeit auf jede Datei ist nicht von ihrem Standort abhängig. In unserem Fall konnten wir auch die Komprimierungsoption „0“ festlegen, da alle Dateien bereits im gzip-Format komprimiert waren.
Die Kunden greifen über nginx auf die Dateien zu, und laut dem 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 folgende Konfiguration:

Die beiden 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 zum Front-Nginx
- Der Front-Nginx versucht, eine Datei vom ersten Upstream, d.h. direkt vom Dateisystem, bereitzustellen.
- Wenn die Datei nicht vorhanden ist, versucht er, sie vom zweiten Upstream bereitzustellen, der die Datei innerhalb des Archivs sucht.
Zweites Problem: wieder "No space left on device"
Dies ist das zweite Problem, mit dem wir konfrontiert wurden, als im Verzeichnis viele Dateien waren.
Wir versuchen, eine Datei zu erstellen, das System beschwert sich, dass kein Platz vorhanden ist. Wir ändern den Dateinamen und versuchen es erneut.
Es klappt.
Sieht ungefähr so aus:

Die Überprüfung der Inodes hat nichts ergeben – es gibt viele freie.
Die Überprüfung des Platzes – das gleiche.
Wir dachten, vielleicht gibt es zu viele Dateien im Verzeichnis, und es gibt dafür eine Begrenzung, aber nein: Maximale Anzahl von Dateien pro Verzeichnis: ~1,3 × 10^20
Eine Datei kann auch erstellt werden, wenn man den Namen ändert.
Fazit – das Problem liegt im Dateinamen.
Weitere Nachforschungen ergaben, dass das Problem im Hash-Algorithmus beim Erstellen des Katalogindex liegt. Bei einer großen Anzahl von Dateien treten Kollisionen auf, mit allen damit verbundenen Folgen. Mehr dazu können Sie hier lesen:
Diese Option kann deaktiviert werden, aber… die Suche nach einer Datei per Namen könnte unvorhersehbar lange dauern, wenn alle Dateien durchgesehen werden müssen.
tune2fs -O "^dir_index" /dev/sdb3
Insgesamt könnte das als vorübergehende Lösung funktionieren.
Lehre: Viele Dateien in einem Verzeichnis sind in der Regel schlecht. So sollte man es nicht machen.
In solchen Fällen erstellt man normalerweise Unterverzeichnisse, basierend auf den ersten Buchstaben des Dateinamens oder anderen Parametern, wie zum Beispiel nach Daten. Das rettet in den meisten Fällen.
Aber die Gesamtzahl der kleinen Dateien ist trotzdem schlecht, selbst wenn sie in Verzeichnissen aufgeteilt sind – dann siehe erstes Problem.
Drittes Problem: Wie man die Liste der Dateien anzeigt, wenn es viele sind.
In unserer Situation, in der wir viele Dateien haben, sind wir auf das Problem gestoßen, wie man den Inhalt des Verzeichnisses anzeigen kann.
Die Standardlösung ist der Befehl ls.
Ok, sehen wir, was mit 4.772.098 Dateien herauskommt:
$ time ls /home/app/express.repository/offercache/ >/dev/null
real 0m30.203s
user 0m28.327s
sys 0m1.876s
30 Sekunden… das ist zu viel. Die meiste Zeit geht für die Verarbeitung der Dateien im User-Space drauf und nicht für die Kernarbeit.
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 Nutzer — Sortierung bei ls
time ls -U /home/app/express.repository/offercache/ >/dev/null
real 0m2.985s
user 0m1.377s
sys 0m1.608s
Problem Nummer vier: hohe LA bei der Arbeit mit Dateien
Gelegentlich tritt die Situation auf, in der man eine Menge Dateien von einer Maschine auf eine andere kopieren muss. Dabei kann der LA oft erheblich steigen, da alles von der Performance der Festplatten abhängt.
Das Vernünftigste, was man tun kann, ist — SSDs zu verwenden. Wirklich großartig. Die einzige Frage ist der Preis für Multi-Terabyte-SSDs.
Aber wenn es sich um normale Festplatten handelt, die Dateien kopiert werden müssen und es sich um ein Produktionssystem handelt, bei dem eine Überlastung zu unzufriedenen Kunden führt? Es gibt mindestens zwei nützliche Werkzeuge: nice und ionice.
nice — reduziert die Priorität des Prozesses, wodurch der Scheduler mehr Zeitquantante an anderen, prioritären Prozessen zuteilt.
In unserer Praxis hat es geholfen, den maximalen Wert (19 ist die niedrigste Priorität, -20 (minus 20) ist der höchste) festzulegen.
ionice — entsprechend wird die Eingabe-/Ausgabepriorität (I/O-Scheduling) angepasst.
Falls Sie RAID verwenden und es plötzlich synchronisieren muss (nach einem fehlgeschlagenen Reboot oder zur Wiederherstellung des RAID-Verbunds nach dem Austausch einer Festplatte), kann es in bestimmten Situationen sinnvoll sein, die Synchronisationsgeschwindigkeit zu reduzieren, damit die übrigen Prozesse einigermaßen vernünftig arbeiten können. Dieser Befehl kann dabei helfen:
echo 1000 > /proc/sys/dev/raid/speed_limit_max
Problem fünf: Wie man Dateien in Echtzeit synchronisiert.
Wir haben all die riesigen Mengen an Dateien, die auf einen zweiten Server gesichert werden müssen, um… Die Dateien werden ständig geschrieben, deshalb müssen sie möglichst schnell kopiert werden, um minimale Verluste zu gewährleisten.
Die Standardlösung: Rsync über SSH.
Das ist eine gute Option, es sei denn, es muss alle paar Sekunden erfolgen. Es gibt viele Dateien. Selbst wenn man sie nicht kopiert, muss man irgendwie verstehen, was sich geändert hat, und mehrere Millionen Dateien zu vergleichen, kostet Zeit und belastet die Festplatten.
Das bedeutet, dass wir sofort wissen müssen, was wir kopieren sollen, ohne jedes Mal einen Vergleich zu starten.
Rettung — lsyncd. Lsyncd — . Es funktioniert ebenfalls über rsync, überwacht jedoch zusätzlich das Dateisystem auf Änderungen mithilfe von inotify und fsevents und startet das Kopieren nur für die Dateien, die neu hinzugekommen oder geändert wurden.
Problem sechs: wie man herausfindet, wer die Platten belastet
Das wissen wahrscheinlich alle, aber dennoch zur Vollständigkeit: zur Überwachung des Datenspeichers gibt es den Befehl iotop — ähnlich wie top, aber er zeigt die Prozesse an, die die Festplatten am intensivsten nutzen.

Übrigens ermöglicht auch das gute alte top zu erkennen, ob es Probleme mit den Platten gibt oder nicht. Dafür gibt es zwei besonders geeignete Parameter: Load Average und IOwait.

Der erste zeigt, wie viele Prozesse auf Bedienung warten; in der Regel ist mehr als 2 bereits ein Alarmzeichen. Bei aktivem Kopieren auf Backup-Server erlauben wir bis zu 6-8, danach gilt die Situation als abnormal.
Der zweite Punkt betrifft, wie stark der Prozessor mit Festplattenoperationen beschäftigt ist. Ein IO-Wait von über 10 % sollte Anlass zur Besorgnis geben, obwohl wir auf unseren Servern mit spezifischen Lastprofilen stabil 40-50 % beobachten, was tatsächlich normal ist.
Damit schließe ich ab, obwohl es sicherlich viele Punkte gibt, mit denen wir noch nicht konfrontiert wurden. Ich würde mich freuen, Kommentare und interessante reale Fälle zu erhalten.
Quelle: habr.com
