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

Báo cáo vấn đề Xem nguồn Hằng đêm · 7,4 của Google. 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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à tác động của worker đến hộp cát.

Worker liên tục là một quá trình chạy trong thời gian dài do máy chủ Bazel bắt đầu, 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 là chính công cụ đó. Để hưởng lợi từ trình thực thi liên tục, công cụ này phải hỗ trợ thực hiện một trình tự 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. Điều tương tự worker có thể được gọi có và không có cờ --persistent_worker trong phần tử cùng một bản dựng và chịu trách nhiệm bắt đầu và trao đổi một cách thích hợp với cũng như tắt trình thực thi khi thoát. Mỗi thực thể worker được chỉ định (nhưng không được chuyển đến) một thư mục làm việc riêng biệt 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.

Trình chạy liên tục được triển khai cho nhiều ngôn ngữ, bao gồm cả Java, Scala, Kotlin và các tính 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 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 các bản dựng, mặc dù làm việc từ xa việc thực thi sẽ được ưu tiên. Đối với những thao tác không hỗ trợ trình thực thi liên tục, Bazel quay lại bắt đầu một phiên bản công cụ cho mỗi hành động. Bạn có thể đặt rõ ràng bản dựng để sử dụng worker ổn định bằng cách đặt chiến lược worker cho các từ viết tắt công cụ hiện hành. Cách hay nhất là trong ví dụ này, bạn nên chỉ định local làm một 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 cường biên dịch tốc độ đáng kể, tuỳ thuộc vào việc 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 nữa đối với tính năng biên dịch gia tăng. Biên dịch Bazel là nhanh gấp 2,5 lần so với người lao động. Để biết thêm thông tin, hãy xem phần "Chọn số lượng worker".

Nếu bạn cũng có môi trường tạo bản dựng từ xa phù hợp với bản dựng cục bộ bạn có thể sử dụng mô hình thử nghiệm chiến lược động, để thực thi một quá trình thực thi từ xa và thực thi worker. Để bật chế độ xem động chiến lược, truyền --experimental_spawn_scheduler cờ. Chiến lược này tự động kích hoạ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 dự phòng.

Chọn số lượng worker

Số lượng thực thể worker mặc định cho mỗi ghi nhớ là 4, nhưng bạn có thể điều chỉnh được với worker_max_instances cờ. Có một 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. Với nhiều nhân viên hơn, nhiều hơn mục tiêu sẽ trả chi phí khởi động khi chạy mã không phải JITt và kết hợp bộ nhớ đệm. Nếu bạn có ít mục tiêu cần xây dựng, một nhân viên duy nhất có thể đưa ra 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ụ: hãy xem vấn đề #8586. Cờ worker_max_instances đặt số lượng thực thể worker tối đa cho mỗi bộ ký hiệu và cờ (xem bên dưới). Vì vậy, trong một 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 tăng dần, lợi ích của nhiều thực thể worker còn nhỏ hơn nữa.

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 6 lõi siêu 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 sẽ được chạy và giá trị trung bình của bốn giá trị cuối cùng được lấy.

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

Hình 1. Biểu đồ mức độ 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ỉ ở mức 14% so với 1 nhân viên. 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 gia tăng thường mang lại nhiều lợi ích hơn nữa. Bản dựng sạch đang 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, trong đặc biệt là trong quá trình phát triển dựa trên thử nghiệm. Ví dụ ở trên cũng có một số mã không phải là Java đóng gói các hành động 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ỏ):

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

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.

Việc tăng tốc độ phụ thuộc vào thay đổi đang được thực hiện. Tốc độ của yếu tố 6 là được đo lường trong tình huống ở 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 worker, được khoá bằng câu thần chú. 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 câu thần chú. Worker không chỉ được tạo riêng cho từng từ viết tắt 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. Xem phần tiếp theo để biết cách cấu hình thao tác 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 câu khẩu hiệu cần được chạy thay vì câu khẩu hiệu có mức độ ưu tiên thông thường. Điều này có thể giúp ưu tiên các hành động luôn ở giai đoạn quan trọng đường dẫn. Nếu có từ 2 worker trở lên thực thi yêu cầu có mức độ ưu tiên cao, thì tất cả không cho các worker khác chạy. Cờ này có thể được sử dụng nhiều lần.

Vượt qua --worker_sandboxing cờ làm cho 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ả đầu vào. Việc thiết lập hộp cát sẽ mất thêm chút thời gian, đặc biệt là trên macOS, nhưng sẽ đảm bảo độ chính xác cao hơn.

Chiến lược phát hành đĩa đơn --worker_quit_after_build cờ hiệu chủ yếu hữu ích cho việc gỡ lỗi và phân tích tài nguyên. 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ể chuyển --worker_verbose đến có được kết quả tốt hơn về những gì mà nhân viên đang làm. Cờ này được phản ánh trong Trường verbosity trong WorkRequest, cho phép các hoạt động triển khai trình thực thi 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ã nhận dạng worker và câu thần chú. 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 đầu tiên sử dụng hành động 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. 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 này nhận được dữ liệu này vào 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.

Ngang bằng kho lưu trữ GitHub này, bạn có thể xem ví dụ về trình bao bọc trình thực thi được viết bằng Java cũng như Python. Nếu bạn đang xử lý JavaScript hoặc TypeScript, @bazel/worker packageví dụ về trình thực thinodejs có thể hữu ích.

Worker ảnh hưởng như thế nào đến tính năng 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 --worker_sandboxing gắn cờ để chạy tất cả worker bên trong hộp cát, đảm bảo mỗi quá trình thực thi công cụ chỉ thấy các tệp đầu vào đáng lẽ 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. Đang sử dụng chiến lược dynamic yêu cầu worker phải tạ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 các 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 đọ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 đó.

Multiplex worker chỉ có thể được tạo hộp cát nếu hoạt động triển khai worker hỗ trợ nó, và hộp cát này phải được bật riêng với 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: