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 các quy tắc hiệu quả của Bazel.
Yêu cầu về nội dung tóm tắt
- Giả định: Nhắm đến tính chính xác, thông lượng, tính dễ sử dụng và độ trễ
- Giả định: Kho lưu trữ quy mô lớn
- Giả định: Ngôn ngữ mô tả tương tự như BUILD
- Trước đây: Việc tách biệt hoàn toàn 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 hiện và lưu vào bộ nhớ đệm từ xa
- Nội tại: Việc sử dụng Thông tin thay đổi cho các bản dựng gia tăng chính xác và nhanh chóng đòi hỏi phải có Mẫu mã hoá bất thường
- Nội tại: Rất khó tránh được mức tiêu thụ thời gian và bộ nhớ bậc hai
Các giả định
Dưới đây là một số giả định về hệ thống bản dựng, chẳng hạn như nhu cầu về tính chính xác, tính 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 bạn viết quy tắc một cách hiệu quả.
Hướng đến độ chính xác, thông lượng, tính dễ sử dụng và độ trễ
Chúng tôi giả định rằng hệ thống bản dựng trước hết cần phải chính xác đối với các bản dựng gia tăng. Đối với một cây nguồn nhất định, đầu ra 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 ước tính đầu tiên, điều này có nghĩa là Bazel cần biết từng đầu vào duy nhất trong 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ỳ đầu vào nào thay đổi. Bazel có thể đạt được độ chính xác ở một mức độ nhất định, vì Bazel tiết lộ 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, chẳng hạn như thay đổi thuộc tính tệp. Hộp cát giúp đảm bảo tính chính xác bằng cách ngăn chặn việc đọc các tệp đầu vào chưa khai báo. Ngoài các giới hạn vốn có của hệ thống, còn có một số vấn đề đã biết về tính chính xác, hầu hết đều liên quan đến Fileset hoặc các quy tắc C++, cả hai đều là vấn đề khó. Chúng tôi đang 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 bản dựng là có thông lượng cao; chúng tôi liên tục mở rộng giới hạn của những việc có thể thực hiện trong quá trình phân bổ máy hiện tại cho một 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 tôi chọn phương pháp dễ sử dụng hơn.
Độ trễ biểu thị thời gian cần thiết từ khi bắt đầu một bản dựng cho đến khi nhận được kết quả dự kiến, cho dù đó là nhật ký kiểm thử từ một bài kiểm thử thành công hay không thành công, hoặc một thông báo lỗi cho biết tệp BUILD
có lỗi chính tả.
Xin lưu ý rằng những mục tiêu này thường trùng lặp; độ trễ cũng 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 tính dễ sử dụng.
Kho lưu trữ quy mô lớn
Hệ thống bản dựng cần hoạt động ở quy mô kho lưu trữ lớn, trong đó quy mô lớn có nghĩa là hệ thống này không vừa với một ổ cứng duy nhất, vì vậy, không thể thực hiện thao tác kiểm xuất đầy đủ 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 duy nhất, 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 một cách độc lập.
Ngôn ngữ mô tả tương tự như BUILD
Trong bối cảnh này, chúng tôi giả định một ngôn ngữ cấu hình gần giống với các tệp BUILD
trong khai báo các quy tắc thư viện và nhị phân cũng như các mối quan hệ 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 một cách độc lập, đồng thời chúng tôi tránh xem xét các tệp nguồn bất cứ khi nào có thể (ngoại trừ sự tồn tại).
Cổ
Có những điểm khác biệt giữa các phiên bản Bazel gây ra nhiều thách thức và một số điểm khác biệt trong số này được nêu trong các phần sau.
Việc tách biệt 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
Về mặt kỹ thuật, mộ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 có sự tách biệt nghiêm ngặt giữa việc tải các gói, sau đó phân tích các quy tắc bằng cách sử dụng một cấu hình (về cơ bản là các cờ dòng lệnh) 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 cho đến nay, mặc dù lõi của Bazel không còn yêu cầu điều này nữa (xem thêm thông tin chi tiết 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 (những thuộc tính mà quy tắc có, các 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 định của tệp đầu ra và giá trị ngầm định của các thuộc tính. Ví dụ: quy tắc java_library có tên là "foo" sẽ ngầm tạo ra một đầu ra có tên là "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, quá trình 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 thao tác; thay vào đó, quá trình này cần tạo một biểu đồ hai phầ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 và lưu vào bộ nhớ đệm từ xa
Việc thực thi và lưu vào bộ nhớ đệm từ xa giúp cải thiện thời gian tạo bản dựng trong các kho lưu trữ lớn khoảng hai bậc độ lớn 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 chuyến khứ hồi 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 biết trước tất cả các đầ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 dấu vân tay hành động duy nhất và yêu cầu trình lập lịch tìm một lượt truy cập vào bộ nhớ đệm. Nếu tìm thấy một lượt truy cập bộ nhớ đệm, trình lập lịch sẽ trả lời bằng các bản tóm tắt của tệp đầu ra; bản thân các tệp sẽ được giải quyết bằng bản tóm tắt sau. Tuy nhiên, điều này áp đặt các hạn chế đối với các quy tắc Bazel, cần 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 để tạo bản dựng gia tăng chính xác và nhanh chóng đòi hỏi các mẫu mã hoá 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 sẽ được đưa 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, đồng thời chúng tôi đã thiết kế Skyframe để xử lý việc này nói chung. Skyframe là một thư viện đồ thị và khung đánh giá lấy một nút mục tiêu (chẳng hạn như "build //foo with these options"), rồi chia nút đó thành các phần cấu thành. Sau đó, các phần này sẽ được đánh giá và kết hợp để tạo ra kết quả này. Trong quá trình này, Skyframe sẽ đọc các gói, phân tích các quy tắc và thực thi các hành động.
Tại mỗi nút, Skyframe theo dõi chính xác những nút mà một nút nhất định đã 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 sẽ 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 các phần phụ thuộc, rồi sử dụng nội dung của các phần phụ thuộc đó để khai báo thêm các phần phụ thuộc. Về nguyên tắc, điều này tương ứng với mô hình mỗi nút một luồng. Tuy nhiên, các bản dựng có kích thước trung bình chứa hàng trăm nghìn nút Skyframe, điều này không dễ thực hiện với công nghệ Java hiện tại (và vì lý do lịch sử, chúng tôi hiện đang bị ràng buộc phải sử dụng Java, vì vậy không có luồng nhẹ và không có phần 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, thì chúng ta có thể phải huỷ bỏ quá trình đánh giá đó và khởi động lại (có thể trong một luồng khác) khi phần phụ thuộc có sẵn. Điều này có nghĩa là 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 nối tiếp có thể được khởi động lại N lần, tốn thời gian O(N^2). Thay vào đó, chúng tôi hướng đến việc khai báo hàng loạt các phần phụ thuộc ngay từ đầu. Đôi khi, việc này đòi hỏi 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 để hạn chế 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ách sử dụng các khái niệm cũ về 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à mọi quyền truy cập vào các nút khác đều phải thông qua khung để khung này có thể theo dõi các phần phụ thuộc tương ứng. Bất kể ngôn ngữ mà hệ thống bản dựng được triển khai hoặc ngôn ngữ mà các quy tắc được viết (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 tiê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 thực hiện một trong hai điều này. Các thư viện hỗ trợ việc chèn phần phụ thuộc của những giao diện cấp thấp này vẫn cần được thiết lập đúng cách cho Skyframe.
Điều này cho thấy rằng bạn nên tránh để tác giả quy tắc tiếp xúc với thời gian chạy ngôn ngữ đầy đủ ngay từ đầu. Nguy cơ vô tình sử dụng các API như vậy là quá lớn – một số lỗi Bazel trong quá khứ là do các quy tắc sử dụng API không an toàn, mặc dù các quy tắc đó được viết bởi nhóm Bazel hoặc các chuyên gia khác trong lĩnh vực này.
Rất khó để tránh mức tiêu thụ bộ nhớ và thời gian bậc hai
Tệ hơn nữa, ngoài các yêu cầu do Skyframe áp đặt, những hạn chế trước đây khi sử dụng Java và sự lỗi thời của API quy tắc, việc vô tình giới thiệu thời gian bậc hai hoặc mức tiêu thụ bộ nhớ là một vấn đề cơ bản trong mọi hệ thống xây dựng dựa trên các quy tắc về thư viện và tệp nhị phân. Có 2 mẫu hình rất phổ biến làm tăng mức tiêu thụ bộ nhớ bậc hai (và do đó làm tăng mức tiêu thụ thời gian bậc hai).
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 bao đóng 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 mỗi thư viện. Theo cách thông thường, chúng ta có thể sử dụng một cách triển khai danh sách tiêu chuẩn; tuy nhiên, cách này đã làm tăng mức tiêu thụ bộ 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.
Quy tắc nhị phân tuỳ thuộc vào các quy tắc thư viện giống nhau – Hãy xem xét trường hợp có một nhóm các tệp nhị phân tuỳ thuộc vào các quy tắc thư viện giống nhau – 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 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 bao đó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 của trình liên kết C++. Ví dụ: thao tác này có thể mở rộng biểu thị 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 lớn của cả hai trường hợp này, vì vậy, chúng tôi đã giới thiệu một nhóm các lớp tập hợp tuỳ chỉnh giúp nén thông tin một cách hiệu quả trong bộ nhớ 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 có ngữ nghĩa tập hợp, vì vậy chúng tôi gọi đó là depset (còn được 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 tiêu thụ bộ nhớ của Bazel trong vài năm qua là các thay đổi để sử dụng depsets thay vì bất kỳ thứ gì đượ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 biệt, ngay cả khi chỉ lặp lại một depset trong mỗi quy tắc, bạn sẽ phải tiêu tốn thời gian bậc hai. Về 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 tập hợp thông thường; không may là việc vô tình truyền một NestedSet đến một trong các phương thức này sẽ dẫn đến hành vi sao chép và làm tăng mức tiêu thụ bộ nhớ bậc hai.