Stałe instancje robocze

Na tej stronie opisujemy, jak używać trwałych instancji roboczych, jakie są zalety, wymagania i jak wpływają one na piaskownicę.

Trwała instancja robocza to długotrwały proces rozpoczęty przez serwer Bazel, który działa jako kod wokół rzeczywistego narzędzia (zwykle jest to kompilator) lub jest sam narzędziem. Aby korzystać z trwałych instancji roboczych, narzędzie musi obsługiwać sekwencję kompilacji, a kod musi poruszać się między interfejsem API narzędzia a formatem żądania/odpowiedzi opisanym poniżej. Ten sam skrypt może zostać wywołany z flagą --persistent_worker i bez niej w tej samej kompilacji i odpowiada za prawidłowe uruchomienie narzędzia i rozmowę z nim, a także wyłączenie instancji roboczych przy wyjściu. Do każdej instancji roboczej jest przypisywany (ale nie jest chromowany) oddzielny katalog roboczy w katalogu <outputBase>/bazel-workers.

Użycie trwałych instancji roboczych to strategia wykonywania, która zmniejsza koszty uruchamiania, umożliwia więcej kompilacji JIT i włącza zapisywanie w pamięci podręcznej np. abstrakcyjnych drzew składniowych w wykonaniu działania. Ta strategia osiąga te ulepszenia poprzez wysyłanie wielu żądań do długotrwałego procesu.

Trwałe instancje robocze są zaimplementowane w wielu językach, takich jak Java, Scala czy Kotlin.

Programy korzystające ze środowiska wykonawczego NodeJS mogą wdrożyć protokół instancji roboczej za pomocą biblioteki pomocniczej @bazel/worker.

Użycie trwałych instancji roboczych

Bazel w wersji 0.27 i nowszych domyślnie używa stałych instancji roboczych podczas wykonywania kompilacji, ale zdalne wykonywanie ma pierwszeństwo. W przypadku działań, które nie obsługują stałych instancji roboczych, Basia zaczyna się rozpoczynać instancję narzędzia dla każdego działania. Możesz wyraźnie ustawić kompilację tak, aby używała trwałych instancji roboczych, ustawiając workerstrategię dla odpowiedniej mnemotechniki. Sprawdzoną metodą jest przykładowe podanie wartości local jako strategii zastępczej dla strategii worker:

bazel build //my:target --strategy=Javac=worker,local

Użycie strategii roboczej zamiast strategii lokalnej może znacznie przyspieszyć kompilację w zależności od implementacji. W przypadku Javy kompilacje mogą być od 2 do 4 razy szybsze, a czasem nawet bardziej pomocne w kompilacji przyrostowej. Kompilacja w Bazach trwa około 2,5 raza szybciej niż pracownicy. Więcej informacji znajdziesz w sekcji Wybieranie liczby instancji roboczych.

Jeśli masz także zdalne środowisko kompilacji, które pasuje do lokalnego środowiska kompilacji, możesz użyć eksperymentalnej strategii dynamicznej , która uruchamia wykonanie zdalne i wykonanie instancji roboczej. Aby włączyć strategię dynamiczną, przekaż flagę --experimental_spawn_scheduler. Ta strategia automatycznie włącza instancje robocze, więc nie trzeba określać strategii worker, ale nadal można używać wartości local lub sandboxed jako kreacji zastępczych.

Wybieranie liczby instancji roboczych

Domyślna liczba instancji roboczych na mnemotechnikę to 4, ale można ją dostosować, używając flagi worker_max_instances. Łączy się właściwe wykorzystanie dostępnych procesorów z ilością kompilacji i ilością trafień rejestrowanych przez JIT. Przy większej liczbie pracowników więcej wartości docelowych będzie pokrywać koszty uruchamiania kodu w stanie niewprowadzonym oraz chłodzenia pamięci podręcznej. Jeśli masz niewielką liczbę celów do utworzenia, jeden proces roboczy może stanowić najlepszy kompromis między szybkością kompilacji a wykorzystaniem zasobów (na przykład w przypadku problemu 8586). Flaga worker_max_instances ustawia maksymalną liczbę instancji roboczych na zestaw mnemotechniczny i ustawiony (zobacz poniżej), więc w wyniku mieszanego systemu możesz uzyskać dużo pamięci, jeśli zachowasz wartość domyślną. W przypadku kompilacji przyrostowych korzyści z wielu instancji roboczych są jeszcze mniejsze.

Ten wykres pokazuje czas kompilowania z bazodanu (cel) //src:bazel na bazu danych 6-rdzeniowego procesora Intel Xeon 3,5 GHz z 64 GB pamięci RAM. W przypadku każdej konfiguracji instancji roboczych jest 5 wyczyszczonych kompilacji, a średnia z 4 ostatnich jest brana pod uwagę.

Wykres przedstawiający wydajność czystych kompilacji

Rysunek 1. Wykres przedstawiający wydajność czystych kompilacji.

W tej konfiguracji dwa instancje robocze zapewniają szybsze kompilowanie, przy czym tylko 14-procentowy wzrost w stosunku do 1 instancji roboczej. Jeśli chcesz użyć mniej pamięci, dobrym rozwiązaniem jest jeden proces roboczy.

Kompilacja przyrostowa zwykle przynosi jeszcze większe korzyści. Czyste kompilacje są względnie rzadkie, ale zmiana jednego pliku między kompilacjami jest powszechna, zwłaszcza w fazie programowania opartego na testach. Powyższy przykład zawiera też działania w języku innym niż Java, które mogą zastąpić czas kompilacji.

Kompilacja tylko źródeł Java (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar) po zmianie wewnętrznej stałej ciągu znaków w AbstractContainerizingSandboxedSpawn.java daje 3-krotne przyspieszenie (średnio 20 kompilacji przyrostowych z jedną kompilacją rozgrzewki):

Wykres przedstawiający wydajność przyrostowych kompilacji

Rysunek 2. Wykres przedstawiający wydajność przyrostowych kompilacji.

Przyspieszenie zależy od wprowadzanej zmiany. Przyspieszenie współczynnika 6 jest mierzone w powyższej sytuacji w przypadku zmiany często używanej stałej.

Modyfikowanie trwałych instancji roboczych

Możesz przekazać flagę --worker_extra_flag, aby określić flagi uruchamiania do instancji roboczych, takie jak mnemotechnika. Na przykład przekazanie kodu --worker_extra_flag=javac=--debug włącza debugowanie tylko w języku Javac. Przy użyciu tej flagi można ustawić tylko jedną flagę instancji roboczej i tylko dla jednej mnemotechniki. Instancje robocze są tworzone nie tylko osobno dla każdej mnemotechniki, ale także dla ich różnych flag startowych. Każda kombinacja flag mnemotechnicznych i startowych jest połączona w postaci WorkerKey, a dla każdego zasobu WorkerKey można utworzyć do worker_max_instances instancji roboczych. Z następnej sekcji dowiesz się, jak w konfiguracji konfiguracji mogą się też znajdować flagi konfiguracji.

Możesz użyć flagi --high_priority_workers, aby określić mnemotechnikę, która ma być uruchomiona w porównaniu z mnemotechniką o normalnym priorytecie. Dzięki temu możesz określić priorytety działań, które zawsze znajdują się na ścieżce krytycznej. Jeśli co najmniej 2 instancje robocze wykonują żądania, wszystkie pozostałe instancje robocze są zablokowane. Ta flaga może zostać użyta wiele razy.

Po przekazaniu flagi --worker_sandboxing każde żądanie robocze używa osobnego katalogu piaskownicy dla wszystkich danych wejściowych. Konfigurowanie piaskownicy zajmuje trochę czasu, zwłaszcza w systemie macOS, ale zapewnia lepszą dokładność.

Flaga --worker_quit_after_build przydaje się głównie do debugowania i profilowania. Ta flaga zmusza wszystkie instancje robocze do zamknięcia po zakończeniu kompilacji. Możesz też przekazać właściwość --worker_verbose, aby uzyskać więcej danych o działaniach pracowników. Ta flaga jest widoczna w polu verbosity w WorkRequest, dzięki czemu implementacje instancji roboczych również mogą być bardziej szczegółowe.

Instancje robocze przechowują logi w katalogu <outputBase>/bazel-workers, na przykład /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log. Nazwa pliku zawiera identyfikator instancji roboczej i mnemotechnika. Ponieważ dla każdej mnemotechniki może być więcej niż jeden WorkerKey, możesz wyświetlić więcej niż worker_max_instances plików dziennika dla danej mnemoniki.

W przypadku kompilacji na Androida szczegółowe informacje znajdziesz na stronie Android Build Performance.

Wdrażanie trwałych instancji roboczych

Informacje o tworzeniu instancji roboczych znajdziesz na stronie tworzenia trwałych instancji roboczych.

Ten przykład przedstawia konfigurację Starlark dla instancji roboczej, która używa JSON:

args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
    output = args_file,
    content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
    mnemonic = "SomeCompiler",
    executable = "bin/some_compiler_wrapper",
    inputs = inputs,
    outputs = outputs,
    arguments = [ "-max_mem=4G",  "@%s" % args_file.path],
    execution_requirements = {
        "supports-workers" : "1", "requires-worker-protocol" : "json" }
)

W przypadku tej definicji pierwsze użycie tego działania rozpoczyna się od uruchomienia wiersza poleceń /bin/some_compiler -max_mem=4G --persistent_worker. Prośba dotycząca kompilacji Foo.java wygląda wtedy tak:

arguments: [ "-g", "-source", "1.5", "Foo.java" ]
inputs: [
  {path: "symlinkfarm/input1" digest: "d49a..." },
  {path: "symlinkfarm/input2", digest: "093d..."},
]

Instancja robocza otrzymuje ją w stdin w formacie JSON rozdzielanym znakami nowego wiersza (ponieważ requires-worker-protocol jest ustawiony na JSON). Instancja robocza następnie wykonuje działanie i wysyła WorkResponse do bazy danych w formacie JSON. Bazel analizuje odpowiedź i ręcznie przekształca ją w format proto WorkResponse. Aby komunikować się z powiązaną osobą roboczą za pomocą kodowania protobuf binarnego zamiast JSON, requires-worker-protocol powinien mieć wartość proto:

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

Jeśli nie podasz requires-worker-protocol w wymaganiach wykonawczych, Bazel domyślnie użyje danych roboczych instancji protobuf.

Bazel pobiera element WorkerKey na podstawie mnemotechniki i udostępnionych flag, więc jeśli ta konfiguracja pozwoli na zmianę parametru max_mem, dla każdej użytej wartości pojawi się oddzielny węzeł roboczy. Może to prowadzić do nadmiernego zużycia pamięci, jeśli jest używanych zbyt wiele wariantów.

Obecnie poszczególne instancje robocze mogą obecnie przetwarzać tylko jedno żądanie. Eksperymentalna funkcja wielu instancji roboczych multipleksu umożliwia używanie wielu wątków, jeśli podstawowe narzędzie jest wielowątkowe i skonfigurowano kod tak, by mógł to zrozumieć.

W tym repozytorium GitHub znajdziesz przykładowe kody instancji roboczych napisane zarówno w języku Java, jak i w Pythonie. Jeśli korzystasz z JavaScriptu lub TypeScriptu, przydatne mogą być pakiety@bazel/worker i przykład roboczy nodejs.

Jak instancje robocze wpływają na tryb piaskownicy?

Użycie strategii worker domyślnie nie powoduje uruchomienia działania w piaskownicy, podobnie jak w przypadku strategii local. Możesz ustawić flagę --worker_sandboxing tak, by uruchamiać wszystkie instancje robocze w piaskownicy, tak by każde wykonanie narzędzia widzieli tylko pliki wejściowe, do których ma ono mieć dostęp. Narzędzie może nadal przekazywać informacje wewnętrznie między żądaniami, na przykład przez pamięć podręczną. Stosując strategię dynamic wymaga działania w trybie piaskownicy.

Aby umożliwić prawidłowe korzystanie z pamięci podręcznej kompilacji wraz z instancjami roboczymi, z każdym plikiem wejściowym jest przekazywane podsumowanie. Dzięki temu kompilatorowi lub kodowi mogą sprawdzać, czy dane wejściowe są prawidłowe bez konieczności odczytu pliku.

Nawet jeśli używasz skrótów wejściowych do ochrony przed niechcianą pamięcią podręczną, pracownicy w trybie piaskownicy udostępniają mniej rygorystyczne piaskownice niż zwykła piaskownica, ponieważ narzędzie może zachować inny stan wewnętrzny, na który miały wpływ poprzednie żądania.

Instancje robocze Multiplex mogą być umieszczone w piaskownicy, tylko jeśli implementacja robocza obsługuje tę instancję, a piaskownicę trzeba włączyć oddzielnie za pomocą flagi --experimental_worker_multiplex_sandboxing. Więcej informacji znajdziesz w dokumencie dotyczącym projektu.

Więcej informacji

Więcej informacji o trwałych instancjach roboczych znajdziesz w: