Przenoszenie Ray do Microsoft Windows

Autor: Johny vino

(Mehrdad Niknami) (28 września 2020 r.)

Tło

Kiedy Ray został po raz pierwszy uruchomiony, był napisany dla systemów operacyjnych Linux i macOS – opartych na UNIX. Brakowało wsparcia dla systemu Windows, najpopularniejszego systemu operacyjnego dla komputerów stacjonarnych, ale jest to ważne dla długoterminowego sukcesu projektu. Chociaż Podsystem Windows dla systemu Linux (WSL) zapewniał możliwą opcję dla niektórych użytkowników, ograniczył obsługę do najnowszych wersji systemu Windows 10 i znacznie utrudnił kodowanie do interakcji z natywnym systemem operacyjnym Windows, co oznaczało złe wrażenia dla użytkowników. Biorąc to pod uwagę, naprawdę mieliśmy nadzieję, że zapewnimy użytkownikom natywną obsługę systemu Windows, jeśli w ogóle będzie to praktyczne.

Jednak przeniesienie promienia do systemu Windows nie było prostym zadaniem. Podobnie jak wiele innych projektów, które po fakcie próbują osiągnąć mobilność, napotkaliśmy wiele wyzwań, których rozwiązania nie były oczywiste. W tym poście na blogu zamierzamy zagłębić się w szczegóły techniczne procesu, który zastosowaliśmy, aby zapewnić zgodność Ray z systemem Windows. Mamy nadzieję, że pomoże to innym projektom o podobnej wizji zrozumieć niektóre z potencjalnych wyzwań i sposób, w jaki można sobie z nimi poradzić.

Przegląd

Modernizacja obsługi systemu Windows stanowiła coraz większe wyzwanie w miarę dalszego rozwoju Ray. Przed rozpoczęciem spodziewaliśmy się, że niektóre aspekty kodu będą stanowić znaczną część naszych wysiłków związanych z przenoszeniem, w tym:

  • Komunikacja między procesami (IPC) i pamięć współdzielona
  • Uchwyty obiektów i uchwyty / deskryptory plików (FD)
  • Tworzenie procesów i zarządzanie nimi, w tym sygnalizacja
  • Zarządzanie plikami, zarządzanie wątkami i asynchroniczne operacje we / wy
  • Użycie powłoki i poleceń systemowych
  • Redis

Biorąc pod uwagę zakres problemów, szybko zdaliśmy sobie sprawę, że zapobieganie wprowadzaniu dalszych niekompatybilności powinno mieć pierwszeństwo przed próbą rozwiązania istniejących zagadnienia. Dlatego podjęliśmy próbę wykonania z grubsza następujących kroków, chociaż jest to nieco uproszczenie, ponieważ czasami rozwiązywane są określone problemy na różnych etapach:

  1. Kompatybilność z zależnościami innych firm
  2. Kompatybilność z Ray (przez puste wstawki & TODO)
  3. Możliwość tworzenia powiązań
  4. Ciągła integracja (CI) (aby zablokować dalsze niekompatybilne zmiany)
  5. Zgodność statyczna (głównie C ++)
  6. Wykonalność w czasie wykonywania (minimalny POC)
  7. Zgodność w czasie wykonywania (głównie Python)
  8. Uruchom ulepszenia czasu (np. obsługa Unicode)
Przez Ashkan Forouzani

Proces rozwoju

Na wysokim poziomie podejścia tutaj mogą różnią się. W przypadku mniejszych projektów bardzo korzystne może być uproszczenie bazy kodu w tym samym czasie, gdy kod jest przenoszony na nową platformę. Jednak zastosowane przez nas podejście, które okazało się bardzo pomocne w przypadku dużej i dynamicznie zmieniającej się bazy kodu, polegało na rozwiązywaniu problemów pojedynczo , utrzymuj zmiany tak prostopadłe do siebie, jak to możliwe, i przedkładaj zachowanie semantyki nad prostotę .

Czasami wymagało to napisania potencjalnie obcego kodu do obsługi warunków, które niekoniecznie wystąpiły w produkcji (powiedzmy, cytowanie ścieżka do pliku, która nigdy nie miałaby spacji). W innych przypadkach wymagało to wprowadzenia bardziej ogólnych, „mechanicznych” rozwiązań, których można było uniknąć (na przykład std::shared_ptr w niektórych przypadkach, gdy inny projekt mógł mieć zezwolono na użycie std::unique_ptr). Jednak zachowanie semantyki istniejącego kodu było absolutnie kluczowe, aby uniknąć ciągłego wprowadzania nowych błędów do bazy kodu, które miałyby wpływ na resztę zespołu. Z perspektywy czasu to podejście było całkiem skuteczne: błędy na innych platformach wynikające ze zmian związanych z systemem Windows były dość rzadkie i najczęściej występowały z powodu zmian w zależnościach niż z powodu zmian semantycznych w bazie kodu.

Kompatybilność (zależności innych firm)

Pierwszą przeszkodą było zapewnienie, że zależności innych firm mogą być budowane w systemie Windows. Chociaż wiele naszych zależności było powszechnie używanymi bibliotekami i większość z nich nie miała głównych niezgodności, nie zawsze było to proste. W szczególności przypadkowa złożoność obfitowała w kilka aspektów problemu.

  • Pliki kompilacji (w szczególności pliki kompilacji Bazel) dla niektórych projektów były czasami nieodpowiednie w systemie Windows, wymagając łatek . Często wynikały one z problemów, które występowały rzadziej na platformach UNIX, takich jak problem cytowania ścieżek plików ze spacjami, uruchomienie odpowiedniego interpretera Pythona lub problem z poprawnym łączeniem bibliotek współdzielonych w porównaniu z bibliotekami statycznymi. Na szczęście jednym z najbardziej pomocnych narzędzi w rozwiązaniu tego problemu był sam Bazel: jego wbudowana możliwość łatania zewnętrznych obszarów roboczych jest, jak szybko się przekonaliśmy, niezwykle pomocna. Pozwoliło to na standardową metodę łatania bibliotek zewnętrznych bez modyfikowania procesów kompilacji w sposób ad-hoc, utrzymując czystość kodu źródłowego.
  • Łańcuchy narzędzi do budowania wykazywały własną złożoność, ponieważ często wpływ na wybór kompilatora lub linkera biblioteki, interfejsy API i definicje, na których można polegać. Niestety, przełączanie kompilatorów na Bazel może być dość uciążliwe . W szczególności w systemie Windows najlepsze doświadczenia często wiąże się z wykorzystaniem narzędzi do kompilacji Microsoft Visual C ++, ale nasze zestawy narzędzi na innych platformach były oparte na GCC lub Clang. Na szczęście łańcuch narzędzi LLVM jest dostarczany z Clang-Cl w systemie Windows, co pozwala na użycie kombinacji funkcji Clang i MSVC, znacznie ułatwiając rozwiązywanie wielu problemów.
  • Zależności między bibliotekami generalnie sprawiały najwięcej trudności. Czasami problem był tak przyziemny, jak brakujący nagłówek lub sprzeczna definicja, którą można było rozwiązać mechanicznie, na co nawet powszechnie używane biblioteki (takie jak Boost) nie były odporne. Rozwiązaniem tego problemu było często kilka odpowiednich definicji makr lub fikcyjnych nagłówków. W innych przypadkach – na przykład w przypadku bibliotek hiredis i Arrow – biblioteki wymagały funkcjonalności POSIX, był niedostępny w systemie Windows (np. sygnały lub możliwość przekazywania deskryptorów plików przez gniazda domeny UNIX). W niektórych przypadkach okazało się to znacznie trudniejsze do rozwiązania. Ponieważ byliśmy bardziej zaniepokojeni kompilacją na tym etapie, owocne było odłożenie implementacji bardziej złożonych interfejsów API na późniejszy etap i wykorzystanie podstawowych kodów pośredniczących lub wyłączenie kodu powodującego naruszenie, aby umożliwić kontynuację kompilacji.

Gdy skończyliśmy kompilować zależności, mogliśmy skupić się na kompilacji samego rdzenia Raya.

Kompilowalność (Ray)

Następną przeszkodą było przekonanie samego Raya kompilować razem ze sklepem Plazma (część Arrow ). Było to trudniejsze, ponieważ kod często w ogóle nie był zaprojektowany dla modelu Windows i w dużym stopniu opierał się na API POSIX. W niektórych przypadkach była to tylko kwestia znalezienia i zastosowania odpowiednich nagłówków (takich jak WinSock2.h zamiast sys/time.h dla struct timeval, co może być zaskakujące) lub tworzenie zamienników (, na przykład dla

unistd.h ). W innych przypadkach wyłączyliśmy niezgodny kod i pozostawiliśmy TODO , aby zająć się w przyszłości.

Chociaż koncepcyjnie było to podobne do przypadku obsługi zależności innych firm, unikalnym problemem wysokiego poziomu, który pojawił się tutaj, był minimalizowanie zmian w bazie kodu , zamiast dostarczania najbardziej eleganckie możliwe rozwiązanie. W szczególności, ponieważ zespół nieustannie aktualizował bazę kodu przy założeniu POSIX API i ponieważ baza kodu nie została jeszcze skompilowana z Windows, rozwiązania, które były „drop-in” i które można było w przejrzysty sposób stosować przy minimalnych zmianach, aby uzyskać pełną kompatybilność w cała baza kodów była znacznie bardziej pomocna w unikaniu konfliktów syntaktycznych lub semantycznych, niż rozwiązania, które miały charakter chirurgiczny. Z tego powodu, zamiast modyfikować każdą witrynę wywołań, stworzyliśmy własny odpowiednik Windows podkładki dla interfejsów API POSIX, które symulowały pożądane zachowanie, nawet jeśli było to nieoptymalne ogólny. Pozwoliło nam to znacznie szybciej zapewnić kompatybilność i powstrzymać rozprzestrzenianie się niekompatybilnych zmian (a później jednolicie zająć się każdym pojedynczym problemem w całej bazie kodu w partii) niż byłoby to możliwe w innym przypadku.

Możliwość tworzenia powiązań

Po osiągnięciu kompilowalności, następnym problemem było prawidłowe połączenie plików obiektowych w celu uzyskania wykonywalnych plików binarnych.Choć teoretycznie proste, problemy z możliwością tworzenia powiązań często wiązały się z wieloma przypadkowymi złożeniami, takimi jak:

  • Niektóre biblioteki systemowe były specyficzne dla platformy i niedostępne lub inne na innych platformach ( na przykład libpthread)
  • Niektóre biblioteki zostały połączone dynamicznie, gdy miały być połączone statycznie (lub odwrotnie)
  • Brak niektórych definicji symboli lub są one w konflikcie (na przykład connect z hiredis w konflikcie z connect dla gniazd)
  • Niektóre zależności działały przypadkowo w systemach POSIX, ale wymagały jawnej obsługi w Bazel w systemie Windows

Rozwiązaniem tych problemów były często przyziemne (choć być może nieoczywiste) zmiany w plikach kompilacji, chociaż w niektórych przypadkach konieczne było załatanie zależności. Tak jak poprzednio, możliwość łatania przez Bazel praktycznie dowolnego pliku z zewnętrznego źródła była niezwykle pomocna w rozwiązywaniu takich problemów.

Autor: Richy Great

Pomyślnie udało się zbudować bazę kodu ważny kamień milowy. Po zintegrowaniu zmian specyficznych dla platformy cały zespół przejmie teraz odpowiedzialność za zapewnienie statycznej przenośności we wszystkich przyszłych zmianach, a wprowadzanie niekompatybilnych zmian zostanie zminimalizowane w całej bazie kodu. ( Dynamiczna przenośność była oczywiście nadal niemożliwa w tym momencie, ponieważ kody pośredniczące często nie miały niezbędnej funkcjonalności w systemie Windows, w wyniku czego kod nie mógł zostać uruchomiony na tym etapie).

Pomyślne zbudowanie bazy kodu było kamieniem milowym. Po zintegrowaniu zmian specyficznych dla platformy cały zespół przejmie teraz odpowiedzialność za zapewnienie statycznej przenośności we wszystkich przyszłych zmianach, a wprowadzanie niekompatybilnych zmian zostanie zminimalizowane w całej bazie kodu. ( Dynamiczna przenośność była oczywiście nadal niemożliwa w tym momencie, ponieważ kody pośredniczące często nie miały niezbędnej funkcjonalności w systemie Windows, w wyniku czego kod nie mógł zostać uruchomiony na tym etapie).

Nie tylko zmniejszyło to obciążenie pracą, ale pozwoliło spowolnić wysiłek związany z przenoszeniem systemu Windows i postępować wolniej i ostrożniej, aby dogłębnie zająć się trudniejszymi niezgodnościami, bez konieczności zwracania uwagi na przełomowe zmiany, które są stale wprowadzane do bazy kodu. Pozwoliło to w dłuższej perspektywie na rozwiązania o wyższej jakości, w tym pewne refaktoryzacje bazy kodu, która potencjalnie wpłynęła na semantykę na innych platformach.

Zgodność statyczna (głównie C / C ++)

To było prawdopodobnie najbardziej czasochłonny etap, na którym kody zgodności zostałyby usunięte lub opracowane, wyłączony kod mógłby zostać ponownie włączony i można by rozwiązać poszczególne problemy. Statyczny charakter C ++ pozwolił diagnostyce kompilatora na poprowadzenie większości takich niezbędnych zmian bez konieczności wykonywania jakiegokolwiek kodu.

Poszczególne zmiany tutaj, w tym niektóre wspomniane wcześniej, byłyby zbyt długie, aby je dogłębnie omówić w całości, a niektóre indywidualne problemy byłyby prawdopodobnie warte ich własnych, długich postów na blogu. Na przykład tworzenie i zarządzanie procesami jest w rzeczywistości zaskakująco złożonym przedsięwzięciem pełnym dziwactw i pułapek (patrz na przykład tutaj ), dla którego wydaje się, że nie ma sensu, biblioteka wieloplatformowa, mimo że jest to stary problem. Częściowo wynika to ze słabych początkowych projektów samych interfejsów API systemu operacyjnego. ( Kod źródłowy Chromium pozwala rzucić okiem na niektóre zawiłości, które są często pomijane w innych miejscach. W rzeczywistości kod źródłowy Chromium może często służyć jako świetna ilustracja obsługi wiele niekompatybilności i subtelności platform.) Nasze początkowe lekceważenie tego faktu skłoniło nas do próby użycia Boost.Process, ale brak jasnej semantyki własności dla POSIX pid_t (który jest używany do obu identyfikacja procesu i własność), a także błędy w samej bibliotece Boost.Process spowodowały, że trudno było znaleźć błędy w bazie kodu, co ostatecznie doprowadziło do naszej decyzji o cofnięciu tej zmiany i wprowadzeniu własnej abstrakcji. Co więcej, biblioteka Boost.Process była dość ciężka, nawet jak na bibliotekę Boost, co znacznie spowolniło nasze kompilacje. Zamiast tego napisaliśmy własne opakowanie dla obiektów procesów. Okazało się to całkiem dobre dla naszych celów. Jednym z naszych wniosków w tym przypadku było rozważenie możliwości dostosowania rozwiązań do naszych własnych potrzeb, a nie założenie, że wcześniej istniejące rozwiązania są najlepszym wyborem .

To był oczywiście tylko urywek problemu zarządzania procesami.Być może warto omówić również inne aspekty związane z przenoszeniem.

Przez Thomas Jensen

Przyjęto fragmenty kodu (takie jak komunikacja z serwerem Plazmy w Arrow) możliwość korzystania z gniazd domeny UNIX (AF_UNIX) w systemie Windows. Podczas gdy najnowsze wersje systemu Windows 10 obsługują gniazda domeny UNIX, implementacje nie są wystarczające do pokrycia wszystkich możliwych zastosowań domeny UNIX gniazda, ani gniazda domeny UNIX nie są szczególnie eleganckie: wymagają ręcznego czyszczenia i mogą pozostawić niepotrzebne pliki w systemie plików w przypadku nieczystego zakończenia procesu oraz nie zapewniają możliwości wysyłania danych pomocniczych (takich jak deskryptory plików) do innego procesu. Ponieważ Ray używa Boost.Asio, najbardziej celowym zamiennikiem dla gniazd domeny UNIX były lokalne gniazda TCP (oba można było wyodrębnić jako gniazda ogólne), więc przełączyliśmy się na to drugie, tylko zastąpienie ścieżek plików zmiennymi wersjami adresów TCP / IP bez konieczności refaktoryzacji bazy kodu.

To nie było wystarczające, ponieważ użycie gniazd TCP nadal nie zapewniało możliwości replikacji deskryptory gniazd do innych procesów. Rzeczywiście, właściwym rozwiązaniem tego problemu byłoby całkowite uniknięcie go. Ponieważ wymagałoby to długoterminowej refaktoryzacji odpowiednich części bazy kodu (zadanie, które podjęli się inni), przejrzyste podejście okazało się jednak bardziej odpowiednie w międzyczasie. Było to utrudnione przez fakt, że w systemach UNIX duplikowanie deskryptora pliku nie wymaga znajomości tożsamości procesu docelowego, podczas gdy w systemie Windows duplikowanie uchwytu wymaga aktywnego manipulowania proces docelowy. Aby rozwiązać te problemy, zaimplementowaliśmy możliwość wymiany deskryptorów plików, zastępując procedurę uzgadniania przy uruchamianiu sklepu Plazmy bardziej wyspecjalizowanym mechanizmem , który ustanawiałby połączenie TCP , poszukaj identyfikatora procesu na drugim końcu (być może powolnej, ale jednorazowej procedury), zduplikuj uchwyt gniazda i poinformuj proces docelowy o nowym dojściu. Chociaż nie jest to rozwiązanie ogólnego przeznaczenia (iw rzeczywistości może być podatne na warunki wyścigowe w ogólnym przypadku) i dość nietypowe podejście, działało dobrze dla celów Raya i może być podejściem do innych projektów, które mają ten sam problem może skorzystać.

Poza tym jednym z największych problemów, z jakim się spodziewaliśmy, była zależność od serwera Redis . Chociaż firma Microsoft Open Technologies (MSOpenTech) wcześniej zaimplementowała port Redis w systemie Windows, projekt został porzucony i w konsekwencji nie obsługiwał wersji Redis, których wymagał Ray . To początkowo sprawiło, że założyliśmy, że nadal będziemy musieli uruchomić serwer Redis w podsystemie Windows dla systemu Linux (WSL), co prawdopodobnie okazało się niewygodne dla użytkowników. Byliśmy więc wdzięczni, że odkryliśmy, że inny programista kontynuował projekt i tworzył później pliki binarne Redis w systemie Windows (patrz tporadowski / redis ). To znacznie uprościło nasz problem i pozwoliło nam zapewnić natywną obsługę Ray dla Windows.

Wreszcie, prawdopodobnie najbardziej znaczącymi przeszkodami, z jakimi się spotkaliśmy (podobnie jak MSOpenTech Redis i większość innych programów obsługujących tylko POSIX) była brak trywialnych substytutów dla niektórych POSIX API w Windows. Niektóre z nich (, takie jak

getppid()) były proste, choć nieco uciążliwe. Prawdopodobnie najtrudniejszym problemem napotkanym podczas całego procesu przenoszenia był problem deskryptorów plików i uchwytów plików. Znaczna część kodu, na którym polegaliśmy (na przykład magazyn Plazmy w Arrow) zakładała użycie deskryptorów plików POSIX (int s). Jednak system Windows natywnie używa HANDLE s, które mają rozmiar wskaźnika i są analogiczne do size_t. Jednak samo w sobie nie jest to znaczący problem, ponieważ środowisko wykonawcze Microsoft Visual C ++ (CRT) zapewnia warstwę podobną do POSIX. Warstwa ta ma jednak ograniczoną funkcjonalność, wymaga tłumaczenia w każdym miejscu wywołania, które jej nie obsługuje, aw szczególności nie można jej używać do takich rzeczy, jak gniazda lub uchwyty pamięci współdzielonej. Co więcej, nie chcieliśmy zakładać, że HANDLE zawsze będzie wystarczająco małe, aby zmieścić się w 32-bitowej liczbie całkowitej, mimo że często tak było, ponieważ nie było jasne, czy okoliczności, o których nie byliśmy świadomi, mogą po cichu złamać to założenie.To znacznie pogorszyło nasze problemy, ponieważ najbardziej oczywistym rozwiązaniem byłoby wykrycie wszystkich int plików, które reprezentują deskryptory plików w bibliotece takiej jak Arrow, i zastąpienie ich (i wszystkich ich zastosowań ) z alternatywnym typem danych, który był procesem podatnym na błędy i wymagał znacznych poprawek do kodu zewnętrznego, powodując znaczne obciążenie konserwacyjne.

Dość trudno było zdecydować, co zrobić na tym etapie. Rozwiązanie firmy MSOpenTech Redis dla tego samego problemu dało jasno do zrozumienia, że ​​było to dość trudne zadanie, ponieważ rozwiązali ten problem przez tworzenie pojedynczej, obejmującej cały proces tablicy deskryptorów plików na górze istniejącej implementacji CRT, wymagającej od nich radzenia sobie z bezpieczeństwem wątków, a także zmuszania ich do przechwytywania wszystkie zastosowania POSIX API (nawet te, które już były w stanie obsługiwać) jedynie do tłumaczenia deskryptorów plików. Zamiast tego zdecydowaliśmy się na nietypowe podejście: rozszerzyliśmy warstwę translacyjną POSIX w CRT . Dokonano tego poprzez zidentyfikowanie niekompatybilnych uchwytów w czasie tworzenia i „wepchnięcie” tych uchwytów do buforów zbędnego potoku, zwracając zamiast tego deskryptor tego potoku. Następnie musieliśmy tylko zmodyfikować miejsca użytkowania tych uchwytów, których zidentyfikowanie było, co najważniejsze, trywialne, ponieważ wszystkie były socket i plików mapowanych w pamięci API. W rzeczywistości pomogło to uniknąć konieczności stosowania poprawek, ponieważ byliśmy w stanie po prostu przekierować wiele funkcji za pomocą makr .

Podczas gdy wysiłek opracowanie tej unikalnej warstwy rozszerzenia (w win32fd.h) było znaczące (i dość nieortodoksyjne), prawdopodobnie było warto, ponieważ warstwa tłumaczenia był w rzeczywistości dość mały w porównaniu i pozwolił nam przekazać większość niezwiązanych ze sobą problemów (takich jak wielowątkowe blokowanie tablicy deskryptorów plików) do interfejsów API CRT. Co więcej, wykorzystując anonimowe potoki z tej samej globalnej tablicy deskryptorów plików (pomimo naszego braku bezpośredniego dostępu do niej), byliśmy w stanie uniknąć przechwytywania i tłumaczenia deskryptory plików dla innych funkcji, które mogą być już obsługiwane bezpośrednio. Dzięki temu znaczna część kodu pozostała zasadniczo niezmieniona przy minimalnym wpływie na wydajność, dopóki później nie mieliśmy szansy na zreformowanie kodu i zapewnienie lepszych opakowań na wyższym poziomie (na przykład przez otoki Boost.Asio). Jest całkiem możliwe, że rozszerzenie tej warstwy pozwoliłoby innym projektom, takim jak Redis, na znacznie płynniejsze przeniesienie do systemu Windows i przy znacznie mniej drastycznych zmianach lub potencjale błędów.

Autor: Andrea Leopardi

Wykonalność w czasie wykonywania (dowód koncepcji)

Kiedy już uznaliśmy, że rdzeń Ray działa prawidłowo, następnym krokiem milowym było zapewnienie, że test Pythona będzie mógł z powodzeniem sprawdzać nasze ścieżki kodu. Początkowo nie ustalaliśmy priorytetów. Okazało się to jednak błędem, ponieważ późniejsze zmiany dokonane przez innych programistów w zespole faktycznie wprowadziły bardziej dynamiczne niezgodności z Windows, a system CI nie był w stanie wykryć takich awarii. Dlatego później za priorytet nadaliśmy wykonanie minimalnego testu na kompilacjach systemu Windows, aby uniknąć dalszych awarii kompilacji.

w większości nasze wysiłki zakończyły się sukcesem, a wszelkie utrzymujące się błędy w rdzeniu Ray znajdowały się w przewidywalnych częściach bazy kodu (chociaż ich rozwiązanie często wymagało przejścia przez kod wieloprocesowy, co nie było trywialne). Jednak po drodze pojawiła się co najmniej jedna nieprzyjemna niespodzianka po stronie C i jedna po stronie Pythona, z których obie (między innymi) zachęciły nas do bardziej aktywnego czytania dokumentacji oprogramowania w przyszłości.

Po stronie C, nasz początkowy uścisk dłoni opakowanie do wymiany uchwytów gniazd polegał na naiwnym zastąpieniu sendmsg i recvmsg z WSASendMsg i WSARecvMsg. Te Windows API były najbliższymi odpowiednikami API POSIX i dlatego wydawały się oczywistym wyborem. Jednak po wykonaniu kod ciągle się zawieszał, a źródło problemu było niejasne. Niektóre debugowanie (w tym wersje do debugowania kompilacji & runtimes) pomogły ujawnić, że problem dotyczy zmiennych stosu przekazywanych do WSASendMsg. Dalsze debugowanie i dokładne sprawdzenie zawartości pamięci sugeruje, że problem mógł dotyczyć pola msg_flags WSAMSG, ponieważ było to jedyne niezainicjowane pole pole.Jednak wydawało się to nieistotne: msg_flags zostało po prostu przetłumaczone z flags w struct msghdr, który nie był używany na wejściu, a był używany tylko jako parametr wyjściowy. Jednak przeczytanie dokumentacji ujawniło problem: w systemie Windows pole służyło również jako parametr wejściowy , a zatem pozostawienie go niezainicjowanego skutkowało nieprzewidywalnym zachowaniem! Było to dla nas dość nieoczekiwane i zaowocowało dwoma ważnymi wnioskami posuniętymi naprzód: uważne przeczytanie dokumentacji każdej funkcji oraz co więcej, inicjalizacja zmiennych nie polega jedynie na zapewnieniu poprawności z obecnymi interfejsami API, ale jest również ważna dla uczynienia kodu odpornym na przyszłe zmiany w docelowych interfejsach API .

Po stronie Pythona napotkaliśmy inny problem. Nasze natywne moduły Pythona początkowo nie ładowały się, pomimo braku oczywistych problemów. Po wielu dniach zgadywania, przechodzenia przez asembler i kod źródłowy CPythona oraz sprawdzania zmiennych w bazie kodu CPythona, stało się jasne, że problemem był brak sufiksu .pyd w dynamicznym Pythonie biblioteki w systemie Windows. Jak się okazuje, z niejasnych powodów Python odmawia załadowania nawet plików .dll w systemie Windows jako modułów Pythona, pomimo faktu, że natywne biblioteki współdzielone można normalnie załadować nawet z dowolnym plikiem rozbudowa. Rzeczywiście, okazało się, że ten fakt został udokumentowany w witrynie Pythona . Niestety, obecność takiej dokumentacji nie mogła jednak oznaczać, że zdaliśmy sobie sprawę, że trzeba jej szukać.

Niemniej jednak, ostatecznie Ray był w stanie pomyślnie działać w systemie Windows, co zakończyło kolejny kamień milowy i dostarczyło dowodu koncepcji przedsięwzięcia.

Autor: Hitesh Choudhary

Kompatybilność w czasie wykonywania (głównie Python)

W tym momencie, kiedyś rdzeń Ray był pracując, mogliśmy skupić się na przenoszeniu kodu wyższego poziomu. Niektóre problemy były dość łatwe do rozwiązania – na przykład niektóre interfejsy API Pythona, które są przeznaczone tylko dla systemu UNIX (np. os.uname()[1]) często mają odpowiednie zamienniki w systemie Windows (takie jak socket.gethostname()), a znalezienie ich wymagało znajomości wyszukiwania wszystkich ich instancji w bazie kodu. Inne problemy były trudniejsze do wyśledzenia lub rozwiązania. Czasami były one spowodowane użyciem poleceń specyficznych dla POSIX (takich jak ps), które wymagały alternatywnych podejść (takich jak użycie psutil dla Pythona). Innym razem były to niezgodności w bibliotekach innych firm. Na przykład, gdy gniazdo rozłącza się w systemie Windows, generowany jest błąd, zamiast powodować puste odczyty. Wydaje się, że biblioteka Pythona dla Redis nie obsługuje tego. Takie różnice w zachowaniu wymagały wyraźnego małpiego łatania , aby uniknąć czasami mylących błędów, które mogłyby wystąpić po zakończeniu pracy Ray.

Chociaż niektóre z takich problemów są dość uciążliwe, ale prawdopodobnie oczekiwane (np. zastąpienie użycia /tmp katalogiem tymczasowym platformy lub uniknięcie założenia, że ​​wszystkie ścieżki bezwzględne zaczynają się od ukośnika), niektóre były nieco nieoczekiwane (na przykład sprzeczne rezerwacje portów ) lub (jak to często bywa) z powodu błędnych założeń, a ich zrozumienie zależy od zrozumienia architektury systemu Windows i jego podejścia do kompatybilności wstecznej.

Jedna z takich historii dotyczy użycia ukośników jako separatorów katalogów w systemie Windows. Ogólnie wydaje się, że działają dobrze i są powszechnie używane przez programistów. Jednak w rzeczywistości jest to spowodowane automatyczną konwersją ukośników na ukośniki odwrotne w bibliotekach podsystemu systemu Windows w trybie użytkownika, a niektóre automatyczne przetwarzanie można powstrzymać, jawnie poprzedzając ścieżki prefiksem \\?\, co jest pomocne przy omijaniu niektórych funkcji zgodności (takich jak długie ścieżki). Jednak nigdy wyraźnie nie korzystaliśmy z takiej ścieżki i założyliśmy, że można oczekiwać, że użytkownicy będą unikać nietypowego użycia w naszych wersjach eksperymentalnych. Jednak później okazało się, że gdy Bazel wywoływał pewne testy Pythona, ścieżki byłyby przetwarzane w tym formacie, aby umożliwić użycie długich ścieżek, a to wyłączało automatyczne tłumaczenie, na którym domyślnie polegaliśmy. To doprowadziło nas do ważnych wniosków: po pierwsze, generalnie lepiej jest używać interfejsów API w sposób najbardziej odpowiedni dla systemu docelowego, ponieważ zapewnia to najmniej możliwości wystąpienia nieoczekiwanych problemów. .Po drugie, i co najważniejsze, zakładanie, że środowisko użytkownika jest przewidywalne, jest po prostu błędem . W rzeczywistości nowoczesne oprogramowanie prawie zawsze polega na uruchamianiu kodu innej firmy, którego dokładnych zachowań nie jesteśmy świadomi. Nawet jeśli można założyć, że użytkownik uniknie problematycznych sytuacji, oprogramowanie innych firm jest całkowicie nieświadome takich ukrytych założeń. Dlatego jest prawdopodobne, że i tak wystąpią, nie tylko dla użytkowników, ale także dla samych twórców oprogramowania, powodując błędy, które są trudniejsze do wyśledzenia niż naprawienie podczas pisania początkowego kodu. Dlatego ważne jest, aby podczas projektowania solidnego systemu nie przywiązywać zbytniej wagi do łatwości obsługi programu (przeciwieństwa „przyjazności dla użytkownika”).

(Dla zabawy: w rzeczywistości w systemie Windows ścieżki może w rzeczywistości zawierać cudzysłowy i wiele innych znaków specjalnych, które normalnie uważa się za niedozwolone. Dzieje się tak podczas korzystania z alternatywnych strumieni danych NTFS. Są one jednak na tyle rzadkie i na tyle złożone, że często nawet standardowe biblioteki językowe ich nie obsługują.)

Jednak po rozwiązaniu najważniejszych problemów wiele testów udało się przejść w systemie Windows, tworząc pierwszą eksperymentalną implementację Ray dla systemu Windows.

Autor: Ross Sneddon

Ulepszenia czasu wykonywania (np. Obsługa Unicode)

W tym momencie większość rdzenia Ray może być używana w systemie Windows, tak jak na innych platformach. Niemniej jednak niektóre problemy pozostają, które wymagają ciągłych wysiłków, aby je rozwiązać.

Jednym z takich problemów jest obsługa Unicode. Ze względów historycznych podsystem trybu użytkownika systemu Windows ma dwie wersje większości interfejsów API: wersję „ANSI” obsługującą zestawy znaków jednobajtowych oraz wersję „Unicode” obsługującą UCS-2 lub UTF-16 (w zależności od dane API, o którym mowa). Niestety, żaden z nich nie jest UTF-8; nawet podstawowa obsługa Unicode wymaga używania ciągów szerokich znaków (na podstawie wchar_t) w całej bazie kodu. (Uwaga: w rzeczywistości Microsoft próbował niedawno wprowadzić UTF-8 jako stronę kodową, ale nie jest on wystarczająco dobrze obsługiwany, aby bezproblemowo rozwiązać ten problem, przynajmniej bez polegania na potencjalnie nieudokumentowanych i kruchych wewnętrznych elementach systemu Windows.)

Tradycyjnie programy Windows obsługują Unicode za pomocą makr, takich jak _T() lub TEXT(), które rozszerzają się do wąskich lub szerokich -literały znaków w zależności od tego, czy określono kompilację Unicode, i używaj TCHAR jako ich ogólnych typów znaków. Podobnie większość interfejsów API języka C ma wersje TCHAR (takie jak _tcslen() zamiast strlen()), aby zapewnić zgodność z obydwoma typami kodu. Jednak migracja bazy kodu opartej na systemie UNIX do tego modelu jest dość skomplikowanym przedsięwzięciem. Nie zostało to jeszcze zrobione w Ray, a zatem w chwili pisania tego tekstu Ray nie obsługuje prawidłowego Unicode w (na przykład) ścieżkach plików w systemie Windows, a najlepszym podejściem do zrobienia tego może być nadal otwarte pytanie.

Inną taką kwestią jest mechanizm komunikacji między procesami. Chociaż gniazda TCP mogą działać dobrze w systemie Windows, są nieoptymalne, ponieważ wprowadzają warstwę niepotrzebnej złożoności w logice (takiej jak przekroczenie limitu czasu, utrzymywanie aktywności, algorytm Nagle), mogą powodować przypadkową dostępność z hostów nielokalnych i mogą wprowadzić pewne obciążenie związane z wydajnością. W przyszłości potoki nazwane mogą stanowić lepszy zamiennik dla gniazd domeny UNIX w systemie Windows; w rzeczywistości, nawet w Linuksie, albo potoki, albo tak zwane abstrakcyjne gniazda domeny UNIX również mogą okazać się lepszą alternatywą, ponieważ nie wymagają zaśmiecania i czyszczenia plików gniazd w systemie plików.

Na koniec, kolejnym przykładem takiego problemu jest zgodność gniazd BSD, a raczej jej brak. doskonała odpowiedź na StackOverflow zawiera szczegółowe omówienie niektórych problemów, ale pokrótce, chociaż wspólne interfejsy API gniazd są pochodnymi oryginalnego interfejsu API gniazd BSD, różne platformy implementują podobne gniazda flagi inaczej. W szczególności konflikty z istniejącymi adresami IP lub portami TCP mogą powodować różne zachowania na różnych platformach. Chociaż trudno jest tu szczegółowo opisać kwestie, efekt końcowy jest taki, że może to utrudnić jednoczesne korzystanie z wielu instancji Ray na tym samym hoście. (W rzeczywistości, ponieważ zależy to od zachowania jądra systemu operacyjnego, ma to również wpływ na WSL). Jest to kolejny znany problem, którego rozwiązanie jest dość skomplikowane i nie do końca rozwiązane w obecnym systemie.

Wniosek

Proces przenoszenia kodu źródłowego, takiego jak Ray do Windows, był cennym doświadczeniem, które podkreśla zalety i wady wielu aspektów tworzenia oprogramowania oraz ich wpływ na utrzymanie kodu.Powyższy opis podkreśla tylko niektóre przeszkody napotkane po drodze. Z procesu można wyciągnąć wiele użytecznych wniosków, z których niektóre mogą być wartościowe do podzielenia się tutaj dla innych projektów, które mają nadzieję osiągnąć podobny cel.

Po pierwsze, w niektórych przypadkach faktycznie odkryliśmy później w kolejnych wersjach niektóre biblioteki (np. hiredis) już rozwiązały niektóre problemy, które rozwiązaliśmy. Rozwiązania nie zawsze były oczywiste, ponieważ (na przykład) wersja hiredis w ostatnich wersjach Redis była w rzeczywistości przestarzałą kopią hiredis, co prowadzi nas do przekonania, że ​​niektóre problemy nie zostały jeszcze rozwiązane. Późniejsze wersje nie zawsze też w pełni rozwiązały wszystkie istniejące problemy ze zgodnością. Niemniej jednak prawdopodobnie zaoszczędziłoby to trochę wysiłku, aby dokładniej sprawdzić istniejące rozwiązania niektórych problemów, aby uniknąć konieczności ich ponownego rozwiązywania.

Autor: John Barkiple

Po drugie, łańcuch dostaw oprogramowania jest często złożony . Błędy mogą naturalnie narastać na każdej warstwie i błędem jest ufać, że nawet powszechnie używane narzędzia open source są „przetestowane w walce” i dlatego są solidne, zwłaszcza gdy są używane w złość . Co więcej, wiele długotrwałych lub powszechnych problemów związanych z inżynierią oprogramowania nie ma zadowalających rozwiązań dostępnych do użycia, szczególnie (ale nie tylko), gdy wymagają one kompatybilności z różnymi systemami. Rzeczywiście, poza zwykłymi dziwactwami, w procesie przenoszenia Raya na Windows, regularnie napotykaliśmy i często zgłaszaliśmy błędy w wielu programach, w tym między innymi błąd Gita w Linux , który wpłynął na użycie Bazel, Redis (Linux) , glog , psutil (błąd analizy, który wpływa na WSL) , grpc , wiele trudne do zidentyfikowania błędy w samym Bazelu (np. 1 , 2 , 3 , 4 ), Travis CI i GitHub Actions , między innymi. To zachęciło nas do zwrócenia większej uwagi również na złożoność naszych zależności.

Po trzecie, inwestowanie w narzędzia i infrastrukturę na dłuższą metę się opłaca. Szybsze kompilacje pozwalają na szybszy rozwój, a mocniejsze narzędzia pozwalają na łatwiejsze rozwiązywanie złożonych problemów. W naszym przypadku użycie Bazela pomogło nam na wiele sposobów, mimo że jest daleki od doskonałości i narzuca stromą krzywą uczenia się. Poświęcenie czasu (prawdopodobnie wielu dni) na poznanie możliwości, mocnych stron i wad nowego narzędzia rzadko jest łatwe, ale prawdopodobnie przyniesie korzyści w utrzymaniu kodu. W naszym przypadku poświęcenie czasu na dogłębne zapoznanie się z dokumentacją Bazel pozwoliło nam znacznie szybciej wskazać mnogość przyszłych problemów i rozwiązań. Co więcej, pomogło nam to również zintegrować narzędzia z Bazel, które najwyraźniej udało się niewielu innym, na przykład narzędzie Clang dołączanie tego, czego używasz .

Po czwarte, i jak wspomniano wcześniej, rozsądnie jest zaangażować się w praktyki bezpiecznego kodowania , takie jak inicjowanie pamięci przed użyciem, nie ma znaczącego kompromisu. Nawet najbardziej uważny inżynier nie jest w stanie przewidzieć przyszłej ewolucji podstawowego systemu, który może po cichu unieważnić założenia.

Wreszcie, jak to zwykle bywa w medycynie, zapobieganie jest najlepszym lekarstwem . Uwzględnienie ewentualnych przyszłych zmian i kodowanie w standardowych interfejsach pozwala na projektowanie kodu bardziej rozszerzalnego, niż można to łatwo osiągnąć po pojawieniu się niezgodności.

Chociaż przeniesienie Ray na Windows nie jest jeszcze ukończone, było dość dotychczasowe sukcesy i mamy nadzieję, że dzielenie się naszym doświadczeniem i rozwiązaniami może posłużyć jako pomocny przewodnik dla innych deweloperów, którzy rozważają podjęcie podobnej podróży.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *