Trang này trình bày cách sử dụng trình thực thi liên tụ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.
Trình thực thi 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,
hoạt động như một 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ừ trình thực thi liên tục, công cụ phải
hỗ trợ thực hiện một chuỗi các lần 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. Bạn có thể gọi cùng một
trình thực thi có hoặc không có cờ --persistent_worker trong cùng một
bản dựng. Trình thực thi này chịu trách nhiệm khởi động và giao tiếp với
công cụ một cách thích hợp, cũng như tắt trình thực thi khi thoát. Mỗi thực thể trình thực thi đượ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.
Việc sử dụng trình thực thi 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, ví dụ: 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 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.
Trình thực thi 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ể sử dụng thư viện trợ giúp @bazel/worker để triển khai giao thức trình thực thi.
Sử dụng trình thực thi 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 bản dựng, mặc dù quá trình thực thi từ xa
được ưu tiên hơn. Đối với các hành động không hỗ trợ trình thực thi liên tục,
Bazel sẽ quay lại khởi động một thực thể công cụ cho mỗi hành động. Bạn có thể đặt bản dựng một cách rõ ràng
để sử dụng trình thực thi liên tục bằng cách đặt worker
chiến lược cho các từ gợi nhớ công cụ á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 strategy:
bazel build //my:target --strategy=Javac=worker,localViệc sử dụng chiến lược trình thực thi thay vì chiến lược cục bộ có thể giúp tăng tốc độ biên dịch đáng kể, tuỳ thuộc vào cách triển khai. Đối với Java, bản dựng có thể nhanh hơn từ 2 đến 4 lần, đôi khi 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 sử dụng trình thực thi. Để biết thêm thông tin chi tiết, hãy xem phần "Chọn số lượng trình thực thi".
Nếu bạn cũng có một môi trường bản dựng từ xa khớp với môi trường bản dựng cục bộ, thì bạn có thể sử dụng chiến lược động thử nghiệm,
chiến lược này sẽ chạy đua giữa quá trình thực thi từ xa và quá trình thực thi trình thực thi. Để 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 trình thực thi, vì vậy, bạn không cần
chỉ định chiến lược worker, nhưng bạn vẫn có thể sử dụng local hoặc sandboxed làm
phương án dự phòng.
Chọn số lượng trình thực thi
Số lượng thực thể trình thực thi mặc định cho mỗi từ gợi nhớ là 4, nhưng có thể điều chỉnh
bằng
worker_max_instances
cờ. Có sự đánh đổi giữa việc tận dụng tốt các CPU hiện có và
số lượng lần biên dịch JIT và lần truy cập bộ nhớ đệm mà bạn nhận được. Khi có nhiều trình thực thi hơn, nhiều
mục tiêu 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 lạnh. Nếu bạn có một số lượng nhỏ mục tiêu để xây dựng, thì một trình thực thi 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ể trình thực thi tối đa cho mỗi
từ gợi 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ữ 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ể trình thực thi 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 siêu phân luồng
với 64 GB RAM. Đối với mỗi cấu hình trình thực thi, 5 bản dựng sạch được chạy và
giá trị trung bình của 4 bản dựng cuối cùng được lấy.

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 trình thực thi cho phép biên dịch nhanh nhất, mặc dù chỉ cải thiện được 14% so với 1 trình thực thi. Một trình thực thi là lựa chọn tốt nếu bạn muốn sử dụng ít bộ nhớ hơn.
Quá trình biên dịch gia tăng thường mang lại nhiều lợi ích hơn. Các bản dựng sạch tương đối hiếm, nhưng việc thay đổi một tệp 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 dựa trên kiểm thử. Ví dụ trên cũng có một số hành động đóng gói không phải Java có thể làm lu mờ thời gian biên dịch gia tăng.
Việc biên dịch lại các nguồn Java chỉ
(//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 của 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 phụ thuộc vào thay đổi đang được thực hiện. Tốc độ tăng gấp 6 lần được đo trong tình huống 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 trình thực thi, được khoá bằng từ gợi nhớ. Ví dụ:
việc truyền --worker_extra_flag=javac=--debug chỉ bật tính năng gỡ lỗi cho Javac.
Bạn chỉ có thể đặt một cờ trình thực thi cho mỗi lần sử dụng cờ này và chỉ cho một từ gợi nhớ.
Trình thực thi không chỉ được tạo riêng cho mỗi từ gợi nhớ mà còn cho
các biến thể trong cờ khởi động. Mỗi tổ hợp từ gợi 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 trình thực thi. Xem phần tiếp theo để biết cách
cấu hình hành động cũng có thể chỉ định các cờ thiết lập.
Việc truyền cờ
--worker_sandboxing
khiến mỗi yêu cầu trình thực thi sử dụng một thư mục hộp cát riêng biệt cho tất cả dữ liệu đầu vào. Việc thiết lập hộp cát mất thêm một chút thời gian,
đặc biệt là trên macOS, nhưng đảm bảo độ chính xác tốt 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ả trình thực thi
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 thông tin đầu ra về những gì trình thực thi đang làm. Cờ này được phản ánh trong trường
verbosity trong WorkRequest, cho phép các cách triển khai trình thực thi cũng chi tiết hơn.
Trình thực thi lưu trữ nhật ký 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ừ gợi nhớ. Vì có thể có nhiều hơn
một WorkerKey cho mỗi từ gợi nhớ, nên bạn có thể thấy nhiều hơn worker_max_instances
tệp nhật ký cho một từ gợi 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
Xem trang tạo trình thực thi liên tục để biết thêm thông tin về cách tạo trình thực thi.
Ví dụ này cho thấy cấu hình Starlark cho một trình thực thi 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 hành động 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ù đặc tả bộ đệm giao thức sử dụng "snake case" (request_id),
nhưng giao thức JSON sử dụng "camel case" (requestId). Trong tài liệu này, chúng tôi sẽ sử dụng
camel case trong các ví dụ JSON, nhưng snake case 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 thực thi 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 đó, trình thực thi thực hiện hành động,
và gửi WorkResponse ở định dạng JSON đến Bazel trên stdout. Sau đó, Bazel
phân tích cú pháp phản hồi này và chuyển đổi thủ công thành proto WorkResponse. Để
giao tiếp với trình thực thi đượ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,
Bazel sẽ đặt mặc định giao tiếp trình thực thi để sử dụng protobuf.
Bazel lấy WorkerKey từ từ gợi 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 trình thực thi riêng biệt sẽ được tạo cho mỗi giá trị được sử dụng. Điều này có thể dẫn đến mứ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 trình thực thi chỉ có thể xử lý một yêu cầu tại một thời điểm. Tính năng trình thực thi đa hợp 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ể thấy các trình bao bọc trình thực thi mẫu được viết bằng Java cũng như bằng Python. Nếu bạn đang làm việc bằng JavaScript hoặc TypeScript, thì gói@bazel/worker và ví dụ về trình thực thi nodejs có thể hữu ích.
Trình thực thi ảnh hưởng đến việc tạo hộp cát như thế nào?
Việc sử dụng chiến lược worker theo mặc định 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ả trình thực thi 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ụ đó phải có. Công cụ
vẫn có thể 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 trình thực thi phải được tạo hộp cát.
Để cho phép sử dụng chính xác bộ nhớ đệm trình biên dịch với trình thực thi, một bản tóm tắt đượ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 đọc tệp.
Ngay cả khi sử dụng 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, trình thực thi được tạo 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ụ 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 đó.
Trình thực thi đa hợp chỉ có thể được tạo hộp cát nếu quá trình triển khai trình thực thi hỗ trợ tính năng này,
và bạn phải bật tính năng tạo hộp cát này riêng 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ề trình thực thi liên tục, hãy xem:
- Bài đăng trên blog về trình thực thi liên tục ban đầu
- Nội dung mô tả về 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à trình thực thi liên tục với Asana
- Giải thích về các chiến lược Bazel
- Cuộc thảo luận thông tin về chiến lược trình thực thi trên danh sách gửi thư bazel-discuss