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

Báo cáo vấn đề Xem nguồn Nightly · 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ể viết các quy tắc Bazel hiệu quả.

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

  • Giả định: Đảm bảo độ chính xác, công suất, mức độ 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 với 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: Sử dụng thông tin thay đổi để xây dựng đúng và tăng trưởng nhanh yêu cầu Mẫu lập trình bất thường
  • Nội tại: Tránh được thời gian bậc hai và mức sử dụng bộ nhớ là rất khó

Các giả định

Dưới đâ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. Chiến lược phát hành đĩa đơn phần sau đây đề cập đến những giả định này và đưa ra các nguyên tắc để đảm bảo được viết một cách hiệu quả.

Hướng đến độ chính xác, công suất, 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 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ề lượng thông tin chính xác mà Bazel có thể làm, vì câu trả lời này 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 chẳng hạn như các thay đổi đối với thuộc tính tệp. Cơ chế 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 được khai báo. Bên cạnh giới hạn nội tại của hệ thống, có một số vấn đề về tính đúng đắn, hầu hết các yêu cầu này đều liên quan đến Fileset hoặc quy tắc C++, cả hai đều khó vấn đề. Chúng tôi đang nỗ lực lâu dài để khắc phụ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 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 tiếp cận chính xác có cùng một (hoặc tương tự) của dịch vụ thực thi từ xa, chúng tôi chọn một dịch vụ dễ sử dụng hơn.

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

Lưu ý rằng các mục tiêu này thường trùng lặp; độ trễ cũng giống như một hàm của thông lượng của dịch vụ thực thi từ xa có liên quan đến độ chính xác để 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ô lớn của các kho lưu trữ lớn tỷ lệ này có nghĩa là nó không vừa với một ổ đĩa cứng, vì vậy không thể thanh toán đầy đủ trên hầu hết các máy dành cho nhà phát triển. 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 khối cầu. Mặc dù về mặt lý thuyết thì có thể đọc tất cả BUILD tệp trên một máy, chúng tôi chưa thể thực hiện việc này trong phạm vi lượ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 với 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 tồn tại).

Cổ

Có sự khác biệt giữa các phiên bản Bazel gây ra thử thách và một số được trình bày 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, một quy tắc chỉ cần biết 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 đ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á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ạ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ù phần cốt lõi của Bazel không còn yêu cầu nó (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 (các thuộc tính của giao diện, loại thuộc tính). Có một số các 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 của các tệp đầu ra và giá trị ngầm ẩn của các thuộc tính. Cho ví dụ: quy tắc java_library có tên 'foo' ngầm tạo ra một kết quả 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ố đặc điểm riêng biệt khiến quy tắc viết trở nên khó khăn và một số trạng thái 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

Quá trình 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 bằng cách có cường độ khoảng 2 bậc so với việc chạy công trình trên một thiết bị máy. Tuy nhiên, quy mô mà theo đó cần thực hiện thật đáng kinh ngạc: dịch vụ thực thi từ xa được thiết kế để xử lý số lượng lớn yêu cầu trên mỗi thứ hai và giao thức cẩn thận tránh các lần trả về không cần thiết cũng như 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 tất cả đầu vào cho một hành động cụ thể trước thời hạn; sau đó hệ thống xây dựng sẽ tính toán một hành động duy nhất vân tay số và yêu cầu trình lập lịch biểu cung cấp lượt truy cập bộ nhớ đệm. Nếu một lần truy cập bộ nhớ đệm được tìm thấy, trình lập lịch biểu phản hồi bằng thông báo của các tệp đầu ra; các tệp đó thông báo để giải quyết sau. Tuy nhiên, điều này áp dụng các hạn chế đối với Bazel . Những 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 để có các bản dựng gia tăng 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à thư viện biểu đồ và khung đánh giá sẽ đưa ra nút mục tiêu (chẳng hạn như 'build //foo với các tuỳ chọn này') và chia nhỏ thành các phần cấu thành của nó, sau đó được đánh giá và kết hợp để tạo ra chỉ số này kết quả. Trong quá trình này, Skyframe đọc các gói, phân tích 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 các nút mà bất kỳ nút nhất định nào được dùng để tính toán đầu ra của riêng nó, từ nút mục tiêu đến các tệp đầu vào ( cũng là các nút Skyframe). Việc biểu đồ này được thể hiện rõ ràng 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 đối với tệp nhập (bao gồm cả việc tạo hoặc xoá tệp nhập), đang 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, chỉ số này tương ứng với mô hình luồng trên mỗi nút. Tuy nhiên, các phiên bản quy mô vừa chứa hàng trăm hàng nghìn nút Skyframe, điều này không dễ dàng thực hiện được với Java hiện tại công nghệ (và vì lý do lịch sử, chúng tôi hiện nay 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ó dữ liệu 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 lại, đ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 phần phụ thuộc N theo tuần tự có thể có khả năng được khởi động lại N lần, tốn O(N^2) thời gian. 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 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ề 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ữ mà hệ thống xây dựng sử dụng được triển khai hoặc viết các quy tắc (không nhất thiết phải là như nhau), tác giả quy tắc không được dùng các thư viện hoặc mẫu chuẩn bỏ qua Khung chân trời. Đố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à bất kỳ thư viện nào thực hiện cả hai 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 những API đó 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, thậm chí 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 về miền.

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

Tệ hơn, ngoài các yêu cầu do Skyframe đặt ra, những hạn chế trước đây khi sử dụng Java cũng như tính 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ớ trong bất kỳ hệ thống xây dựng nào dựa trên thư viện và các quy tắc nhị phân. Có hai các mẫu rất phổ biến dẫn đến mức tiêu thụ bộ nhớ bậc hai (và do đó) 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 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 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 mỗi thư viện. Nói chung, 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 classpath, hai mục thứ hai, ba mục thứ ba, v.v. bật, với tổng số 1+2+3+...+N = O(N^2) mục nhập.

  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ụ: có thể mở rộng phần biểu diễn 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 bị ảnh hưởng rất nhiều 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 bộ sưu tập tuỳ chỉnh nén thông tin trong bộ nhớ 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 để hỗ trợ 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, vô tình truyền một NestedSet đến một trong các phương thức này dẫn đến việc sao chép và giới thiệu lại mức tiêu thụ bộ nhớ bậc hai.