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 cứng nhắc giữa quá trình 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 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 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 đề cập đến những giả định này và đưa ra nguyên tắc để đả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 ướ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 đ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ó rò rỉ một số thông tin như ngày / giờ xây dựng và bỏ qua một số loại thay đổi 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 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ó. 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 đẩy các 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 chân 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 để bắt đầu xây 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 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ô 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 thanh toán đầ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, 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 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á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ả 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 các tệp BUILD trong việc 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á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 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 khác biệt này được nêu trong các phần sau.
Việc tách biệt cứng nhắc giữa quá trình 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 để thực thi từ xa. Tuy nhiên, cơ sở mã Bazel ban đầu đã tách biệt nghiêm ngặt 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 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ù cốt 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 mô tả khai báo về giao diện quy tắc (các thuộc tính mà nó có, cá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 phần 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 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 theo 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 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 phải 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 dấu vân tay hành động duy nhất và yêu cầu trình lập lịch tìm lượt truy cập bộ nhớ đệm. Nếu tìm thấy 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; 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 này cũng đúng đối 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 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 nút đó 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 đó, 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 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 quy trình phát hiện phần phụ thuộc. Mỗi nút có thể khai báo các phần phụ thuộc, sau đó 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 ánh xạ tốt đến 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 gọn nhẹ và không có sự tiếp nối).
Thay vào đó, Bazel sử dụng 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ó, chúng tôi có thể phải huỷ đá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 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á 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 tôi hướng đến việc khai báo hàng loạt các phần 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ó 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 phần 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 chiếu 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 phần 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 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 này được nhóm Bazel hoặc các chuyên gia khác trong lĩnh vực viết.
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ử khi 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 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).
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 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ể triển khai danh sách tiêu chuẩn; tuy nhiên, điều này đã gây ra 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 các 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 các 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 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 chịu ảnh hưởng nặng nề bởi 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 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ả việc chỉ lặp lại một depset trong mỗi quy tắc cũng sẽ gây ra mức tiêu thụ 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; rất tiếc, việc vô tình truyền một NestedSet cho một trong các phương thức này sẽ dẫn đến hành vi sao chép và gây ra mức tiêu thụ bộ nhớ bậc hai.