Hệ thống xây dựng dựa trên nhiệm vụ

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

Trang này đề cập đến các hệ thống xây dựng dựa trên nhiệm vụ, cách hoạt động và một số chức năng có thể xảy ra với các hệ thống dựa trên nhiệm vụ. Sau các tập lệnh shell, các hệ thống xây dựng dựa trên tác vụ là sự phát triển logic tiếp theo của quy trình xây dựng.

Tìm hiểu về các hệ thống xây dựng dựa trên nhiệm vụ

Trong hệ thống xây dựng dựa trên nhiệm vụ, đơn vị công việc cơ bản là nhiệm vụ. Mỗi tác vụ là một tập lệnh có thể thực thi bất kỳ loại logic nào, còn tác vụ sẽ chỉ định các tác vụ khác làm phần phụ thuộc phải chạy trước chúng. Hầu hết các hệ thống xây dựng lớn đang dùng hiện nay, chẳng hạn như Ant, Maven, Gradle, Grunt và Rake, đều dựa trên nhiệm vụ. Thay vì tập lệnh shell, hầu hết các hệ thống xây dựng hiện đại đều yêu cầu kỹ sư tạo các tệp bản dựng mô tả cách thực hiện bản dựng.

Hãy lấy ví dụ này trong Hướng dẫn sử dụng Kiến thức:

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

Tệp bản dựng được viết bằng XML và xác định một số siêu dữ liệu đơn giản về bản dựng cùng với danh sách các tác vụ (các thẻ <target> trong XML). (Ant sử dụng từ target để biểu thị một task (tác vụ) và từ task (tác vụ) để chỉ lệnh.) Mỗi tác vụ sẽ thực thi một danh sách các lệnh mà Ant có thể xác định, bao gồm cả việc tạo và xoá thư mục, chạy javac và tạo tệp JAR. Các trình bổ trợ do người dùng cung cấp có thể mở rộng tập lệnh này để bao gồm mọi loại logic. Mỗi tác vụ cũng có thể xác định các nhiệm vụ phụ thuộc thông qua thuộc tính phụ thuộc. Các phần phụ thuộc này tạo thành một biểu đồ không chu trình, như trong Hình 1.

Biểu đồ acrylic cho thấy các phần phụ thuộc

Hình 1. Một đồ thị không chu trình thể hiện các phần phụ thuộc

Người dùng thực hiện bản dựng bằng cách cung cấp nhiệm vụ cho công cụ dòng lệnh của Ant. Ví dụ: khi người dùng nhập ant dist, Ant sẽ thực hiện các bước sau:

  1. Tải một tệp có tên build.xml trong thư mục hiện tại và phân tích cú pháp tệp đó để tạo cấu trúc biểu đồ như trong Hình 1.
  2. Tìm tác vụ có tên dist đã được cung cấp trên dòng lệnh và phát hiện tác vụ này có một phần phụ thuộc vào tác vụ có tên là compile.
  3. Tìm tác vụ có tên compile và phát hiện ra rằng tác vụ này có một phần phụ thuộc vào tác vụ có tên là init.
  4. Tìm tác vụ có tên init và phát hiện tác vụ không có phần phụ thuộc.
  5. Thực thi các lệnh được xác định trong tác vụ init.
  6. Thực thi các lệnh được xác định trong tác vụ compile vì tất cả phần phụ thuộc của tác vụ đó đều đã được chạy.
  7. Thực thi các lệnh được xác định trong tác vụ dist vì tất cả phần phụ thuộc của tác vụ đó đều đã được chạy.

Cuối cùng, mã được Ant thực thi khi chạy tác vụ dist tương đương với tập lệnh shell sau đây:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

Khi cú pháp bị xoá, tệp bản dựng và tập lệnh bản dựng thực sự không quá khác nhau. Nhưng chúng tôi đã thu được rất nhiều lợi ích nhờ việc này. Chúng ta có thể tạo các tệp bản dựng mới trong các thư mục khác và liên kết các tệp đó với nhau. Chúng tôi có thể dễ dàng thêm các công việc mới phụ thuộc vào các công việc hiện có theo cách tuỳ ý và phức tạp. Chúng tôi chỉ cần chuyển tên của một tác vụ duy nhất cho công cụ dòng lệnh ant và công cụ này xác định mọi thứ cần chạy.

Ant là một phần mềm phần mềm cũ, được phát hành lần đầu vào năm 2000. Các công cụ khác như Maven và Gradle đã cải thiện Ant trong những năm qua và về cơ bản đã thay thế Ant bằng cách thêm các tính năng như tự động quản lý các phần phụ thuộc bên ngoài và cú pháp rõ ràng hơn mà không cần XML. Tuy nhiên, bản chất của các hệ thống mới này vẫn không đổi: chúng cho phép kỹ sư viết tập lệnh bản dựng theo cách có nguyên tắc và theo mô-đun dưới dạng các nhiệm vụ, đồng thời cung cấp các công cụ để thực thi các nhiệm vụ đó và quản lý các phần phụ thuộc giữa chúng.

Mặt đen tối của hệ thống xây dựng dựa trên nhiệm vụ

Vì về cơ bản, các công cụ này cho phép các kỹ sư xác định bất kỳ tập lệnh nào là một nhiệm vụ, nên chúng cực kỳ mạnh mẽ, cho phép bạn làm được hầu hết mọi việc mà bạn có thể hình dung về chúng. Tuy nhiên, sức mạnh đó đi kèm với nhiều hạn chế và hệ thống xây dựng dựa trên nhiệm vụ có thể gặp khó khăn khi xử lý các tập lệnh bản dựng ngày càng phức tạp hơn. Vấn đề với các hệ thống như vậy là chúng thực sự cung cấp cho các kỹ sư quá nhiều năng lượng và không đủ năng lượng cho hệ thống. Vì hệ thống không biết các tập lệnh đang làm gì nên hiệu suất sẽ bị ảnh hưởng, vì hệ thống phải rất thận trọng trong cách lên lịch và thực thi các bước xây dựng. Và không có cách nào để hệ thống xác nhận rằng mỗi tập lệnh đang hoạt động đúng cách, vì vậy, các tập lệnh có xu hướng ngày càng phức tạp và trở thành một thứ khác cần được gỡ lỗi.

Khó khăn trong việc song song hoá các bước tạo bản dựng

Các máy trạm phát triển hiện đại khá mạnh mẽ, với nhiều lõi có khả năng thực thi song song nhiều bước xây dựng. Tuy nhiên, các hệ thống dựa trên tác vụ thường không thể thực hiện song song quá trình thực thi tác vụ ngay cả khi có vẻ như chúng có thể làm được. Giả sử công việc A phụ thuộc vào công việc B và C. Vì nhiệm vụ B và C không phụ thuộc lẫn nhau, nên liệu có an toàn để chạy chúng cùng lúc để hệ thống có thể chuyển sang tác vụ A nhanh hơn không? Có thể, nếu họ không sử dụng bất kỳ tài nguyên nào giống nhau. Nhưng có thể không – có thể cả hai đều sử dụng cùng một tệp để theo dõi trạng thái và chạy các trạng thái đó cùng lúc sẽ gây ra xung đột. Nhìn chung, hệ thống không có cách nào để biết, vì vậy hệ thống phải gặp rủi ro về các xung đột này (dẫn đến các vấn đề về bản dựng hiếm gặp nhưng rất khó gỡ lỗi), hoặc phải hạn chế toàn bộ bản dựng để chỉ chạy trên một luồng duy nhất trong một quy trình. Đây có thể là sự lãng phí lớn đối với một bộ máy phát triển mạnh mẽ và loại bỏ hoàn toàn khả năng phân phối bản dựng trên nhiều máy.

Khó tạo bản dựng tăng dần

Một hệ thống xây dựng hiệu quả cho phép kỹ sư thực hiện các bản dựng tăng dần đáng tin cậy, sao cho một thay đổi nhỏ là không yêu cầu xây dựng lại toàn bộ cơ sở mã từ đầu. Điều này đặc biệt quan trọng nếu hệ thống xây dựng hoạt động chậm và không thể thực hiện song song các bước xây dựng vì những lý do nêu trên. Nhưng tiếc là các hệ thống xây dựng dựa trên tác vụ cũng gặp khó khăn ở đây. Vì nhiệm vụ có thể làm bất cứ việc gì, nên nhìn chung, không có cách nào để kiểm tra xem chúng đã được thực hiện hay chưa. Nhiều tác vụ chỉ cần lấy một tập hợp các tệp nguồn và chạy một trình biên dịch để tạo một tập hợp các tệp nhị phân; do đó, bạn không cần chạy lại các tệp này nếu các tệp nguồn cơ bản không thay đổi. Tuy nhiên, nếu không có thông tin bổ sung thì hệ thống không thể nói chắc chắn điều này – có thể tác vụ tải xuống một tệp có thể đã thay đổi, hoặc có thể tác vụ ghi một dấu thời gian có thể khác nhau trong mỗi lần chạy. Để đảm bảo độ chính xác, hệ thống thường phải chạy lại mọi tác vụ trong mỗi bản dựng. Một số hệ thống xây dựng cố gắng hỗ trợ các bản dựng tăng dần bằng cách cho phép kỹ sư chỉ định các điều kiện cần chạy lại một tác vụ. Đôi khi điều này khả thi, nhưng thường là một vấn đề phức tạp hơn nhiều so với những gì bạn nghĩ. Ví dụ: trong các ngôn ngữ như C++ cho phép các tệp khác đưa trực tiếp vào, không thể xác định toàn bộ tập hợp các tệp phải xem để thay đổi nếu không phân tích cú pháp nguồn đầu vào. Các kỹ sư thường sử dụng lối tắt. Các lối tắt này có thể gây ra những vấn đề hiếm gặp và khó chịu, trong đó kết quả tác vụ được sử dụng lại ngay cả khi không nên. Khi điều này thường xuyên xảy ra, các kỹ sư có thói quen chạy sạch trước mỗi bản dựng để có trạng thái mới, hoàn toàn huỷ bỏ mục đích tạo bản dựng tăng dần ngay từ đầu. Việc tìm ra thời điểm cần chạy lại một tác vụ là việc cần làm tinh tế đến mức đáng ngạc nhiên. Công việc này do máy học xử lý hiệu quả hơn con người.

Khó duy trì và gỡ lỗi tập lệnh

Cuối cùng, các tập lệnh bản dựng do các hệ thống xây dựng dựa trên nhiệm vụ áp đặt thường rất khó xử lý. Mặc dù thường ít được giám sát hơn, nhưng các tập lệnh bản dựng đều là mã giống như hệ thống đang được tạo và là nơi dễ dàng để lỗi ẩn đi. Dưới đây là một số ví dụ về lỗi rất thường gặp khi làm việc với hệ thống xây dựng dựa trên tác vụ:

  • Tác vụ A phụ thuộc vào nhiệm vụ B để tạo một tệp cụ thể làm đầu ra. Chủ sở hữu của tác vụ B không nhận ra rằng các tác vụ khác dựa vào tác vụ này, vì vậy họ thay đổi tác vụ đó để tạo ra đầu ra ở một vị trí khác. Không thể phát hiện điều này cho đến khi ai đó cố gắng chạy tác vụ A và nhận thấy rằng tác vụ không thành công.
  • Nhiệm vụ A phụ thuộc vào nhiệm vụ B, tuỳ thuộc vào nhiệm vụ C, tức là tạo một tệp cụ thể dưới dạng đầu ra mà nhiệm vụ A cần. Chủ sở hữu của nhiệm vụ B quyết định rằng không cần phụ thuộc vào nhiệm vụ C nữa, điều này khiến nhiệm vụ A không thành công mặc dù nhiệm vụ B hoàn toàn không quan tâm đến nhiệm vụ C!
  • Nhà phát triển của một tác vụ mới vô tình đưa ra giả định về máy đang chạy tác vụ đó, chẳng hạn như vị trí của một công cụ hoặc giá trị của các biến môi trường cụ thể. Tác vụ sẽ chạy trên máy của họ nhưng sẽ không thành công bất cứ khi nào nhà phát triển khác thử.
  • Một tác vụ chứa thành phần không xác định, chẳng hạn như tải một tệp xuống từ Internet hoặc thêm dấu thời gian vào bản dựng. Giờ đây, mọi người có thể nhận được kết quả khác nhau mỗi khi chạy bản dựng, nghĩa là không phải lúc nào các kỹ sư cũng có thể tái tạo và khắc phục lỗi của nhau hoặc lỗi của nhau xảy ra trên hệ thống xây dựng tự động.
  • Những tác vụ có nhiều phần phụ thuộc có thể tạo ra điều kiện tranh đấu. Nếu tác vụ A phụ thuộc vào cả tác vụ B và tác vụ C, còn tác vụ B và C đều sửa đổi cùng một tệp, thì tác vụ A sẽ nhận được kết quả khác nhau tuỳ thuộc vào tác vụ B và C hoàn thành trước.

Không có cách thức chung để giải quyết các vấn đề về hiệu suất, độ chính xác hoặc khả năng bảo trì trong khung dựa trên nhiệm vụ được nêu ở đây. Do các kỹ sư có thể viết mã tuỳ ý chạy trong quá trình tạo bản dựng, thì hệ thống sẽ không thể có đủ thông tin để luôn chạy các bản dựng một cách nhanh chóng và chính xác. Để giải quyết vấn đề này, chúng tôi cần để các kỹ sư nắm quyền kiểm soát và giao lại cho hệ thống, đồng thời định nghĩa lại vai trò của hệ thống, không phải là các tác vụ đang chạy mà là tạo ra cấu phần phần mềm.

Phương pháp này đã dẫn đến việc tạo ra các hệ thống xây dựng dựa trên cấu phần phần mềm, như Blaze và Bazel.