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

Báo cáo sự cố Xem nguồn

Trang này trình bày cách sử dụng trình thực thi liên tục, các lợi ích, yêu cầu và tác động của trình thực thi đối với hộp cát.

Trình thực thi liên tục là một quá trình lâu 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 chính là công cụ. Để hưởng lợi từ các trình thực thi liên tục, công cụ 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 phải 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ả dưới đây. Cùng một trình thực thi có thể được gọi khi 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à trò chuyện một cách thích hợp với công cụ, 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 chưa bị chroot) một thư mục đang hoạt động riêng trong <outputBase>/bazel-workers.

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, 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 giúp đạ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.v.

Các chương trình sử dụng thời gian chạy NodeJS có thể 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 các bản dựng, mặc dù hoạt động thực thi từ xa sẽ được ưu tiên. Đối với các thao tác không hỗ trợ worker liên tục, Bazel sẽ quay lại bắt đầu một thực thể công cụ cho mỗi 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 trình thực thi liên tục bằng cách đặt chiến lược worker cho tính năng ghi nhớ công cụ áp dụng. Tốt nhất là bạn nên chỉ định local làm phương thức 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. Việc biên dịch Bazel nhanh gấp khoảng 2,5 lần so với worker. Để biết thêm thông tin chi tiết, hãy xem phần "Chọn số lượng nhân viên".

Nếu cũng có một môi trường tạo bản dựng từ xa phù hợp với môi trường xây dựng cục bộ, bạn có thể sử dụng chiến lược động thử nghiệm để chạy lệnh thực thi từ xa và 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 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 nhân viên

Số lượng thực thể trình chạy mặc định cho mỗi ký tự ghi nhớ là 4, nhưng bạn có thể điều chỉnh bằng cờ worker_max_instances. Có sự đánh đổi giữa việc tận dụng tốt CPU có sẵn và số lượng truy cập bộ nhớ đệm và biên dịch JIT mà bạn nhận được. Với nhiều worker hơn, nhiều mục tiêu hơn sẽ trả chi phí khởi động để chạy mã không JIT và đánh vào bộ nhớ đệm nguội. Nếu bạn có ít mục tiêu cần xây dựng, thì một trình thực thi có thể đưa ra lựa chọn đá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 chạy tối đa cho mỗi ký tự ghi nhớ và tập hợp 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 tăng dần, 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 của 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 64 GB RAM. Đối với mỗi cấu hình trình chạy, 5 bản dựng sạch sẽ được chạy và 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 bản dựng sạch

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

Đối với cấu hình này, 2 trình thực thi có tốc độ biên dịch nhanh nhất, mặc dù chỉ cải thiện 14% so với một trình chạy. Một trình thực thi là lựa chọn phù hợp nếu bạn 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. Các bản dựng sạch là tương đối hiếm, 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 dựa trên 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 hằng số chuỗi nội bộ trong AbstractContainerizingSandboxedSpawn.java sẽ giúp tăng tốc gấp 3 lần (trung bình 20 bản dựng tăng dần có 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 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 hệ số 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 trình thực thi, được khoá theo ký tự 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. Chỉ có thể đặt một cờ worker cho mỗi lần sử dụng cờ này và chỉ cho một mục ghi nhớ. Worker không chỉ được tạo riêng cho mỗi thuộc tính ghi nhớ mà còn đối với 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, tối đa worker_max_instances worker có thể được tạo. Hãy 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.

Bạn có thể sử dụng cờ --high_priority_workers để chỉ định một tính năng ghi nhớ có mức độ ưu tiên bình thường. Điều này có thể giúp ưu tiên các hành động luôn nằm trên đường dẫn quan trọng. Nếu có hai 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ả trình thực thi khác đều sẽ bị ngăn chạy. Bạn có thể sử dụng cờ này nhiều lần.

Việc chuyển cờ --worker_sandboxing sẽ khiến mỗi yêu cầu trình thực thi (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 mang lại sự đảm bảo về độ chính xác tốt hơn.

Cờ --worker_quit_after_build chủ yếu hữu ích khi gỡ lỗi và phân tích tài nguyên. Cờ này buộc tất cả trình thực thi phải thoát sau khi quá trình tạo bản dựng hoàn tất. Bạn cũng có thể truyền --worker_verbose để nhận thêm kết quả 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 quá trình triển khai trình thực thi cũng 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ã nhận dạng worker và phần ghi nhớ. Vì có thể có nhiều WorkerKey cho mỗi kỷ niệm, nên bạn có thể thấy nhiều hơn worker_max_instances tệp nhật ký cho một kỷ niệm cụ thể.

Đố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 một trình thực thi.

Dưới đây là ví dụ về 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 hành động này đầu tiên sẽ bắt đầu bằng việc thực thi dòng lệnh /bin/some_compiler -max_mem=4G --persistent_worker. Khi đó, 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 "trường hợp con rắn" (request_id), giao thức JSON sử dụng "trường hợp lạc đà" (requestId). Trong tài liệu này, chúng tôi sẽ sử dụng trường hợp lạc đà trong ví dụ về JSON, nhưng trường hợp rắn khi nói về trường bất kể giao thức nào.

{
  "arguments": [ "-g", "-source", "1.5", "Foo.java" ]
  "inputs": [
    { "path": "symlinkfarm/input1", "digest": "d49a..." },
    { "path": "symlinkfarm/input2", "digest": "093d..." },
  ],
}

Trình thực thi này nhận được nội dung 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 thao tác và gửi WorkResponse ở định dạng JSON cho Bazel trên stdout của nó. Sau đó, Bazel phân tích cú pháp phản hồi này và chuyển đổi phản hồi theo cách thủ công thành một proto WorkResponse. Để giao tiếp với worker 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ẽ mặc định giao tiếp của worker để sử dụng protobuf.

Bazel lấy WorkerKey từ tính năng 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 trình thực thi riêng sẽ được sinh ra 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 multiplex 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 được điều này.

Trong kho lưu trữ GitHub này, bạn có thể xem các ví dụ về trình bao bọc trình thực thi được viết trong Java cũng như trong Python. Nếu bạn đang dùng JavaScript hoặc TypeScript, thì gói@bazel/workerví dụ về trình thực thinodejs có thể rất hữu ích.

Trình thực thi ả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 hành động 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 mà công cụ đó cần có. Công cụ này vẫn có thể 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. Khi 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 đúng bộ nhớ đệm của trình biên dịch với trình thực thi, một chuỗi đại diện sẽ được truyền cùng với 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ệ mà không cần phải đọc tệp hay không.

Ngay cả khi sử dụng chuỗi đại diện đầu vào để ngăn chặn tình trạng lưu vào bộ nhớ đệm không mong muốn, trình thực thi trong hộp cát vẫn 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ữ trạng thái nội bộ khác đã bị các yêu cầu trước đó ảnh hưởng.

Bạn chỉ có thể tạo hộp cát cho trình thực thi Multiplex nếu phương thức triển khai của trình thực thi đó hỗ trợ hộp cát đó, đồng thời bạn 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: