Hệ thống xây dựng dựa trên cấu phần phần mềm

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

Trang này đề cập đến hệ thống xây dựng dựa trên cấu phần phần mềm và triết lý đằng sau hoạt động sáng tạo của chúng. Bazel là một hệ thống xây dựng dựa trên cấu phần phần mềm. Mặc dù các hệ thống xây dựng dựa trên tác vụ là một bước tốt trong các tập lệnh bản dựng, nhưng chúng có quá nhiều sức mạnh cho từng kỹ sư bằng cách cho phép chúng xác định các tác vụ của riêng mình.

Các hệ thống xây dựng dựa trên cấu phần phần mềm có một số ít tác vụ do hệ thống xác định mà các kỹ sư có thể định cấu hình theo cách hạn chế. Các kỹ sư vẫn cho hệ thống biết nội dung để xây dựng, nhưng hệ thống xây dựng sẽ xác định cách xây dựng. Cũng như các hệ thống xây dựng dựa trên tác vụ, các hệ thống xây dựng dựa trên cấu phần phần mềm, chẳng hạn như Bazel, vẫn có tệp bản dựng, nhưng nội dung của các tệp bản dựng đó rất khác. Thay vì là một tập lệnh bắt buộc trong ngôn ngữ kịch bản hoàn chỉnh Turing mô tả cách tạo kết quả đầu ra, các tệp bản dựng trong Bazel là một tệp kê khai khai báo mô tả một tập hợp cấu phần phần mềm để tạo, các phần phụ thuộc và một tập hợp các tùy chọn giới hạn ảnh hưởng đến cách chúng được tạo. Khi kỹ sư chạy bazel trên dòng lệnh, họ sẽ chỉ định một tập hợp các mục tiêu cần tạo (những gì) và Bazel chịu trách nhiệm định cấu hình, chạy và lập lịch các bước biên dịch (cách). Vì giờ đây, hệ thống xây dựng có toàn quyền kiểm soát những công cụ cần chạy vào thời điểm đó, nên có thể đảm bảo chắc chắn hơn nhiều, cho phép hệ thống hoạt động hiệu quả hơn nhiều trong khi vẫn đảm bảo độ chính xác.

Góc nhìn chức năng

Dễ dàng tạo sự tương đồng giữa hệ thống xây dựng dựa trên cấu phần phần mềm và hoạt động lập trình chức năng. Các ngôn ngữ lập trình bắt buộc truyền thống (chẳng hạn như Java, C và Python) chỉ định danh sách các câu lệnh cần thực thi lần lượt, giống như cách các hệ thống xây dựng dựa trên tác vụ cho phép lập trình viên xác định một loạt các bước để thực thi. Ngược lại, các ngôn ngữ lập trình hàm (chẳng hạn như Haskell và ML) có cấu trúc giống như một loạt các phương trình toán học. Trong các ngôn ngữ hàm, lập trình viên mô tả một phép tính cần thực hiện, nhưng để lại thông tin chi tiết về thời điểm và chính xác cách tính toán đó được thực thi cho trình biên dịch.

Phương pháp này sẽ ánh xạ ý tưởng khai báo tệp kê khai trong hệ thống xây dựng dựa trên cấu phần phần mềm và cho phép hệ thống tìm ra cách thực thi bản dựng. Không thể dễ dàng biểu thị nhiều vấn đề bằng cách sử dụng lập trình chức năng, nhưng những vấn đề được hưởng lợi rất nhiều từ ngôn ngữ đó: ngôn ngữ thường có thể tạo ra sự song song không đáng kể cho các chương trình như vậy và đảm bảo chắc chắn về tính chính xác sẽ không thể có được bằng một ngôn ngữ bắt buộc. Những vấn đề dễ dàng nhất để thể hiện bằng cách sử dụng lập trình chức năng là những vấn đề đơn giản liên quan đến việc chuyển đổi một phần dữ liệu thành một phần dữ liệu khác bằng một loạt các quy tắc hoặc hàm. Và đó chính xác là một hệ thống xây dựng: toàn bộ hệ thống là một hàm toán học hiệu quả lấy các tệp nguồn (và các công cụ như trình biên dịch) làm đầu vào và tạo ra các tệp nhị phân. Vì vậy, không có gì đáng ngạc nhiên khi cơ sở dữ liệu hoạt động tốt dựa trên nguyên lý lập trình chức năng.

Tìm hiểu về hệ thống xây dựng dựa trên cấu phần phần mềm

Hệ thống xây dựng của Google, Blaze, là hệ thống xây dựng dựa trên cấu phần mềm đầu tiên. Bazel là phiên bản nguồn mở của Blaze.

Dưới đây là giao diện của một tệp bản dựng (thường có tên là BUILD) trong Bazel:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

Trong Bazel, tệp BUILD xác định mục tiêu. Hai loại mục tiêu ở đây là java_binaryjava_library. Mỗi mục tiêu tương ứng với một cấu phần phần mềm có thể được hệ thống tạo ra: mục tiêu nhị phân tạo ra tệp nhị phân có thể được thực thi trực tiếp và mục tiêu thư viện tạo ra các thư viện mà tệp nhị phân hoặc các thư viện khác có thể sử dụng. Mỗi mục tiêu có:

  • name: cách mục tiêu được tham chiếu trên dòng lệnh và theo các mục tiêu khác
  • srcs: các tệp nguồn cần được biên dịch để tạo cấu phần phần mềm cho mục tiêu
  • deps: các mục tiêu khác phải được tạo trước mục tiêu này và liên kết với mục tiêu đó

Các phần phụ thuộc có thể nằm trong cùng một gói (chẳng hạn như phần phụ thuộc của MyBinary trên :mylib) hoặc trên một gói khác trong cùng một hệ thống phân cấp nguồn (chẳng hạn như phần phụ thuộc của mylib đối với //java/com/example/common).

Tương tự như với hệ thống xây dựng dựa trên tác vụ, bạn thực hiện các bản dựng bằng công cụ dòng lệnh của Bazel. Để tạo mục tiêu MyBinary, bạn hãy chạy bazel build :MyBinary. Sau khi nhập lệnh đó lần đầu tiên trong một kho lưu trữ sạch, Bazel:

  1. Phân tích cú pháp mọi tệp BUILD trong không gian làm việc để tạo biểu đồ cho các phần phụ thuộc trong số các cấu phần phần mềm.
  2. Sử dụng biểu đồ để xác định các phần phụ thuộc bắc cầu của MyBinary; tức là mọi mục tiêu mà MyBinary phụ thuộc và mọi mục tiêu mà các mục tiêu đó phụ thuộc định kỳ.
  3. Xây dựng từng phần phụ thuộc đó theo thứ tự. Bazel bắt đầu bằng cách xây dựng từng mục tiêu không có các phần phụ thuộc khác và theo dõi những phần phụ thuộc vẫn cần được xây dựng cho từng mục tiêu. Ngay khi tất cả phần phụ thuộc của mục tiêu được tạo, Bazel bắt đầu tạo mục tiêu đó. Quá trình này sẽ tiếp tục cho đến khi một trong các phần phụ thuộc bắc cầu của MyBinary được tạo.
  4. Xây dựng MyBinary để tạo một tệp nhị phân có thể thực thi cuối cùng liên kết đến tất cả phần phụ thuộc đã được tạo ở bước 3.

Về cơ bản, có vẻ như những gì đang xảy ra ở đây không khác nhiều so với những gì đã xảy ra khi bạn dùng một hệ thống xây dựng dựa trên nhiệm vụ. Trên thực tế, kết quả cuối cùng là tệp nhị phân và quy trình tạo kết quả sẽ liên quan đến việc phân tích nhiều bước để tìm các phần phụ thuộc, sau đó chạy các bước đó theo thứ tự. Tuy nhiên, có những điểm khác biệt quan trọng. Bước đầu tiên xuất hiện trong bước 3: vì Bazel biết rằng mỗi mục tiêu chỉ tạo ra một thư viện Java nên nó biết rằng tất cả những gì cần làm là chạy trình biên dịch Java thay vì một tập lệnh tuỳ ý do người dùng xác định, vì vậy, nó an toàn để chạy song song các bước này. Điều này có thể tạo ra thứ tự cải thiện hiệu suất theo mức độ cao hơn so với xây dựng các mục tiêu lần lượt trên máy đa nhân và chỉ có thể thực hiện được vì phương pháp dựa trên cấu phần phần mềm để cho hệ thống xây dựng tự chịu trách nhiệm về chiến lược thực thi để có thể đảm bảo mạnh mẽ hơn về tính song song.

Tuy nhiên, lợi ích nằm ngoài phạm vi song song. Điều tiếp theo mà phương pháp này cho chúng ta biết rõ khi nhà phát triển nhập bazel build :MyBinary lần thứ hai mà không thực hiện bất kỳ thay đổi nào: Bazel thoát trong chưa đầy một giây bằng thông báo cho biết mục tiêu đã được cập nhật. Điều này có thể xảy ra do mô hình lập trình chức năng mà chúng ta đã nói ở trên. Bazel biết rằng mỗi mục tiêu chỉ là kết quả của việc chạy trình biên dịch Java và dữ liệu đầu ra từ trình biên dịch Java chỉ phụ thuộc vào dữ liệu đầu vào, miễn là dữ liệu đầu vào không thay đổi, kết quả có thể sử dụng lại được. Bản phân tích này hoạt động ở mọi cấp độ; nếu MyBinary.java thay đổi, Bazel sẽ biết tạo lại MyBinary nhưng sử dụng lại mylib. Nếu tệp nguồn cho //java/com/example/common thay đổi, Bazel biết sẽ tạo lại thư viện đó, mylibMyBinary, nhưng sử dụng lại //java/com/example/myproduct/otherlib. Vì Bazel biết về các thuộc tính của công cụ mà nó chạy ở mỗi bước, nên ứng dụng chỉ có thể tạo lại bộ cấu phần phần mềm tối thiểu mỗi lần trong khi vẫn đảm bảo rằng sẽ không tạo ra các bản dựng cũ.

Việc điều chỉnh lại quy trình xây dựng về mặt cấu phần phần mềm thay vì tác vụ phải tinh tế nhưng mạnh mẽ. Bằng cách giảm tính linh hoạt của trình lập trình, hệ thống xây dựng có thể biết thêm về những gì đang được thực hiện ở mọi bước của bản dựng. Công cụ này có thể sử dụng kiến thức này để giúp bản dựng hiệu quả hơn nhiều bằng cách song song quy trình xây dựng và sử dụng lại kết quả của các bản dựng đó. Tuy nhiên, đây thực sự chỉ là bước đầu tiên. Những khối xây dựng song song và tái sử dụng này là cơ sở để tạo ra một hệ thống xây dựng được phân phối và có khả năng mở rộng cao.

Các thủ thuật khác của Bazel

Về cơ bản, các hệ thống xây dựng dựa trên cấu phần phần mềm giải quyết các vấn đề về song song và tái sử dụng vốn có trong các hệ thống xây dựng dựa trên tác vụ. Tuy nhiên, vẫn còn một số vấn đề chúng tôi chưa giải quyết trước đó. Bazel có những cách thông minh để giải quyết những vấn đề này. Chúng ta nên thảo luận về vấn đề này trước khi tiếp tục.

Công cụ phụ thuộc

Một vấn đề chúng tôi gặp phải trước đó là các bản dựng phụ thuộc vào các công cụ được cài đặt trên máy và việc tái tạo bản dựng trên các hệ thống có thể khó khăn do các phiên bản hoặc vị trí công cụ khác nhau. Vấn đề này càng trở nên khó khăn hơn nữa khi dự án của bạn sử dụng những ngôn ngữ yêu cầu nhiều công cụ dựa trên nền tảng mà những công cụ đó đang được xây dựng hoặc biên dịch (chẳng hạn như Windows và Linux), và mỗi nền tảng đó lại cần đến một bộ công cụ khác nhau để thực hiện cùng một công việc.

Bazel giải quyết phần đầu tiên của vấn đề này bằng cách xem các công cụ là phần phụ thuộc của mỗi mục tiêu. Mỗi java_library trong không gian làm việc ngầm phụ thuộc vào một trình biên dịch Java, mặc định là một trình biên dịch nổi tiếng. Bất cứ khi nào Bazel tạo một java_library, công cụ này sẽ kiểm tra để đảm bảo rằng trình biên dịch được chỉ định đang có sẵn tại một vị trí đã biết. Cũng giống như mọi phần phụ thuộc khác, nếu trình biên dịch Java thay đổi, mọi cấu phần phần mềm phụ thuộc vào đó sẽ được tạo lại.

Bazel giải quyết phần thứ hai của vấn đề, tính độc lập của nền tảng, bằng cách đặt cấu hình bản dựng. Thay vì nhắm mục tiêu tuỳ thuộc trực tiếp vào các công cụ, các mục tiêu này phụ thuộc vào loại cấu hình:

  • Cấu hình máy chủ: các công cụ xây dựng chạy trong quá trình xây dựng
  • Cấu hình mục tiêu: tạo tệp nhị phân bạn cuối cùng đã yêu cầu

Mở rộng hệ thống xây dựng

Bazel đi kèm với mục tiêu cho một số ngôn ngữ lập trình phổ biến ngay từ đầu, nhưng các kỹ sư sẽ luôn muốn làm được nhiều việc hơn. Một phần lợi ích của hệ thống dựa trên tác vụ là khả năng linh hoạt trong việc hỗ trợ bất kỳ loại quy trình xây dựng nào và tốt hơn là không nên từ bỏ điều đó trong hệ thống xây dựng dựa trên cấu phần phần mềm. May mắn là Bazel cho phép mở rộng các loại mục tiêu được hỗ trợ bằng cách thêm các quy tắc tùy chỉnh.

Để xác định một quy tắc trong Bazel, tác giả quy tắc sẽ khai báo các dữ liệu đầu vào mà quy tắc yêu cầu (dưới dạng các thuộc tính được truyền trong tệp BUILD) và tập hợp kết quả đầu ra cố định mà quy tắc tạo ra. Tác giả cũng xác định các hành động sẽ được tạo ra bởi quy tắc đó. Mỗi hành động khai báo dữ liệu đầu vào và đầu ra của mình, chạy một tệp thực thi cụ thể hoặc ghi một chuỗi cụ thể vào một tệp và có thể được kết nối với các hành động khác thông qua dữ liệu đầu vào và đầu ra của hành động đó. Điều này có nghĩa là các hành động là đơn vị có thể kết hợp cấp thấp nhất trong hệ thống xây dựng — một hành động có thể làm bất cứ điều gì nó muốn miễn là chỉ sử dụng các đầu vào và đầu ra đã khai báo, và Bazel xử lý việc lên lịch các hành động và lưu vào bộ nhớ đệm các kết quả của chúng cho phù hợp.

Hệ thống không bị ràng buộc bởi vì không có cách nào để ngăn nhà phát triển hành động thực hiện một hành động nào đó như đưa ra một quy trình không xác định trong hành động của họ. Tuy nhiên, việc này không xảy ra thường xuyên trong thực tế và việc tăng khả năng sử dụng sai mục đích xuống cấp hành động sẽ làm giảm đáng kể cơ hội xảy ra lỗi. Các quy tắc hỗ trợ nhiều ngôn ngữ và công cụ phổ biến hiện có sẵn rộng rãi trên mạng, và hầu hết các dự án sẽ không bao giờ cần xác định quy tắc của riêng mình. Ngay cả đối với những quy tắc đó, định nghĩa quy tắc chỉ cần được xác định ở một vị trí trung tâm trong kho lưu trữ, nghĩa là hầu hết các kỹ sư sẽ có thể sử dụng các quy tắc đó mà không phải lo lắng về việc triển khai.

Cô lập môi trường

Có vẻ như các hành động có thể gặp sự cố giống như nhiệm vụ trong các hệ thống khác — có phải vẫn có khả năng ghi các hành động mà cả hai ghi vào cùng một tệp và cuối cùng lại xung đột với nhau không? Trên thực tế, Bazel làm cho những xung đột này không thể xảy ra bằng cách sử dụng hộp cát. Trên các hệ thống được hỗ trợ, mọi thao tác đều được tách riêng khỏi mọi thao tác khác thông qua hộp cát hệ thống tệp. Trên thực tế, mỗi hành động chỉ có thể xem một chế độ xem bị hạn chế của hệ thống tệp, bao gồm cả những dữ liệu đầu vào đã khai báo và mọi kết quả mà hệ thống đã tạo ra. Điều này được các hệ thống, chẳng hạn như LXC trên Linux, chính là công nghệ phía sau Docker thực thi. Điều này có nghĩa là các hành động không thể xung đột với nhau vì chúng không thể đọc bất kỳ tệp nào mà họ không khai báo và bất kỳ tệp nào mà họ viết nhưng không khai báo sẽ bị loại bỏ khi hành động đó kết thúc. Bazel cũng sử dụng hộp cát để hạn chế các thao tác giao tiếp qua mạng.

Đảm bảo các phần phụ thuộc bên ngoài có tính xác định

Vẫn còn một vấn đề: các hệ thống xây dựng thường cần tải các phần phụ thuộc (dù là công cụ hay thư viện) xuống từ các nguồn bên ngoài thay vì xây dựng trực tiếp. Bạn có thể thấy điều này trong ví dụ thông qua phần phụ thuộc @com_google_common_guava_guava//jar. Phần phụ thuộc này sẽ tải tệp JAR từ Maven xuống.

Việc phụ thuộc vào các tệp bên ngoài không gian làm việc hiện tại là rất rủi ro. Những tệp đó có thể thay đổi bất cứ lúc nào, có thể yêu cầu hệ thống xây dựng liên tục kiểm tra xem các tệp đó có mới hay không. Nếu tệp từ xa thay đổi mà không có thay đổi tương ứng trong mã nguồn không gian làm việc, thì tệp đó cũng có thể dẫn đến các bản dựng không thể tái tạo — một bản dựng có thể hoạt động vào một ngày và không thành công tiếp theo mà không có lý do rõ ràng do một thay đổi về phần phụ thuộc không được chú ý. Cuối cùng, phần phụ thuộc bên ngoài có thể gây ra rủi ro bảo mật rất lớn khi nó thuộc sở hữu của bên thứ ba: nếu kẻ tấn công có thể xâm nhập vào máy chủ bên thứ ba đó, thì họ có thể thay thế tệp phụ thuộc bằng thiết kế của riêng mình, từ đó có thể kiểm soát môi trường tạo bản dựng và dữ liệu đầu ra.

Vấn đề cơ bản là chúng tôi muốn hệ thống xây dựng nhận biết các tệp này mà không phải kiểm tra chúng trong quyền kiểm soát nguồn. Việc cập nhật phần phụ thuộc cần phải là một lựa chọn hợp lý, nhưng bạn nên thực hiện lựa chọn này một lần ở vị trí trung tâm thay vì các kỹ sư cá nhân quản lý hoặc tự động quản lý. Điều này là do ngay cả với mô hình "Trực tiếp", chúng ta vẫn muốn các bản dựng có tính quyết định, nghĩa là nếu kiểm tra một cam kết từ tuần trước, bạn sẽ thấy các phần phụ thuộc như thời điểm đó chứ không phải như hiện tại.

Bazel và một số hệ thống xây dựng khác giải quyết vấn đề này bằng cách yêu cầu tệp kê khai trên toàn không gian làm việc liệt kê băm mã hoá cho mọi phần phụ thuộc bên ngoài trong không gian làm việc. Hàm băm là một cách ngắn gọn để thể hiện tệp riêng biệt mà không cần kiểm tra toàn bộ tệp thành quyền kiểm soát nguồn. Bất cứ khi nào một phần phụ thuộc bên ngoài mới được tham chiếu từ một không gian làm việc, hàm băm của phần phụ thuộc đó sẽ được thêm vào tệp kê khai theo cách thủ công hoặc tự động. Khi chạy một bản dựng, Bazel sẽ kiểm tra hàm băm thực tế của phần phụ thuộc được lưu vào bộ nhớ đệm so với hàm băm dự kiến đã xác định trong tệp kê khai và chỉ tải lại tệp đó xuống nếu hàm băm khác nhau.

Nếu cấu phần phần mềm chúng ta tải xuống có một hàm băm khác với hàm băm được khai báo trong tệp kê khai, thì bản dựng sẽ không hoạt động trừ khi hàm băm trong tệp kê khai được cập nhật. Bạn có thể thực hiện việc này tự động, nhưng thay đổi đó phải được phê duyệt và kiểm tra trong nguồn kiểm soát trước khi bản dựng chấp nhận phần phụ thuộc mới. Điều này có nghĩa là bạn sẽ luôn có bản ghi về thời điểm cập nhật phần phụ thuộc, và phần phụ thuộc bên ngoài không thể thay đổi nếu không có thay đổi tương ứng trong nguồn không gian làm việc. Điều này cũng có nghĩa là khi kiểm tra phiên bản mã nguồn cũ, bản dựng đó được đảm bảo sử dụng các phần phụ thuộc giống như phiên bản mà bạn đã sử dụng khi kiểm tra phiên bản đó (nếu không, các phần phụ thuộc đó sẽ không còn nữa).

Tất nhiên, vẫn có thể xảy ra sự cố nếu máy chủ từ xa không hoạt động hoặc bắt đầu phân phát dữ liệu bị hỏng – điều này có thể khiến tất cả bản dựng của bạn bắt đầu không hoạt động nếu bạn không có sẵn bản sao khác của phần phụ thuộc đó. Để tránh vấn đề này, bạn nên phản ánh tất cả phần phụ thuộc vào các máy chủ hoặc dịch vụ mà bạn tin tưởng và kiểm soát đối với mọi dự án không quan trọng. Nếu không, bạn sẽ luôn phải chịu trách nhiệm của bên thứ ba về khả năng sử dụng hệ thống xây dựng, ngay cả khi hàm băm đã đăng ký đảm bảo tính bảo mật.