Hệ thống công trình dựa trên công việc

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

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

Tìm hiểu về hệ thống xây dựng dựa trên tác 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 nhiệm vụ là một tập lệnh có thể thực thi bất kỳ loại logic nào và các nhiệm vụ chỉ định các nhiệm vụ khác dưới dạng các phần phụ thuộc phải chạy trước các nhiệm vụ đó. Hầu hết các hệ thống xây dựng lớn được sử dụng hiện nay, chẳng hạn như Ant, Maven, Gradle, Grunt và Rake, đều dựa trên tác vụ. Thay vì tập lệnh môi trường 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 tệp xây 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 Ant:

<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ụ (thẻ <target> trong XML). (Ant sử dụng từ target để đại diện cho một tác vụ và sử dụng từ tác vụ để tham chiếu đến lệnh.) Mỗi nhiệm vụ sẽ thực thi một danh sách các lệnh có thể do Ant xác định, trong đó có việc tạo và xoá các thư mục, chạy javac và tạo một tệp JAR. Tập hợp các lệnh này có thể được mở rộng bằng trình bổ trợ do người dùng cung cấp để bao gồm mọi loại logic. Mỗi nhiệm 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 đồ tuần hoàn, như trong Hình 1.

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

Hình 1. Một biểu đồ không chu trình hiển thị 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. Chẳng hạn như 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 thấy tác vụ đó có phần phụ thuộc trên tác vụ có tên compile.
  3. Tìm tác vụ có tên compile và phát hiện thấy tác vụ đó có phần phụ thuộc trên tác vụ có tên init.
  4. Tìm tác vụ có tên init và phát hiện ra 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ã do Ant thực thi khi chạy tác vụ dist tương đương với tập lệnh shell sau:

./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ị loại bỏ, tệp bản dựng và tập lệnh bản dựng thực sự không quá khác biệt. Nhưng chúng ta đã thu được rất nhiều điều nhờ làm được việc này. Chúng ta có thể tạo tệp xây dựng mới trong các thư mục khác và liên kết chúng với nhau. Chúng ta có thể dễ dàng thêm các nhiệm vụ mới phụ thuộc vào các nhiệm vụ hiện có theo cách tùy ý và phức tạp. Chúng ta chỉ cần truyền tên của một tác vụ vào công cụ dòng lệnh ant và công cụ này sẽ xác định mọi thứ cần chạy.

Ant là một 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 can thiệp và về cơ bản đã thay thế nó 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à một 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 hơn này vẫn giữ nguyên: cho phép các kỹ sư viết tập lệnh bản dựng theo cách đơn giản và theo mô-đun dưới dạng các tác vụ, đồng thời cung cấp công cụ để thực thi các tác vụ đó và quản lý các phần phụ thuộc trong số đó.

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

Vì các công cụ này về cơ bản cho phép kỹ sư xác định bất kỳ tập lệnh nào dưới dạng một tác vụ nên chúng cực kỳ mạnh mẽ, cho phép bạn làm được khá nhiều việc mà bạn có thể hình dung được với chúng. Tuy nhiên, sức mạnh đó đi kèm với các hạn chế và các hệ thống xây dựng dựa trên tác vụ có thể trở nên khó xử lý khi tập lệnh bản dựng trở nên phức tạp hơn. Vấn đề với các hệ thống đó là chúng thực sự cung cấp quá nhiều năng lượng cho các kỹ sư và không có đủ điện năng cho hệ thống. Vì hệ thống không biết 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 tạo bản 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 làm như mong muốn, vì vậy, các tập lệnh có xu hướng phát triển phức tạp và trở thành một vấn đề khác cần gỡ lỗi.

Khó song song các bước xây dựng

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

Khó thực hiện các bản dựng tăng dần

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

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

Cuối cùng, tập lệnh bản dựng do hệ thống xây dựng dựa trên tác vụ áp dụng thường chỉ khó hoạt động. Mặc dù các tệp này thường ít được giám sát hơn, nhưng tập lệnh bản dựng cũng là mã giống như hệ thống đang được tạo và là nơi dễ mắc lỗi. Dưới đây là một số ví dụ về các lỗi rất phổ biến khi làm việc với hệ thống xây dựng dựa trên tác vụ:

  • Nhiệm vụ A phụ thuộc vào nhiệm vụ B để tạo một tệp cụ thể dưới dạng đầu ra. Chủ sở hữu của nhiệm vụ B không nhận ra rằng các nhiệm vụ khác dựa vào đó, vì vậy họ thay đổi để tạo kết quả ở 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 tác vụ không thành công.
  • Nhiệm vụ A phụ thuộc vào nhiệm vụ B, tùy thuộc vào nhiệm vụ C, việc này sẽ tạo ra một tệp cụ thể dưới dạng đầu ra mà nhiệm vụ A cần đến. Chủ sở hữu nhiệm vụ B quyết định không cần phụ thuộc vào nhiệm vụ C nữa, điều này sẽ khiến nhiệm vụ A bị lỗi 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 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ể. Nhiệm vụ này hoạt động trên máy của họ, nhưng không thành công bất cứ khi nào nhà phát triển khác dùng thử.
  • Nhiệm vụ chứa một thành phần không xác định, chẳng hạn như tải tệp xuống từ Internet hoặc thêm dấu thời gian vào một bản dựng. Giờ đây, mọi người có thể nhận được các kết quả khác nhau mỗi khi họ chạy bản dựng, nghĩa là các kỹ sư không phải lúc nào cũng có thể tái tạo và khắc phục lỗi hoặc các lỗi 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 các tình huống tương tranh. Nếu nhiệm vụ A phụ thuộc vào cả nhiệm vụ B và nhiệm vụ C, đồng thời nhiệm vụ B và C đều sửa đổi cùng một tệp, thì nhiệm vụ A sẽ nhận được kết quả khác nhau tùy thuộc vào việc nhiệm vụ B và C hoàn thành trước.

Không có cách thức chung nào để 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 làm việc theo nhiệm vụ được nêu tại đây. Miễn là các kỹ sư có thể viết mã tuỳ ý chạy trong quá trình xây dựng, hệ thống không thể có đủ thông tin để luôn có thể 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 lấy một số sức mạnh từ các kỹ sư và đặt nó vào tay hệ thống và tái định nghĩa vai trò của hệ thống không phải là các tác vụ đang chạy, mà là tạo các 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.