Teleportiere den Prozess auf einen anderen Computer! 

Einmal teilte ein Kollege seine Gedanken über die API für verteilte Computercluster mit, und ich antwortete scherzhaft: „Eine ideale API wäre natürlich ein einfacher Aufruf, telefork()damit Ihr Prozess auf jedem Computer im Cluster aktiviert wird und den Wert der Instanz-ID zurückgibt.“ Aber am Ende hat mich diese Idee in Besitz genommen. Ich konnte nicht verstehen, warum es so dumm und einfach ist, viel einfacher als jede API für Remote-Arbeit, und warum Computersysteme dazu nicht in der Lage zu sein scheinen. Ich schien auch zu verstehen, wie dies umgesetzt werden kann, und ich hatte bereits einen guten Namen, was der schwierigste Teil eines Projekts ist. Also musste ich arbeiten.

Am ersten Wochenende fertigte er einen einfachen Prototyp an, und am zweiten Wochenende brachte er eine Demo, die es konnteProzess zu einer riesigen virtuellen Maschine in der Cloud, vertreiben Sie das Rendern der Pfadverfolgung auf mehreren Kernen und teleforken Sie den Prozess dann zurück. All dies ist in einer einfachen API verpackt.

Das Video zeigt, dass das Rendern auf einer 64-Core-VM in der Cloud in 8 Sekunden abgeschlossen ist (plus 6 Sekunden für das Hin- und Herbewegen von Teleforks). Das gleiche Rendern lokal in einem Container auf meinem Laptop dauert 40 Sekunden:


Wie ist es möglich, den Prozess zu teleportieren? Hier ist, was dieser Artikel erklären sollte! Die Grundidee ist, dass der Linux-Prozess auf niedriger Ebene nur wenige Komponenten enthält. Sie brauchen nur eine Möglichkeit, jeden von ihnen vom Spender wiederherzustellen, ihn über das Netzwerk zu übertragen und in den geklonten Prozess zu kopieren.

Sie könnten denken: "Aber wie kann man [etwas Schwieriges wie eine TCP-Verbindung] replizieren?" Ja wirklich. Tatsächlich tolerieren wir solche komplizierten Dinge nicht, um den Code einfach zu halten. Das heißt, es ist nur eine unterhaltsame technische Demo , die wahrscheinlich nicht in der Produktion verwendet werden sollte. Aber sie weiß immer noch, wie man eine breite Klasse von meist rechnerischen Aufgaben teleportiert!

Wie sieht es aus


Ich habe den Code als Rust-Bibliothek implementiert, aber theoretisch können Sie das Programm in die C-API einbinden und dann die FFI-Bindungen durchlaufen, um sogar den Python-Prozess zu teleportieren. Die Implementierung umfasst nur etwa 500 Codezeilen (plus 200 Kommentarzeilen):

use telefork::{telefork, TeleforkLocation};

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let destination = args.get(1).expect("expected arg: address of teleserver");

    let mut stream = std::net::TcpStream::connect(destination).unwrap();
    match telefork(&mut stream).unwrap() {
        TeleforkLocation::Child(val) => {
            println!("I teleported to another computer and was passed {}!", val);
        }
        TeleforkLocation::Parent => println!("Done sending!"),
    };
}

Ich habe auch einen Helfer namens yoyoTeleforks an den Server geschrieben, die übertragene Schließung durchgeführt und dann Teleforks zurück. Dies schafft die Illusion, dass Sie einen Code einfach auf einem Remote-Server ausführen können, beispielsweise mit viel größerer Verarbeitungsleistung.

// load the scene locally, this might require loading local scene files to memory
let scene = create_scene();
let mut backbuffer = vec![Vec3::new(0.0, 0.0, 0.0); width * height];
telefork::yoyo(destination, || {
  // do a big ray tracing job on the remote server with many cores!
  render_scene(&scene, width, height, &mut backbuffer);
});
// write out the result to the local file system
save_png_file(width, height, &backbuffer);

Linux-Prozessanatomie


Mal sehen, wie der Prozess unter Linux aussieht (auf dem das Mutter-Host-Betriebssystem ausgeführt wird telefork):



  • (memory mappings): , . «» 4 . /proc/<pid>/maps. , , .

    • , , ( ).
  • : , . , , - , , , . , .
  • : , . - , . , , , TCP-, .
    • . stdin/stdout/stderr, 0, 1 2.
    • , , , .
  • Verschiedenes : Es gibt einige andere Teile des Prozessstatus, die sich in der Replikationskomplexität unterscheiden. In den meisten Fällen spielen sie jedoch keine Rolle, z. B. brk (Heap-Zeiger). Einige von ihnen können nur mit Hilfe seltsamer Tricks oder spezieller Systemaufrufe wie PR_SET_MM_MAP wiederhergestellt werden , was die Wiederherstellung erschwert.

Somit kann die grundlegende Implementierung teleforkmit einer einfachen Zuordnung von Speicher und Registern der Hauptthreads erfolgen. Dies sollte für einfache Programme ausreichen, die hauptsächlich Berechnungen durchführen, ohne mit Betriebssystemressourcen wie Dateien zu interagieren (für die Teleportation reicht es im Prinzip aus, die Datei im System zu öffnen und vor dem Aufruf zu schließen telefork).

Wie man einen Prozess teleforkiert


Ich war nicht der erste, der darüber nachdachte, Prozesse auf einem anderen Computer neu zu erstellen. Der Debugger für das Debuggen und Aufzeichnen von rr macht also sehr ähnliche Dinge . Ich habe dem Autor dieses Programms @rocallahan ein paar Fragen gesendet , und er hat mir vom CRIU- System für die „heiße“ Migration von Containern zwischen Hosts erzählt . CRIU kann den Linux-Prozess auf ein anderes System übertragen, unterstützt die Wiederherstellung aller Arten von Dateideskriptoren und anderen Zuständen. Der Code ist jedoch sehr komplex und verwendet viele Systemaufrufe, die spezielle Kernel-Assemblys und Root-Berechtigungen erfordern. Über den Link von der CRIU-Wiki-Seite fand ich DMTCP, das für Snapshots verteilter Aufgaben auf Supercomputern erstellt wurde, damit diese später neu gestartet werden können, sowie dieses ProgrammDer Code erwies sich als einfacher .

Diese Beispiele haben mich nicht gezwungen, Versuche zur Implementierung meines eigenen Systems abzubrechen, da es sich um äußerst komplexe Programme handelt, die spezielle Läufer und Infrastruktur erfordern, und ich wollte die einfachste Teleportation von Prozessen als Bibliotheksaufruf implementieren. Also habe ich die Fragmente des Quellcodes rr, CRIU, DMTCP und einige ptrace-Beispiele studiert - und mein eigenes Verfahren zusammengestellt telefork. Meine Methode funktioniert auf ihre eigene Art und Weise, es ist eine Mischung aus verschiedenen Techniken.

Um einen Prozess zu teleportieren, müssen Sie einige Arbeiten im ursprünglichen Prozess ausführen telefork, der aufgerufen wird, und einige Arbeiten auf der Seite des Funktionsaufrufs, der den Streaming-Prozess auf dem Server empfängt und aus dem Stream (Funktion) neu erstellttelepad) Sie können gleichzeitig erfolgen, aber alle Serialisierungen können auch vor dem Herunterladen durchgeführt werden, z. B. indem sie in eine Datei abgelegt und später heruntergeladen werden.

Das Folgende ist eine vereinfachte Übersicht über beide Prozesse. Wenn Sie im Detail verstehen möchten, empfehle ich, den Quellcode zu lesen . Es ist in einer Datei enthalten und eng auskommentiert, um es zu lesen und zu verstehen, wie alles funktioniert.

Einreichen eines Prozesses mit telefork


Die Funktion teleforkempfängt einen Stream mit Schreibfähigkeit, mit dem sie den gesamten Status ihres Prozesses überträgt.

  1. «» . , , . fork .
  2. . /proc/<pid>/maps , . proc_maps crate.
  3. . DMTCP, , , . , [vdso], , , .
  4. . , , process_vm_readv , .
  5. Register übertragen . Ich benutze die Option PTRACE_GETREGSfür den ptrace -Systemaufruf . Hiermit können Sie alle Werte des Registers des untergeordneten Prozesses abrufen. Dann schreibe ich sie einfach in eine Nachricht auf dem Kanal.

Ausführen von Systemaufrufen in einem untergeordneten Prozess


Um den Zielprozess in eine Kopie des eingehenden Prozesses umzuwandeln, müssen Sie den Prozess zwingen, eine Reihe von Systemaufrufen ohne Zugriff auf Code selbst auszuführen, da wir alles gelöscht haben. Wir führen Remote-Systemaufrufe mit ptrace durch , einem universellen Systemaufruf zum Manipulieren und Überprüfen anderer Prozesse:

  1. syscall. syscall , . , process_vm_readv [vdso] , , , syscall Linux, . , [vdso].
  2. , PTRACE_SETREGS. syscall, rax Linux, rdi, rsi, rdx, r10, r8, r9.
  3. Führen Sie mit dem Parameter einen Schritt ausPTRACE_SINGLESTEP , um den Befehl syscall auszuführen.
  4. Lesen Sie die Register mit PTRACE_GETREGS, um den Syscall-Rückgabewert wiederherzustellen, und prüfen Sie, ob dies erfolgreich war.

Prozessabnahme in telepad


Mit diesem und den bereits beschriebenen Grundelementen können wir den Prozess neu erstellen:

  1. Fork einen eingefrorenen untergeordneten Prozess . Ähnlich wie beim Senden, aber dieses Mal benötigen wir einen untergeordneten Prozess, den wir manipulieren können, um ihn in einen Klon des übertragenen Prozesses zu verwandeln.
  2. Überprüfen Sie die Speicherzuordnungskarten . Dieses Mal müssen wir alle vorhandenen Speicherzuordnungskarten kennen, um sie zu entfernen und Platz für den eingehenden Prozess zu schaffen.
  3. . , munmap.
  4. . mremap, .
  5. . mmap , process_vm_writev .
  6. . PTRACE_SETREGS , , rax. raise(SIGSTOP), . , telepad.
    • Ein beliebiger Wert wird verwendet, damit der Telefork-Server den Dateideskriptor der TCP-Verbindung, in die der Prozess eingegeben wurde, übertragen und Daten zurücksenden oder, falls vorhanden yoyo, an dieselbe Verbindung zurück teleportieren kann.
  7. Starten Sie den Prozess mit dem neuen Inhalt neu PTRACE_DETACH.

Kompetentere Umsetzung


Einige Teile meiner Telefork-Implementierung sind nicht perfekt gestaltet. Ich weiß, wie man sie repariert, aber in der aktuellen Form mag ich das System, und manchmal sind sie wirklich schwer zu reparieren. Hier einige interessante Beispiele:

  • (vDSO). mremap vDSO , DMTCP, , . vDSO, . - , CPU glibc vDSO . , vDSO, syscall, rr, vDSO vDSO .
  • brk . DMTCP, , brk , brk . , , — PR_SET_MM_MAP, .
  • . Rust « », , FS GS, , , - glibc pid tid, . CRIU, PID TID .
  • . , , , / , / FUSE. , TCP-, DMTCP CRIU , perf_event_open.
  • . fork() Unix , , .


Ich denke, Sie haben bereits verstanden, dass Sie mit den richtigen Low-Level-Schnittstellen einige verrückte Dinge implementieren können, die jemandem unmöglich erschienen. Hier einige Gedanken zur Entwicklung der Grundideen von Telefork. Obwohl ein Großteil der oben genannten Funktionen wahrscheinlich nur auf einem völlig neuen oder festen Kernel vollständig implementiert werden kann:

  • Cluster Telefork . Die ursprüngliche Inspirationsquelle für Telefork war die Idee, einen Prozess auf alle Maschinen in einem Computercluster zu streamen. Es kann sich sogar herausstellen, dass UDP-Multicast- oder Peer-to-Peer-Methoden implementiert werden, um die Verteilung über den gesamten Cluster zu beschleunigen. Sie möchten wahrscheinlich auch Kommunikationsprimitive haben.
  • . CRIU , - userfaultfd. , SIGSEGV mmap. , ,  — .
  • ! , . userfaultfd userfaultfd, , , MESI, . , , .  — , . , , , . : syscall, -, syscall, . , . , , , . , , . , , ( , ) , .


Ich mag es wirklich, weil hier ein Beispiel für eine meiner Lieblingstechniken ist - in eine weniger bekannte Abstraktionsebene einzutauchen, die relativ leicht das erfüllt, was wir für fast unmöglich hielten. Teleportationsberechnungen scheinen unmöglich oder sehr schwierig zu sein. Möglicherweise müssen Methoden wie das Serialisieren des gesamten Status, das Kopieren der ausführbaren Binärdatei auf den Remotecomputer und das Starten mit speziellen Befehlszeilenflags zum erneuten Laden des Status erforderlich sein. Aber nein, alles ist viel einfacher. Unter Ihrer bevorzugten Programmiersprache befindet sich eine Abstraktionsschicht, in der Sie eine relativ einfache Teilmenge von Funktionen auswählen und über das Wochenende die Teleportation der meisten reinen Berechnungen in jeder Programmiersprache in 500 Codezeilen implementieren können. Ich glaubedass ein solches Tauchen auf eine andere Abstraktionsebene oft zu einfacheren und universelleren Lösungen führt. Ein anderes meiner Projekte wie dieses istNumderline .

Auf den ersten Blick scheinen solche Projekte extreme Hacks zu sein, und das ist es größtenteils. Sie tun Dinge, die niemand erwartet, und wenn sie brechen, tun sie dies auf der Abstraktionsebene, auf der ähnliche Programme nicht funktionieren sollten - zum Beispiel verschwinden Ihre Dateideskriptoren auf mysteriöse Weise. Aber manchmal kann man die Abstraktionsebene richtig einstellen und mögliche Situationen codieren, so dass am Ende alles reibungslos und magisch funktioniert. Ich denke, die guten Beispiele hier sind rr (obwohl Telefork es geschafft hat, es zu entlassen) und die Cloud-Migration von virtuellen Maschinen in Echtzeit (tatsächlich Telefork auf Hypervisor-Ebene).

Ich präsentiere diese Dinge auch gerne als Ideen für alternative Arbeitsweisen von Computersystemen. Warum sind unsere Cluster-Computing-APIs so viel komplizierter als ein einfaches Programm, das Funktionen in einen Cluster übersetzt? Warum ist die Programmierung von Netzwerksystemen so viel komplizierter als Multithreading? Natürlich können Sie alle möglichen guten Gründe angeben, aber diese basieren normalerweise darauf, wie schwierig es ist, ein Beispiel für vorhandene Systeme zu erstellen. Oder funktioniert mit der richtigen Abstraktion oder mit ausreichendem Aufwand alles einfach und nahtlos? Grundsätzlich ist nichts unmöglich.





All Articles