Trang này trình bày cách sử dụng worker ổn định, các lợi ích, yêu cầu và cách worker ảnh hưởng đến hộp cát.
Worker ổn định là một quy trình chạy trong thời gian dài do máy chủ Bazel khởi động, đóng vai trò là trình bao bọc xung quanh công cụ thực tế (thường là trình biên dịch) hoặc chính là công cụ đó. Để hưởng lợi từ worker ổn định, công cụ phải hỗ trợ thực hiện một chuỗi các quá trình biên dịch và trình bao bọc cần dịch giữa API của công cụ và định dạng yêu cầu/phản hồi được mô tả bên dưới. Cùng một worker có thể được gọi có và không có cờ --persistent_worker
trong cùng một bản dựng, đồng thời chịu trách nhiệm khởi động và trao đổi với công cụ này một cách thích hợp, cũng như tắt các worker khi thoát. Mỗi thực thể worker được chỉ định (nhưng không bị can thiệp vào hệ thống) một thư mục đang hoạt động riêng trong <outputBase>/bazel-workers
.
Việc sử dụng worker ổn định là một chiến lược thực thi giúp giảm hao tổn khi khởi động, cho phép biên dịch JIT nhiều hơn và cho phép lưu vào bộ nhớ đệm, chẳng hạn như cây cú pháp trừu tượng trong quá trình thực thi hành động. Chiến lược này đạt được những điểm cải tiến này bằng cách gửi nhiều yêu cầu đến một quy trình chạy trong thời gian dài.
Worker ổn định được triển khai cho nhiều ngôn ngữ, bao gồm Java, Scala, Kotlin và nhiều ngôn ngữ khác.
Các chương trình sử dụng thời gian chạy NodeJS có thể dùng thư viện trình trợ giúp @bazel/worker để triển khai giao thức worker.
Sử dụng worker liên tục
Bazel 0.27 trở lên sử dụng trình thực thi liên tục theo mặc định khi thực thi các bản dựng, mặc dù phương thức thực thi từ xa được ưu tiên. Đối với các thao tác không hỗ trợ worker ổn định, Bazel sẽ quay lại việc khởi động một thực thể công cụ cho mỗi thao tác. Bạn có thể thiết lập rõ ràng bản dựng của mình để sử dụng worker liên tục bằng cách đặt chiến lược worker
cho công cụ ghi nhớ có thể áp dụng. Theo phương pháp hay nhất, ví dụ này bao gồm việc chỉ định local
làm phương án dự phòng cho chiến lược worker
:
bazel build //my:target --strategy=Javac=worker,local
Việc sử dụng chiến lược worker thay vì chiến lược cục bộ có thể tăng đáng kể tốc độ biên dịch, tuỳ thuộc vào cách triển khai. Đối với Java, các bản dựng có thể nhanh hơn từ 2 đến 4 lần, đôi khi cao hơn khi biên dịch gia tăng. Quá trình biên dịch Bazel có tốc độ nhanh gấp khoảng 2,5 lần đối với worker. Để biết thêm thông tin, hãy xem phần "Chọn số lượng worker".
Nếu cũng có môi trường bản dựng từ xa phù hợp với môi trường bản dựng cục bộ, bạn có thể sử dụng chiến lược động thử nghiệm. Chiến lược này sẽ chạy song song một quá trình thực thi từ xa và một quá trình thực thi worker. Để bật chiến lược động, hãy truyền cờ --experimental_spawn_scheduler. Chiến lược này tự động bật worker, vì vậy, bạn không cần chỉ định chiến lược worker
, nhưng vẫn có thể sử dụng local
hoặc sandboxed
làm phương án dự phòng.
Chọn số lượng worker
Số lượng thực thể worker mặc định cho mỗi câu thần chú là 4, nhưng bạn có thể điều chỉnh bằng cờ worker_max_instances
. Có sự đánh đổi giữa việc tận dụng tốt các CPU có sẵn và số lần biên dịch JIT và số lượt truy cập vào bộ nhớ đệm mà bạn nhận được. Khi có nhiều worker hơn, nhiều mục tiêu hơn sẽ phải trả chi phí khởi động khi chạy mã không phải JIT và truy cập vào bộ nhớ đệm nguội. Nếu bạn có một số lượng nhỏ mục tiêu cần tạo, thì một worker có thể mang lại sự đánh đổi tốt nhất giữa tốc độ biên dịch và mức sử dụng tài nguyên (ví dụ: xem vấn đề #8586).
Cờ worker_max_instances
đặt số lượng thực thể worker tối đa cho mỗi bộ nhớ đệm và cờ (xem bên dưới). Vì vậy, trong hệ thống kết hợp, bạn có thể sử dụng khá nhiều bộ nhớ nếu giữ nguyên giá trị mặc định. Đối với các bản dựng gia tăng, lợi ích của nhiều thực thể worker thậm chí còn nhỏ hơn.
Biểu đồ này cho thấy thời gian biên dịch từ đầu cho Bazel (mục tiêu //src:bazel
) trên máy trạm Linux Intel Xeon 3,5 GHz siêu phân luồng 6 nhân với RAM 64 GB. Đối với mỗi cấu hình worker, hệ thống sẽ chạy 5 bản dựng sạch và lấy trung bình của 4 bản dựng gần đây nhất.
Hình 1. Biểu đồ về hiệu suất cải thiện của các bản dựng sạch.
Đối với cấu hình này, hai worker sẽ biên dịch nhanh nhất, mặc dù chỉ cải thiện được 14% so với một worker. Một worker là một lựa chọn phù hợp nếu bạn muốn sử dụng ít bộ nhớ hơn.
Quá trình biên dịch tăng dần thường mang lại nhiều lợi ích hơn nữa. Các bản dựng sạch tương đối hiếm, nhưng việc thay đổi một tệp duy nhất giữa các lần biên dịch là phổ biến, đặc biệt là trong quá trình phát triển do kiểm thử. Ví dụ trên cũng có một số thao tác đóng gói không phải Java có thể làm lu mờ thời gian biên dịch gia tăng.
Chỉ biên dịch lại các nguồn Java (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
) sau khi thay đổi hằng số chuỗi nội bộ trong AbstractContainerizingSandboxedSpawn.java sẽ tăng tốc độ lên gấp 3 lần (trung bình 20 bản dựng gia tăng với một bản dựng khởi động bị loại bỏ):
Hình 2. Biểu đồ về hiệu suất cải thiện của các bản dựng gia tăng.
Tốc độ tăng tốc phụ thuộc vào thay đổi đang được thực hiện. Tốc độ tăng lên gấp 6 lần được đo trong trường hợp trên khi một hằng số thường dùng được thay đổi.
Sửa đổi trình thực thi liên tục
Bạn có thể truyền cờ --worker_extra_flag
để chỉ định cờ khởi động cho các worker, được khoá bằng ghi nhớ. Ví dụ: việc truyền --worker_extra_flag=javac=--debug
sẽ chỉ bật tính năng gỡ lỗi cho Javac.
Bạn chỉ có thể đặt một cờ worker cho mỗi lần sử dụng cờ này và chỉ cho một ghi nhớ.
Worker không chỉ được tạo riêng cho từng câu thần chú mà còn cho các biến thể trong cờ khởi động. Mỗi tổ hợp cờ ghi nhớ và cờ khởi động được kết hợp thành một WorkerKey
và đối với mỗi WorkerKey
, bạn có thể tạo tối đa worker_max_instances
worker. Hãy xem phần tiếp theo để biết cách cấu hình hành động cũng có thể chỉ định cờ thiết lập.
Bạn có thể sử dụng cờ --high_priority_workers
để chỉ định một ghi nhớ sẽ được chạy ưu tiên cho bộ nhớ có mức độ ưu tiên thông thường. Điều này có thể giúp ưu tiên các thao tác luôn nằm trên đường dẫn quan trọng. Nếu có hai hoặc nhiều worker có mức độ ưu tiên cao đang thực thi các yêu cầu, thì tất cả worker khác sẽ không được chạy. Bạn có thể sử dụng cờ này nhiều lần.
Việc chuyển cờ --worker_sandboxing
sẽ khiến mỗi yêu cầu worker sử dụng một thư mục hộp cát riêng cho tất cả dữ liệu đầu vào. Việc thiết lập hộp cát sẽ mất thêm thời gian, đặc biệt là trên macOS, nhưng giúp đảm bảo độ chính xác cao hơn.
Cờ --worker_quit_after_build
chủ yếu hữu ích khi gỡ lỗi và lập hồ sơ. Cờ này buộc tất cả worker phải thoát sau khi bản dựng hoàn tất. Bạn cũng có thể truyền --worker_verbose
để nhận thêm kết quả về những việc mà worker đang làm. Cờ này được phản ánh trong trường verbosity
trong WorkRequest
, cho phép việc triển khai worker cũng chi tiết hơn.
Worker lưu trữ nhật ký của chúng trong thư mục <outputBase>/bazel-workers
, ví dụ: /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
.
Tên tệp bao gồm mã trình thực thi và tên ghi nhớ. Vì có thể có nhiều WorkerKey
cho mỗi câu thần chú, nên bạn có thể thấy nhiều tệp nhật ký worker_max_instances
cho một câu thần chú nhất định.
Đối với các bản dựng Android, hãy xem thông tin chi tiết tại trang Hiệu suất bản dựng Android.
Triển khai trình thực thi liên tục
Hãy xem trang tạo worker liên tục để biết thêm thông tin về cách tạo worker.
Ví dụ này cho thấy cấu hình Starlark cho một worker sử dụng 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" }
)
Với định nghĩa này, lần sử dụng đầu tiên của thao tác này sẽ bắt đầu bằng cách thực thi dòng lệnh /bin/some_compiler -max_mem=4G --persistent_worker
. Sau đó, yêu cầu biên dịch Foo.java
sẽ có dạng như sau:
LƯU Ý: Mặc dù thông số kỹ thuật của vùng đệm giao thức sử dụng "kiểu viết thường" (request_id
), nhưng giao thức JSON sử dụng "kiểu viết hoa chữ cái đầu" (requestId
). Trong tài liệu này, chúng ta sẽ sử dụng kiểu viết hoa chữ cái đầu trong các ví dụ về JSON, nhưng sử dụng kiểu viết thường khi nói về trường bất kể giao thức.
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
Worker nhận được thông tin này trên stdin
ở định dạng JSON được phân tách bằng dòng mới (vì requires-worker-protocol
được đặt thành JSON). Sau đó, worker sẽ thực hiện hành động và gửi WorkResponse
ở định dạng JSON đến Bazel trên stdout. Sau đó, Bazel sẽ phân tích cú pháp phản hồi này và chuyển đổi phản hồi đó thành một proto WorkResponse
theo cách thủ công. Để giao tiếp với worker được liên kết bằng protobuf được mã hoá nhị phân thay vì JSON, requires-worker-protocol
sẽ được đặt thành proto
, như sau:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
Nếu bạn không đưa requires-worker-protocol
vào các yêu cầu thực thi, thì Bazel sẽ đặt mặc định giao tiếp của worker là sử dụng protobuf.
Bazel lấy WorkerKey
từ câu thần chú và cờ dùng chung, vì vậy, nếu cấu hình này cho phép thay đổi tham số max_mem
, thì một worker riêng sẽ được tạo cho mỗi giá trị được sử dụng. Điều này có thể dẫn đến việc tiêu thụ bộ nhớ quá mức nếu sử dụng quá nhiều biến thể.
Hiện tại, mỗi worker chỉ có thể xử lý một yêu cầu tại một thời điểm. Tính năng nhiều worker đa kênh thử nghiệm cho phép sử dụng nhiều luồng nếu công cụ cơ bản là đa luồng và trình bao bọc được thiết lập để hiểu điều này.
Trong kho lưu trữ GitHub này, bạn có thể xem các trình bao bọc worker mẫu được viết bằng Java cũng như Python. Nếu bạn đang làm việc trong JavaScript hoặc TypeScript, thì gói @bazel/worker và ví dụ về worker nodejs có thể hữu ích.
Trình thực thi ảnh hưởng như thế nào đến hộp cát?
Theo mặc định, việc sử dụng chiến lược worker
sẽ không chạy hành động trong hộp cát, tương tự như chiến lược local
. Bạn có thể đặt cờ --worker_sandboxing
để chạy tất cả worker bên trong hộp cát, đảm bảo mỗi lần thực thi công cụ chỉ thấy các tệp đầu vào mà công cụ đó dự kiến sẽ có. Có thể công cụ này vẫn làm rò rỉ thông tin giữa các yêu cầu trong nội bộ, chẳng hạn như thông qua bộ nhớ đệm. Việc sử dụng chiến lược dynamic
yêu cầu các worker phải được đưa vào hộp cát.
Để cho phép sử dụng chính xác bộ nhớ đệm của trình biên dịch với worker, một chuỗi đại diện sẽ được truyền cùng với mỗi tệp đầu vào. Do đó, trình biên dịch hoặc trình bao bọc có thể kiểm tra xem dữ liệu đầu vào có còn hợp lệ hay không mà không cần phải đọc tệp.
Ngay cả khi sử dụng chuỗi đại diện đầu vào để bảo vệ khỏi việc lưu vào bộ nhớ đệm không mong muốn, worker trong hộp cát vẫn cung cấp tính năng hộp cát ít nghiêm ngặt hơn so với hộp cát thuần tuý, vì công cụ này có thể giữ lại trạng thái nội bộ khác đã bị ảnh hưởng bởi các yêu cầu trước đó.
Chỉ có thể tạo hộp cát nếu hoạt động triển khai trình thực thi có hỗ trợ hộp cát này, đồng thời phải bật riêng hộp cát này bằng cờ --experimental_worker_multiplex_sandboxing
. Xem thêm thông tin chi tiết trong tài liệu thiết kế).
Tài liệu đọc thêm
Để biết thêm thông tin về worker liên tục, hãy xem:
- Bài đăng ban đầu trên blog về worker cố định
- Mô tả cách triển khai Haskell
- Bài đăng trên blog của Mike Morearty
- Phát triển giao diện người dùng bằng Bazel: Angular/TypeScript và Worker ổn định với Asana
- Giải thích về các chiến lược Bazel
- Thảo luận về chiến lược nhân viên thông tin trên danh sách gửi thư thảo luận về bazel