Überblick
Diese Frage ist wie folgt aufgebaut:
Ich gebe zunächst einige Hintergrundinformationen darüber, warum ich mich für dieses Thema interessiere und wie es ein Problem lösen würde, mit dem ich mich befasse. Dann stelle ich die eigentliche eigenständige Frage zum Zwischenspeichern von Dateisystemen. Wenn Sie also nicht an der Motivation interessiert sind (einige C ++ - Projekt-Build-Setups), überspringen Sie einfach den ersten Abschnitt.
Das anfängliche Problem: Verknüpfen von gemeinsam genutzten Bibliotheken
Ich suche nach einer Möglichkeit, die Bauzeiten unseres Projekts zu beschleunigen. Das Setup ist wie folgt: Ein Verzeichnis (nennen wir es workarea
) befindet sich in einer NFS-Freigabe. Es enthält zunächst nur Quellcode und Makefiles. Anschließend erstellt der Erstellungsprozess zunächst statische Bibliotheken in workarea/lib
und erstellt dann gemeinsam genutzte Bibliotheken workarea/dll
unter Verwendung der statischen Bibliotheken in workarea/lib
. Während der Erstellung der gemeinsam genutzten Bibliotheken werden diese nicht nur geschrieben, sondern auch wieder gelesen, znm
um zum Zeitpunkt der Verknüpfung zu überprüfen, ob keine Symbole fehlen. Wenn viele Jobs parallel verwendet werden (z. B. make -j 20 oder make -j 40), werden die Erstellungszeiten schnell von der Verknüpfungszeit dominiert. In diesem Fall wird die Verknüpfungsleistung durch die Dateisystemleistung eingeschränkt. Beispielsweise dauert das parallele Verknüpfen mit 20 Jobs in der NFS-Freigabe ungefähr 35 Sekunden, in einem RAM-Laufwerk jedoch nur 5 Sekunden. Beachten Sie, dass die Verwendung von rsync zum Zurückkopieren dll
auf die NFS-Freigabe weitere 6 Sekunden dauert. Daher ist die Arbeit in einem RAM-Laufwerk und die anschließende Synchronisierung mit NFS viel schneller als die direkte Arbeit in der NFS-Freigabe. Ich suche nach einer Möglichkeit, die schnelle Leistung zu erzielen, ohne Dateien explizit zwischen der NFS-Freigabe und dem RAM-Laufwerk zu kopieren / zu verknüpfen.
Beachten Sie, dass unsere NFS-Freigabe bereits einen Cache verwendet, dieser Cache jedoch nur Lesezugriffe zwischenspeichern kann.
AFAIK, NFS erfordert, dass ein NFS-Client einen Schreibvorgang möglicherweise erst bestätigt, wenn der NFS-Server den Abschluss des Schreibvorgangs bestätigt, sodass der Client keinen lokalen Schreibpuffer verwenden kann und der Schreibdurchsatz (auch bei Spitzen) durch die Netzwerkgeschwindigkeit begrenzt ist. Dies begrenzt den kombinierten Schreibdurchsatz in unserem Setup effektiv auf ungefähr 80 MB / s.
Die Leseleistung ist jedoch viel besser, da ein Lesecache verwendet wird. Wenn ich in NFS eine Verknüpfung (Erstellung des Inhalts von dll
) mit workarea/lib
und workarea/dll
eine Verknüpfung mit dem RAM-Laufwerk herstelle, ist die Leistung immer noch gut - ungefähr 5 Sekunden. Beachten Sie, dass der Erstellungsprozess mit dem workarea/*
Speichern in der NFS-Freigabe abgeschlossen sein muss: Er lib
muss sich in der Freigabe (oder einem dauerhaften Mount) befinden, um schnelle inkrementelle Erstellungen zu ermöglichendll
muss sich in NFS befinden, damit Rechencomputer, die Jobs mit diesen DLLs starten, auf sie zugreifen können.
Daher möchte ich eine Lösung für das unten stehende Problem anwenden workarea/dll
und möglicherweise auch workarea/lib
(letztere, um die Kompilierungszeiten zu verbessern). Die unten angegebenen Anforderungen an schnelle Rüstzeiten werden durch die Notwendigkeit verursacht, schnelle inkrementelle Builds durchzuführen und nur bei Bedarf Daten zu kopieren.
Aktualisieren
Ich hätte wahrscheinlich etwas genauer auf das Build-Setup eingehen sollen. Hier einige weitere Details: Kompilierungseinheiten werden in einem temporären Verzeichnis (in / tmp) in O-Dateien kompiliert. Diese werden dann bei lib
Verwendung in statische Bibliotheken zusammengeführt ar
. Der gesamte Erstellungsprozess ist inkrementell:
- Kompilierungseinheiten werden nur neu kompiliert, wenn sich die Kompilierungseinheit selbst (die .C-Datei) oder ein enthaltener Header geändert hat (unter Verwendung von vom Compiler generierten Abhängigkeitsdateien, die in enthalten sind
make
). - Statische Bibliotheken werden nur aktualisiert, wenn eine ihrer Kompilierungseinheiten neu kompiliert wurde.
- Freigegebene Bibliotheken werden nur erneut verknüpft, wenn sich eine ihrer statischen Bibliotheken geändert hat. Symbole von gemeinsam genutzten Bibliotheken werden nur dann erneut überprüft, wenn die von den gemeinsam genutzten Bibliotheken bereitgestellten Symbole geändert werden, wenn die gemeinsam genutzte Bibliothek selbst aktualisiert wurde.
Dennoch sind häufig vollständige oder nahezu vollständige Neuerstellungen erforderlich, da mehrere Compiler ( gcc
, clang
), Compilerversionen, Kompilierungsmodi ( release
, debug
), C ++ - Standards ( C++97
, C++11
) und zusätzliche Modifikationen (z. B. libubsan
) verwendet werden können. Alle Kombinationen verwenden effektiv unterschiedliche Verzeichnisse lib
und dll
Verzeichnisse, sodass zwischen den Setups gewechselt und inkrementell basierend auf dem letzten Build für genau dieses Setup erstellt werden kann. Bei inkrementellen Builds müssen häufig nur wenige Dateien neu kompiliert werden, was sehr wenig Zeit in Anspruch nimmt, aber das erneute Verknüpfen von (möglicherweise großen) gemeinsam genutzten Bibliotheken auslöst, was viel länger dauert.
Update 2
In der Zwischenzeit habe ich etwas über die nocto
NFS-Mount-Option gelernt , die anscheinend mein Problem bei praktisch allen NFS-Implementierungen mit Ausnahme von Linux lösen könnte, da Linux Schreibpuffer immer leert close()
, auch mit nocto
. Wir haben bereits einige andere Dinge ausprobiert: Zum Beispiel könnten wir einen anderen lokalen NFS-Server mit async
aktivierter Funktion verwenden, der als Schreibpuffer dient und den Haupt-NFS-Mount exportiert. Leider führt der NFS-Server selbst in diesem Fall keine Schreibpufferung durch. Dies async
bedeutet lediglich, dass der Server das zugrunde liegende Dateisystem nicht zwingt, in einen stabilen Speicher zu leeren, und implizit ein Schreibpuffer verwendet wird, falls das zugrunde liegende Dateisystem einen Schreibpuffer verwendet (wie dies anscheinend für das Dateisystem der Fall ist) auf dem physischen Laufwerk).
Wir haben sogar über die Option nachgedacht, eine virtuelle Nicht-Linux-Maschine auf derselben Box zu verwenden, auf der die Haupt-NFS-Freigabe mithilfe nocto
eines Schreibpuffers und dieses gepufferten Mount über einen anderen NFS-Server bereitgestellt wird, haben sie jedoch nicht getestet und möchten dies gerne tun Vermeiden Sie eine solche Lösung.
Wir haben auch mehrere FUSE
Dateisystem-Wrapper gefunden, die als Caches dienen, aber keiner von diesen hat die Schreibpufferung implementiert.
Zwischenspeichern und Puffern eines Verzeichnisses
Betrachten Sie ein Verzeichnis, nennen wir es orig
, das sich in einem langsamen Dateisystem befindet, z. B. eine NFS-Freigabe. Für eine kurze Zeitspanne (z. B. Sekunden oder Minuten, aber das sollte sowieso keine Rolle spielen) möchte ich eine vollständig zwischengespeicherte und gepufferte Ansicht der orig
Verwendung eines Verzeichnisses erstellen cache
, das sich in einem schnellen Dateisystem befindet, z. B. einer lokalen Festplatte oder sogar einer RAM-Laufwerk. Der Cache sollte beispielsweise über einen Mount zugänglich sein cached_view
und keine Root-Rechte erfordern. Ich gehe davon aus, dass es während der gesamten Lebensdauer des Caches keine direkten Lese- oder Schreibzugriffe gibt orig
(neben dem Cache selbst natürlich). Mit vollständig zwischengespeichert und gepuffert meine ich Folgendes:
- Leseabfragen werden beantwortet, indem die Abfrage einmal an das Dateisystem von weitergeleitet
orig
, das Ergebnis zwischengespeichert und von da an verwendet wird cache
Schreibabfragen werden direkt geschrieben und nach Abschluss bestätigt, dh der Cache ist auch ein Schreibpuffer. Dies sollte sogar passieren, wennclose()
die geschriebene Datei aufgerufen wird. Im Hintergrund werden die Schreibvorgänge dann weitergeleitet (möglicherweise mithilfe einer Warteschlange) anorig
. Leseanfragen zu geschriebenen Daten werdencache
natürlich mit den Daten in beantwortet .
Außerdem brauche ich:
- Der Cache bietet eine Funktion zum Herunterfahren des Caches, mit der alle Schreibvorgänge gelöscht werden
orig
. Die Laufzeit des Löschens sollte nur von der Größe der geschriebenen Dateien abhängen, nicht von allen Dateien. Danach konnte man wieder sicher zugreifenorig
. - Die Einrichtungszeit ist schnell, z. B. hängt die Initialisierung des Caches möglicherweise nur von der Anzahl der Dateien ab
orig
, nicht jedoch von der Größe der Dateien.orig
Daher istorig
dascache
einmalige Kopieren keine Option.
Schließlich wäre ich auch in Ordnung mit einer Lösung, die kein anderes Dateisystem als Cache verwendet, sondern nur Caches im Hauptspeicher (die Server haben viel RAM). Beachten Sie, dass die Verwendung von integrierten Caches von z. B. NFS keine Option ist, da AFAIK NFS keine Schreibpuffer zulässt (siehe erster Abschnitt).
In meinem Setup könnte ich ein etwas schlechteres Verhalten emulieren, indem ich den Inhalt von orig
to verknüpfe cache
und dann damit arbeite cache
(da alle Schreibvorgänge tatsächlich Dateien durch neue Dateien ersetzen. In diesem Fall werden die Symlinks durch die aktualisierten Versionen ersetzt) und die geänderten Versionen erneut synchronisiere Dateien orig
danach. Dies entspricht nicht genau den oben genannten Anforderungen, z. B. werden Lesevorgänge nicht nur einmal ausgeführt und Dateien durch Symlinks ersetzt, was für einige Anwendungen natürlich einen Unterschied macht.
Ich gehe davon aus, dass dies nicht der richtige Weg ist, um dies zu lösen (selbst in meiner einfacheren Einstellung), und vielleicht ist sich jemand einer saubereren (und schnelleren!) Lösung bewusst.
cachefilesd
könnte es wahrscheinlich besser machen?Antworten:
Wow, überrascht, dass noch niemand mit "Overlays" geantwortet hat.
Eigentlich habe ich zwei Vorschläge. Die erste besteht darin, Overlays zu verwenden, was im Grunde genau das ist, was Sie beschreiben, mit einer Einschränkung. Mit Overlayfs (Standard seit Linux 3.18 oder so) können Sie aus zwei virtuell zusammengeführten Verzeichnisbäumen lesen, während Sie nur in einen von ihnen schreiben. Was Sie tun würden, ist, den schnellen Speicher (wie tmpfs) auf das NFS-Volume zu legen und dann Ihre Kompilierung in der überlagerten Fusion der beiden durchzuführen. Wenn Sie fertig sind, wurden in NFS keine Dateien mehr geschrieben, und das andere Dateisystem enthält alle Ihre Änderungen. Wenn Sie die Änderungen beibehalten möchten, können Sie sie einfach wieder mit dem NFS synchronisieren. Sie können sogar Dateien ausschließen, die Sie nicht interessieren, oder einfach einige Dateien aus dem Ergebnis auswählen.
Sie können ein relativ einfaches Beispiel für Overlays in einem kleinen Projekt von mir sehen: https://github.com/nrdvana/squash-portage/blob/master/squash-portage.sh Dieses Skript zeigt auch, wie Sie UnionFS für den Fall verwenden Ich bin auf einem älteren Kernel, der keine Overlays hat.
In meinem Fall dauert der Befehl rsync, mit dem Gentoo seine Softwarebibliothek aktualisiert, wahnsinnig lange, da er Millionen winziger Festplattenschreibvorgänge enthält. Ich benutze Overlays, um alle Änderungen an tmpfs zu schreiben, und dann mksquashfs, um ein komprimiertes Bild des Baums zu erstellen. Dann werfe ich die tmpfs weg und montiere das komprimierte Image an seiner Stelle.
Mein zweiter Vorschlag ist ein "out of tree" Build. Die Idee ist, dass Sie den Quellcode und die Makefiles in einem Baum haben und automake anweisen, alle Zwischendateien in einem separaten Baum zu generieren, der den ersten spiegelt.
Wenn Sie Glück haben, kann Ihr Build-Tool (Automake oder so weiter) dies bereits. Wenn Sie kein Glück haben, müssen Sie möglicherweise einige Kopfschmerzen damit verbringen, an Ihren Makefiles zu basteln.
quelle