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 ra các hệ thống này. 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 bản dựng dựa trên tác vụ là một bước tiến so với tập lệnh bản dựng, nhưng chúng lại trao quá nhiều quyền cho từng kỹ sư bằng cách cho phép họ xác định các tác vụ của riêng mình.
Hệ thống bản 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 cần xây dựng những gì, nhưng hệ thống xây dựng sẽ xác định cách xây dựng. Giống như hệ thống bản dựng dựa trên tác vụ, hệ thống bản 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 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ữ kịch bản hoàn chỉnh Turing mô tả cách tạo ra một đầu ra, buildfile 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 của chúng và một tập hợp các lựa chọn hạn chế ảnh hưởng đến cách chúng được tạo. Khi chạy bazel
trên dòng lệnh, các kỹ sư sẽ chỉ định một nhóm mục tiêu để tạo (what), đồng thời 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 (how). Vì hệ thống bản dựng hiện 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ào, nên hệ thống này 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 mà vẫn đảm bảo tính chính xác.
Góc độ chức năng
Bạn có thể dễ dàng so sánh giữa hệ thống bản 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 sẽ được thực thi lần lượt, theo cách tương tự như các hệ thống bản 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 chuỗi các phương trình toán học. 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 hiện chính xác phép tính đó cho trình biên dịch.
Điều này tương ứng với ý tưởng khai báo một tệp kê khai trong hệ thống bản 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. Nhiều vấn đề không thể dễ dàng thể hiện bằng lập trình hàm, nhưng những vấn đề có thể thể hiện được sẽ hưởng lợi rất nhiều từ lập trình hàm: ngôn ngữ này thường có thể song song hoá các chương trình như vậy một cách dễ dàng và đưa ra những đảm bảo chắc chắn về tính chính xác của chúng mà không thể có trong ngôn ngữ mệnh lệnh. Những vấn đề dễ biểu thị nhất bằng cách sử dụng 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à những gì mà hệ thống xây dựng mang lại: toàn bộ hệ thống này 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 đầ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ạc nhiên khi hệ thống này hoạt động hiệu quả khi xây dựng dựa trên các nguyên tắc của lập trình hàm.
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 phần mềm đầu tiên. Bazel là phiên bản mã nguồn mở của Blaze.
Sau đây là nội dung 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, các tệp BUILD
xác định các mục tiêu – hai loại mục tiêu ở đây là java_binary
và java_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 nhị phân tạo ra các tệp nhị phân có thể thực thi trực tiếp và mục tiêu thư viện tạo ra các thư viện mà các 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à bởi các mục tiêu khácsrcs
: các tệp nguồn được biên dịch để tạo cấu phần phần mềm cho mục tiêudeps
: 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
vào :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
vào //java/com/example/common
).
Giống như các hệ thống bản 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 chạy bazel build :MyBinary
. Sau khi bạn nhập lệnh đó lần đầu tiên trong một kho lưu trữ sạch, Bazel sẽ:
- 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. - 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à những mục tiêu đó phụ thuộc vào, một cách đệ quy. - 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 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 tiếp tục cho đến khi mọi phần phụ thuộc bắc cầu của
MyBinary
được tạo. - Tạo
MyBinary
để tạo ra một tệp thực thi nhị phân cuối cùng liên kết trong 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 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 bản 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 tệp nhị phân đó bao gồm việc phân tích một loạt các bước để tìm ra 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. Thao tác đầ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 ra một thư viện Java, nên Bazel 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, Bazel biết rằng có thể chạy song song các bước này. Điều này có thể giúp cải thiện hiệu suất theo cấp số nhân so với việc tạo các mục tiêu từng mục tiêu một trên một máy nhiều 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 để hệ thống bản dựng chịu trách nhiệm về chiến lược thực thi của riêng hệ thống, nhờ đó, hệ thống có thể đưa ra những đảm bảo chắc chắn hơn về tính song song.
Tuy nhiên, lợi ích của việc 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 vòng 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 là do mô hình lập trình chức năng mà chúng ta đã đề cập 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à Bazel 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 trình biên dịch đó, vì vậy, miễn là đầu vào không thay đổi, đầu ra có thể được sử dụng lại.
Và quá trình phân tích này hoạt động ở mọi cấp độ; nếu MyBinary.java
thay đổi, Bazel biết cần phải tạo lại MyBinary
nhưng sử dụng lại mylib
. Nếu một tệp nguồn cho //java/com/example/common
thay đổi, Bazel sẽ biết cách tạo lại thư viện đó, mylib
và MyBinary
, 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 những công cụ mà nó chạy ở mỗi bước, nên Bazel chỉ có thể tạo lại bộ tối thiểu các cấu phần phần mềm mỗi lần trong khi đảm bảo rằng nó sẽ không tạo ra các bản dựng cũ.
Việc định hình lại quy trình tạo theo cấu phần phần mềm thay vì theo tác vụ tuy tinh tế nhưng lại rất hiệu quả. Bằng cách giảm tính linh hoạt mà lập trình viên có thể sử dụng, hệ thống bản dựng có thể biết thêm về những việc đang được thực hiện ở mỗi bước của bản dựng. Nó có thể sử dụng kiến thức này để tạo bản dựng hiệu quả hơn nhiều bằng cách song song hoá các quy trình tạo và sử dụng lại đầu ra của chúng. Nhưng đây chỉ là bước đầu tiên và các khối song song và sử dụng lại này tạo thành cơ sở cho một hệ thống bản dựng phân tán và có khả năng mở rộng cao.
Các thủ thuật hay khác về Bazel
Về cơ bản, hệ thống bản 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à khả năng sử dụng lại vốn có trong hệ thống bản dựng dựa trên tác vụ. Nhưng vẫn còn một vài vấn đề xuất hiện trước đó mà chúng ta chưa giải quyết. Bazel có những 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ề chúng trước khi tiếp tục.
Công cụ dưới dạng các phần phụ thuộc
Một vấn đề mà chúng tôi gặp phải trước đây là các bản dựng phụ thuộc vào những công cụ được cài đặt trên máy của chúng tôi. Việc sao chép các bản dựng trên nhiều 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 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 coi các công cụ là phần phụ thuộc của từng mục tiêu. Mọi java_library
trong không gian làm việc đều phụ thuộc ngầm vào một trình biên dịch Java, theo 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
, nó 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, thì 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 đề, 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ụ của chúng, cá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ủ lưu trữ: các công cụ tạo 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
Mở rộng hệ thống xây dựng
Bazel có sẵn các mục tiêu cho một số ngôn ngữ lập trình phổ biến, nhưng các kỹ sư sẽ luôn muốn làm nhiều hơn nữa. Một phần lợi ích của 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à sẽ tốt hơn nếu không 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 tuỳ chỉnh.
Để xác định một quy tắc trong Bazel, tác giả quy tắc khai báo các đầ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à bộ đầu ra cố định mà quy tắc tạo ra. Tác giả cũng xác định những hành động sẽ được tạo theo quy tắc đó. Mỗi thao tác 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 một tệp và có thể được kết nối với các thao tác khác thông qua đầu vào và đầu ra. Điều này có nghĩa là các thao tác là đơn vị có thể kết hợp ở cấp thấp nhất trong hệ thống bản dựng – một thao tác có thể làm bất cứ điều gì mà thao tác đó muốn, miễn là chỉ sử dụng các đầu vào và đầu ra đã khai báo, đồng thời Bazel sẽ lo việc lên lịch 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 hoàn toàn đáng tin cậy 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 quy trình không xác định vào 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 các khả năng lạm dụng 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 có sẵn trên mạng và hầu hết các dự án sẽ không bao giờ cần xác định các quy tắc riêng. Ngay cả đối với những người 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 những quy tắc đó mà không cần 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 vấn đề tương tự như các tác vụ trong những hệ thống khác. Chẳng phải 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 những 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 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. Trên thực tế, mỗi thao tác chỉ có thể thấy một chế độ xem bị hạn chế của hệ thống tệp, bao gồm cả các đầu vào mà thao tác đó đã khai báo và mọi đầu ra mà thao tác đó đã tạo. Đ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ì chúng 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 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 đề: hệ thống xây dựng thường cần tải các phần phụ thuộc (cho 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à điều mạo hiểm. Những tệp đó có thể thay đổi bất cứ lúc nào, có khả năng yêu cầu hệ thống bản dựng liên tục kiểm tra xem chúng 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 hôm sau mà không có lý do rõ ràng nào do thay đổi về 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 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ần phụ thuộc bằng một tệp do chúng tự thiết kế, có khả năng cho phép chúng kiểm soát hoàn toàn môi trường xây dựng và đầu ra của bạn.
Vấn đề cơ bản là chúng ta muốn hệ thống xây dựng nhận biết được những tệp này mà không cần phải kiểm tra chúng trong quá trình kiểm soát nguồn. Việc cập nhật một phần phụ thuộc phải là một lựa chọn có ý thức, nhưng lựa chọn đó chỉ nên được thực hiện một lần ở một nơi tập trung thay vì do từng kỹ sư quản lý hoặc do hệ thống tự động quản lý. Lý do là ngay cả với mô hình "Live at Head", chúng tôi vẫn muốn các bản dựng có tính xác định. Điều này có 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 của mình như chúng đã từng chứ không phải như chúng hiện tại.
Bazel và một số hệ thống bản 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ê một 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. Băm là một cách ngắn gọn để biểu thị tệp một cách duy nhất mà không cần kiểm tra toàn bộ tệp vào hệ thống 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 đượ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 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ừ phi 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 trong quy trình 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 kiểm tra một phiên bản cũ hơn của mã nguồn, bản dựng chắc chắn sẽ sử dụng các phần phụ thuộc giống như khi 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, đây vẫn có thể là một vấn đề nếu máy chủ từ xa ngừ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 gặp lỗi nếu bạn không có bản sao nào khác của phần phụ thuộc đó. Để tránh vấn đề này, đối với mọi dự án không tầm thường, bạn nên sao chép 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 về tính khả dụng của hệ thống bản 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 đó.