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

Báo cáo vấn đề Xem nguồn Nightly/3}

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 trình thực thi vào 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. 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 khởi động và trao đổi với công cụ này một cách thích hợp, cũng như tắt các worker khi thoát. Mỗi thực thể worker được chỉ định (nhưng không bị can thiệp vào hệ thống) một thư mục đang hoạt động riêng trong <outputBase>/bazel-workers.

Sử dụng worker ổn định là một chiến lược thực thi giúp giảm mức 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 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 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ình trợ giúp @bazel/worker để triển khai giao thức worker.

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 các bản dựng, mặc dù phương thức thực thi từ xa được ưu tiên. Đối với các thao tác không hỗ trợ trình thực thi liên tục, Bazel sẽ quay lại bắt đầu một thực thể công cụ cho từng thao tác. Bạn có thể thiết lập rõ ràng bản dựng của mình để sử dụng worker liên tục bằng cách đặt chiến lược worker cho công cụ ghi nhớ có thể áp dụng. Tốt nhất là bạn nên 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 đáng kể tốc độ biên dịch, tuỳ thuộc vào cách 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 cao hơn khi biên dịch gia tăng. Quá trình biên dịch Bazel có tốc độ nhanh gấp khoảng 2,5 lần đối với worker. Để biết thêm thông tin, hãy xem phần "Chọn số lượng worker".

Nếu cũng có môi trường tạo bản dựng từ xa phù hợp với môi trường tạo bản 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 chạy đua quá trình thực thi từ xa và thực thi trình thực thi. Để bật chiến lược động, hãy chuyể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 phải chỉ định chiến lược worker, nhưng bạn vẫn có thể sử dụng local hoặc sandboxed làm chiến lược 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 bằng cờ worker_max_instances. Có một sự đánh đổi giữa việc sử dụng hiệu quả CPU có sẵn và số lượng quá trình biên dịch JIT cũng như số lượt truy cập vào bộ nhớ đệm bạn nhận được. Khi có nhiều worker hơn, sẽ có nhiều mục tiêu hơn sẽ trả chi phí khởi động khi chạy mã không được JITt và truy cập vào bộ nhớ đệm nguội. Nếu bạn có ít mục tiêu cần xây dựng, một worker 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ể worker tối đa cho mỗi bộ ghi nhớ và 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 thực thể 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áy trạm Linux Intel Xeon 3,5 GHz siêu phân luồng 6 nhân với RAM 64 GB. Đố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 4 bản dựng cuối cùng sẽ được lấy.

Biểu đồ mức độ cải thiện hiệu suất của 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 quá trình biên dịch nhanh nhất, mặc dù chỉ cải thiện được 14% so với 1 worker. Bạn nên dùng một worker nếu 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. Bản dựng sạch rất hiếm khi được thay đổi, nhưng việc thay đổi một tệp giữa các quá trình biên dịch là phổ biến, đặc biệt là trong quá trình phát triển theo hướng 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 tăng dần trong một bản 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 tăng dần

Hình 2. Biểu đồ cải thiện hiệu suất của các bản dựng tăng dần.

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 được đo lường trong trường hợp 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 các worker, được khoá bằng ghi nhớ. Ví dụ: thao tá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 ghi nhớ. Worker không chỉ được tạo riêng cho từng ghi nhớ mà còn cho những biến thể về cờ khởi động của chú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à có thể tạo cho mỗi worker WorkerKey (tối đa worker_max_instances). Hãy 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 ghi nhớ sẽ được chạy ưu tiên cho bộ nhớ có mức độ ưu tiên thông thường. Điều này có thể giúp ưu tiên các thao tác luôn nằm trên đường dẫn quan trọng. Nếu có 2 hoặc nhiều trình thực thi có mức độ ưu tiên cao thực thi yêu cầu, thì tất cả các trình thực thi khác sẽ bị ngăn chạy. Cờ này có thể được sử dụng nhiều lần.

Việc chuyển cờ --worker_sandboxing sẽ 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ả dữ liệu đầu vào. Việc thiết lập sandbox sẽ mất thêm thời gian, đặc biệt là trên macOS, nhưng giúp đả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à phân tích tài nguyên. Cờ này buộc tất cả worker phải thoát sau khi hoàn tất một bản dựng. Bạn cũng có thể truyền --worker_verbose để nhận thêm kết quả về những việc mà worker đang làm. Cờ này được phản ánh trong trường verbosity của WorkRequest, cho phép các phương thức triển khai trình thực thi chi tiết hơn.

Worker 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ên ghi nhớ. Vì có thể có nhiều WorkerKey cho mỗi 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 ghi nhớ 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

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ụ dưới đâ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. Sau đó, yêu cầu biên dịch Foo.java sẽ có dạng như sau:

LƯU Ý: Mặc dù quy cách vùng đệm giao thức sử dụng "kiểu viết con rắn" (request_id), nhưng giao thức JSON lại sử dụng "kiểu viết lạc đà" (requestId). Trong tài liệu này, chúng tôi sẽ sử dụng kiểu viết lạc đà trong các ví dụ về JSON, nhưng kiểu viết hoa rắn khi nói về trường bất kể giao thức là gì.

{
  "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 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 này sẽ thực hiện hành động và gửi WorkResponse có định dạng JSON cho 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 thiết lập 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ẽ mặc định cho phép giao tiếp của worker sử dụng protobuf.

Bazel lấy WorkerKey từ ghi nhớ 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 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 multix worker 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 trong JavaScript hoặc TypeScript, thì gói@bazel/workerví dụ về trình thực thinodejs có thể hữu ích.

Worker ảnh hưởng như thế nào đến 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 sandbox, tương tự như chiến lược local. Bạn có thể thiết lập cờ --worker_sandboxing để 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ủa công cụ này chỉ thấy các tệp đầu vào đáng lẽ phải có. Có thể công cụ này vẫn làm rò rỉ thông tin giữa các yêu cầu trong nội bộ, chẳng hạn như thông qua bộ nhớ đệm. 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 của trình biên dịch với worker, một chuỗi đại diện sẽ được truyền cùng từng 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 chuỗi đại diện đầu vào để bảo vệ khỏi hoạt động lưu không mong muốn vào bộ nhớ đệm, các trình thực thi trong hộp cát cung cấp 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 đó.

Chỉ có thể tạo hộp cát nếu hoạt động triển khai worker hỗ trợ hộp cát này, đồng thời phải bật riêng hộp cát này bằng cờ --experimental_worker_multiplex_sandboxing. Hãy 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: