Ray auf Microsoft Windows portieren

Von Johny vino

(Mehrdad Niknami) (28. September 2020)

Hintergrund

Als Ray zum ersten Mal gestartet wurde, wurde es für Linux- und macOS-UNIX-basierte Betriebssysteme geschrieben. Die Unterstützung für Windows, das beliebteste Desktop-Betriebssystem, fehlte, war jedoch wichtig für den langfristigen Erfolg des Projekts. Während das Windows-Subsystem für Linux (WSL) für einige Benutzer eine mögliche Option darstellte, beschränkte es die Unterstützung auf neuere Versionen von Windows 10 und erschwerte den Code erheblich mit dem nativen Windows-Betriebssystem zu interagieren, was für die Benutzer eine schlechte Erfahrung darstellte. Angesichts dieser Tatsache hofften wir wirklich, den Benutzern native Windows-Unterstützung bieten zu können, wenn dies überhaupt praktikabel ist.

Das Portieren von Ray nach Windows war jedoch keine triviale Aufgabe. Wie viele andere Projekte, die nachträglich versuchen, Portabilität zu erreichen, stießen wir auf viele Herausforderungen, deren Lösungen nicht offensichtlich waren. In diesem Blog-Beitrag möchten wir uns eingehend mit den technischen Details des Prozesses befassen, den wir befolgt haben, um Ray mit Windows kompatibel zu machen. Wir hoffen, dass dies anderen Projekten mit einer ähnlichen Vision helfen kann, einige der potenziellen Herausforderungen zu verstehen und zu verstehen, wie sie behandelt werden können.

Übersicht

Die Unterstützung für die Nachrüstung von Windows war immer größer Herausforderung, da die Entwicklung auf Ray weiter voranschritt. Bevor wir begannen, erwarteten wir, dass bestimmte Aspekte des Codes einen wesentlichen Teil unserer Portabilitätsbemühungen ausmachen würden, einschließlich der folgenden:

  • Interprozesskommunikation (IPC) und gemeinsam genutzter Speicher
  • Objekthandles und Dateihandles / Deskriptoren (FDs)
  • Prozesslaichen und -verwaltung, einschließlich Signalisierung
  • Dateiverwaltung, Threadverwaltung und asynchrone E / A
  • Verwendung von Shell- und Systembefehlen
  • Redis

Angesichts des Ausmaßes der Probleme wurde uns schnell klar, dass die Verhinderung weiterer Inkompatibilitäten Vorrang vor dem Versuch haben sollte, vorhandene Probleme zu beheben Probleme. Wir haben daher versucht, ungefähr die folgenden Schritte auszuführen, obwohl dies eine gewisse Vereinfachung darstellt, da bestimmte Probleme manchmal in verschiedenen Phasen behandelt wurden:

  1. Kompilierbarkeit für Abhängigkeiten von Drittanbietern
  2. Kompilierbarkeit für Ray (über leere Stubs & TODOs)
  3. Verknüpfbarkeit
  4. Kontinuierliche Integration (CI) (um weitere inkompatible Änderungen zu blockieren)
  5. Statische Kompatibilität (meistens C ++)
  6. Laufzeitausführbarkeit (minimaler POC)
  7. Laufzeitkompatibilität (meistens Python)
  8. Ausführen Zeitverbesserungen (z. B. Unicode-Unterstützung)
By Ashkan Forouzani

Entwicklungsprozess

Auf hohem Niveau können die hier beschriebenen Ansätze variieren. Bei kleineren Projekten kann es von großem Vorteil sein, die Codebasis zu vereinfachen, während der Code auf eine neue Plattform portiert wird. Der Ansatz, den wir gewählt haben und der sich für eine große und sich dynamisch ändernde Codebasis als sehr hilfreich erwies, bestand darin, Probleme einzeln zu lösen , Änderungen so orthogonal wie möglich halten und Priorisieren Sie die Erhaltung der Semantik gegenüber der Einfachheit .

Manchmal war es erforderlich, potenziell irrelevanten Code zu schreiben, um Bedingungen zu handhaben, die möglicherweise nicht unbedingt in der Produktion aufgetreten sind (z. B. unter Angabe von a) Dateipfad, der niemals Leerzeichen enthalten würde). Zu anderen Zeiten erforderte dies die Einführung allgemeinerer, „mechanischer“ Lösungen, die möglicherweise vermieden werden konnten (z. B. die Verwendung einer std::shared_ptr für bestimmte Fälle, in denen möglicherweise ein anderes Design vorliegt wurde erlaubt, ein std::unique_ptr zu verwenden). Die Beibehaltung der Semantik des vorhandenen Codes war jedoch von entscheidender Bedeutung, um die ständige Einführung neuer Fehler in die Codebasis zu vermeiden, die den Rest des Teams betroffen hätten. Im Nachhinein war dieser Ansatz recht erfolgreich: Fehler auf anderen Plattformen aufgrund von Änderungen im Zusammenhang mit Windows waren recht selten und traten am häufigsten aufgrund von Änderungen der Abhängigkeiten als aufgrund semantischer Änderungen in der Codebasis auf.

Kompilierbarkeit (Abhängigkeiten von Drittanbietern)

Die erste Hürde bestand darin, sicherzustellen, dass Abhängigkeiten von Drittanbietern selbst unter Windows erstellt werden können. Während viele unserer Abhängigkeiten weit verbreitete Bibliotheken waren und die meisten keine größeren Inkompatibilitäten aufwiesen, war dies nicht immer einfach. Insbesondere zufällige Komplexitäten waren in mehreren Facetten des Problems reichlich vorhanden.

  • Die Build-Dateien (insbesondere Bazel-Build-Dateien) für einige Projekte waren unter Windows manchmal unzureichend und erforderten -Patches . Häufig waren dies auf Probleme zurückzuführen, die auf UNIX-Plattformen seltener auftraten, z. B. das Problem, dass Dateipfade mit Leerzeichen zitiert Starten des richtigen Python-Interpreters oder das Problem der korrekten Verknüpfung von gemeinsam genutzten Bibliotheken mit statischen Bibliotheken. Zum Glück war Bazel selbst eines der hilfreichsten Tools zur Lösung dieses Problems: Die integrierte Fähigkeit, externe Arbeitsbereiche zu patchen, ist, wie wir schnell gelernt haben, äußerst hilfreich. Es ermöglichte eine Standardmethode zum Patchen externer Bibliotheken, ohne die Erstellungsprozesse ad-hoc zu ändern und die Codebasis sauber zu halten.
  • Die Erstellungs-Toolchains zeigten ihre eigene Komplexität, da die Auswahl des Compilers oder Linkers häufig betroffen war die Bibliotheken, APIs und Definitionen, auf die man sich verlassen kann. Und leider kann das Wechseln von Compilern auf Bazel ziemlich umständlich sein. Insbesondere unter Windows ist die Verwendung der Microsoft Visual C ++ – Build-Tools häufig die beste Erfahrung. Unsere Toolchains auf anderen Plattformen basierten jedoch auf GCC oder Clang. Glücklicherweise enthält die LLVM-Toolchain unter Windows Clang-Cl , wodurch eine Mischung aus Clang- und MSVC-Funktionen verwendet werden kann, wodurch viele Probleme viel einfacher zu lösen sind. Bibliotheksabhängigkeiten waren im Allgemeinen am schwierigsten. Manchmal war das Problem so banal wie ein fehlender Header oder eine widersprüchliche Definition, die mechanisch behandelt werden konnte und gegen die selbst weit verbreitete Bibliotheken (wie Boost) nicht immun waren. Die Lösungen für diese waren oft einige geeignete Makrodefinitionen oder Dummy-Header. In anderen Fällen – beispielsweise für die Bibliotheken employeeis und Arrow – benötigten Bibliotheken POSIX-Funktionen war unter Windows nicht verfügbar (z. B. Signale oder die Möglichkeit, Dateideskriptoren über UNIX-Domain-Sockets zu übergeben). Diese erwiesen sich in einigen Fällen als viel schwieriger zu lösen. Da wir uns zu diesem Zeitpunkt mehr Gedanken über die Kompilierbarkeit machten, war es jedoch fruchtbar, die Implementierung komplexerer APIs auf ein späteres Stadium zu verschieben und grundlegende Stubs zu verwenden oder den fehlerhaften Code zu deaktivieren, damit der Build fortgesetzt werden kann.

Sobald wir mit dem Kompilieren von Abhängigkeiten fertig waren, konnten wir uns auf das Kompilieren des Kerns von Ray selbst konzentrieren.

Kompilierbarkeit (Ray)

Die nächste Hürde bestand darin, Ray selbst dazu zu bringen Kompilieren Sie zusammen mit dem Plasma -Speicher (Teil des -Pfeils ). Dies war schwieriger, da der Code häufig überhaupt nicht für das Windows-Modell entwickelt wurde und sich stark auf POSIX-APIs stützte. In einigen Fällen ging es lediglich darum, die entsprechenden Header zu finden und zu verwenden (z. B. WinSock2.h anstelle von sys/time.h für struct timeval, was überraschend sein kann) oder Erstellen von Ersatz ( wie für

unistd.h ). In anderen Fällen haben wir den inkompatiblen Code deaktiviert und TODOs zur zukünftigen Adressierung überlassen.

Dies war konzeptionell dem Fall von ähnlich Bei der Behandlung von Abhängigkeiten von Drittanbietern stellte sich heraus, dass Änderungen an der Codebasis minimierte, anstatt die bereitzustellen eleganteste Lösung möglich. Insbesondere, da das Team die Codebasis unter der Annahme einer POSIX-API kontinuierlich aktualisierte und die Codebasis noch nicht mit Windows kompiliert wurde, wurden Lösungen gefunden, die „Drop-In“ waren und mit minimalen Änderungen transparent eingesetzt werden konnten, um eine umfassende Kompatibilität zu erzielen Die gesamte Codebasis war viel hilfreicher, um syntaktische oder semantische Zusammenführungskonflikte zu vermeiden, als Lösungen, die chirurgischer Natur waren. Aus diesem Grund haben wir, anstatt jede Aufrufsite zu ändern, unsere eigenen äquivalenten Windows Shims für POSIX-APIs erstellt, die das gewünschte Verhalten simulierten, auch wenn dies nicht optimal war insgesamt. Dies ermöglichte es uns, die Kompilierbarkeit sicherzustellen und die Verbreitung inkompatibler Änderungen viel schneller zu stoppen (und später jedes einzelne Problem über die gesamte Codebasis im Batch einheitlich anzugehen), als dies sonst möglich wäre.

Verknüpfbarkeit

Sobald die Kompilierbarkeit erreicht war, war das nächste Problem die ordnungsgemäße Verknüpfung von Objektdateien, um ausführbare Binärdateien zu erhalten.Obwohl theoretisch einfach, waren Verknüpfbarkeitsprobleme häufig mit vielen zufälligen Komplexitäten verbunden, wie z. B. den folgenden:

  • Bestimmte Systembibliotheken waren plattformspezifisch und auf anderen Plattformen nicht verfügbar oder unterschiedlich ( wie libpthread)
  • Bestimmte Bibliotheken wurden dynamisch verknüpft, wenn erwartet wurde, dass sie verknüpft werden statisch (oder umgekehrt)
  • Bestimmte Symboldefinitionen fehlten oder waren widersprüchlich (z. B. connect von employeeis Konflikt mit connect für Sockets)
  • Bestimmte Abhängigkeiten funktionierten zufällig auf POSIX-Systemen, erforderten jedoch eine explizite Behandlung in Bazel unter Windows

Die Lösungen für diese waren häufig banale (wenn auch nicht offensichtliche) Änderungen an den Build-Dateien, obwohl in einigen Fällen Abhängigkeiten gepatcht werden mussten. Nach wie vor war Bazels Fähigkeit, praktisch jede Datei von einer externen Quelle zu patchen, äußerst hilfreich, um solche Probleme zu beheben.

Von Richy Great

Die Codebasis erfolgreich zu erstellen war ein wichtiger Meilenstein. Sobald die plattformspezifischen Änderungen integriert waren, würde das gesamte Team nun die Verantwortung dafür übernehmen, die statische Portabilität bei allen zukünftigen Änderungen sicherzustellen, und die Einführung inkompatibler Änderungen würde in der gesamten Codebasis minimiert. ( Dynamische Portabilität war zu diesem Zeitpunkt natürlich noch nicht möglich, da den Stubs unter Windows häufig die erforderliche Funktionalität fehlte und der Code daher zu diesem Zeitpunkt nicht ausgeführt werden konnte.)

Es war ein wichtiger Meilenstein, die Codebasis erfolgreich zu erstellen. Sobald die plattformspezifischen Änderungen integriert waren, würde das gesamte Team nun die Verantwortung dafür übernehmen, die statische Portabilität bei allen zukünftigen Änderungen sicherzustellen, und die Einführung inkompatibler Änderungen würde in der gesamten Codebasis minimiert. ( Dynamische Portabilität war zu diesem Zeitpunkt natürlich noch nicht möglich, da den Stubs unter Windows häufig die erforderliche Funktionalität fehlte und der Code daher zu diesem Zeitpunkt nicht ausgeführt werden konnte.)

Dies verringerte nicht nur die Arbeitslast, sondern ermöglichte es auch, die Windows-Portabilitätsbemühungen zu verlangsamen und langsamer und sorgfältiger vorzugehen, um schwierigere Inkompatibilitäten eingehend zu beheben, ohne die Aufmerksamkeit darauf lenken zu müssen, dass ständig Änderungen in die Codebasis eingeführt werden. Dies ermöglichte auf lange Sicht Lösungen von höherer Qualität, einschließlich einiger Umgestaltungen der Codebasis, die möglicherweise die Semantik auf anderen Plattformen beeinflussten.

Statische Kompatibilität (meistens C / C ++)

Dies war Möglicherweise die zeitaufwändigste Phase, in der Kompatibilitätsstubs entfernt oder ausgearbeitet werden, deaktivierter Code wieder aktiviert und einzelne Probleme behoben werden können. Die statische Natur von C ++ ermöglichte es der Compilerdiagnose, die meisten der erforderlichen Änderungen zu steuern, ohne dass Code ausgeführt werden muss.

Die einzelnen Änderungen hier, einschließlich einiger zuvor erwähnter, wären zu lang, um sie eingehend zu erörtern in ihrer Gesamtheit und bestimmte einzelne Themen wären wahrscheinlich ihrer eigenen langen Blog-Beiträge würdig. Das Laichen und Verwalten von Prozessen zum Beispiel ist tatsächlich ein überraschend komplexes Unterfangen voller Eigenheiten und Fallstricke (siehe hier zum Beispiel ), für das es anscheinend nichts Gutes gibt. plattformübergreifende Bibliothek, obwohl dies ein altes Problem ist. Ein Teil davon ist auf schlechte anfängliche Designs in den Betriebssystem-APIs selbst zurückzuführen. ( Der Chromium-Quellcode bietet einen Einblick in einige der Komplexitäten, die an anderer Stelle häufig außer Acht gelassen werden. In der Tat kann der Chromium-Quellcode häufig als gutes Beispiel für die Handhabung dienen Viele Plattforminkompatibilitäten und Feinheiten.) Unsere anfängliche Missachtung dieser Tatsache führte dazu, dass wir versuchten, Boost.Process zu verwenden, aber das Fehlen einer klaren Besitzersemantik für POSIXs pid_t (das für beide verwendet wird) Prozessidentifikation und -besitz) sowie Fehler in der Boost.Process-Bibliothek selbst führten dazu, dass Fehler in der Codebasis nur schwer zu finden waren, was letztendlich zu unserer Entscheidung führte, diese Änderung rückgängig zu machen und unsere eigene Abstraktion einzuführen. Darüber hinaus war die Boost.Process-Bibliothek selbst für eine Boost-Bibliothek ziemlich schwergewichtig, was unsere Builds erheblich verlangsamte. Stattdessen haben wir unseren eigenen Wrapper für Prozessobjekte geschrieben. Dies stellte sich für unsere Zwecke als recht gut heraus. In diesem Fall haben wir unter anderem in Betracht gezogen, Lösungen auf unsere eigenen Bedürfnisse zuzuschneiden und nicht davon auszugehen, dass bereits vorhandene Lösungen die beste Wahl sind .

Dies war natürlich nur ein kleiner Einblick in das Thema Prozessmanagement.Einige andere Aspekte der Portabilitätsbemühungen sollten möglicherweise ebenfalls näher erläutert werden.

By Thomas Jensen

Teile der Codebasis (z. B. Kommunikation mit dem Plasma-Server in Arrow) werden angenommen die Möglichkeit, UNIX-Domain-Sockets (AF_UNIX) unter Windows zu verwenden. Während neuere Versionen von Windows 10 do UNIX-Domain-Sockets unterstützen, reichen die Implementierungen nicht aus, um alle möglichen Verwendungen von UNIX-Domain abzudecken Sockets und UNIX-Domain-Sockets sind auch nicht besonders elegant: Sie erfordern eine manuelle Bereinigung und können im Falle einer nicht sauberen Beendigung eines Prozesses unnötige Dateien im Dateisystem hinterlassen. Sie bieten keine Möglichkeit, Zusatzdaten zu senden (z Dateideskriptoren) zu einem anderen Prozess. Da Ray Boost.Asio verwendet, waren lokale TCP-Sockets (beide als allgemeine Sockets abstrahiert) der zweckmäßigste Ersatz für UNIX-Domain-Sockets. Daher haben wir zu letzterem gewechselt, lediglich Ersetzen von Dateipfaden durch stringisierte Versionen von TCP / IP-Adressen, ohne dass die Codebasis umgestaltet werden muss.

Dies war nicht ausreichend, da die Verwendung von TCP-Sockets immer noch keine Replikationsmöglichkeit bot Socket-Deskriptoren in andere Prozesse. In der Tat wäre eine angemessene Lösung für dieses Problem gewesen, es insgesamt zu vermeiden. Da dies eine längerfristige Umgestaltung der relevanten Teile der Codebasis erfordern würde (eine Aufgabe, die andere übernommen haben), erschien ein transparenter Ansatz in der Zwischenzeit angemessener. Dies wurde durch die Tatsache erschwert, dass auf UNIX-basierten Systemen das Duplizieren eines Dateideskriptors keine Kenntnis der Identität des Zielprozesses erfordert, während unter Windows das Duplizieren eines Handles eine aktive Manipulation des erfordert Zielprozess. Um diese Probleme zu beheben, haben wir die Möglichkeit implementiert, Dateideskriptoren auszutauschen, indem wir das Handshake-Verfahren beim Starten des Plasma-Speichers durch einen spezielleren -Mechanismus ersetzt haben, der eine TCP-Verbindung herstellen würde Suchen Sie die ID des Prozesses am anderen Ende (eine möglicherweise langsame, aber einmalige Prozedur), duplizieren Sie das Socket-Handle und informieren Sie den Zielprozess über das neue Handle. Dies ist zwar keine Allzwecklösung (und im allgemeinen Fall möglicherweise anfällig für Rennbedingungen) und ein recht ungewöhnlicher Ansatz, aber für Ray-Zwecke hat er gut funktioniert und kann ein Ansatz für andere Projekte sein, die mit demselben Problem konfrontiert sind könnte davon profitieren.

Darüber hinaus war eines der größten Probleme, mit denen wir zu rechnen hatten, unsere Redis -Serverabhängigkeit. Während Microsoft Open Technologies (MSOpenTech) zuvor einen Redis-Port für Windows implementiert hatte, wurde das Projekt abgebrochen und unterstützte folglich nicht die von Ray benötigten Redis-Versionen . Dies ließ uns zunächst davon ausgehen, dass wir den Redis-Server weiterhin auf dem Windows-Subsystem für Linux (WSL) ausführen müssen, was sich für Benutzer wahrscheinlich als unpraktisch erwiesen hätte. Wir waren sehr dankbar, dass ein anderer Entwickler das Projekt fortgesetzt hatte, um spätere Redis-Binärdateien unter Windows zu erstellen (siehe tporadowski / redis ). Dies hat unser Problem enorm vereinfacht und es uns ermöglicht, Ray für Windows nativ zu unterstützen.

Schließlich waren möglicherweise die größten Hürden, mit denen wir konfrontiert waren (wie MSOpenTech Redis und die meisten anderen Nur-POSIX-Programme) das Fehlen trivialer Substitute für einige POSIX-APIs unter Windows. Einige davon ( wie

getppid()) waren unkompliziert, wenn auch etwas langweilig. Möglicherweise war das schwierigste Problem, das während des gesamten Portierungsprozesses auftrat, das der Dateideskriptoren im Vergleich zu den Dateihandles. Ein Großteil des Codes, auf den wir uns verlassen haben (wie der Plasma-Speicher in Arrow), hat die Verwendung von POSIX-Dateideskriptoren (int s) angenommen. Windows verwendet jedoch nativ HANDLE s, die zeigergroß und analog zu size_t sind. An sich ist dies jedoch kein wesentliches Problem, da die Microsoft Visual C ++ – Laufzeit (CRT) eine POSIX-ähnliche Ebene bereitstellt. Die Funktionalität der Ebene ist jedoch eingeschränkt, sie erfordert Übersetzungen an jeder Anrufstelle, die sie nicht unterstützt, und kann insbesondere nicht für Sockets oder Handles für gemeinsam genutzten Speicher verwendet werden. Darüber hinaus wollten wir nicht davon ausgehen, dass HANDLE s immer klein genug ist, um in eine 32-Bit-Ganzzahl zu passen, obwohl dies häufig der Fall war, da unklar war, ob Umstände, von denen wir nichts wussten, könnten diese Annahme stillschweigend brechen.Dies verschärfte unsere Probleme erheblich, da die naheliegendste Lösung darin bestanden hätte, alle int zu erkennen, die Dateideskriptoren in einer Bibliothek wie Arrow darstellen, und sie (und alle ihre Verwendungen) zu ersetzen ) mit einem alternativen Datentyp, der ein fehleranfälliger Prozess war und erhebliche Patches für externen Code beinhaltete, was zu einem erheblichen Wartungsaufwand führte.

In dieser Phase war es ziemlich schwierig zu entscheiden, was zu tun ist. Die -Lösung von MSOpenTech Redis für dasselbe Problem machte deutlich, dass dies eine ziemlich entmutigende Aufgabe war, da sie dieses Problem durch Erstellen einer einzelnen prozessweiten Dateideskriptortabelle über der vorhandenen CRT-Implementierung, sodass sie sich mit der Thread-Sicherheit befassen und sie zum Abfangen von Alle Verwendungen von POSIX-APIs (auch solche, die bereits verarbeitet werden konnten) dienen lediglich der Übersetzung von Dateideskriptoren. Stattdessen haben wir uns für einen ungewöhnlichen Ansatz entschieden: Wir haben die POSIX-Übersetzungsschicht in der CRT erweitert . Dazu wurden inkompatible Handles zum Zeitpunkt der Erstellung identifiziert und diese Handles in die Puffer einer überflüssigen Pipe „geschoben“, wobei stattdessen der Deskriptor dieser Pipe zurückgegeben wurde. Wir mussten dann nur die Verwendungsseiten dieser Handles ändern, die entscheidend trivial zu identifizieren waren, da sie alle Socket und speicherabgebildete Datei APIs. Tatsächlich hat dies dazu beigetragen, die Notwendigkeit eines Patches zu vermeiden, da wir lediglich viele Funktionen über -Makros umleiten konnten.

Die Entwicklung dieser einzigartigen Erweiterungsschicht (in win32fd.h) war signifikant (und ziemlich unorthodox), es hat sich wahrscheinlich gelohnt, da die Übersetzungsschicht war im Vergleich tatsächlich recht klein und ermöglichte es uns, die meisten nicht zusammenhängenden Probleme (wie das Sperren der Dateideskriptortabelle mit mehreren Threads) an die CRT-APIs zu delegieren. Durch Nutzung anonymer Pipes aus derselben globalen Dateideskriptortabelle (trotz unseres fehlenden direkten Zugriffs darauf) konnten wir außerdem vermeiden, dass wir abfangen und übersetzen mussten Dateideskriptoren für andere Funktionen, die bereits direkt ausgeführt werden könnten. Dadurch konnte ein Großteil des Codes bei minimalen Auswirkungen auf die Leistung im Wesentlichen unverändert bleiben, bis wir später die Möglichkeit hatten, den Code umzugestalten und bessere Wrapper auf einer höheren Ebene bereitzustellen (z. B. über Boost.Asio-Wrapper). Es ist durchaus möglich, dass durch eine Erweiterung dieser Ebene andere Projekte wie Redis viel nahtloser und mit weitaus weniger drastischen Änderungen oder potenziellen Fehlern auf Windows portiert werden können.

Von Andrea Leopardi

Laufzeitausführbarkeit (Proof-of-Concept)

Nachdem wir der Ansicht waren, dass der Ray-Kern ordnungsgemäß funktioniert, bestand der nächste Meilenstein darin, sicherzustellen, dass ein Python-Test unsere Codepfade erfolgreich ausführen kann. Anfangs haben wir dies nicht priorisiert. Dies erwies sich jedoch als Fehler, da spätere Änderungen durch andere Entwickler im Team tatsächlich zu dynamischeren Inkompatibilitäten mit Windows führten und das CI-System solche Brüche nicht erkennen konnte. Wir haben es daher später zur Priorität gemacht, einen Minimaltest für die Windows-Builds auszuführen, um weitere Unterbrechungen des Builds zu vermeiden.

Für die Zum größten Teil waren unsere Bemühungen erfolgreich, und alle verbleibenden Fehler im Ray-Kern befanden sich in vorhersehbaren Teilen der Codebasis (obwohl das Beheben dieser Fehler häufig das Durchlaufen von Multiprozesscode erforderte, was alles andere als trivial war). Es gab jedoch mindestens eine etwas unangenehme Überraschung auf der C-Seite und eine auf der Python-Seite, die uns (unter anderem) dazu ermutigten, die Softwaredokumentation in Zukunft proaktiver zu lesen.

Auf der C-Seite beruhte unser anfänglicher Handshake-Wrapper für den Austausch von Socket-Handles auf dem naiven Ersetzen von sendmsg und recvmsg mit WSASendMsg und WSARecvMsg. Diese Windows-APIs entsprachen am ehesten den POSIX-APIs und schienen daher eine naheliegende Wahl zu sein. Bei der Ausführung stürzte der Code jedoch ständig ab, und die Ursache des Problems war unklar. Einige Debugging-Vorgänge (einschließlich Debug-Versionen von Builds & -Laufzeiten) haben gezeigt, dass das Problem bei den an WSASendMsg übergebenen Stapelvariablen lag. Weiteres Debuggen und genaue Überprüfung des Speicherinhalts deuteten darauf hin, dass das Problem möglicherweise das Feld msg_flags von WSAMSG war, da dies das einzige nicht initialisierte Feld war Feld.Dies schien jedoch irrelevant zu sein: msg_flags wurde lediglich von flags in struct msghdr übersetzt. Dies wurde bei der Eingabe nicht verwendet und wurde lediglich als Ausgabeparameter verwendet. Das Lesen der Dokumentation ergab jedoch das Problem: Unter Windows diente das Feld auch als Eingabeparameter , sodass die Nichtinitialisierung zu einem unvorhersehbaren Verhalten führte! Dies war für uns ziemlich unerwartet und führte zu zwei wichtigen Fortschritten: , um die Dokumentation jeder Funktion sorgfältig zu lesen und Darüber hinaus geht es bei der Initialisierung von -Variablen nicht nur darum, die Korrektheit mit aktuellen APIs sicherzustellen, sondern auch darum, den Code für zukünftige Änderungen an den Ziel-APIs .

Auf der Python-Seite ist ein anderes Problem aufgetreten. Unsere nativen Python-Module konnten trotz fehlender offensichtlicher Probleme zunächst nicht geladen werden. Nach mehrtägigen Vermutungen, dem Durchlaufen der Assembly und des CPython-Quellcodes und dem Überprüfen der Variablen in der CPython-Codebasis stellte sich heraus, dass das Problem das Fehlen eines .pyd -Suffixes in dynamischem Python war Bibliotheken unter Windows. Wie sich herausstellt, weigert sich Python aus uns unklaren Gründen, selbst .dll -Dateien unter Windows als Python-Module zu laden, obwohl native gemeinsam genutzte Bibliotheken normalerweise sogar mit jeder Datei geladen werden können Erweiterung. In der Tat stellte sich heraus, dass diese Tatsache auf der Python-Website dokumentiert war. Leider konnte das Vorhandensein einer solchen Dokumentation unmöglich bedeuten, dass wir nach ihr suchten.

Trotzdem konnte Ray schließlich erfolgreich unter Windows ausgeführt werden, und dies schloss den nächsten Meilenstein ab und lieferte einen Beweis des Konzepts für das Unternehmen.

Von Hitesh Choudhary

Laufzeitkompatibilität (meistens Python)

Zu diesem Zeitpunkt war einmal der Kern von Ray Bei der Arbeit konnten wir uns auf die Portierung von Code auf höherer Ebene konzentrieren. Einige Probleme waren recht einfach zu beheben – beispielsweise haben einige Python-APIs, die nur UNIX enthalten (z. B. os.uname()[1]), unter Windows häufig geeignete Ersetzungen (z. B. ), und sie zu finden, war eine Frage des Wissens, nach allen Instanzen von ihnen in der Codebasis zu suchen. Andere Probleme waren schwieriger zu finden oder zu lösen. Manchmal waren sie auf die Verwendung von POSIX-spezifischen Befehlen (wie ps) zurückzuführen, die alternative Ansätze erforderten (wie die Verwendung von psutil für Python). In anderen Fällen waren sie auf Inkompatibilitäten in Bibliotheken von Drittanbietern zurückzuführen. Wenn beispielsweise ein Socket unter Windows getrennt wird, wird ein Fehler ausgelöst, der nicht zu leeren Lesevorgängen führt. Die Python-Bibliothek für Redis schien dies nicht zu handhaben. Solche Verhaltensunterschiede erforderten explizites Affen-Patching , um gelegentlich verwirrende Fehler zu vermeiden, die beim Beenden von Ray auftreten würden.

Während einige solche Probleme auftreten ziemlich langweilig, aber wahrscheinlich zu erwarten (z. B. das Ersetzen von Verwendungen von /tmp durch das temporäre Verzeichnis der Plattform oder das Vermeiden der Annahme, dass alle absoluten Pfade mit einem Schrägstrich beginnen), waren einige etwas unerwartet (z Konflikte mit Portreservierungen ) oder (wie so oft) aufgrund falscher Annahmen und deren Verständnis hängen vom Verständnis der Windows-Architektur und ihrer Ansätze zur Abwärtskompatibilität ab.

Eine solche Geschichte dreht sich um die Verwendung von Schrägstrichen als Verzeichnistrennzeichen unter Windows. Im Allgemeinen scheinen diese gut zu funktionieren und werden häufig von Entwicklern verwendet. Dies ist jedoch tatsächlich auf die automatische Konvertierung von Schrägstrichen in Backslashes in den Windows-Subsystembibliotheken im Benutzermodus zurückzuführen, und bestimmte automatische Verarbeitungen können unterdrückt werden, indem Pfaden explizit ein Präfix \\?\ vorangestellt wird. Dies ist hilfreich, um bestimmte Kompatibilitätsfunktionen (z. B. lange Pfade) zu umgehen. Wir haben einen solchen Pfad jedoch nie explizit verwendet und gingen davon aus, dass von den Benutzern erwartet werden kann, dass sie eine ungewöhnliche Verwendung in unseren experimentellen Versionen vermeiden. Später stellte sich jedoch heraus, dass beim Aufrufen bestimmter Python-Tests durch Bazel Pfade in diesem Format verarbeitet wurden, um die Verwendung langer Pfade zu ermöglichen, und dies deaktivierte die automatische Übersetzung, auf die wir uns implizit stützten. Dies führte uns zu wichtigen Erkenntnissen: Erstens ist es im Allgemeinen besser, APIs auf die für das Zielsystem am besten geeignete Weise zu verwenden, da dies die geringste Wahrscheinlichkeit für das Auftreten unerwarteter Probleme bietet .Zweitens und vor allem ist es einfach ein Trugschluss anzunehmen, dass die Umgebung eines Benutzers vorhersehbar ist . Die Realität ist, dass moderne Software fast immer darauf angewiesen ist, Code von Drittanbietern auszuführen, dessen genaues Verhalten uns nicht bekannt ist. Selbst wenn davon ausgegangen werden kann, dass ein Benutzer problematische Situationen vermeidet, sind sich Software von Drittanbietern solcher versteckten Annahmen überhaupt nicht bewusst. Daher ist es wahrscheinlich, dass sie ohnehin nicht nur bei Benutzern, sondern auch bei den Softwareentwicklern selbst auftreten, was zu Fehlern führt, die beim Schreiben des ursprünglichen Codes schwieriger aufzuspüren als zu beheben sind. Daher ist es wichtig, beim Entwerfen eines robusten Systems zu vermeiden, dass der Programmfreundlichkeit (das Gegenteil von „Benutzerfreundlichkeit“) zu viel Gewicht beigemessen wird.

(Spaß beiseite: In der Tat unter Windows Pfade kann in der Tat Anführungszeichen und viele andere Sonderzeichen enthalten, von denen normalerweise angenommen wird, dass sie unzulässig sind. Dies tritt auf, wenn alternative NTFS-Datenströme verwendet werden. Diese sind jedoch selten und komplex genug, dass selbst Standard-Sprachbibliotheken sie häufig nicht verarbeiten.)

Nachdem die wichtigsten Probleme behoben waren, konnten jedoch viele Tests Windows weitergeben und die erste experimentelle Windows-Implementierung von Ray erstellen.

Von Ross Sneddon

Laufzeitverbesserungen (z. B. Unicode-Unterstützung)

Zu diesem Zeitpunkt kann ein Großteil des Kerns von Ray unter Windows genauso verwendet werden wie auf anderen Plattformen. Dennoch bleiben einige Probleme bestehen, die fortlaufende Anstrengungen erfordern, um sie zu beheben.

Die Unicode-Unterstützung ist ein solches Problem. Aus historischen Gründen verfügt das Windows-Subsystem im Benutzermodus über zwei Versionen der meisten APIs: eine ANSI-Version, die Einzelbyte-Zeichensätze unterstützt, und eine Unicode-Version, die UCS-2 oder UTF-16 unterstützt (abhängig von der Angaben zur betreffenden API). Leider ist keines davon UTF-8; Selbst für die grundlegende Unterstützung von Unicode müssen Zeichenfolgen mit breiten Zeichen (basierend auf wchar_t) in der gesamten Codebasis verwendet werden. (nb: Tatsächlich hat Microsoft kürzlich versucht, UTF-8 als Codepage einzuführen, aber es wird nicht gut genug unterstützt, um dieses Problem nahtlos zu beheben, zumindest ohne sich auf potenziell undokumentierte und spröde Windows-Interna zu verlassen.)

Traditionell verarbeiten Windows-Programme Unicode mithilfe von Makros wie _T() oder TEXT(), die auf schmal oder breit erweitert werden -Zeichenliterale, abhängig davon, ob ein Unicode-Build angegeben ist, und verwenden Sie TCHAR als generische Zeichentypen. In ähnlicher Weise haben die meisten C-APIs TCHAR -abhängige Versionen (z. B. _tcslen() anstelle von strlen()) um die Kompatibilität mit beiden Codetypen zu ermöglichen. Die Migration einer UNIX-basierten Codebasis auf dieses Modell ist jedoch ein ziemlich kompliziertes Unterfangen. Dies wurde in Ray noch nicht durchgeführt, und daher unterstützt Ray zum jetzigen Zeitpunkt keinen ordnungsgemäßen Unicode in (zum Beispiel) Dateipfaden unter Windows, und der beste Ansatz hierfür ist möglicherweise noch eine offene Frage.

Ein weiteres solches Problem ist der prozessübergreifende Kommunikationsmechanismus. Während TCP-Sockets unter Windows einwandfrei funktionieren können, sind sie nicht optimal, da sie eine Schicht unnötiger Komplexität in die Logik einführen (z. B. Zeitüberschreitungen, Keep-Alives, Nagles Algorithmus), zu einer versehentlichen Erreichbarkeit durch nicht lokale Hosts führen können Führen Sie einen gewissen Leistungsaufwand ein. In Zukunft könnten Named Pipes einen besseren Ersatz für UNIX-Domain-Sockets unter Windows bieten. Selbst unter Linux können sich Pipes oder sogenannte abstract UNIX-Domain-Sockets als bessere Alternativen erweisen. da sie keine Unordnung und Bereinigung von Socket-Dateien im Dateisystem erfordern.

Ein weiteres Beispiel für ein solches Problem ist die Kompatibilität von BSD-Sockets bzw. deren Fehlen. Eine ausgezeichnete Antwort auf StackOverflow behandelt einige der Probleme ausführlich, aber kurz, während gängige Socket-APIs Ableitungen der ursprünglichen BSD-Socket-API sind, implementieren verschiedene Plattformen ähnliche Sockets Flaggen anders. Insbesondere Konflikte mit vorhandenen IP-Adressen oder TCP-Ports können plattformübergreifend zu unterschiedlichen Verhaltensweisen führen. Obwohl die Probleme hier schwer zu beschreiben sind, ist das Endergebnis, dass es möglicherweise schwierig ist, mehrere Instanzen von Ray gleichzeitig auf demselben Host zu verwenden. (Da dies vom Verhalten des Betriebssystemkerns abhängt, wirkt es sich auch auf die WSL aus.) Dies ist ein weiteres bekanntes Problem, dessen Lösung im aktuellen System eher involviert und nicht vollständig behoben ist.

Schlussfolgerung

Das Portieren einer Codebasis wie der von Ray nach Windows war eine wertvolle Erfahrung, die die Vor- und Nachteile vieler Aspekte der Softwareentwicklung und ihre Auswirkungen auf die Codepflege hervorhebt.Die vorstehende Beschreibung hebt nur einige der Hindernisse hervor, auf die Sie unterwegs gestoßen sind. Aus dem Prozess können viele nützliche Schlussfolgerungen gezogen werden, von denen einige wertvoll sein können, um sie hier für andere Projekte zu teilen, in der Hoffnung, ein ähnliches Ziel zu erreichen.

Erstens haben wir in einigen Fällen diese späteren Versionen tatsächlich später gefunden Einige Bibliotheken (z. B. Hiredis) hatten bereits einige Probleme gelöst, die wir angegangen waren. Die Lösungen waren nicht immer offensichtlich, da (zum Beispiel) die Version von Hiredis in den jüngsten Redis-Versionen tatsächlich eine veraltete Kopie von Hiredis war, was uns zu der Annahme führte, dass einige Probleme noch nicht behoben wurden. Spätere Überarbeitungen haben auch nicht immer alle vorhandenen Kompatibilitätsprobleme vollständig behoben. Trotzdem hätte es möglicherweise ein wenig Mühe gespart, nach vorhandenen Lösungen für einige Probleme zu suchen, um zu vermeiden, dass sie erneut gelöst werden müssen.

Von John Barkiple

Zweitens Die Software-Lieferkette ist häufig komplex. . Fehler können sich natürlich auf jeder Ebene verstärken, und es ist ein Trugschluss, darauf zu vertrauen, dass selbst weit verbreitete Open-Source-Tools „kampferprobt“ und daher robust sind, insbesondere wenn sie in verwendet werden Wut . Darüber hinaus stehen vielen langjährigen oder häufig auftretenden Softwareentwicklungsproblemen keine zufriedenstellenden Lösungen zur Verfügung, insbesondere (aber nicht nur), wenn sie eine Kompatibilität zwischen verschiedenen Systemen erfordern. Abgesehen von bloßen Macken stießen wir bei der Portierung von Ray auf Windows regelmäßig auf Fehler in zahlreichen Softwareteilen und meldeten diese häufig, einschließlich, aber nicht beschränkt auf einen Git-Fehler Linux , das die Bazel-Nutzung beeinflusst hat, Redis (Linux) , glog , psutil (Analysefehler, der die WSL betrifft) , grpc , many schwer zu identifizierende Fehler in Bazel selbst (z. B. 1 , 2 , 3 , 4 ), Travis CI und GitHub-Aktionen unter anderem. Dies ermutigte uns, auch der Komplexität unserer Abhängigkeiten mehr Aufmerksamkeit zu schenken.

Drittens in Werkzeuge und Infrastruktur zu investieren zahlt sich langfristig aus. Schnellere Builds ermöglichen eine schnellere Entwicklung und leistungsstärkere Tools ermöglichen eine einfachere Lösung komplexer Probleme. In unserem Fall hat uns die Verwendung von Bazel in vielerlei Hinsicht geholfen, obwohl es alles andere als perfekt ist und eine steile Lernkurve auferlegt. Es ist selten einfach, etwas Zeit (möglicherweise mehrere Tage) zu investieren, um die Fähigkeiten, Stärken und Mängel eines neuen Tools zu erlernen, aber wahrscheinlich von Vorteil für die Code-Wartung. In unserem Fall konnten wir durch eine gründliche Lektüre der Bazel-Dokumentation eine Vielzahl zukünftiger Probleme und Lösungen viel schneller lokalisieren. Darüber hinaus hat es uns auch geholfen, Tools in Bazel zu integrieren, die anscheinend nur wenigen anderen gelungen sind, wie beispielsweise Clangs Include-What-You-Use-Tool .

Viertens und wie bereits erwähnt, ist es ratsam, sichere Codierungspraktiken durchzuführen, z. B. den Speicher vor der Verwendung zu initialisieren, wenn Es gibt keinen signifikanten Kompromiss. Selbst der vorsichtigste Ingenieur kann nicht unbedingt die zukünftige Entwicklung des zugrunde liegenden Systems vorhersagen, die Annahmen stillschweigend ungültig machen könnte.

Schließlich, wie es in der Medizin allgemein der Fall ist, Prävention ist das beste Heilmittel . Die Berücksichtigung möglicher zukünftiger Entwicklungen und die Codierung auf standardisierte Schnittstellen ermöglichen ein erweiterbareres Code-Design, als dies nach Auftreten von Inkompatibilitäten leicht möglich ist.

Der Port von Ray zu Windows ist zwar noch nicht vollständig, aber durchaus abgeschlossen Bisher erfolgreich, und wir hoffen, dass der Austausch unserer Erfahrungen und Lösungen als hilfreicher Leitfaden für andere Entwickler dienen kann, die eine ähnliche Reise in Betracht ziehen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.