Trang này cung cấp thông tin tổng quan ở cấp độ cao về các vấn đề và thách thức cụ thể khi viết các quy tắc Bazel hiệu quả.
Yêu cầu tóm tắt
- Giả định: Mục tiêu là 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ả giống BUILD
- Cổ: 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
- Có sẵn: Khó thực hiện việc thực thi và lưu vào bộ nhớ đệm từ xa
- Có sẵn: Việc sử dụng thông tin thay đổi để xây 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
- Có sẵn: Khó tránh được mức tiêu thụ bộ nhớ và thời gian 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, 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 giải quyết những giả định này và đưa ra hướng dẫn để đảm bảo các quy tắc được viết một cách hiệu quả.
Mục tiêu là tính 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 xây dựng cần phải chính xác trước hết và quan trọng nhất đố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 kết quả trông như thế nào. Trong lần xấp xỉ đầu tiên, điều này có nghĩa là Bazel cần biết từng đầu vào duy nhất đ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ỳ đầu vào nào thay đổi. Có những giới hạn về mức độ chính xác mà Bazel có thể đạt được, vì nó 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ư thay đổi đối vớ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 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ó 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ó khăn. 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 xây dựng là có thông lượng cao; chúng tôi đang liên tục mở rộng giới hạn của những gì có thể thực hiện trong việc 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 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 xây 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 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 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ư tí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 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 ổ 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 và đánh giá
hàng trăm nghìn glob. Mặc dù về mặt lý thuyết, 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 vẫn chưa thể thực hiện việc này trong một
khoảng thời gian và bộ nhớ hợp lý. Do đó, điều quan trọng là có thể tải và phân tích cú pháp các tệp BUILD
một cách độc lập.
Ngôn ngữ mô tả giống 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 BUILD tệp trong việc khai báo các quy tắc thư viện và nhị phân
và các mối quan hệ phụ thuộc lẫn nhau. Các tệp BUILD có thể được đọc và phân tích cú pháp một cách độc lập,
và chúng tôi tránh thậm chí nhìn vào 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 thách thức và một số điểm trong số này được nêu trong các phần sau.
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
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 hành động ngay trước khi hành động đó đượ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 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 ngày nay, mặc dù lõi của Bazel không còn yêu cầu điều đó 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 một nội dung mô tả khai báo về giao diện quy tắc (thuộc tính mà nó có, loại thuộc tính). Có một số trường hợp ngoại lệ trong đó 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 các tệp đầu ra và giá trị ngầm ẩn của các thuộc tính. Ví dụ: một 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 đồ xây 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 đó, nó cần tạo một biểu đồ hai phần có hướng một phần gồm 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 mối quan hệ phụ thuộc của nó.
Có sẵn
Có một số thuộc tính vốn có 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 hiện việc 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 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à nó 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 đi 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ần truy cập bộ nhớ đệm. Nếu tìm thấy một lần 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 các tệp đầu ra; chính các tệp này 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 để xây 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 tôi đã 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 tương tự cũng đúng đối với việc tải gói và phân tích quy tắc, và chúng tôi đã thiết kế Skyframe để xử lý việc 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ư "xây dựng //foo với các tuỳ chọn này") và chia nhỏ thành các phần cấu thành, sau đó được đá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 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à bất kỳ nút nào đã sử dụng để tính toán đầu ra của riêng nó, từ nút mục tiêu xuống đến các tệp đầu vào (cũng là các nút Skyframe). Việc biểu diễn 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 bị ảnh hưởng bởi một thay đổi nhất định đối với một tệp đầu vào (bao gồm cả việc tạo hoặc xoá một 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 quy trình khám phá mối quan hệ phụ thuộc. Mỗi nút có thể khai báo các mối quan hệ phụ thuộc, sau đó sử dụng nội dung của các mối quan hệ phụ thuộc đó để khai báo thêm các mối quan hệ phụ thuộc. Về nguyên tắc, điều này ánh xạ tốt đến mô hình mỗi luồng cho mỗi nút. 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ó sự 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 mối quan hệ phụ thuộc chưa có sẵn, chúng ta có thể phải huỷ đánh giá đó và khởi động lại (có thể trong một luồng khác), khi mối quan hệ phụ thuộc có sẵn. Điều này lần lượt có nghĩa là các nút không nên thực hiện việc này quá nhiều; a nút khai báo N mối quan hệ 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 tôi hướng đến việc khai báo hàng loạt các mối quan hệ phụ thuộc trước, đôi khi yêu cầu 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ó sẵn 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ề 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 mối quan hệ phụ thuộc tương ứng. Bất kể ngôn ngữ mà hệ thống xây 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ư bất kỳ hình thức phản ánh nào và bất kỳ thư viện nào thực hiện một trong hai điều đó. Các thư viện hỗ trợ việc chèn mối quan hệ phụ thuộc của các 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 trước hết, 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 đủ. 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 các 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 miền khác.
Khó tránh được 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, các ràng buộc lịch sử của việc sử dụng Java và tính lỗi thời của API quy tắc, việc vô tình đưa vào mức tiêu thụ bộ nhớ hoặc thời gian bậc hai là một vấn đề cơ bản trong bất kỳ hệ thống xây dựng nào dựa trên các quy tắc thư viện và nhị phân. Có hai mẫu rất phổ biến đưa vào mức tiêu thụ bộ nhớ bậc hai (và do đó 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 một chuỗi quy tắc thư viện A phụ thuộc vào B, phụ thuộc vào C, và v.v. Sau đó, chúng ta muốn tính toán một số thuộc tính trên bao đó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 mỗi thư viện. Một cách đơn giản, chúng ta có thể lấy một cách triển khai danh sách tiêu chuẩn; tuy nhiên, điều này đã đưa vào 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.
Các quy tắc 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 các tệp nhị phân phụ thuộc vào cùng một thư viện quy tắc – 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 trình liên kết C++. Ví dụ: nó có thể mở rộng biểu diễn chuỗi dòng lệnh của hành động 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 tập hợp tuỳ chỉnh để tránh độ phức tạp bậc hai
Bazel bị ảnh hưởng nặng nề bởi cả hai tình huống này, vì vậy, chúng tôi đã giới thiệu một tập hợp 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 nó 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 depset 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, mức tiêu thụ thời gian bậc hai sẽ được đưa trở lại. Về nội bộ, NestedSets cũng có một số phương thức trợ giúp để tạo điều kiện cho khả năng tương tác với các lớp tập hợp thông thường; rất tiếc, 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à đưa trở lại mức tiêu thụ bộ nhớ bậc hai.