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

Báo cáo vấn đề Xem nguồn Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Trang này trình bày về 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ý đằng sau việc tạo 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ù hệ thống xây dựng dựa trên tác vụ là một bước tiến tốt so với tập lệnh bản dựng, nhưng hệ thống này lại trao quá nhiều quyền cho từng kỹ sư bằng cách cho phép họ xác định tác vụ của riêng mình.

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à 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 tạo, nhưng hệ thống xây dựng sẽ xác định cách tạo nội dung đó. Giố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 nhau. Thay vì là một tập hợp các lệnh bắt buộc trong ngôn ngữ tập lệnh Turing-complete mô tả cách tạo đầu ra, tệp bản dựng trong Bazel là một tệp kê khai 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 của chúng và một tập hợp các tuỳ chọn hạn chế ảnh hưởng đến cách tạo 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 sẽ 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ì hệ thống xây dựng hiện có toàn quyền kiểm soát các công cụ cần chạy khi 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 tính chính xác.

Quan điểm chức năng

Dễ dàng so sánh giữa hệ thống xây dựng dựa trên cấu phần phần mềm và lập trình chức năng. Các ngôn ngữ lập trình mệnh lệnh 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, giống như cách 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 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 thực thi phép tính đó cho trình biên dịch.

Điều này liên quan đến việc 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 được diễn đạt bằng cách lập trình hàm, nhưng những vấn đề được hưởng lợi rất nhiều từ phương pháp này: ngôn ngữ thường có thể dễ dàng song song hoá các chương trình như vậy và đảm bảo chắc chắn về tính chính xác của các chương trình đó, điều không thể thực hiện được bằng ngôn ngữ mệnh lệnh. Những vấn đề dễ dàng nhất để thể hiện bằng cách lập trình hàm là những vấn đề chỉ 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 cách sử dụng một loạt 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 là một hàm toán học hiệu quả, lấy 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 tệp nhị phân làm dữ liệu đầu ra. Vì vậy, không có gì ngạc nhiên khi hệ thống xây dựng dựa trên các nguyên tắc của lập trình hàm hoạt động hiệu quả.

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

Blaze, hệ thống xây dựng của Google, là hệ thống xây dựng dựa trên cấu phần 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 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 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: mục tiêu tệp nhị phân tạo ra các 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 có thể được các tệp nhị phân hoặc thư viện khác sử dụng. Mỗi mục tiêu đều có:

  • name: cách tham chiếu mục tiêu trên dòng lệnh và các mục tiêu khác
  • srcs: các tệp nguồn cần 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à được liên kết vào mục tiêu này

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ệ 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).

Cũng giống như các hệ thống xây dựng dựa trên tác vụ, bạn thực hiện 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 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 đồ phần phụ thuộc giữa 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ào và mọi mục tiêu mà các mục tiêu đó phụ thuộc vào, theo đệ quy.
  3. Tạo từng phần phụ thuộc đó theo thứ tự. Bazel bắt đầu bằng cách tạo từng mục tiêu không có phần phụ thuộc nào khác và theo dõi những phần phụ thuộc nào 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 tạo mục tiêu đó. Quá trình này sẽ tiếp tục cho đến khi mọi phần phụ thuộc bắc cầu của MyBinary được tạo.
  4. Tạo MyBinary để tạo một tệp nhị phân có thể thực thi cuối cùng liên kết với tất cả các phần phụ thuộc được tạo ở bước 3.

Về cơ bản, có vẻ như những gì đang diễn 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ật vậy, kết quả cuối cùng là cùng một tệp nhị phân và quy trình tạo kết quả này liên quan đến việc phân tích một loạt các bước để tìm các phần phụ thuộc giữa các bước đó, sau đó chạy các bước đó theo thứ tự. Tuy nhiên, có một số đ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 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ó biết rằng có thể chạy song song các bước này một cách an toàn. Điều này có thể mang lại hiệu suất cải thiện theo thứ tự lớn so với việc tạo các mục tiêu xây dựng lần lượt trên máy nhiều nhân, và điều này chỉ có thể xảy ra vì phương pháp dựa trên cấu phần phần mềm để hệ thống xây dựng chịu trách nhiệm về chiến lược thực thi của riêng nó để có thể đảm bảo chắc chắn hơn về tính song song.

Tuy nhiên, lợi ích của tính năng này không chỉ dừng lại ở tính song song. Điều tiếp theo mà phương pháp này mang lại cho chúng ta sẽ 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 sẽ thoát trong chưa đầy một giây với 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 hàm mà chúng ta đã thảo luậ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 một 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 đầu vào của nó, vì vậy, miễn là đầu vào không thay đổi, đầu ra có thể được sử dụng lại. Và hoạt động phân tích này hoạt động ở mọi cấp; nếu MyBinary.java thay đổi, Bazel sẽ biết phải 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 sẽ biết cách 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ác công cụ mà nó chạy ở mỗi bước, nên mỗi lần chỉ có thể tạo lại tập hợp cấu phần phần mềm tối thiểu trong khi đảm bảo rằng công cụ này sẽ không tạo ra các bản dựng cũ.

Việc định hình lại quy trình xây dựng theo cấu phần phần mềm thay vì tác vụ là một thay đổi tinh tế nhưng mạnh mẽ. Bằng cách giảm tính linh hoạt cho lập trình viên, hệ thống bản dựng có thể biết thêm về những gì đang diễn ra ở 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 chạy song song các quy trình bản dựng và sử dụng lại đầu ra của các quy trình đó. Nhưng đây thực sự chỉ là bước đầu tiên và các khối xây dựng song song và tái sử dụng này là nền tảng cho một hệ thống xây dựng phân tán và có khả năng mở rộng cao.

Các thủ thuật khác về Bazel

Về cơ bản, 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ề tính song song và sử dụng lại vốn có trong 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 đề đã xuất hiện trước đó mà chúng ta chưa giải quyết. Bazel có các cách thông minh để giải quyết từng vấn đề này và chúng ta nên thảo luận về các cách đó 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 ta 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ạo lại các bản dựng trên các hệ thống có thể gặp 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 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 mà chúng đang được xây dựng 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 một bộ công cụ hơi khác để 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 đều 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 tạo một java_library, Bazel sẽ kiểm tra để đảm bảo rằng trình biên dịch đã 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 trình biên dịch đó 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ì mục tiêu phụ thuộc trực tiếp vào công cụ, mục tiêu phụ thuộc vào các loại cấu hình:

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

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

Bazel đi kèm với các 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ợ mọi loại quy trình xây dựng và tốt hơn hết là bạn không nên từ bỏ điều đó trong một 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 tuỳ 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ạng 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 hành động sẽ được tạo bằng quy tắc đó. Mỗi hành động khai báo đầ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 tệp và có thể được kết nối với các hành động khác thông qua đầu vào và đầu ra của hành động đó. Điều này có nghĩa là hành động là đơn vị có khả năng 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ì miễn là hành động đó chỉ sử dụng dữ liệu đầu vào và đầu ra đã khai báo. Ngoài ra, Bazel sẽ chịu trách nhiệm lên lịch các hành động và lưu kết quả của các hành động đó vào bộ nhớ đệm khi thích hợp.

Hệ thống này không hoàn toàn an toàn vì không có cách nào để ngăn nhà phát triển hành động thực hiện một việc nào đó như giới thiệu 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 thường xảy ra trong thực tế và việc đẩy khả năng lạm dụng xuống cấp hành động sẽ 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 các dự án sẽ không bao giờ cần xác định quy tắc riêng. Ngay cả đối với những người có, bạn chỉ cần xác định định nghĩa quy tắc ở một nơi 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ách ly môi trường

Có vẻ như các thao tác có thể gặp phải các vấn đề tương tự như các tác vụ trong các hệ thống khác – không phải bạn vẫn có thể viết các thao tác vừa ghi vào cùng một tệp vừa xung đột với nhau sao? Trên thực tế, Bazel ngăn chặn các xung đột này bằng cách sử dụng tính năng hộp cát. Trên các hệ thống được hỗ trợ, mọi thao tác đều được tách biệt với mọi thao tác khác thông qua một hộp cát hệ thống tệp. Về cơ bản, mỗi thao tác chỉ có thể xem một chế độ xem bị hạn chế của hệ thống tệp, bao gồm các dữ liệu đầu vào mà thao tác đó đã khai báo và mọi dữ liệu đầu ra mà thao tác đó đã tạo ra. Điều này được thực thi bởi các hệ thống như LXC trên Linux, cùng một công nghệ đằng sau Docker. Điều này có nghĩa là các thao tác không thể xung đột với nhau vì không thể đọc bất kỳ tệp nào mà 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 thao tác hoàn tất. Bazel cũng sử dụng hộp cát để hạn chế các hành động giao tiếp qua mạng.

Tạo phần phụ thuộc bên ngoài có tính chất xác định

Vẫn còn một vấn đề: 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ì 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 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 một 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 này 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 hoạt động vào ngày tiếp theo mà không có lý do rõ ràng do một thay đổi phần phụ thuộc không được chú ý. Cuối cùng, một 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 phần phụ thuộc đó 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 đó, thì chúng có thể thay thế tệp phần phụ thuộc bằng một tệp do chúng thiết kế, có thể cho phép chúng kiểm soát toàn bộ môi trường xây dựng và đầu ra của môi trường đó.

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 các tệp đó vào hệ thống quản lý phiên bản. Việc cập nhật phần phụ thuộc phải là một lựa chọn có ý thức, nhưng lựa chọn đó phải được thực hiện một lần ở một nơi trung tâm thay vì do các kỹ sư cá nhân quản lý hoặc do hệ thống tự động quản lý. Lý do là ngay cả với mô hình "Trực tiếp tại đầu", 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 kiểm tra một thay đổi từ tuần trước, bạn sẽ thấy các phần phụ thuộc như trước đây thay vì 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 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ã 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 để biểu thị duy nhất tệp mà không cần kiểm tra toàn bộ tệp vào hệ thống quản lý 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 được xác định trong tệp kê khai và chỉ tải lại tệp nếu hàm băm khác.

Nếu cấu phần phần mềm mà chúng ta 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. Bạn có thể thực hiện việc này một cách tự động, nhưng thay đổi đó phải được phê duyệt và kiểm tra vào hệ thống quản lý 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 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 một phiên bản mã nguồn cũ, bản dựng được đảm bảo sẽ sử dụng cùng các phần phụ thuộc mà bản dựng đó đang 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 các phần phụ thuộc đó không còn nữa).

Tất nhiên, vẫn có thể xảy ra vấn đề 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 thành công nếu bạn không có bản sao khác của phần phụ thuộc đó. Để tránh vấn đề này, đối với mọi dự án không quan trọng, bạn nên phản ánh tất cả các phần phụ thuộc của dự án đó vào các 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ẽ luôn phụ thuộc vào bên thứ ba để biết tình trạng sẵn có của hệ thống xây dựng, ngay cả khi các hàm băm đã kiểm tra đảm bảo tính bảo mật của hệ thống đó.