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

Trang này đề cập đến các hệ thống xây dựng dựa trên cấu phần phần mềm và triết lý tạo ra các hệ thố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 đệm tốt so với tập lệnh bản dựng, nhưng các hệ thống này lại trao quá nhiều năng lực cho từng kỹ sư khi để họ xác định những 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 cần xây dựng, nhưng hệ thống xây dựng sẽ xác định cách xây dựng. Tương tự 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ó các tệp bản dựng, nhưng nội dung của các tệp xây dựng đó rất khác nhau. Thay vì là một tập lệnh bắt buộc trong ngôn ngữ tập lệnh hoàn chỉnh Turing mô tả cách tạo đầ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ác cấu phần phần mềm cần tạo, các phần phụ thuộc và một tập hợp các tuỳ chọn giới hạn ảnh hưởng đến cách xây dựng các cấu phần phần mềm đó. Khi các 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 (cái gì) và Bazel chịu trách nhiệm định cấu hình, chạy và lên lịch các bước biên dịch (cách thức). Vì giờ đây, hệ thống xây dựng có toàn quyền kiểm soát các công cụ sẽ chạy vào thời điểm nào, nên hệ thống có thể đưa ra những đả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.

Từ góc nhìn chức năng

Bạn có thể dễ dàng so sánh giữa các hệ thống xây dựng dựa trên cấu phần phần mềm và phương thức 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 thực thi lần lượt, theo cách tương tự như 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 cần thực thi. Ngược lại, các ngôn ngữ lập trình chức năng (chẳng hạn như Haskell và ML) có cấu trúc giống với một loạt phương trình toán học hơn. Trong các ngôn ngữ chức năng, 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à cách thức thực hiện phép tính đó cho trình biên dịch.

Điều này liên quan đến ý 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 hiểu cách thực thi bản dựng. Nhiều vấn đề không thể dễ dàng biểu thị bằng cách sử dụng lập trình chức năng, nhưng sẽ có những vấn đề hưởng lợi rất nhiều từ đó: ngôn ngữ thường có thể đơn giản hoá song song các chương trình như vậy và đưa ra đảm bảo chắc chắn về tính chính xác của chúng. Điều này không thể làm được trong ngôn ngữ bắt buộc. Vấn đề dễ nhất để thể hiện khi sử dụng lập trình chức năng là những vấn đề chỉ đơn giản liên quan đến việc chuyển đổi một phần dữ liệu thành 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à hệ thống xây dựng: toàn bộ hệ thống thực sự là một hàm toán học lấy các tệp nguồn (và các công cụ như trình biên dịch) làm dữ liệu đầu vào và tạo ra các tệp nhị phân làm đầu ra. Vì vậy, không có gì đáng ngạc nhiên khi công cụ này hoạt động tốt khi xây dựng hệ thống xây dựng xoay quanh các 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 đầu tiên dựa trên cấu phần phần mềm. Bazel là phiên bản nguồn mở của Blaze.

Dưới đây là ví dụ về một tệp buildfile (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, các tệp BUILD xác định các 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 mà hệ thống có thể tạo ra: các mục tiêu nhị phân tạo ra các tệp nhị phân có thể được thực thi trực tiếp, còn các mục tiêu thư viện tạo ra các thư viện mà tệp nhị phân hoặc thư viện khác có thể sử dụng. Mọi mục tiêu đề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 hệ phân cấp nguồn (chẳng hạn như phần phụ thuộc của mylib trên //java/com/example/common).

Tương tự như với các hệ thống xây dựng dựa trên tác vụ, bạn tạo 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 cần chạy bazel build :MyBinary. Sau khi nhập lệnh đó lần đầu tiên vào 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 đồ các phần phụ thuộc giữa các cấu phần phần mềm.
  2. Sử dụng biểu đồ này để 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ào và mọi mục tiêu mà các mục tiêu đó phụ thuộc vào một cách đệ quy.
  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 tạo cho từng mục tiêu. Ngay khi tất cả các phần phụ thuộc của một mục tiêu được tạo, Bazel sẽ bắt đầu xây dựng mục tiêu đó. Quá trình này tiếp tục cho đến khi từng 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 tệp nhị phân có thể thực thi cuối cùng liên kết trong tất cả các phần phụ thuộ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 sử dụng hệ thống xây dựng dựa trên tác vụ. Thực tế, kết quả cuối cùng vẫn là cùng một tệp nhị phân và quy trình tạo tệp nhị phân bao gồm phân tích một loạt các bước để tìm các phần phụ thuộc giữa chúng, 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. Lệnh đầu tiên xuất hiện ở bước 3: vì Bazel biết rằng mỗi mục tiêu chỉ tạo một thư viện Java, nên Bazel chỉ cần chạy trình biên dịch Java thay vì tập lệnh do người dùng xác định tuỳ ý. Nhờ đó, Bazel sẽ an toàn để chạy song song các bước này. Điều này có thể cải thiện hiệu suất so với việc xây dựng từng mục tiêu một trên máy đa lõi, 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 sẽ cho phép hệ thống xây dựng tự chịu trách nhiệm về chiến lược thực thi của riêng mình để có thể đảm bảo chắc chắn hơn về tính song song.

Tuy nhiên, lợi ích này không chỉ giới hạn ở tính năng tải song song. Điều tiếp theo mà phương pháp này mang lại cho chúng ta trở nên rõ ràng 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 với một 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 đến trước đó – 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à biết rằng đầu ra của trình biên dịch Java chỉ phụ thuộc vào các dữ liệu đầu vào, miễn là dữ liệu đầu vào không thay đổi, đầu ra có thể được sử dụng lại. Và bản phân tích này hoạt động ở mọi cấp độ; nếu MyBinary.java thay đổi, Bazel biết sẽ 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ẽ xây dựng lại thư viện đó, mylibMyBinary, nhưng sử dụng lại //java/com/example/myproduct/otherlib. Vì biết về các thuộc tính của các công cụ mà Bazel đang chạy ở mỗi bước, nên Bazel chỉ có thể tạo lại bộ cấu phần phần mềm tối thiểu mỗi lần mà vẫn đảm bảo rằng công cụ này sẽ không tạo ra các bản dựng lỗi thời.

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ụ là một việc tinh tế nhưng hiệu quả. Bằng cách giảm tính linh hoạt mà lập trình viên tiếp xúc, 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. KPS có thể sử dụng kiến thức này để tăng hiệu quả của bản dựng bằng cách tải song song các quy trình xây dựng và sử dụng lại kết quả đầu ra của các quy trình này. Nhưng đây thực sự chỉ là bước đầu tiên, và những thành phần của tính năng song song và tái sử dụng này tạo thành cơ sở cho 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 tiện lợi khác của Bazel

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ơ bản các vấn đề xảy ra với tính năng tải song song và sử dụng lại vốn có trong các hệ thống xây dựng dựa trên tác vụ. Nhưng vẫn còn một số vấn đề phát sinh trước đó mà chúng tôi chưa giải quyết. Bazel có những cách giải quyết thông minh trong số này và chúng ta nên thảo luận về chúng trước khi tiếp tục.

Công cụ dưới dạng phần 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ài đặt trên máy của chúng tôi. Do đó, việc tái tạo các bản dựng trên các hệ thống có thể khó khăn do các phiên bản công cụ hoặc vị trí khác nhau. Vấn đề thậm chí còn khó khăn hơn khi dự án của bạn sử dụng các ngôn ngữ yêu cầu các công cụ khác nhau dựa trên nền tảng được tạo hoặc biên dịch (chẳng hạn như Windows so với Linux) và mỗi nền tảng đó yêu cầu bộ công cụ khác một chút để 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 coi các công cụ là phần phụ thuộc cho từng 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 phổ biến. Bất cứ khi nào Bazel tạo một java_library, Bazel sẽ kiểm tra để đảm bảo rằng trình biên dịch được chỉ định có sẵn tại một vị trí đã biết. 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 phần phụ thuộc đó sẽ được tạo lại.

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

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

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

Bazel đặt ra 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 nhiều việc hơn. Một phần lợi ích của các hệ thống dựa trên tác vụ là tính linh hoạt trong việc hỗ trợ bất kỳ loại quy trình xây dựng nào. tốt hơn là không nên bỏ qua 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 quy tắc tuỳ chỉnh.

Để xác định một quy tắc trong Bazel, tác giả quy tắc khai báo các dữ liệu đầu vào mà quy tắc đó yêu cầu (ở dạng các thuộc tính được truyền trong tệp BUILD) và tập hợp đầu ra cố định mà quy tắc tạo ra. Tác giả cũng xác định các thao tác sẽ được tạo ra theo quy tắc đó. Mỗi hành động đều khai báo các dữ liệu đầu vào và đầu ra, 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, đồng thời 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à thao tác 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 thao tác có thể thực hiện bất cứ tuỳ chọn nào, miễn là chỉ sử dụng dữ liệu đầu vào và đầu ra đã khai báo, đồng thời Bazel sẽ xử lý việc lên lịch cho các thao tác và lưu kết quả vào bộ nhớ đệm khi thích hợp.

Hệ thống này không chắc chắn vì không có cách nào để ngăn nhà phát triển hành động làm những việc như đưa ra một quy trình không xác định trong hành động của họ. Tuy nhiên, điều này không xảy ra thường xuyên trong thực tế và việc đẩy mạnh khả năng vi phạm xuống đến cấp hành động đã làm giảm đáng kể khả năng xảy ra lỗi. Các quy tắc hỗ trợ nhiều ngôn ngữ và công cụ phổ biến được cung cấp rộng rãi trên mạng, và hầu hết dự án sẽ không bao giờ cần phải xác định quy tắc riêng. 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ác thao tác có vẻ như có thể gặp vấn đề tương tự như các tác vụ trong các hệ thống khác – chẳng phải vẫn có thể viết những thao tác cả ghi vào cùng một tệp và kết thúc xung đột với nhau phải không? Trên thực tế, Bazel ngăn chặn các xung đột này bằng cách sử dụng hộp cát. Trên các hệ thống được hỗ trợ, mỗi hành động được tách biệt với mọi hành động khác thông qua một hộp cát hệ thống tệp. Nhờ đó, mỗi thao tác chỉ có thể xem một khung hiển thị bị hạn chế của hệ thống tệp, bao gồm các dữ liệu đầu vào mà nó đã khai báo và mọi kết quả mà hệ thống đã tạo. Việc này được các hệ thống như LXC trên Linux thực thi, công nghệ tương tự như Docker. Đ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 chúng không khai báo và mọi tệp mà chúng ghi 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.

Xác định các phần phụ thuộc bên ngoài

Vẫn còn một vấn đề nữa là: các hệ thống xây dựng thường cần tải các phần phụ thuộc xuống (cho dù là công cụ hay thư viện) từ các nguồn bên ngoài thay vì trực tiếp tạo các phần phụ thuộc đó. 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 một tệp JAR xuống từ Maven.

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. Các 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 của không gian làm việc, thì điều đó cũng có thể dẫn đến các bản dựng không thể mô phỏng – một bản dựng có thể hoạt động vào một ngày và không hoạt động vào ngày hôm sau mà không có lý do rõ ràng do sự thay đổi phần phụ thuộc không được phát hiện. 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 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ủ của bên thứ ba, chúng có thể thay thế tệp phụ thuộc bằng tệp nào đó bằng thiết kế của riêng họ, có khả năng trao cho họ toàn quyền kiểm soát môi trường bản dựng và đầu ra của máy chủ đó.

Vấn đề cơ bản là chúng ta muốn hệ thống xây dựng nhận biết được các tệp này mà không cần phải kiểm tra chúng trong chế độ kiểm soát nguồn. Bạn nên chọn cập nhật phần phụ thuộc, nhưng nên thực hiện lựa chọn đó một lần ở một vị trí trung tâm thay vì do từng kỹ sư quản lý hoặc do hệ thống tự động quản lý. Điều này là do ngay cả với mô hình "Trực tiếp", chúng tôi vẫn muốn các bản dựng mang tính xác định. Điều này có nghĩa là nếu bạn xem một cam kết từ tuần trước, thì bạn sẽ thấy các phần phụ thuộc như hiện tại thay vì 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 một tệp kê khai trên toàn không gian làm việc liệt kê hàm băm mật mã 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 để biểu thị riêng một tệp mà không cần kiểm tra toàn bộ tệp trong chế độ kiểm soát nguồn. Mỗi khi 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 đã lưu vào bộ nhớ đệm so với hàm băm dự kiến được 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 tôi tải xuống có 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 thành công trừ khi hàm băm trong tệp kê khai được cập nhật. Quá trình này có thể được thực hiện tự động, nhưng thay đổi đó phải được phê duyệt và kiểm tra trong phần kiểm soát nguồn 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à luôn có bản ghi về thời điểm một phần phụ thuộc được cập nhật 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 bạn kiểm tra phiên bản mã nguồn cũ, bản dựng đảm bảo sẽ sử dụng chính các phần phụ thuộc đã được sử dụng tại thời điểm phiên bản đó được kiểm tra (nếu không, bản dựng sẽ không thành công nếu những phần phụ thuộc đó không còn nữa).

Tất nhiên, vấn đề vẫn có thể xảy ra 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ả cá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 một bản sao khác của phần phụ thuộc đó. Để tránh sự cố này, đối với mọi dự án không quan trọng, bạn nên đồng bộ hoá tất cả phần phụ thuộc của dự án đó lên máy chủ hoặc dịch vụ mà bạn tin tưởng và kiểm soát. Nếu không, bạn sẽ phải nhờ bên thứ ba cung cấp khả năng sử dụng hệ thống xây dựng, ngay cả khi các hàm băm đã đăng ký đảm bảo tính bảo mật của hệ thống đó.