Linear: Now
85

Thủ thuật

Linear tối ưu quy trình CI: Giải quyết nút thắt khi dùng AI lập trình, giảm thời gian chờ PR

(giờ Việt Nam)

Tóm tắt AI

Đội ngũ kỹ thuật tại Linear chia sẻ cách tinh chỉnh hệ thống CI để xử lý khối lượng code lớn từ AI, giúp giảm thời gian chờ duyệt PR và rút ngắn một nửa thời gian chạy kiểm thử đơn vị.

Bản dịch AI

AI coding has made CI a bottleneck, so we reworked ours to keep up

Đầu năm nay, tôi mở Linear và thấy Tuomas, CTO của chúng tôi, đã giao cho tôi một đầu việc với tiêu đề “Chi phí CI đang rất cao”. Trong khi xử lý việc đó, anh ấy cũng muốn tôi tăng tốc CI.

Các tác nhân (agents) đã giúp việc vận chuyển mã nguồn nhanh hơn theo cấp số nhân, nhưng việc xác thực các thay đổi đó lại không theo kịp tốc độ tương tự. Mọi PR vẫn phải đi qua CI, vì vậy khi quá trình phát triển tăng tốc, CI trở thành một nút thắt cổ chai, đẩy chi phí hạ tầng lên cao và khiến các lập trình viên cũng như các tác nhân phải chờ đợi phản hồi lâu hơn.

Trong nỗ lực tối ưu hóa hiệu suất CI tại Linear, chúng tôi tập trung vào thời gian chờ đợi của PR trên CI và lượng thời gian runner tiêu thụ. Mặc dù bộ kiểm thử (test suites) của chúng tôi đã tăng gần gấp bốn lần kể từ đầu năm, chúng tôi vẫn giảm thời gian chờ đợi pull request từ hơn 6 phút xuống còn hơn 5 phút một chút, đồng thời cắt giảm khoảng một nửa thời gian runner cho mỗi bài kiểm thử.

Performance metrics chart showing test coverage increase (blue line) and machine time reduction (white line) from January through September.Performance metrics chart showing test coverage increase (blue line) and machine time reduction (white line) from January through September.

Đây là hiệu suất của bộ kiểm thử được lập chỉ mục theo tuần đầu tiên của tháng 1. Đường màu trắng, theo dõi thời gian máy cho mỗi bài kiểm thử, tăng vọt khi chúng tôi thêm các test shards (phân đoạn kiểm thử) giúp rút ngắn thời gian chờ nhưng lại tốn nhiều thời gian máy hơn, và tăng trở lại trong các sự cố treo khi checkout.

Nhìn chung, chúng tôi đã cải thiện CI theo bốn cách:

Cơ sở mã của Linear chủ yếu là TypeScript, nhưng nhiều tối ưu hóa trong số này có thể áp dụng cho các ngôn ngữ và chuỗi công cụ khác nhau.

Nâng cấp hạ tầng và công cụ

Một số thành quả sớm nhất của chúng tôi hầu như không cần tối ưu hóa chính CI. Việc chuyển khối lượng công việc từ GitHub Actions sang các runner của bên thứ ba với CPU nhanh hơn, bộ nhớ hiệu năng cao hơn và hạ tầng cache tốt hơn đã cung cấp cho chúng tôi những cỗ máy nhanh hơn để chạy cùng một pipeline. Trong so sánh tương đương giữa hai ngày trước và sau khi chuyển đổi, các tác vụ chạy nhanh hơn trung bình 34%, với một số khối lượng công việc như tsc giảm tới 52%.

Ngoài ra, việc hiện đại hóa chuỗi công cụ cũng mang lại hiệu quả. Chuyển sang tsgo, trình biên dịch TypeScript gốc, đã cắt giảm 73% thời gian trung bình hàng tuần của quá trình kiểm tra tsc, đủ lớn để loại bỏ hoàn toàn nút thắt cổ chai từ việc kiểm tra kiểu (typechecking).

Lint mà không cần trình kiểm tra kiểu

Linting là một mục tiêu sớm khác. Một vài quy tắc lint tùy chỉnh của chúng tôi phụ thuộc vào thông tin kiểu của TypeScript, để thực thi một hạn chế hoặc áp dụng tự động sửa lỗi (autofix). Điều đó có nghĩa là mỗi lần chạy lint đều phải xây dựng toàn bộ đồ thị kiểu trước khi đánh giá các quy tắc đó, khiến linting trở thành một trong những tác vụ CI tốn nhiều bộ nhớ nhất.

Chúng tôi đã viết lại các quy tắc để sử dụng phân tích tĩnh trên cây cú pháp trừu tượng (abstract syntax tree), xác định các cấu trúc giống hàm và các mẫu bảo vệ (guard patterns) mà không cần thông tin kiểu. Điều đó cho phép ESLint loại bỏ hoàn toàn TypeScript, giảm 68% thời gian lint API và 55% thời gian lint toàn bộ kho lưu trữ. Mức sử dụng bộ nhớ cũng giảm đáng kể.

Việc loại bỏ sự phụ thuộc vào thông tin kiểu cũng giúp quá trình chuyển sang Oxlint sau này của chúng tôi dễ dàng hơn nhiều vì các quy tắc hoạt động thuần túy trên cú pháp rất dễ chuyển đổi. Bản thân Oxlint đã giảm số phút runner CI dành cho việc linting.

Tối ưu hóa các tác vụ kiểm soát các công việc khác

Với hạ tầng cơ sở và các kiểm tra riêng lẻ chạy nhanh hơn, chúng tôi nhìn nhận CI như một hệ thống tổng thể. Điều đó thu hút sự chú ý của chúng tôi vào các tác vụ nhỏ nằm trước mọi thứ khác. Mỗi lần chạy đều bắt đầu bằng việc kiểm tra xem PR đã chạm vào những đường dẫn nào và liệu các bài kiểm thử này đã vượt qua cho cùng đầu vào đó chưa. Chúng tôi kiểm soát các bước đó ở cấp độ tác vụ để công việc bị bỏ qua không bao giờ chiếm dụng runner, nhưng điều đó cũng đặt chúng trực tiếp vào đường dẫn quan trọng (critical path). Không có tám test shard API nào có thể bắt đầu cho đến khi chúng hoàn tất, khiến ngay cả những sự chậm trễ nhỏ cũng trở nên quan trọng một cách bất thường.

Chỉ lấy những gì mỗi tác vụ cần

Một số quy trình làm việc của chúng tôi bắt đầu bằng một tác vụ phát hiện thay đổi để quyết định những gì sẽ chạy tiếp theo; ví dụ, nó kiểm tra xem diff có chứa migration cơ sở dữ liệu hay không và xuất ra tín hiệu được sử dụng để lập lịch các kiểm tra CI cơ sở dữ liệu liên quan. Các tác vụ này đã checkout toàn bộ cây làm việc mặc dù chúng chỉ cần một phần nhỏ trong đó. Chúng tôi đã giới hạn độ sâu fetch, giúp giảm thời gian của các bước kiểm soát chậm nhất từ 94 giây xuống còn 20 giây, và loại bỏ hoàn toàn checkout khỏi các tác vụ không cần cây làm việc, giảm thời gian từ 27 giây xuống còn 7 giây. Đối với các sự kiện commit push và merge-queue, nơi chúng tôi phải diff các đường dẫn, chúng tôi thấy rằng việc checkout thưa thớt (sparse), không có blob với lịch sử hạn chế là đủ, tiết kiệm thêm khoảng 11 giây.

Performance distribution histogram: blue bars (After 8s median) and gray bars (Before 26s median), showing reduced load times after optimization.Performance distribution histogram: blue bars (After 8s median) and gray bars (Before 26s median), showing reduced load times after optimization.

Thời gian trung bình của tác vụ phát hiện thay đổi đã giảm từ 26 xuống 8 giây, p90 từ 31 xuống 12 giây và lần chạy chậm nhất từ 138 xuống 37 giây.

Làm cho checkout trở nên linh hoạt hơn

Sau khi thay đổi hạ tầng runner cơ bản, chúng tôi nhận thấy thời gian checkout (với actions/checkout) trong các tác vụ đã lâu hơn và đôi khi bị treo. Vì các runner của bên thứ ba nằm ngoài mạng của GitHub, chúng dựa vào liên kết IP trực tiếp để kết nối với GitHub. Nhà cung cấp đã truy vết các lỗi treo này là do sự suy giảm gián đoạn trên liên kết đó. Một số quy trình làm việc của chúng tôi bắt đầu bằng checkout, vì vậy một lần fetch bị đình trệ có thể làm chậm toàn bộ quá trình chạy CI.

Để thích ứng với sự không ổn định của mạng, chúng tôi đã thay thế actions/checkout bằng một composite action của riêng mình, có khả năng thử lại với cơ chế backoff, đồng thời thiết lập GIT_HTTP_LOW_SPEED_LIMIT và GIT_HTTP_LOW_SPEED_TIME để kết nối bị đình trệ sẽ hủy sau khoảng 30 giây thay vì bị treo. Chúng tôi cũng sử dụng bộ nhớ đệm checkout, giúp duy trì một bản sao git liên tục trên đĩa cứng. Kết quả là số lần chạy mà một tác vụ quan trọng phải chờ checkout hoàn tất đã giảm đi đáng kể.

Giảm thiểu những gì nằm trên đường dẫn quan trọng

Không phải mọi tác vụ trên đường dẫn quan trọng đều cần thiết phải ở đó. Chúng tôi đã ghi các đánh dấu cache như một phần của bước kiểm tra cuối cùng trước khi merge, điều đó có nghĩa là một pull request có thể nằm trong hàng đợi merge ngay cả khi các bài kiểm thử của nó đã vượt qua. Chúng tôi đã chuyển việc ghi đó vào một tác vụ chạy sau khi các test shard hoàn tất nhưng không kiểm soát bất cứ điều gì, giúp tiết kiệm 42 giây từ đường dẫn merge cho mỗi pull request API và mục nhập hàng đợi merge.

Tổng hợp lại, những thay đổi này đã giảm khoảng một phút cho bước kiểm tra cần thiết đối với các pull request API khi xảy ra cache miss, đồng thời giảm số lần khởi động runner.

Giảm thiết lập lặp lại

Từ đó, chúng tôi chuyển sang chi phí thiết lập lặp lại trên mỗi tác vụ, như khởi động runner, cài đặt gói và cung cấp các phụ thuộc xây dựng. Chi phí chung đó có nghĩa là một tác vụ chỉ mất vài giây để thực hiện công việc hữu ích có thể tiêu tốn cả phút thời gian hạ tầng. Dưới đây là một vài bước chúng tôi đã thực hiện để giải quyết vấn đề đó:

Cài đặt trước các phụ thuộc dùng chung trong image CI

Mỗi test shard API của chúng tôi mất từ 7 đến 8 giây để cài đặt cùng một Postgres client bằng apt trong mỗi lần chạy. Chúng tôi đã chuyển nó vào một image cơ sở CI nhỏ chứa Node và client, để mỗi shard có thể bắt đầu từ một môi trường đã sẵn sàng chạy. Sau đó, chúng tôi đã thêm các tiêu đề xây dựng gốc (native build headers) cần thiết vào image sau khi phát hiện ra rằng việc tải xuống chúng trong quá trình thiết lập đôi khi có thể gây treo, giúp rút ngắn thời gian chờ đợi.

Chỉ cài đặt các phụ thuộc mà mỗi tác vụ cần

Cơ sở mã của Linear là một monorepo được quản lý dưới dạng pnpm workspace. Quy trình kiểm thử API của chúng tôi đã cài đặt toàn bộ workspace mặc dù nó chỉ cần gói API và các phụ thuộc của nó. Việc giới hạn cài đặt chỉ ở gói API đã cắt giảm thời gian pnpm install từ 44-73 giây xuống còn 16-18 giây. Chúng tôi đã áp dụng mô hình tương tự cho các tác vụ lân cận API, vốn trước đó đều cài đặt toàn bộ kho lưu trữ và tải lên một bộ nhớ đệm phụ thuộc mà các lần chạy sau hầu như không bao giờ sử dụng đến.

Đừng cache khi việc xây dựng lại nhanh hơn

Chúng tôi cũng đã thử nghiệm việc cache node_modules và thấy rằng việc xây dựng lại nhanh hơn. Khóa cache phụ thuộc vào một lockfile thay đổi thường xuyên, và ngay cả khi cache hit cũng mất khoảng 28 giây để khôi phục, so với khoảng 7,5 giây cho một lần cài đặt có chọn lọc. Bộ nhớ đệm đã làm tăng thời gian lưu và sự biến thiên mà không mang lại cho chúng tôi bất kỳ lợi thế rõ ràng nào.

Tổng hợp lại, ba thay đổi này đã giảm thời gian thiết lập mỗi shard khoảng 44%, từ 110-140 giây xuống còn 67-73 giây.

GitHub Actions workflow comparison showing test-api runs with step-by-step timing breakdown; left run (4m 57s) versus right run (3m 14s), demonstrating performance improvements across pipeline stages

Thời lượng p95 của một test shard

Đọc bài gốc

Bài viết được AI dịch và tổng hợp tự động từ Linear: Now. Liên kết bài gốc ở phía trên. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.