Đường cong cấp số nhân đằng sau sự tồn đọng trong các dự án mã nguồn mở
Bài viết phân tích lý do tại sao các dự án mã nguồn mở phổ biến thường bị tồn đọng hàng trăm Pull Request trong nhiều tháng, sử dụng ví dụ thực tế từ Jellyfin và lý thuyết hàng đợi. Tác giả chỉ ra rằng việc người duy trì bị quá tải 100% thực sự làm tăng thời gian chờ theo cấp số nhân, đồng thời đề xuất các giải pháp quy trình như giới hạn kích thước PR, kiểm soát chất lượng đầu vào và phân tầng người duyệt để giải quyết nút thắt cổ chai.
Tôi đã cố gắng đóng góp một tính năng cho dự án Jellyfin web được hơn một năm nay. Tôi đã mở ba Pull Request (PR), thậm chí nhận được hai sự chấp thuận (approval), nhưng số lượng mã được hợp nhất (merge) vẫn bằng không.
Tính năng này tương đối nhỏ và cô lập: khi người dùng điều chỉnh độ lệch phụ đề trong trình phát, nó sẽ thêm một dòng thời gian hiển thị chính xác phụ đề nào sẽ xuất hiện khi nào, giúp người dùng biết mình đang làm gì thay vì phải phỏng đoán.
Một người duyệt (reviewer) đã đưa ra phản hồi chi tiết vào ngày đầu tiên. Tôi đã chia nhỏ công việc khi được yêu cầu, phản hồi ngay trong ngày và thay đổi phong cách mã để phù hợp với dự án. PR tái cấu trúc của tôi đã nhận được hai sự chấp thuận và hiện đang nằm im lìm đó. PR nhỏ nhất, chỉ thêm 49 dòng, chưa từng nhận được bất kỳ sự xem xét nào từ con người.
Khi tôi nêu vấn đề này trong cộng đồng Jellyfin, một người đã đùa rằng một năm thực sự chưa phải là quá dài đối với các PR của dự án này. Người duy trì chính cho biết danh sách chờ "tệ hại... đối với mọi người" và mỗi giây anh ấy không dành để duyệt thì tình hình càng tồi tệ hơn. Tôi đã hỏi xem việc giới hạn kích thước PR, yêu cầu đề xuất hay thay đổi quy trình có giúp ích gì không. Câu trả lời chủ yếu là "chỉ có một người duy trì thôi".
Tuy nhiên, tôi tin rằng vấn đề không chỉ đơn giản là thiếu người.
Một vấn đề phổ biến
Đây không phải là trường hợp hiếm gặp; hầu hết các dự án mã nguồn mở phổ biến đều gặp phải vấn đề tương tự. CPython có hơn 2.200 PR đang mở. Tại Hội thượng ngôn ngữ Python năm 2022, một nhà phát triển cốt lõi đã trình bày cụ thể về tình trạng tồn đọng này và chỉ ra một vấn đề "con gà và quả trứng": không có người duyệt tích cực cho một mô-đun đồng nghĩa với không có bài duyệt, điều này dẫn đến việc không có người duyệt mới được đào tạo.
Evan You, người tạo ra Vue.js, cũng đã công khai nói về việc khối lượng các vấn đề (issues) trở nên quá lớn để theo kịp khi dự án phát triển. Một khảo sát của Tidelift năm 2024 cho thấy 60% người duy trì mã nguồn mở đã bỏ cuộc hoặc cân nhắc bỏ cuộc. Báo cáo của Quỹ Ford phát hiện ra rằng đại đa số các dự án mã nguồn mở được duy trì bởi một hoặc hai người. Jellyfin chỉ là nơi tôi va phải nó. Và tôi nghĩ giải pháp không phải là "tìm thêm người duy trì", mà là thay đổi cách dòng chảy công việc vận hành qua người duy trì duy nhất mà bạn đang có.
Thời gian trôi đi ở đâu?
Trong tổng số 368 ngày trải qua trên ba PR của tôi, màu xanh lá là những ngày thực sự có người duyệt, viết mã hoặc phản hồi. Phần còn lại là chờ đợi.
Công việc thực hiện trong những khoảng thời gian ngắn đó rất tốt: các bài duyệt mã chi tiết, thảo luận kiến trúc, chia nhỏ PR khi được yêu cầu và giải quyết mọi vấn đề. Nhưng không có kết quả nào ra đời vì sau hai lần chấp thuận, PR vẫn không được hợp nhất.
Jellyfin web hiện có khoảng 200 PR đang mở và hợp nhất khoảng 20 đến 35 PR mã thực tế mỗi tháng (sau khi loại bỏ các bản cập nhật phụ thuộc tự động). Có khoảng 77 PR tính năng đang mở, nhưng các tính năng chỉ chiếm khoảng 21% những gì thực sự được hợp nhất; các bản sửa lỗi (bug fixes) lọt qua được, còn các tính năng thì nằm im.
Toán học của hàng đợi
Donald Reinertsen, trong cuốn The Principles of Product Development Flow, đã áp dụng lý thuyết hàng đợi vào phát triển sản phẩm. Ý tưởng cốt lõi là: khi một tài nguyên đạt gần 100% công suất sử dụng, hàng đợi sẽ tăng trưởng theo cấp số nhân, không phải tuyến tính.
Công thức hàng đợi M/M/1 là: thời gian chờ = công suất / (1 - công suất).
- Ở mức công suất 50%, thời gian chờ là 1x.
- Ở mức 80%, thời gian chờ là 4x.
- Ở mức 90%, thời gian chờ là 9x.
- Ở mức 95%, thời gian chờ là 19x.
Người duy trì đã nói rằng mỗi giây không duyệt làm mọi thứ tồi tệ hơn, và chính xác đó là cái bẫy. Cảm giác rằng sự nhàn rỗi là lãng phí thúc đẩy bạn hướng tới 100% công suất, nơi đường cong đi thẳng đứng. Bạn không thể làm việc chăm chỉ hơn để vượt qua một cấp số nhân.
Định luật Little đưa ra một con số cụ thể: thời gian chu kỳ = công việc đang thực hiện (WIP) / thông lượng. Với 200 PR trong hàng đợi và khoảng 30 lần hợp nhất mỗi tháng, thời gian chu kỳ trung bình tính ra là 6,7 tháng. Đó không phải là sự tồn đọng tạm thời, đó đơn giản là những gì hệ thống này làm ở mức tải này.
Vòng xoáy tử thần của kích thước lô (Batch Size)
Các PR lớn mất nhiều thời gian để duyệt hơn, nên chúng bị chất đống. Những người đóng góp thấy đống công việc đó và nghĩ rằng mình cũng nên gộp thêm nhiều thay đổi trong lúc chờ đợi, điều này làm cho PR lớn hơn, từ đó việc duyệt chậm hơn, và đống công việc lại càng lớn hơn. Reinertsen gọi đây là "Vòng xoáy tử thần của kích thước lô" và tôi cái tên này rất chính xác.
Trong số 200 PR đang mở trên Jellyfin web, 30 PR có xung đột khi hợp nhất (merge conflicts) và 31 PR được gắn nhãn là cũ (stale). Đây là những PR khó hợp nhất hơn so với khi mới mở, và tình hình chỉ tồi tệ hơn theo thời gian chứ không thể tự khắc phục.
Phản hồi chậm lãng phí thời gian của mọi người
Một bài duyệt trở nên ít hữu ích hơn nếu nó mất nhiều thời gian. Ở mức 3 ngày, người đóng góp vẫn còn nắm bắt được bối cảnh và có thể thực hiện thay đổi nhanh chóng. Ở mức 3 tháng, họ đã chuyển sang việc khác, cơ sở mã đã thay đổi, và việc rebase (cập nhật mã) trở thành một dự án riêng biệt.
Một nghiên cứu năm 2021 đã xem xét hơn 265.000 PR trên các dự án GitHub phổ biến và phát hiện ra rằng lý do phổ biến nhất khiến người đóng góp bỏ cuộc PR của họ là các trở ngại họ gặp phải và các rào cản do người duy trì đặt ra trong quá trình xem xét, không phải là sự từ chối thẳng thừng.
Những giải pháp có thể giúp ích
Nguyên lý về các ràng buộc (Theory of Constraints) nói rằng: hãy tìm nút thắt cổ chai, ép giá trị tối đa từ nó, và làm cho mọi thứ khác phục vụ nó. Tôi nghĩ tất cả những điều sau đây có thể thực hiện được mà không cần người duy trì mới; chúng là về việc bảo vệ thời gian bạn đã có.
Giới hạn kích thước PR
Một giới hạn cứng về số dòng thay đổi cho các PR tính năng, có thể là 300 dòng, hoặc ít nhất là các nhãn kích thước tự động để thúc đẩy người đóng góp hướng tới các PR nhỏ hơn, mỗi PR có thể hợp nhất độc lập. Các PR nhỏ được duyệt nhanh hơn, rủi ro thấp hơn và mang lại phản hồi nhanh hơn. Nỗi sợ lớn nhất của dự án là làm hỏng khả năng phát lại mà không có kiểm thử, vì vậy những thay đổi nhỏ nên dễ được chấp nhận hơn. Một con bot tự động gắn cờ cho các PR quá lớn sẽ biến lời khuyên này thành một ràng buộc thực tế. Kubernetes làm điều này với các nhãn kích thước tự động để người duyệt có thể chọn những gì phù hợp với thời gian của họ.
Kiểm soát chất lượng trước khi đến nút thắt cổ chai
Mỗi phút một người duy trì dành cho một PR có CI bị lỗi, mô tả bị thiếu hoặc xung đột khi hợp nhất là lãng phí. Và không giống như thời gian mất ở bất kỳ điểm nào khác trong quy trình, thời gian mất ở nút thắt cổ chai là thời gian mất của toàn bộ hệ thống. Jellyfin đã có mẫu PR với danh sách kiểm tra và CI chạy kiểm tra mã. Bước tiếp theo: tự động gắn nhãn PR là "chưa sẵn sàng" khi CI báo đỏ, có thể tự động đóng các PR có xung đột sau một khoảng thời gian nhất định. Homebrew dựa nhiều vào tự động hóa để duy trì khối lượng công việc bền vững cho người duy trì.
Giới hạn công việc đang tiến hành (WIP)
Một người thực sự có thể duyệt bao nhiêu PR cùng một lúc? Có thể là 5, có thể là 10, nhưng chắc chắn không phải là 200. Một giới hạn cứng đối với số PR đang được duyệt tích cực, nơi bạn phải hoàn thành hoặc từ chối trước khi bắt đầu cái mới, sẽ giúp ích rất nhiều. Việc chuyển đổi giữa các PR được duyệt một nửa có nghĩa là phải đọc lại bối cảnh và kiểm tra lại những gì đã thay đổi. Với 200 PR đang mở, xu hướng là lướt qua nhiều cái, nhưng làm việc sâu trên một vài cái với sự ép buộc hoàn thành sẽ hiệu quả hơn và không tốn chi phí để thử.
Ưu tiên giá trị hơn kích thước
Không phải tất cả PR đều có giá trị như nhau. Một bản sửa lỗi bảo mật có chi phí chờ đợi rất lớn. Một thay đổi thẩm mỹ có chi phí gần bằng không. Nhưng cũng có một chi phí ít rõ ràng hơn. Nếu con đường ít kháng cự nhất là hợp nhất các tái cấu trúc và sửa lỗi vì chúng nhỏ hơn và an toàn hơn, thì các tính năng sẽ không bao giờ được đưa vào. Ứng dụng sẽ bị đình trệ, người đóng góp ngừng xuất hiện vì họ thấy một dự án chỉ thực hiện bảo trì, và đó là một loại vòng xoáy tử thần khác. Hệ thống nên giúp các tính năng dễ dàng được đưa vào, không chỉ là khả thi về lý thuyết. Điều đó có nghĩa là các tính năng cần một con đường rõ ràng: đề xuất, giới hạn kích thước, ưu tiên duyệt.
Thiết lập nhịp độ (Cadence)
Một khung thời gian cố định hàng tuần để phân loại (triage) PR, nơi bạn đi qua các PR mới, hoàn thành việc duyệt các PR đang tiến hành và đóng các PR cũ với một thông báo lịch sự. Điểm quan trọng không phải là lịch trình本身, mà là mọi thứ trở nên có thể dự đoán được. Hiện tại không có cách nào để biết khi nào phản hồi sẽ xuất hiện, có thể là 3 ngày hoặc 300 ngày. Nếu không có tính dự đoán đó, người đóng góp bắt đầu nghĩ "để tôi thêm cái này nữa trong lúc chờ đợi", và đó là cách PR phát triển từ 50 dòng lên 500 dòng. Một nhịp độ hàng tuần biến "phản hồi đầu tiên trong vòng 7 ngày" thành điều mà người đóng góp có thể thực sự dựa vào.
Xây dựng tầng người duyệt
Một vài người đóng góp thường xuyên có trạng thái người duyệt, nơi họ có thể chấp thuận nhưng không hợp nhất, và người duy trì chính sẽ kiểm tra nhanh các PR đã được chấp thuận (nhanh hơn nhiều so với việc duyệt toàn bộ từ đầu). Danh sách kiểm tra PR đã yêu cầu người đóng góp duyệt PR của người khác trước khi PR của họ được chú ý, đó là một bản năng đúng đắn, và việc biến nó thành cấu trúc với các vai trò người duyệt thực sự sẽ nhân dung lượng duyệt.
Yêu cầu đề xuất cho tính năng
Tài liệu nói rằng các tính năng nên bắt đầu bằng một đề xuất nhưng nó không được thực thi. Việc biến nó thành một cổng cứng sẽ có nghĩa là các PR tính năng không có đề xuất được chấp thuận sẽ không vào hàng đợi duyệt. Đây là điều tôi cảm thấy cá nhân nhất. Tôi đã hỏi nhiều lần xem cần gì để tính năng của tôi được đưa vào, cách tiếp cận đúng là gì, thứ tự làm việc là thế nào, và tôi chưa bao giờ nhận được câu trả lời. Một quy trình đề xuất đã buộc cuộc trò chuyện đó xảy ra trước khi tôi viết bất kỳ mã nào, chúng ta có thể đã đồng ý về phạm vi và tôi sẽ biết dự án thực sự cần gì thay vì phải phỏng đoán. Quy trình RFC của Rust có lẽ là ví dụ rõ ràng nhất. Nó được tạo ra vào năm 2014 vì các tính năng liên tục được đưa vào mà không có sự đồng thuận trước, và nó cho phép dự án mở rộng quy mô đóng góp mà không cần mở rộng nhóm cốt lõi cùng tốc độ.
Điểm thực sự
Cuộc trò chuyện mà tôi đã có bị mắc kẹt ở chỗ "chỉ có một người duy trì, nên PR mất mãi mãi" và tôi không chấp nhận điều đó. Nút thắt cổ chai không phải là chỉ có một người, mà là 200 PR chất lượng và kích thước khác nhau đều đi qua một điểm mà không có bất kỳ kiểm soát dòng chảy nào. Thời gian của người duy trì (thực sự là rất lớn) bị tiêu tốn vào các PR không có mô tả, các PR tính năng 1000 dòng, các PR có xung đột đã nằm đó nhiều tháng — tất cả các nhu cầu không tạo ra sản lượng nào. Bảo vệ thời gian đó là một điều hoàn toàn khác so với việc yêu cầu thêm thời gian.
Tôi sử dụng Jellyfin mỗi ngày và tôi muốn đóng góp cho nó. Có lẽ một số ý tưởng này đáng để thử, hoặc có lẽ ai đó có thể chỉ cho tôi biết cách hợp nhất dòng thời gian phụ đề và tôi sẽ ngừng nói về lý thuyết hàng đợi mãi mãi. Thành thật mà nói, tôi sẽ chấp nhận bất kỳ điều nào.



