Thách thức của việc viết quy tắc

Báo cáo vấn đề Xem nguồn Nightly · 8.0 . 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Trang này cung cấp thông tin tổng quan về các vấn đề và thách thức cụ thể khi viết quy tắc Bazel hiệu quả.

Tóm tắt các yêu cầu

  • Giả định: Hướng đến độ chính xác, tốc độ truyền dữ liệu, khả năng dễ sử dụng và độ trễ
  • Giả định: Kho lưu trữ quy mô lớn
  • Giả định: Ngôn ngữ mô tả giống như BUILD
  • Trước đây: Việc phân tách cứng giữa Tải, Phân tích và Thực thi đã lỗi thời nhưng vẫn ảnh hưởng đến API
  • Nội tại: Khó thực thi từ xa và lưu vào bộ nhớ đệm
  • Nội tại: Việc sử dụng Thông tin thay đổi cho các bản dựng tăng dần chính xác và nhanh đòi hỏi các Mẫu lập trình bất thường
  • Nội tại: Khó tránh được thời gian và mức tiêu thụ bộ nhớ theo phương trình bậc hai

Các giả định

Sau đây là một số giả định được đưa ra về hệ thống xây dựng, chẳng hạn như nhu cầu về tính chính xác, dễ sử dụng, thông lượng và kho lưu trữ quy mô lớn. Các phần sau đây sẽ giải quyết những giả định này và đưa ra các nguyên tắc để đảm bảo rằng các quy tắc được viết theo cách hiệu quả.

Hướng đến độ chính xác, tốc độ truyền tải, dễ sử dụng và độ trễ

Chúng tôi giả định rằng hệ thống xây dựng cần phải chính xác trước tiên và quan trọng nhất là đối với các bản dựng gia tăng. Đối với một cây nguồn nhất định, kết quả của cùng một bản dựng phải luôn giống nhau, bất kể cây đầu ra trông như thế nào. Trong lần gần đúng đầu tiên, điều này có nghĩa là Bazel cần biết mọi dữ liệu đầu vào đi vào một bước xây dựng nhất định, để có thể chạy lại bước đó nếu có bất kỳ dữ liệu đầu vào nào thay đổi. Có giới hạn về độ chính xác của Bazel, vì công cụ này rò rỉ một số thông tin như ngày / giờ của bản dựng và bỏ qua một số loại thay đổi nhất định như thay đổi đối với thuộc tính tệp. Tính năng hộp cát giúp đảm bảo tính chính xác bằng cách ngăn việc đọc các tệp đầu vào chưa khai báo. Ngoài các giới hạn nội tại của hệ thống, còn có một số vấn đề về tính chính xác đã biết, hầu hết đều liên quan đến Fileset hoặc các quy tắc C++, cả hai đều là những vấn đề khó. Chúng tôi đã nỗ lực lâu dài để khắc phục những vấn đề này.

Mục tiêu thứ hai của hệ thống xây dựng là có thông lượng cao; chúng tôi liên tục đẩy ranh giới của những việc có thể làm trong phạm vi phân bổ máy hiện tại cho dịch vụ thực thi từ xa. Nếu dịch vụ thực thi từ xa bị quá tải, thì không ai có thể hoàn thành công việc.

Tiếp theo là tính dễ sử dụng. Trong số nhiều phương pháp chính xác có cùng (hoặc tương tự) dấu vết của dịch vụ thực thi từ xa, chúng ta chọn phương pháp dễ sử dụng hơn.

Độ trễ biểu thị thời gian từ khi bắt đầu một bản dựng đến khi nhận được kết quả dự kiến, cho dù đó là nhật ký kiểm thử từ một kiểm thử thành công hay không thành công, hoặc thông báo lỗi cho biết tệp BUILD có lỗi chính tả.

Xin lưu ý rằng các mục tiêu này thường trùng lặp; độ trễ là một hàm của thông lượng của dịch vụ thực thi từ xa cũng như độ chính xác liên quan đến khả năng dễ sử dụng.

Kho lưu trữ quy mô lớn

Hệ thống xây dựng cần hoạt động ở quy mô của các kho lưu trữ lớn, trong đó quy mô lớn có nghĩa là không vừa với một ổ đĩa cứng, vì vậy, không thể kiểm tra toàn bộ trên hầu hết các máy của nhà phát triển. Một bản dựng có kích thước trung bình sẽ cần đọc và phân tích cú pháp hàng chục nghìn tệp BUILD, đồng thời đánh giá hàng trăm nghìn glob. Mặc dù về lý thuyết, bạn có thể đọc tất cả các tệp BUILD trên một máy, nhưng chúng tôi chưa thể làm như vậy trong một khoảng thời gian và bộ nhớ hợp lý. Do đó, điều quan trọng là các tệp BUILD có thể được tải và phân tích cú pháp độc lập.

Ngôn ngữ mô tả giống như BUILD

Trong ngữ cảnh này, chúng ta giả định một ngôn ngữ cấu hình tương tự như các tệp BUILD trong phần khai báo quy tắc nhị phân và thư viện cũng như các phần phụ thuộc lẫn nhau của chúng. Các tệp BUILD có thể được đọc và phân tích cú pháp độc lập, đồng thời chúng ta thậm chí tránh xem xét các tệp nguồn bất cứ khi nào có thể (ngoại trừ việc xác định sự tồn tại).

Cổ

Có sự khác biệt giữa các phiên bản Bazel gây ra các thách thức và một số phiên bản trong số này được nêu trong các phần sau.

Việc tách biệt rõ ràng giữa việc tải, phân tích và thực thi đã lỗi thời nhưng vẫn ảnh hưởng đến API

Về mặt kỹ thuật, quy tắc chỉ cần biết các tệp đầu vào và đầu ra của một thao tác ngay trước khi thao tác đó được gửi đến quá trình thực thi từ xa. Tuy nhiên, cơ sở mã Bazel ban đầu đã tách biệt nghiêm ngặt các gói tải, sau đó phân tích các quy tắc bằng cách sử dụng cấu hình (cờ dòng lệnh, về cơ bản) và chỉ sau đó mới chạy bất kỳ hành động nào. Sự khác biệt này vẫn là một phần của API quy tắc hiện nay, mặc dù cốt lõi của Bazel không còn yêu cầu điều này nữa (thông tin chi tiết hơn ở bên dưới).

Điều đó có nghĩa là API quy tắc yêu cầu nội dung mô tả khai báo về giao diện quy tắc (các thuộc tính của giao diện, loại thuộc tính). Có một số trường hợp ngoại lệ mà API cho phép mã tuỳ chỉnh chạy trong giai đoạn tải để tính toán tên ngầm ẩn của tệp đầu ra và giá trị ngầm ẩn của các thuộc tính. Ví dụ: quy tắc java_library có tên "foo" sẽ ngầm tạo ra một đầu ra có tên "libfoo.jar", có thể được tham chiếu từ các quy tắc khác trong biểu đồ bản dựng.

Hơn nữa, việc phân tích một quy tắc không thể đọc bất kỳ tệp nguồn nào hoặc kiểm tra đầu ra của một hành động; thay vào đó, quy tắc này cần tạo một biểu đồ hai phân đoạn có hướng một phần của các bước xây dựng và tên tệp đầu ra chỉ được xác định từ chính quy tắc và các phần phụ thuộc của quy tắc đó.

Nội tại

Có một số thuộc tính nội tại khiến việc viết quy tắc trở nên khó khăn và một số thuộc tính phổ biến nhất được mô tả trong các phần sau.

Khó thực thi từ xa và lưu vào bộ nhớ đệm

Việc thực thi từ xa và lưu vào bộ nhớ đệm giúp cải thiện thời gian xây dựng trong các kho lưu trữ lớn khoảng hai bậc so với việc chạy bản dựng trên một máy duy nhất. Tuy nhiên, quy mô mà dịch vụ này cần thực hiện là rất lớn: dịch vụ thực thi từ xa của Google được thiết kế để xử lý một số lượng lớn yêu cầu mỗi giây và giao thức này cẩn thận tránh các lượt truy cập không cần thiết cũng như công việc không cần thiết ở phía dịch vụ.

Tại thời điểm này, giao thức yêu cầu hệ thống xây dựng phải biết trước tất cả dữ liệu đầu vào cho một hành động nhất định; sau đó, hệ thống xây dựng sẽ tính toán một vân tay hành động duy nhất và yêu cầu trình lập lịch biểu về một lượt truy cập vào bộ nhớ đệm. Nếu tìm thấy một lần truy cập vào bộ nhớ đệm, trình lập lịch biểu sẽ trả lời bằng chuỗi đại diện của các tệp đầu ra; các tệp đó sẽ được phân phát theo chuỗi đại diện sau này. Tuy nhiên, điều này áp đặt các quy tắc hạn chế đối với Bazel, cần phải khai báo trước tất cả các tệp đầu vào.

Việc sử dụng thông tin thay đổi cho các bản dựng tăng dần chính xác và nhanh chóng đòi hỏi các mẫu lập trình bất thường

Ở trên, chúng ta đã lập luận rằng để chính xác, Bazel cần biết tất cả các tệp đầu vào đi vào một bước xây dựng để phát hiện xem bước xây dựng đó có còn mới nhất hay không. Điều này cũng đúng với việc tải gói và phân tích quy tắc. Chúng tôi đã thiết kế Skyframe để xử lý vấn đề này nói chung. Skyframe là một thư viện biểu đồ và khung đánh giá lấy một nút mục tiêu (chẳng hạn như "build //foo with these options") và chia nhỏ nút đó thành các phần cấu thành, sau đó đánh giá và kết hợp để tạo ra kết quả này. Trong quá trình này, Skyframe đọc các gói, phân tích quy tắc và thực thi hành động.

Tại mỗi nút, Skyframe theo dõi chính xác những nút mà bất kỳ nút nào được dùng để tính toán đầu ra của chính nó, từ nút mục tiêu cho đến các tệp đầu vào (cũng là các nút Skyframe). Việc biểu thị rõ ràng biểu đồ này trong bộ nhớ cho phép hệ thống xây dựng xác định chính xác những nút nào chịu ảnh hưởng của một thay đổi nhất định đối với tệp đầu vào (bao gồm cả việc tạo hoặc xoá tệp đầu vào), thực hiện lượng công việc tối thiểu để khôi phục cây đầu ra về trạng thái dự kiến.

Trong quá trình này, mỗi nút thực hiện một quy trình khám phá phần phụ thuộc. Mỗi nút có thể khai báo phần phụ thuộc, sau đó sử dụng nội dung của các phần phụ thuộc đó để khai báo các phần phụ thuộc khác. Về nguyên tắc, mô hình này liên kết tốt với mô hình luồng trên mỗi nút. Tuy nhiên, các bản dựng cỡ trung bình chứa hàng trăm nghìn nút Skyframe, điều này không dễ dàng thực hiện được với công nghệ Java hiện tại (và vì lý do lịch sử, chúng tôi hiện đang gắn liền với việc sử dụng Java, vì vậy không có luồng nhẹ và không có tiếp tục).

Thay vào đó, Bazel sử dụng một nhóm luồng có kích thước cố định. Tuy nhiên, điều đó có nghĩa là nếu một nút khai báo một phần phụ thuộc chưa có sẵn, chúng ta có thể phải huỷ quá trình đánh giá đó và bắt đầu lại (có thể trong một luồng khác) khi phần phụ thuộc đó có sẵn. Điều này đồng nghĩa với việc các nút không nên thực hiện việc này quá mức; một nút khai báo N phần phụ thuộc theo tuần tự có thể được khởi động lại N lần, tốn thời gian O(N^2). Thay vào đó, chúng ta hướng đến việc khai báo trước hàng loạt các phần phụ thuộc, đôi khi cần phải sắp xếp lại mã hoặc thậm chí chia một nút thành nhiều nút để giới hạn số lần khởi động lại.

Xin lưu ý rằng công nghệ này hiện không có trong API quy tắc; thay vào đó, API quy tắc vẫn được xác định bằng các khái niệm cũ về các giai đoạn tải, phân tích và thực thi. Tuy nhiên, một hạn chế cơ bản là tất cả các quyền truy cập vào các nút khác đều phải thông qua khung để có thể theo dõi các phần phụ thuộc tương ứng. Bất kể ngôn ngữ triển khai hệ thống xây dựng hoặc ngôn ngữ viết quy tắc (không nhất thiết phải giống nhau), tác giả quy tắc không được sử dụng các thư viện hoặc mẫu chuẩn bỏ qua Skyframe. Đối với Java, điều đó có nghĩa là tránh java.io.File cũng như mọi hình thức phản chiếu và mọi thư viện có thực hiện việc này. Bạn vẫn cần thiết lập đúng cách các thư viện hỗ trợ chèn phần phụ thuộc của các giao diện cấp thấp này cho Skyframe.

Trước tiên, bạn nên tránh cho phép tác giả quy tắc tiếp xúc với môi trường thời gian chạy ngôn ngữ đầy đủ. Nguy cơ vô tình sử dụng các API như vậy là quá lớn – một số lỗi Bazel trước đây là do các quy tắc sử dụng API không an toàn, mặc dù các quy tắc này do nhóm Bazel hoặc các chuyên gia khác trong lĩnh vực này viết.

Khó tránh được thời gian và mức sử dụng bộ nhớ theo hàm bậc hai

Tệ hơn nữa, ngoài các yêu cầu do Skyframe đưa ra, các quy tắc ràng buộc trước đây khi sử dụng Java và API quy tắc đã lỗi thời, việc vô tình đưa ra thời gian hoặc mức sử dụng bộ nhớ theo phương trình bậc hai là một vấn đề cơ bản trong mọi hệ thống xây dựng dựa trên thư viện và quy tắc nhị phân. Có hai mẫu rất phổ biến gây ra mức tiêu thụ bộ nhớ bậc hai (và do đó là mức tiêu thụ thời gian bậc hai).

  1. Chuỗi quy tắc thư viện – Hãy xem xét trường hợp chuỗi quy tắc thư viện A phụ thuộc vào B, phụ thuộc vào C, v.v. Sau đó, chúng ta muốn tính toán một số thuộc tính trên tập hợp đóng bắc cầu của các quy tắc này, chẳng hạn như đường dẫn lớp thời gian chạy Java hoặc lệnh trình liên kết C++ cho từng thư viện. Theo cách đơn giản, chúng ta có thể triển khai danh sách tiêu chuẩn; tuy nhiên, điều này đã làm tăng mức tiêu thụ bộ nhớ theo phương trình bậc hai: thư viện đầu tiên chứa một mục trên đường dẫn lớp, thư viện thứ hai chứa hai mục, thư viện thứ ba chứa ba mục, v.v., tổng cộng là 1+2+3+...+N = O(N^2) mục.

  2. Quy tắc tệp nhị phân phụ thuộc vào cùng một quy tắc thư viện – Hãy xem xét trường hợp một tập hợp tệp nhị phân phụ thuộc vào cùng một quy tắc thư viện – chẳng hạn như nếu bạn có một số quy tắc kiểm thử kiểm thử cùng một mã thư viện. Giả sử trong số N quy tắc, một nửa là quy tắc nhị phân và một nửa còn lại là quy tắc thư viện. Bây giờ, hãy xem xét rằng mỗi tệp nhị phân tạo một bản sao của một số thuộc tính được tính toán trên tập hợp đóng bắc cầu của các quy tắc thư viện, chẳng hạn như đường dẫn lớp thời gian chạy Java hoặc dòng lệnh trình liên kết C++. Ví dụ: nó có thể mở rộng phần trình bày chuỗi dòng lệnh của thao tác liên kết C++. N/2 bản sao của N/2 phần tử là bộ nhớ O(N^2).

Các lớp bộ sưu tập tuỳ chỉnh để tránh độ phức tạp bậc hai

Bazel chịu ảnh hưởng nặng nề của cả hai trường hợp này, vì vậy, chúng tôi đã giới thiệu một tập hợp các lớp bộ sưu tập tuỳ chỉnh giúp nén thông tin trong bộ nhớ một cách hiệu quả bằng cách tránh sao chép ở mỗi bước. Hầu hết các cấu trúc dữ liệu này đều đã đặt ngữ nghĩa, vì vậy, chúng tôi gọi là depset (còn gọi là NestedSet trong quá trình triển khai nội bộ). Phần lớn các thay đổi để giảm mức sử dụng bộ nhớ của Bazel trong vài năm qua là các thay đổi để sử dụng depset thay vì bất kỳ nội dung nào được sử dụng trước đó.

Rất tiếc, việc sử dụng depset không tự động giải quyết tất cả các vấn đề; cụ thể, ngay cả việc lặp lại một depset trong mỗi quy tắc cũng sẽ làm tăng mức tiêu thụ thời gian theo phương trình bậc hai. Trong nội bộ, NestedSets cũng có một số phương thức trợ giúp để tạo điều kiện tương tác với các lớp bộ sưu tập thông thường; thật không may, việc vô tình truyền NestedSet đến một trong các phương thức này sẽ dẫn đến hành vi sao chép và tái hiện mức tiêu thụ bộ nhớ bậc hai.