Nhân viên liên tục

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,local

Việ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.

Biểu đồ cải thiện hiệu suất của các bản dựng sạch

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ỏ):

Biểu đồ cải thiện hiệu suất của các bản dựng gia tăng

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/workerví 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: