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

Báo cáo vấn đề Xem nguồn Hằng đêm · 7.3 · 7.2 · 7.1 · 7 · 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
  • Mang tính lịch sử: Việc phân tách hoàn toàn giữa quá trình tải, phân tích và thực thi mới là Đã 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 tính chính xác, dễ sử dụng, công suất 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 trước tiên, hệ thống xây dựng cần phải chính xác nhất với đối với các bản dựng tăng dần. Đối với một cây nguồn nhất định, dữ liệu đầ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 thích. Trong phép gần đúng đầu tiên, điều này có nghĩa là Bazel cần biết mọi đầ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ó của đầu và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 ta vĩnh viễn bứt phá những ranh giới của những gì có thể làm được trong hiện tại phân bổ máy cho dịch vụ thực thi từ xa. Nếu thực thi từ xa dịch vụ bị quá tải, 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 trường hợp này, chúng tôi giả định ngôn ngữ cấu hình gần giống với tệp BUILD trong phần khai báo thư viện và quy tắc nhị phân và sự phụ thuộc lẫn nhau của chúng. BUILD tệp có thể được đọc và phân tích cú pháp một cách độc lập, và thậm chí là tránh xem các tệp nguồn bất cứ khi nào có thể (ngoại trừ 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.

Quá trình phân tách 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 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ề quy tắc giao diện (giao diện người dùng có những thuộc tính nào, loại thuộc tính nào). 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. Để ví dụ: quy tắc java_library có tên 'foo' ngầm tạo ra một đầ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, việc phân tích 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 đó, phương thức này cần tạo một bipartit hướng một phần đồ thị các bước tạo và tên tệp đầu ra chỉ được xác định thông qua quy tắc và các phần phụ thuộc của nó.

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ý một 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ư những 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 tôi đã lập luận rằng để chính xác, Bazel cần biết tất cả thông tin đầu vào các tệp chuyển vào một bước tạo bản dựng để phát hiện xem bước tạo bản dựng đó là vẫn được cập nhật. Điều này cũng đúng 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ý điều 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 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 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.

Mỗi nút thực hiện một quy trình khám phá phần phụ thuộc. Một 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 khác nữa. 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ó chuỗi 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 phần phụ thuộc chưa có sẵn, chúng ta có thể phải huỷ phần phụ thuộc đó và khởi động lại nó (có thể trong một luồng khác), khi phần phụ thuộc là sẵn có. Đổ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 tôi hướng đến việc khai báo trước hàng loạt các phần phụ thuộc mà đôi khi đòi hỏi phải sắp xếp lại mã hoặc thậm chí là phân tách một nút thành nhiều nút để hạn chế số lần khởi động lại.

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 định nghĩa bằng các khái niệm cũ về tải, phân tích, và giai đoạn thực thi. Tuy nhiên, 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 đi qua 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 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. Các thư viện hỗ trợ phần phụ thuộc việc chèn 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 Khung chân trời.

Điều này đặc biệt khuyên bạn nên tránh hiển thị tác giả quy tắc cho môi trường thời gian chạy ngôn ngữ đầy đủ ngay từ đầu. 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.

Tránh sử dụng thời gian bậc hai và khó sử dụng bộ nhớ

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 trong 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 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 nhị phân tuỳ thuộc vào cùng một quy tắc của 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 các quy tắc đó — chẳng hạn như nếu bạn có một số quy tắc kiểm thử thử nghiệm giống nhau mã thư viện. Giả sử trong N quy tắc, một nửa quy tắc là các quy tắc nhị phân và các quy tắc còn lại của thư viện. Bây giờ, hãy giả sử 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 dựa trên trạng thái đó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ụ: 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++. Không có 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 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 đã thiết lập ngữ nghĩa, nên chúng tôi gọi nó là phần phụ thuộc (còn được gọi là NestedSet trong quá trình triển khai nội bộ). Phần lớn những thay đổi nhằm giảm mức tiêu thụ bộ nhớ của Bazel trong vài năm qua các thay đổi để sử dụng phần phụ thuộc thay vì bất kỳ phần tử 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ả sự cố; cụ thể, thậm chí việc chỉ lặp lại quá trình gỡ bỏ trong mỗi quy tắc lại được giới thiệu lại tiêu thụ thời gian 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.