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

Trang này trình bày cách sử dụng worker liên tục, lợi ích, yêu cầu và cách worker ả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, 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ừ worker liên tục, công cụ này phải hỗ trợ thực hiện một chuỗi 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. Bạn có thể gọi cùng một worker có và không có cờ --persistent_worker trong cùng một bản dựng. Worker 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 worker khi thoát. Mỗi thực thể 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.

Việc sử dụng 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, 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 đ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 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 worker.

Sử dụng worker liên tục

Bazel 0.27 trở lên sử dụng worker 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. Đối với những hành động không hỗ trợ worker liên tục, Bazel sẽ quay lại khởi động một thực thể công cụ cho từng hành động. Bạn có thể đặt bản dựng một cách rõ ràng để sử dụng worker 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:

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ể 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 worker. Để biết thêm chi tiết, hãy xem phần "Chọn số lượng worker".

Nếu bạn 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ộ, thì bạn có thể sử dụng chiến lược thử nghiệm động, chiến lược này sẽ chạy quá trình thực thi từ xa và 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 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 worker

Số lượng thực thể worker 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ờ. Bạn cần cân bằng giữa việc tận dụng CPU hiện có và số lượng quá trình biên dịch JIT và lượt truy cập 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 bộ nhớ đệm lạnh. Nếu bạn có một số ít mục tiêu cần xây dựng, thì một worker 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 thực thể worker 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ể worker 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 Intel Xeon 3,5 GHz Linux 6 lõi siêu phân luồng với 64 GB RAM. Đối với mỗi cấu hình worker, 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 worker giúp 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 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à đ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ố 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 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 3 lần (trung bình 20 bản dựng gia tăng với 1 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 được thực hiện. Tốc độ tăng gấp 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 worker liên tục

Bạn có thể truyền cờ --worker_extra_flag để chỉ định cờ khởi động cho worker, được khoá theo 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ờ worker cho mỗi lần sử dụng cờ này và chỉ cho một từ gợi nhớ. Worker không chỉ được tạo riêng cho từng 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 worker. 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.

Việc truyền cờ --worker_sandboxing 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ả các đầ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ả 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 thông tin đầu ra về những gì worker đ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 worker cũng chi tiết hơn.

Worker lưu 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ã worker và từ gợi nhớ. Vì có thể có nhiều WorkerKey cho mỗi từ gợi nhớ, nên bạn có thể thấy nhiều tệp nhật ký hơn worker_max_instances 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 worker 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 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), 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..." },
  ],
}

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 thực hiện hành động và gửi WorkResponse được đị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 thủ công thành proto 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 đưa requires-worker-protocol vào các yêu cầu thực thi, Bazel sẽ đặt mặc định giao tiếp worker để 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 worker 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 worker chỉ có thể xử lý một yêu cầu tại một thời điểm. Tính năng worker đ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ể xem 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 bằng JavaScript hoặc TypeScript, thì gói@bazel/workerví dụ về worker nodejs có thể hữu ích.

Worker ả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ả 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ụ đó phải có. Công cụ này 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 worker 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 worker, 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 đầ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 việc lưu vào bộ nhớ đệm không mong muốn, worker đượ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ụ 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ể tạo hộp cát cho worker đa hợp nếu cách triển khai worker hỗ trợ tính năng này và bạn phải bật riêng tính năng tạo 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: