Trang này trình bày cách sử dụng trình thực thi liên tục, các lợi ích, yêu cầu và cách trình thực thi ảnh hưởng đến việc tạo hộp cát.
Worker liên tục là một quy trình chạy trong thời gian dài do máy chủ Bazel khởi động, có chức năng như một trình bao bọc xung quanh công cụ thực tế (thường là một trình biên dịch) hoặc chính công cụ đó. Để tận dụng các worker liên tục, công cụ này phải hỗ trợ thực hiện một chuỗi các quy 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 bắt đầu và tương tác với công cụ một cách thích hợp, cũng như tắt các worker khi thoát. Mỗi phiên bản worker được chỉ định (nhưng không được chroot) một thư mục làm việc riêng biệt trong <outputBase>/bazel-workers
.
Sử dụng các worker liên tục là một chiến lược thực thi giúp giảm chi phí 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ác 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.
Các worker liên tục đượ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ợ 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 các worker liên tục theo mặc định khi thực thi các bản dựng, mặc dù quá trình thực thi từ xa sẽ được ưu tiên. Đối với những thao tác không hỗ trợ worker liên tục, Bazel sẽ quay lại việc khởi động một phiên bản công cụ cho mỗi thao tác. Bạn có thể đặt rõ ràng bản dựng để sử dụng các worker liên tục bằng cách đặt chiến lược worker
cho các từ viết tắt công cụ 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 tốc độ biên dịch đáng kể, tuỳ thuộc vào quá trình 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 còn nhanh hơn đối với quá trình biên dịch gia tăng. Việc biên dịch Bazel nhanh hơn khoảng 2,5 lần khi có worker. Để biết thêm thông tin, hãy xem phần "Chọn số lượng nhân viên".
Nếu có một môi trường xây dựng từ xa phù hợp với môi trường xây 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 quy trình thực thi từ xa và một quy trình thực thi của 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 các worker, nên bạn không cần chỉ định chiến lược worker
, nhưng vẫn có thể dùng local
hoặc sandboxed
làm phương án dự phòng.
Chọn số lượng nhân viên
Số lượng phiên bản worker mặc định cho mỗi từ khoá ghi nhớ 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 hiệu quả các CPU hiện có và số lượng lần biên dịch JIT cũng như số lần truy cập bộ nhớ đệm mà bạn nhận được. Với 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à gặp phải bộ nhớ đệm lạnh. Nếu bạn có một số ít mục tiêu cần tạo, thì một worker duy nhất có thể mang lại sự cân bằng 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 tối đa của các thực thể worker trên mỗi bộ nhớ và bộ cờ (xem bên dưới), vì vậy, trong một hệ thống hỗn hợp, bạn có thể sử dụng khá nhiều bộ nhớ nếu giữ giá trị mặc định. Đối với các bản dựng gia tăng, lợi ích của nhiều phiên bản 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ột máy trạm Linux Intel Xeon 3,5 GHz 6 lõi có siêu phân luồng với RAM 64 GB. Đối với mỗi cấu hình worker, 5 bản dựng sạch sẽ được chạy và lấy giá trị trung bình của 4 bản dựng cuối cùng.
Hình 1. Biểu đồ cải thiện hiệu suất của các bản dựng sạch.
Đối với cấu hình này, 2 worker sẽ cho tốc độ biên dịch nhanh nhất, mặc dù chỉ cải thiện 14% so với 1 worker. Một worker là lựa chọn phù hợp nếu bạn muốn sử dụng ít bộ nhớ hơn.
Việc biên dịch gia tăng 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à điều thường thấy, đặc biệt là trong quá trình phát triển dựa trên 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 một hằng số chuỗi nội bộ trong AbstractContainerizingSandboxedSpawn.java giúp tăng tốc 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 đồ cải thiện hiệu suất của các bản dựng gia tăng.
Tốc độ tăng tốc phụ thuộc vào thay đổi được thực hiện. Tốc độ tăng lên 6 lần được đo trong trường hợp trên khi một hằng số thường dùng bị 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ác cờ khởi động cho worker, được khoá bằng từ viết tắt. Ví dụ: việc truyền --worker_extra_flag=javac=--debug
chỉ bật chế độ 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 mã nhớ.
Các worker không chỉ được tạo riêng cho từng mã nhớ mà còn cho các biến thể trong cờ khởi động của chúng. Mỗi tổ hợp gồm mã nhớ và cờ khởi động sẽ đượ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. Xem phần tiếp theo để biết cách cấu hình thao tác cũng có thể chỉ định các cờ thiết lập.
Bạn có thể dùng cờ --high_priority_workers
để chỉ định một mã nhớ nên được chạy thay vì mã nhớ có mức độ ưu tiên bình thường. Điều này có thể giúp ưu tiên những thao tác luôn nằm trong đường dẫn quan trọng. Nếu có từ 2 worker có mức độ ưu tiên cao trở lên đang thực thi các yêu cầu, thì tất cả các worker khác sẽ không thể chạy. Bạn có thể dùng cờ này nhiều lần.
Việc truyền cờ --worker_sandboxing
khiến mỗi yêu cầu của worker sử dụng một thư mục hộp cát riêng cho tất cả các đầu vào của yêu cầu đó. Việc thiết lập hộp cát mất thêm thời gian, đặc biệt là trên macOS, nhưng đảm bảo độ chính xác cao hơn.
Cờ --worker_quit_after_build
chủ yếu hữu ích cho việc gỡ lỗi và lập hồ sơ. Cờ này buộc tất cả các worker phải thoát sau khi quá trình tạo bản dựng hoàn tất. Bạn cũng có thể truyền --worker_verbose
để nhận thêm thông tin đầu ra về những việc mà các worker đang làm. Cờ này được phản ánh trong trường verbosity
trong WorkRequest
, cho phép các triển khai worker cũng chi tiết hơn.
Các worker lưu trữ nhật ký của mình 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ã nhận dạng người dùng và từ khoá ghi nhớ. Vì có thể có nhiều WorkerKey
cho mỗi câu ghi nhớ, nên bạn có thể thấy nhiều hơn worker_max_instances
tệp nhật ký cho một câu ghi nhớ nhất định.
Đối với các bản dựng Android, hãy xem thông tin chi tiết trên 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 đầu tiên sử dụng thao tác này sẽ bắt đầu bằng việc thực thi dòng lệnh /bin/some_compiler -max_mem=4G --persistent_worker
. Khi đó, yêu cầu biên dịch Foo.java
sẽ có dạng như sau:
LƯU Ý: Mặc dù quy cách bộ đệm giao thức sử dụng "kiểu rắn" (request_id
), nhưng giao thức JSON sử dụng "kiểu lạc đà" (requestId
). Trong tài liệu này, chúng ta sẽ sử dụng kiểu lạc đà trong các ví dụ về JSON, nhưng kiểu rắn 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..." },
],
}
Trình chạy nhận được thông tin này trên stdin
ở định dạng JSON được phân tách theo 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
có định dạng JSON đến Bazel trên stdout của nó. Sau đó, Bazel sẽ phân tích cú pháp phản hồi này và chuyển đổi theo cách thủ công thành một giao thức WorkResponse
. Để 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 thêm requires-worker-protocol
vào các yêu cầu thực thi, Bazel sẽ mặc định sử dụng protobuf cho hoạt động giao tiếp của worker.
Bazel lấy WorkerKey
từ mã ghi nhớ và các 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 biệt sẽ được tạo cho mỗi giá trị được dùng. Điều này có thể dẫn đến việc tiêu thụ quá nhiều bộ nhớ nếu bạn sử dụng quá nhiều biến thể.
Hiện tại, mỗi nhân viên chỉ có thể xử lý một yêu cầu tại một thời điểm. Tính năng multiplex workers (trình thực thi ghép 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ữ này trên GitHub, bạn có thể thấy các trình bao bọc worker mẫu được viết bằng Java cũng như bằng 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.
Worker ảnh hưởng như thế nào đến việc tạo hộp cát?
Theo mặc định, việc sử dụng chiến lược worker
sẽ không chạy thao tác trong một 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ả các worker trong hộp cát, đảm bảo rằng mỗi lần thực thi công cụ chỉ thấy các tệp đầu vào mà công cụ đó phải có. Công cụ này vẫn có thể làm rò rỉ thông tin giữa các yêu cầu 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 cách ly.
Để cho phép sử dụng đúng bộ nhớ đệm của trình biên dịch với các worker, một bản tóm tắt 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 các bản tóm tắt đầu vào để ngăn chặn việc lưu vào bộ nhớ đệm không mong muốn, các worker trong hộp cát cung cấp khả năng tạo 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ữ trạng thái nội bộ khác đã bị ảnh hưởng bởi các yêu cầu trước đó.
Bạn chỉ có thể đưa các worker ghép kênh vào hộp cát nếu quá trình triển khai worker hỗ trợ tính năng này và bạn phải bật tính năng hộp cát riêng biệt 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ề các worker liên tục
- Nội dung mô tả về việc triển khai Haskell {: .external}
- Bài đăng trên blog của Mike Morearty {: .external}
- Phát triển giao diện người dùng bằng Bazel: Angular/TypeScript và Persistent Workers với Asana {: .external}
- Giải thích về các chiến lược của Bazel {: .external}
- Thảo luận về chiến lược worker mang tính thông tin trên danh sách gửi thư bazel-discuss {: .external}