Thách thức đối với quy tắc viết

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

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

Yêu cầu về bản tóm tắt

  • Giả định: Mục tiêu về chính xác, thông lượ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ư bản dựng
  • Dữ liệu trong quá khứ: Việc phân tách cứng giữa Hoạt động 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: Thực thi và lưu vào bộ nhớ đệm từ xa rất khó
  • Bản chất: Việc sử dụng thông tin thay đổi cho các bản dựng gia tăng nhanh chóng và chính xác đòi hỏi các mẫu lập trình bất thường
  • Bản chất: Tránh tiêu tốn thời gian và lượng bộ nhớ bậc hai

Các giả định

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

Cố gắng đảm bảo độ chính xác, thông lượng, dễ sử dụng và độ trễ

Chúng tôi giả định rằng trước tiên, hệ thống xây dựng cần phải chính xác và tuân thủ 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. Ở giá trị gần đúng đầu tiên, điều này có nghĩa là Bazel cần biết từng đầu vào chuyển đến một bước xây dựng nhất định để có thể chạy lại bước đó nếu bất kỳ đầu vào nào thay đổi. Có giới hạn về cách Bazel có thể nhận được chính xác, vì Bazel bị 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, chẳng hạn như các 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 không cho phép đọc các tệp đầu vào chưa được khai báo. Bên cạnh giới hạn nội tại của hệ thống, có một vài vấn đề đã biết về độ chính xác, hầu hết đều liên quan đến các tệp Fileset hoặc quy tắc C++, đều là những vấn đề cứng. Chúng tôi sẽ nỗ lực rất nhiều để khắc phục các 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 đẩy vĩnh viễn ranh giới 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 dịch vụ thực thi từ xa. Nếu dịch vụ thực thi từ xa bị quá tải, không ai có thể hoàn thành công việc.

Tiếp theo, dễ sử dụng hơn. Trong nhiều phương pháp chính xác với cùng một dấu vết (hoặc tương 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ễ cho biết thời gian từ khi bắt đầu một bản dựng đến khi nhận được kết quả dự định, cho dù đó là nhật ký kiểm thử từ một kiểm thử đạt hay không đạt, 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 nhau; độ trễ cũng là một hàm thông lượng của dịch vụ thực thi từ xa mà mức độ chính xác cũng phù hợp với 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 trên quy mô các kho lưu trữ lớn, trong đó quy mô lớn sẽ không phù hợp với một ổ đĩa cứng duy nhất. Vì vậy, không thể thanh toán 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 sự cố liên quan. Theo lý thuyết, 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 việc đó trong một khoảng thời gian và bộ nhớ hợp lý. Do đó, điều quan trọng là bạn phải 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 như bản dựng

Trong ngữ cảnh này, chúng tôi giả định một ngôn ngữ cấu hình tương tự như tệp BUILD trong phần khai báo thư viện và quy tắc nhị phân cũng như các phần phụ thuộc của hai tệp đó. Bạn có thể đọc và phân tích cú pháp các tệp BUILD một cách độc lập và thậm chí chúng tôi cũng tránh phải 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 khó khăn và một số phiên bản được nêu trong các phần sau.

Việc phân tách cứ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, chỉ cần một quy tắc là biết các tệp đầu vào và đầu ra của hành động ngay trước khi hành động được gửi tới thực thi từ xa. Tuy nhiên, cơ sở mã Bazel ban đầu có sự phân tách nghiêm ngặt của 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 (về cơ bản là cờ dòng lệnh) và sau đó chỉ chạy bất kỳ hành động nào. Điểm 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 như vậy nữa (xem thêm thông tin chi tiết bên dưới).

Tức là API quy tắc yêu cầu nội dung mô tả mang tính khai báo về giao diện quy tắc (thuộc tính đó, loại thuộc tính). Có một số trường hợp ngoại lệ mà API cho phép chạy mã tuỳ chỉnh trong giai đoạn tải để tính toán tên ngầm của các tệp đầu ra và giá trị ngầm ẩn của thuộc tính. Ví dụ: quy tắc java_library có tên "foo" ngầm ẩn tạo ra kết quả đầ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, quy 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 kết quả của một hành động; thay vào đó, quy tắc cần tạo một biểu đồ hai phần có hướng dẫn một phần về các bước tạo bản 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 đó.

Hàm nội tại

Có một số thuộc tính hàm nội tại khiến các quy tắc viết trở nên khó khăn và một số quy tắc 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 và lưu vào bộ nhớ đệm từ xa sẽ cải thiện thời gian xây dựng trong các kho lưu trữ lớn thêm khoảng hai bậc độ lớn so với việc chạy bản dựng trên một máy. Tuy nhiên, quy mô mà nó cần thực hiện đáng kinh ngạc: dịch vụ thực thi từ xa của Google được thiết kế để xử lý một số lượng lớn các yêu cầu mỗi giây và giao thức này tránh được các vòng tròn không cần thiết cũng như các công việc không cần thiết về 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 mọi 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 cho một lần truy cập 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 biểu sẽ phản hồi bằng thông báo của các tệp đầu ra; các tệp đó sẽ được giải quyết bằng thông báo tổng hợp sau đó. Tuy nhiên, điều này áp dụng các hạn chế đối với quy tắc Bazel. Các quy tắc này 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 cho các bản dựng gia tăng nhanh chóng và chính xác đòi hỏi các mẫu mã hóa 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 chuyển sang bước xây dựng để phát hiện xem bước xây dựng đó có còn cập nhật hay không. Điều này cũng đúng với tính năng tải gói và phân tích quy tắc. Chúng tôi cũng đã 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 nút mục tiêu (chẳng hạn như "build //foo with these options") và chia nhỏ thành các thành phần, sau đó được đánh giá và kết hợp để mang lại 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 bất kỳ được dùng để tính toán đầu ra của riêng mình, tất cả các nút từ nút mục tiêu cho đến các tệp đầu vào (cũng chính là các nút Skyframe). Việc thể hiện biểu đồ này một cách rõ ràng trong bộ nhớ cho phép hệ thống xây dựng xác định chính xác 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ự định.

Theo đó, 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, 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 hơn nữa. Về nguyên tắc, điều này ánh xạ tốt đến mô hình luồng trên 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ễ dàng với công nghệ Java hiện tại (và vì lý do lịch sử, chúng tôi hiện đang liên kết với việc sử dụng Java, vì vậy, không có luồng nhẹ và không tiếp tục).

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 phần phụ thuộc chưa có sẵn, thì chúng ta có thể phải huỷ việc đá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ại có nghĩa là các nút không được làm việc này quá mức; một nút khai báo N phần phụ thuộc tuần tự có thể được khởi động lại N lần, làm tiêu tốn thời gian O(N^2). Thay vào đó, chúng tôi hướng đến việc khai báo trước hàng loạt phần phụ thuộc, mà đôi khi yêu cầu sắp xếp lại mã hoặc thậm chí tách 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 chưa 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ả quyền truy cập vào các nút khác phải 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ữ viết (các quy tắc này 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 Skyky. Đố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 làm việc đó. 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 vẫn cần được thiết lập chính xác cho Skyframe.

Việc này đặc biệt nên tránh sử dụng tác giả quy tắc cho môi trường thời gian chạy đầy đủ ngôn ngữ ngay từ đầu. Mối nguy hiểm của việc vô tình sử dụng những API như vậy quá lớn – một số lỗi Bazel trước đây là do quy tắc sử dụng API không an toàn, mặc dù quy tắc do nhóm Bazel hoặc các chuyên gia về miền khác viết.

Tránh thời gian bậc hai và mức sử dụng bộ nhớ gặp khó khăn

Để các vấn đề trở nên tệ hơn, ngoài các yêu cầu do Skyframe đặt ra, các hạn chế trước đây của việc sử dụng Java và lỗi thời của API quy tắc, việc vô tình đưa ra 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 thư viện và các 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 đó tiêu tốn thời gian hằng phương trình).

  1. Chuỗi quy tắc thư viện – Hãy xem xét trường hợp về 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. Sau đó, chúng tôi muốn tính toán một số thuộc tính qua quá trình đó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. Về cơ bản, chúng ta có thể triển khai danh sách chuẩn; tuy nhiên, điều này đã giới thiệu 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, ba phần tử thứ ba, v.v. đối với tổng số 1 + 2 + 3 +... + N = O(N^2).

  2. Quy tắc nhị phân tùy 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 nhóm 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ố các quy tắc N, một nửa các quy tắc là các quy tắc nhị phân, và một nửa các quy tắc thư viện còn lại. Bây giờ, hãy xem xét 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 qua việc đóng 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 bản trình bày chuỗi dòng lệnh của hành động liên kết C++. N/2 bản sao của các phần tử N/2 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 lớn của cả hai tình huống này, vì vậy, chúng tôi đã ra mắt một tập hợp các lớp thu thậ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 sử dụng bản sao ở mỗi bước. Hầu hết tất cả 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 nó 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 tiêu thụ bộ nhớ của Bazel trong vài năm qua là các thay đổi để sử dụng phần phụ thuộc thay vì bất kỳ công cụ nào đã được sử dụng trước đó.

Rất tiếc, việc sử dụng phần phụ thuộc không tự động giải quyết tất cả các vấn đề; đặc biệt, thậm chí việc chỉ lặp lại một phần phụ thuộc trong mỗi quy tắc sẽ giới thiệu lại mức tiêu thụ thời gian hằng quý. Trong nội bộ, NestedSets cũng có một số phương thức trợ giúp để hỗ trợ khả năng 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 tới một trong các phương thức này sẽ dẫn đến việc sao chép hành vi và dẫn đến mức tiêu thụ bộ nhớ bậc hai.