Zwischenspeichern und puffern Sie ein Verzeichnis vorübergehend (um einen Erstellungsprozess auf einer NFS-Freigabe zu beschleunigen).

7

Ü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/libund erstellt dann gemeinsam genutzte Bibliotheken workarea/dllunter Verwendung der statischen Bibliotheken in workarea/lib. Während der Erstellung der gemeinsam genutzten Bibliotheken werden diese nicht nur geschrieben, sondern auch wieder gelesen, znmum 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 dllauf 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/libund workarea/dlleine 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 libmuss sich in der Freigabe (oder einem dauerhaften Mount) befinden, um schnelle inkrementelle Erstellungen zu ermöglichendllmuss 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/dllund 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 libVerwendung 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 libund dllVerzeichnisse, 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 noctoNFS-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 asyncaktivierter 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 asyncbedeutet 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 noctoeines 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 FUSEDateisystem-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 origVerwendung 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_viewund 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:

  1. Leseabfragen werden beantwortet, indem die Abfrage einmal an das Dateisystem von weitergeleitet orig, das Ergebnis zwischengespeichert und von da an verwendet wird
  2. cacheSchreibabfragen werden direkt geschrieben und nach Abschluss bestätigt, dh der Cache ist auch ein Schreibpuffer. Dies sollte sogar passieren, wenn close()die geschriebene Datei aufgerufen wird. Im Hintergrund werden die Schreibvorgänge dann weitergeleitet (möglicherweise mithilfe einer Warteschlange) an orig. Leseanfragen zu geschriebenen Daten werden cachenatürlich mit den Daten in beantwortet .

Außerdem brauche ich:

  1. 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 zugreifen orig.
  2. 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. origDaher ist origdas cacheeinmalige 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 origto verknüpfe cacheund 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 origdanach. 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.

Jasilvan
quelle
ccache beantwortet Ihre Frage nicht, aber ich denke, es löst Ihr Problem.
Gilles 'SO - hör auf böse zu sein'
1
ccache wirkt sich nur auf die Kompilierung aus. Mein Engpass ist der Durchsatz des Dateisystems während der Verknüpfung, wobei ccache nicht hilft.
Jasilvan
1
Oh, ich verstehe. Was Sie wirklich wollen, ist nicht nur Caching, sondern Schreibpufferung. Sie melden, dass ein Schreibvorgang erfolgreich war, bevor er an den NFS-Server übertragen wird. Durch das bloße Schreib-Caching bleibt eine lokale Kopie der zu schreibenden Daten erhalten, die jedoch erst von der Schreibanforderung zurückgegeben wird, wenn der Server die Daten empfangen hat. Dies ist der Engpass, den Sie beseitigen möchten.
Gilles 'SO - hör auf böse zu sein'
Ah, okay. Ich bin mit der in diesem Bereich gebräuchlichen Terminologie nicht allzu vertraut. Vielen Dank, dass Sie darauf hingewiesen haben. Ich werde die Frage entsprechend aktualisieren.
Jasilvan
Ich würde auch gerne die Antwort hören. Overlayfs klingt zu manuell, cachefilesdkönnte es wahrscheinlich besser machen?
Anton

Antworten:

2

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.

M Conrad
quelle
Ich kompiliere Julia-lang in einem separaten Verzeichnis und obwohl es hilft, sind 25 Minuten gegenüber 8 Minuten für lokale Kopien ein zu großer Unterschied. Overlayfs ist nett, aber das manuelle Rsync ist nicht das, was ich hier wirklich will. Ich brauche sowohl Lese- als auch Schreibpufferung. Irgendeine andere Idee zum Teilen?
Anton
@Anton Wenn Sie meinen Overlay-Vorschlag verwenden, kann er nicht schneller werden. Die Überlagerung stoppt alle Schreibvorgänge in NFS, sodass Sie durch die Lesegeschwindigkeit von NFS eingeschränkt sind. Sie können an der Optimierung von NFS-Lesevorgängen arbeiten. Wenn Sie jedoch die NFS-Dateien lokal zwischenspeichern möchten, ohne den Server bei jedem Zugriff zu überprüfen, verwenden Sie sie nicht wirklich als "Netzwerk-Dateisystem" und sollten dies überdenken Deine Tore. Wenn also jemand eine der Dateien remote ändert, wird die Änderung in Ihrem Build nicht angezeigt, wenn Sie die Lesevorgänge vollständig zwischenspeichern.
M Conrad
25 Minuten und 8 Minuten klingen beide extrem. Führen Sie den Build parallel aus? Verfügt Ihr System über genügend RAM, um den Datei-Cache bereitzustellen?
M Conrad
Vielen Dank. Für andere Projekte verwende ich die Freigabe, um Quellen von verschiedenen Computern zu ändern. Für Julia-lang kann es als automatische Sicherung oder rsync angesehen werden. Ja, es ist in einigen Teilen parallel, aber die Sprachlaufzeit ist eine große Sache. Es umfasst das Kompilieren von LLVM und vielen anderen Bibliotheken sowie den seriellen Teil des Zusammenstellens von Julia-Bildern und des Testens, was die Skalierbarkeit einschränkt. Ccache funktioniert übrigens gut, es reduziert sogar diese 8 Minuten auf 5 bei lokaler Erstellung und löst fast das 25-Minuten-Problem
Anton
1
Hört sich so an, als hätten Sie es meistens mit ccache behoben, aber wenn NFS nur für die intermittierende Speicherung vorgesehen ist, können Sie ein Build-Skript in Betracht ziehen, das rsync ausführt, um eine lokale Kopie aufzufrischen, dann make ausführt und dann rsync ausführt, um die Ergebnisse wieder in NFS zu speichern. Es ist nur "manuell", bis Sie es automatisieren ;-)
M Conrad