OpenAI

2 tháng 11, 2026

Kỹ thuật

Mở rộng quy mô Git tại OpenAI

Xây dựng hệ thống quản lý mã nguồn đáp ứng quy mô tác nhân, đồng thời đầu tư vào hệ sinh thái Git.

Tác giả: Aaron Friel, Taylor Blau và Ted Nyman, thành viên đội ngũ kỹ thuật

Đang tải…

Trong năm qua, quy mô và nhu cầu sử dụng kho mã hợp nhất của chúng tôi đã tăng với tốc độ đáng kinh ngạc. Rõ ràng, chúng tôi cần cải tổ hệ thống quản lý mã nguồn để đáp ứng quy mô công việc đồng thời mà Codex cho phép thực hiện.

Chúng tôi đã xây dựng Pando, một hệ thống backend mới cho kho lưu trữ Git, mà không làm gián đoạn công việc kỹ thuật hay thay đổi cách đội ngũ sử dụng Git. Pando hiện là nền tảng cho toàn bộ hoạt động phát triển trong kho mã hợp nhất của OpenAI. Pando được thiết kế để đáp ứng hai mục tiêu đầy tham vọng. Thứ nhất là khả năng ghi mở rộng theo chiều ngang, tối ưu cho việc đẩy đồng thời lên nhiều nhánh. Thứ hai là tối đa hóa thông lượng đọc, để máy khách có thể tải dữ liệu đủ nhanh nhằm tận dụng hết băng thông mạng sẵn có.

Chúng tôi đạt các mục tiêu này mà vẫn duy trì khả năng tương thích với giao thức và phần mềm máy khách Git, giúp cả kỹ sư lẫn tác nhân đưa Pando vào quy trình hiện có một cách liền mạch. Trong môi trường vận hành thực tế, Pando giảm độ trễ đẩy trung vị xuống còn gần một phần năm. Hiện hệ thống phân phối gần một petabyte dữ liệu Git mỗi ngày.

Trong bài viết này, chúng tôi sẽ trình bày động lực xây dựng hệ thống quản lý mã nguồn mới và lý do chọn duy trì khả năng tương thích với Git. Chúng tôi sẽ so sánh hệ thống của mình với các phương pháp mở rộng Git phổ biến, đồng thời giải thích vì sao Pando có khả năng đặc biệt phù hợp với nhu cầu mở rộng của OpenAI. Chúng tôi cũng sẽ bàn về các bước tiếp theo và cách tiếp tục đóng góp cho Git để hỗ trợ công việc ở quy mô tác nhân.

Vì sao chúng tôi cần hệ thống quản lý mã nguồn mới

Codex đã thay đổi quy mô và tốc độ phát triển tại OpenAI, giúp các kỹ sư thực hiện song song nhiều công việc hơn hẳn. Điều đó đồng nghĩa với nhiều nhánh, nhiều lần đẩy và hợp nhất mã hơn, cùng nhiều lần lấy và sao chép dữ liệu hơn từ môi trường phát triển, tác vụ nghiên cứu và CI.

Những nhu cầu này cộng dồn và khuếch đại lẫn nhau. Giờ đây, một kỹ sư có thể đồng thời chuẩn bị nhiều thay đổi sẵn sàng cho CI, và mỗi lần đẩy có thể kích hoạt nhiều tác vụ cùng lấy một commit.

Từ khoảng cuối năm ngoái, các hệ thống hỗ trợ kho mã hợp nhất của chúng tôi bắt đầu chật vật đáp ứng nhu cầu. Việc lấy và đẩy dữ liệu bị chậm, các lần hợp nhất mã bị lỗi, và có lúc độ tin cậy giảm xuống dưới 90%. Chúng tôi đã chạm đến giới hạn của chính Git, hay của cách chúng tôi vận hành kho mã hợp nhất? Git ra đời từ một cộng đồng trao đổi các bản vá nhân hệ điều hành qua email. Trong hơn hai thập kỷ, các nhà phát triển đã điều chỉnh công cụ xoay quanh Git khi cách xây dựng phần mềm thay đổi. Khi bắt đầu gặp trở ngại trong kho mã hợp nhất của mình, chúng tôi coi việc tiếp tục kế thừa những thành quả đó là điều quan trọng.

Chúng tôi nhận thấy có thể giữ quy trình quen thuộc ấy, đồng thời xây dựng lại backend để xử lý song song các lần đẩy lên nhánh khác nhau và phục vụ cùng một lịch sử cho nhiều bên đọc cùng lúc. Chọn Git làm nền tảng cho quy trình quản lý mã nguồn cũng có nghĩa là những cải tiến cho phần mềm máy khách có thể hữu ích cả bên ngoài OpenAI. Chúng tôi muốn đóng góp những cải tiến từ bản Git của mình⁠(mở trong cửa sổ mới) vào dự án gốc, chia sẻ chúng với cộng đồng rộng lớn hơn.

Thiết kế Git cho quy mô lớn

Mở rộng hệ thống quản lý mã nguồn có hai khía cạnh cốt yếu: nhiều bên ghi đẩy lên các nhánh khác nhau và nhiều bên đọc lấy cùng một lịch sử. Pando tối ưu cho cả hai.

Thứ nhất, Pando điều phối cập nhật cho từng nhánh một cách độc lập. Các cập nhật trên những nhánh không liên quan có thể tiến hành song song, và bên đọc khi lấy một nhánh sẽ nhận được lịch sử cần thiết cho phiên bản đó. Điều phải đánh đổi là bên đọc xem nhiều nhánh có thể thấy trạng thái của chúng ở các thời điểm khác nhau.

Thứ hai, Pando tái sử dụng tệp gói (định dạng của Git để nén nhiều đối tượng cùng nhau) giữa các lần lấy dữ liệu, thay vì tạo tệp gói riêng cho từng máy khách. Máy khách có thể tải lại một số đối tượng đã có, nhưng máy chủ có thể gửi cùng dữ liệu cho nhiều máy khách mà chỉ cần rất ít tính toán.

Những lựa chọn này cho phép chúng tôi mở rộng khả năng ghi trên nhiều nhánh và phục vụ yêu cầu đọc từ bộ nhớ đệm. Để hiểu cách chúng phối hợp với nhau, hãy bắt đầu từ cách Git lưu trữ và truyền lịch sử.

Kiến thức cơ bản về lưu trữ đối tượng trong Git

Git phải giữ đồng bộ hai thành phần của kho lưu trữ: đối tượng và tham chiếu.

Kho lưu trữ Git ghi lại ảnh chụp trạng thái của các tệp và lịch sử liên kết chúng dưới dạng đồ thị đối tượng. Đối tượng có thể là blob, cây, commit hoặc thẻ. Blob chứa nội dung của một tệp riêng lẻ, còn cây liên kết tên với blob hoặc cây khác để tạo thành cấu trúc thư mục. Commit trỏ đến một cây gốc và không có, có một hoặc nhiều commit cha, tạo thành lịch sử của kho lưu trữ.

Đối tượng được định danh bằng giá trị băm của kiểu và nội dung đối tượng. Đối tượng cũng bất biến: ghi lại một tệp đã thay đổi sẽ tạo blob mới, còn tệp và thư mục không thay đổi có thể tái sử dụng đối tượng hiện có.

Nhánh và thẻ là các tham chiếu, hay “ref”, trỏ đến commit. Thực hiện commit trên một nhánh sẽ tạo commit mới và cập nhật tham chiếu của nhánh để trỏ đến commit đó.

Nhánh refs/heads/main trỏ đến commit C2, có commit cha là C1. C1 trỏ đến cây gốc T1, trong đó parser.py tham chiếu đến blob B1. C2 trỏ đến cây gốc T2, trong đó parser.py tham chiếu đến blob B2. Trong cả hai cây gốc, docs/ đều tham chiếu đến cùng một cây T3. Mục README.md trong cây này trỏ đến blob B3, còn usage.md trỏ đến blob B4.

Các đối tượng Git tham chiếu lẫn nhau bằng ID: commit trỏ đến cây, còn cây trỏ đến cây khác và blob. Nhánh refs/heads/main trỏ đến commit C2, có commit cha là C1. C1 trỏ đến cây gốc T1, trong đó parser.py tham chiếu đến blob B1. C2 trỏ đến cây gốc T2, trong đó parser.py tham chiếu đến blob B2. Trong cả hai cây gốc, docs/ đều tham chiếu đến cùng một cây T3. Mục README.md trong cây này trỏ đến blob B3, còn usage.md trỏ đến blob B4.

Git lưu đối tượng ở trạng thái “rời” hoặc “đóng gói”. Đối tượng rời được lưu trong các tệp riêng, định danh bằng ID đối tượng. Đối tượng đóng gói được lưu chung trong một tệp gói, cho phép lưu các đối tượng tương tự dưới dạng phần chênh lệch so với nhau.

Hai phiên bản của parser.py xuất hiện trong $GIT_DIR/objects. Ở dạng đối tượng rời, blob A chứa parse(), còn blob B chứa cả parse() và parse_file(). Tệp gói lưu toàn bộ A và biểu diễn B bằng chỉ dẫn sao chép toàn bộ A rồi nối thêm parse_file(). Tệp *.idx đi kèm xác định vị trí của cả hai blob trong tệp gói theo ID đối tượng.

Git có thể lưu blob thành các tệp riêng hoặc đóng gói chúng cùng nhau, mã hóa một blob dưới dạng phần chênh lệch so với blob khác.

Trong Git, một đối tượng được coi là có thể truy tới từ một commit nếu Git có thể tìm thấy nó bằng cách lần theo các liên kết từ commit đó qua các commit cha, cây và blob. Trong một lần lấy dữ liệu, máy khách cho máy chủ biết những commit mình cần và những commit đã có. Máy chủ tìm các đối tượng có thể truy tới từ những commit được yêu cầu, loại bỏ các đối tượng mà nó biết máy khách đã có, rồi gửi phần còn lại dưới dạng tệp gói có cùng định dạng Git dùng trên đĩa. Một lần sao chép đầy đủ bắt đầu khi chưa có đối tượng nào, nên cần mọi đối tượng có thể truy tới từ lịch sử đang được sao chép.

Thao tác đẩy truyền đối tượng theo chiều ngược lại bằng cách yêu cầu máy chủ cập nhật một hoặc nhiều tham chiếu. Máy khách dùng tập hợp tham chiếu do máy chủ công bố để tránh gửi các đối tượng máy chủ đã có. Vì vậy, gói được gửi có thể phụ thuộc vào đối tượng hiện có: chỉ nhận gói thôi chưa đủ để tái dựng lịch sử mới.

Nhân bản kho lưu trữ Git có nghĩa là giữ cho cả đối tượng lẫn tham chiếu đều sẵn có trên nhiều máy chủ. Trước khi một bản sao có thể phục vụ giá trị mới của tham chiếu, nó phải cung cấp được các đối tượng có thể truy tới từ giá trị đó. Dịch vụ cũng cần điều phối các cập nhật tham chiếu cạnh tranh nhau, tức những lần đẩy cố chuyển cùng một nhánh từ cùng đầu nhánh cũ đến các commit mới khác nhau. Những hệ thống áp đặt một thứ tự duy nhất cho mọi thao tác ghi vào kho lưu trữ có thể khiến các lần đẩy lên nhánh không liên quan phải chờ nhau.

Các mô hình dịch vụ Git phân tán hiện có

Khi thiết kế Pando, chúng tôi đánh giá các phương pháp thông thường để mở rộng hệ thống quản lý mã nguồn bằng Git: một phương pháp điều phối cập nhật tham chiếu giữa các bản sao kho lưu trữ bằng cam kết ba pha (3PC, giao thức điều phối cam kết giao dịch trên nhiều nút); phương pháp còn lại sắp thứ tự cập nhật qua nhật ký ghi trước dùng chung (WAL, nhật ký ghi thay đổi trước khi áp dụng) trong kho blob.

Cam kết ba pha (3PC)

Trong thiết kế cam kết ba pha mà chúng tôi mô phỏng, dịch vụ Git giữ bản sao đầy đủ của kho lưu trữ trên nhiều nút bản sao. Khi bất kỳ máy chủ nào nhận một lần đẩy, tệp gói đầu vào được sao chép và gửi song song đến tất cả bản sao. Để công bố lần đẩy, đa số bản sao phải chấp nhận cập nhật tham chiếu. Máy chủ quản lý quá trình này đóng vai trò nút điều phối, cần nhiều vòng trao đổi với từng bản sao khác.

Để chấp nhận cập nhật tham chiếu, mỗi bản sao khóa tham chiếu cục bộ và kiểm tra xem nó vẫn trỏ đến commit mà máy khách kỳ vọng hay không. Sau đó, nút điều phối gửi thông điệp tiền cam kết đến các bản sao đã chấp nhận. Khi đa số xác nhận thông điệp tiền cam kết, nút điều phối yêu cầu chúng thực hiện cập nhật. Sau đó, bất kỳ bản sao nào đã cập nhật đều có thể công bố đầu nhánh mới từ kho lưu trữ cục bộ của mình.

Duy trì các bản sao đầy đủ cho phép phục vụ yêu cầu đọc ngay tại nút và bảo đảm máy khách thấy trạng thái nhất quán của mọi tham chiếu, bất kể chọn bản sao nào. Hệ quả là các tham chiếu được cập nhật lần lượt, và mỗi lần đẩy cần nhiều lượt trao đổi khứ hồi để điều phối cập nhật tham chiếu giữa các bản sao.

Việc chờ đủ số bản sao đồng thuận khiến hiệu năng đĩa của bản sao và độ trễ liên lạc ảnh hưởng trực tiếp đến quá trình đẩy. Khi lượng ghi tăng, các bản sao phải theo kịp cả gói đầu vào lẫn lưu lượng đọc.

Nhật ký ghi trước (WAL)

Trong thiết kế dựa trên WAL mà chúng tôi mô phỏng, dịch vụ Git dùng một tệp kê khai chung trong kho blob để sắp thứ tự cập nhật trên toàn kho lưu trữ. Khi nhận một lần đẩy, máy chủ tải tệp gói đầu vào lên kho blob và chuẩn bị cập nhật tham chiếu trong bản sao cục bộ của kho lưu trữ. Để công bố lần đẩy, máy chủ thêm một mục vào tệp kê khai chung trong kho blob, ghi lại cập nhật tham chiếu và trỏ đến tệp gói. Sau đó, máy chủ Git cập nhật tệp kê khai bằng thao tác so sánh và hoán đổi. Thao tác này thay thế tệp kê khai một cách nguyên tử, hoặc từ chối thay đổi nếu không thể thực hiện. Nếu một bên ghi khác đã cập nhật tệp trong thời gian đó, máy chủ đọc các mục vừa được thêm, kiểm tra cập nhật tham chiếu của mình còn hợp lệ rồi thử lại bằng tệp gói đã tải lên.

Các bên ghi có thể tải tệp gói lên song song, nhưng ngay cả những lần đẩy lên nhánh không liên quan cũng phải cập nhật cùng một tệp kê khai. Thời gian khứ hồi của một thao tác so sánh và hoán đổi thành công giới hạn tần suất kho lưu trữ có thể công bố tệp kê khai mới. Kho blob có độ trễ thấp giúp bước này diễn ra nhanh. Xử lý theo lô cho phép một tệp kê khai cập nhật nhiều tham chiếu cùng lúc, nhưng phải đánh đổi bằng việc xếp hàng cập nhật và tăng độ trễ.

Kho blob có thể nhận các tệp gói song song, nhưng các cập nhật vào tệp kê khai chung vẫn tranh chấp với nhau. Chỉ tăng năng lực của kho blob không giải quyết được nút thắt đó. Mỗi bản sao vẫn phải đọc và lập chỉ mục cho các gói từ kho blob trước khi có thể phục vụ chúng tại nút. Nếu tốc độ ghi đầu vào vượt tốc độ xử lý này, các bản sao sẽ tụt lại và máy khách phải chờ lâu hơn để lấy những commit vừa được đẩy.

Thiết kế kiến trúc Pando

Đó là lý do chúng tôi xây dựng Pando. Pando là kiến trúc mới để phục vụ kho lưu trữ Git, với mục tiêu mở rộng nhằm đáp ứng nhiều máy khách đẩy dữ liệu đồng thời. Dưới đây là cách hệ thống hoạt động:

Xem lại các ràng buộc

Trước khi phát triển Pando, chúng tôi tin rằng có thể đáp ứng các yêu cầu mà không phải từ bỏ Git. Tuy nhiên, chúng tôi cần chấp nhận đánh đổi một số điều mà hầu hết hệ thống Git mặc định coi là yêu cầu bắt buộc:

  • Tính nhất quán trên toàn kho lưu trữ: Để cung cấp ảnh chụp nhất quán của mọi tham chiếu tại cùng một thời điểm, cần có sự phối hợp trên toàn kho lưu trữ. Trong hệ thống sắp thứ tự chung cho mọi cập nhật tham chiếu, các lần đẩy lên những nhánh không liên quan phải tranh chấp tại một điểm điều phối duy nhất.

  • Tệp gói riêng cho từng máy khách: Nếu hai máy khách có lịch sử cục bộ khác nhau cùng lấy một nhánh, máy chủ có thể phải tạo một gói riêng cho mỗi máy để tránh gửi những đối tượng chúng đã có. Việc tạo các phản hồi này tiêu tốn CPU và I/O đĩa, khiến hệ thống khó mở rộng để phục vụ lượng lớn bên đọc.

Thay vào đó, Pando cung cấp mô hình nhất quán ít nghiêm ngặt hơn (nhất quán nhân quả) để mở rộng khả năng ghi, đồng thời tái sử dụng tệp gói để mở rộng khả năng đọc.

Nhất quán nhân quả (CC)

Khi máy khách đẩy lên một nhánh, Pando lưu tệp gói nhận được dưới dạng blob bất biến trong kho blob. Mỗi khi nhánh thay đổi vị trí trỏ, Pando ghi một phiên bản tham chiếu vào kho siêu dữ liệu, gồm đầu nhánh mới, gói cung cấp đầu nhánh đó và mọi phiên bản tham chiếu trước đó mà gói phụ thuộc vào. Cả kho blob và kho siêu dữ liệu đều có thể mở rộng theo chiều ngang.

Để lấy một commit hoặc tham chiếu từ Pando, cần phát lại một tập hợp phiên bản tham chiếu (tệp gói) cho máy khách, qua đó tái dựng trạng thái của máy khách đã tạo ra phiên bản tham chiếu đang được lấy. Cách này cung cấp mức nhất quán yếu hơn trên toàn kho lưu trữ, nhưng vẫn bảo đảm khả năng tuần tự hóa các cập nhật cho từng tham chiếu. Các máy khách chỉ xung đột khi đồng thời đẩy lên cùng một tham chiếu. Máy khách đọc nhiều nhánh không thể mặc định rằng các lần đọc đó phản ánh cùng một thời điểm trên toàn kho lưu trữ.

Bên trái, ba gói liên tiếp của nhánh chính lần lượt chứa các commit A, B và C; D và E; rồi F. Bên phải, nhánh chính và nhánh chủ đề tách ra tại C. Nhánh chính tiến đến G, còn nhánh chủ đề tiến qua D, E và F. Gói mới nhất của nhánh chủ đề chứa D, E và F. Đường viền nét đứt đánh dấu D và E cũng có trong gói trước đó của nhánh chủ đề. Mũi tên xanh lam cho biết gói mới phụ thuộc vào nhánh chính tại G. Commit cha của F trong Git là E.

Mỗi lần đẩy lên nhánh chính sẽ thêm một gói gia tăng, chứa một hoặc nhiều commit. Mỗi lần đẩy lên nhánh phụ đều gửi lại các commit trước đó của nhánh này, để máy khách có phiên bản nhánh chính đã được công bố có thể lấy nhánh phụ chỉ bằng một gói.

Đây là một dạng nhất quán nhân quả. Để lấy một tham chiếu, bên đọc quan sát lịch sử của bên ghi đã tạo ra tham chiếu đó. Trong quá trình đẩy, Pando kiểm soát phần lịch sử chung mà máy khách và máy chủ thống nhất, bảo đảm máy khách tạo ra các tệp gói dễ dùng chung hơn.

Do đó, Pando không cung cấp trạng thái của mọi tham chiếu tại cùng một thời điểm trong các thao tác Git, cũng không bảo đảm thứ tự chung của các cập nhật tham chiếu trong một lần sao chép hoặc lấy dữ liệu. Cụ thể, giả sử cả hai nhánh foo và bar đều được cập nhật trong quá trình thương lượng cho một lần sao chép đầy đủ. Trong quá trình thương lượng, một máy khách có thể thấy foo ở phiên bản 1 và bar ở phiên bản 2, còn máy khách khác thấy foo ở phiên bản 2 và bar ở phiên bản 1. Trên thực tế, người dùng và tác nhân không phụ thuộc vào thứ tự toàn cục của các cập nhật tham chiếu, mà dựa vào thứ tự cập nhật của từng tham chiếu.

So sánh các phương pháp

Trong mô phỏng ba mô hình nhất quán này, chúng tôi cấu hình 1.000 máy khách, mỗi máy chọn một nhánh riêng rồi liên tục đẩy các commit riêng biệt, đồng thời đo tổng thời gian thực tế và độ trễ từ lúc đẩy đến lúc có thể lấy dữ liệu. Chỉ số thứ hai rất quan trọng đối với các khối lượng công việc, vì nó đo thời gian khứ hồi để người dùng và tác nhân cộng tác hoặc sử dụng những hệ thống khác dựa vào quản lý phiên bản. Mô phỏng bao quát phần cứng từ phổ thông đến cao cấp cho máy ảo và hệ thống lưu trữ blob. Trong mô phỏng này, chúng tôi đã nới lỏng yêu cầu nhất quán trên toàn kho lưu trữ đối với giao thức cam kết ba pha.

Simulating Git Server Implementations

Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.

Three-phase commit

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Object-store WAL

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Pando

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Writer progress through verified fetches

  • Three-phase commit
  • Object-store WAL
  • Pando
05001,00000 s
  • Waiting
  • Pushing
  • All pushes acknowledged
0 sLater

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Experiment settings

Preparing simulation…

Results

Play the comparison to see the recorded results.

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Chạy mô phỏng này để so sánh Pando với hai phương pháp khác. Chọn hạ tầng từ phần cứng phổ thông đến cao cấp, số máy khách và các thiết lập khác để xem kết quả so sánh.

Hệ thống cam kết ba pha được mô phỏng có độ trễ ghi tăng đáng kể khi chịu tải do đĩa đạt ngưỡng bão hòa. Trong khi đó, các hệ thống nhật ký ghi trước có độ trễ đọc tăng đáng kể vì cùng lý do, khi máy chủ chật vật đọc nhật ký cho kịp. Cả trong thử nghiệm thực tế trên kho mã hợp nhất của chúng tôi lẫn trong mô phỏng này, Pando hỗ trợ hàng nghìn máy khách đẩy đồng thời mà không làm giảm hiệu năng khi chịu tải.

Khi xử lý một lần đẩy, Pando xác thực tệp gói đầu vào trong lúc truyền trực tiếp tới kho blob, mà không giải gói vào kho lưu trữ cục bộ. Khi máy khách bắt đầu lấy dữ liệu, Pando chọn những gói đã lưu mà bên đọc cần, rồi bên đọc tải chúng xuống. Không máy chủ nào cần áp dụng tệp gói vào kho lưu trữ cục bộ hay thực hiện các thao tác giải gói và đóng gói tốn kém. Vì chủ yếu thực hiện thương lượng và chọn tệp gói, máy chủ Pando có thể chạy trên phần cứng phổ thông.

Nhờ đó, Pando có thể mở rộng đến giới hạn của máy khách, mạng kết nối giữa máy khách và Pando, cũng như dịch vụ lưu trữ blob. Khi có đủ năng lực phục vụ, mỗi máy khách làm việc trên các nhánh độc lập có thể truyền gói với tốc độ tối đa mà kết nối của mình cho phép. Để phục vụ thêm máy khách ở tốc độ đó, chúng tôi có thể bổ sung máy chủ Pando và đồng thời mở rộng kho siêu dữ liệu cùng kho blob.

Pushing and Fetching a Branch in Pando

Pando stores the writer’s packfile in blob storage and records the ref version in metadata service. On fetch, Pando selects the packs the reader needs, and Express serves them.

Stream the push

  • Packfile bytes
  • Metadata
  • Request / acknowledgment
  • Packfile URIs

Một tác nhân chỉnh sửa tệp, tạo commit cục bộ rồi chạy git push. Thao tác này tạo tệp gói chứa các thay đổi đó để cập nhật một nhánh. Pando nhận gói và truyền trực tiếp tới kho blob. Sau khi xác thực lần đẩy đầu vào bằng một lượt giải mã, Pando dùng giao dịch đa khóa để ghi phiên bản tham chiếu vào kho siêu dữ liệu và cập nhật con trỏ tham chiếu một cách nguyên tử. Nhờ đó, Pando cung cấp tính nhất quán “đọc được dữ liệu mình vừa ghi”. Toàn bộ thao tác ghi này cần O(1)O(1) lần đọc và ghi siêu dữ liệu.

Khi máy khách khác lấy nhánh đó, Pando chọn một tập hợp gói đã lưu dựa trên lịch sử máy khách đã có. Thao tác này cần O(log⁡Δ)O(\log \Delta) lần đọc siêu dữ liệu, trong đó Δ\Delta là khoảng cách ngắn nhất tính theo số phiên bản tham chiếu giữa dữ liệu máy khách đã có và dữ liệu muốn lấy. Máy chủ trả về các URI tệp gói, rồi máy khách tải xuống qua Express, dịch vụ lưu tệp gói trong bộ nhớ đệm theo khu vực.

Thiết kế này dựa trên ba dịch vụ cốt lõi trong kiến trúc Pando: đẩy, lấy dữ liệu và gộp nén.

Mở rộng khả năng đẩy dữ liệu

Khi máy khách đẩy dữ liệu, máy chủ Git thông thường giống như người chơi poker lật hết bài, cho máy khách biết mọi nhánh mình có. Sau đó, máy khách tập hợp các commit, cây và blob để tạo tệp gói loại trừ những đối tượng mà nó biết máy chủ đã có.

Công bố nhiều tham chiếu hơn có thể giảm dung lượng mỗi lần đẩy. Nhưng điều đó cũng có thể khiến gói khó tái sử dụng hơn, vì máy khách khác có thể thiếu những đối tượng mà bên ghi đã loại ra.

Pando chọn cách tiếp cận khác. Thay vì lật hết bài, Pando chỉ công bố một tập hợp nhánh chọn lọc cho máy khách. Có hai lý do cho lựa chọn này:

  1. Cung cấp trạng thái nhất quán của mọi tham chiếu rất tốn kém. Với hàng triệu nhánh, riêng dữ liệu công bố tham chiếu có thể vượt 100 MiB. Để cung cấp dữ liệu này, mỗi máy chủ phải duy trì ánh xạ luôn cập nhật, kéo theo chi phí điều phối đáng kể, hoặc thực hiện truy vấn cơ sở dữ liệu có chi phí tăng theo số tham chiếu.

  2. Khi chỉ nhận một tập con các tham chiếu, máy khách sẽ tạo gói chỉ phụ thuộc vào những gì máy chủ cho phép rõ ràng là có thể loại ra. Theo dõi các phụ thuộc giúp Pando tái sử dụng tệp gói do máy khách tạo trong một lần đẩy để phản hồi một lần lấy dữ liệu sau đó.

Máy chủ Git càng công bố nhiều tham chiếu, máy khách đang đẩy càng có nhiều căn cứ để loại trừ đối tượng, nhờ đó tạo được gói nhỏ hơn. Tuy nhiên, để tái sử dụng các tệp gói này, máy chủ phải chứng minh được rằng máy khách lấy dữ liệu có đủ mọi phụ thuộc. Nếu không, tệp gói có thể thiếu đối tượng mà máy khách cần.

Ngược lại, nếu máy chủ Git không công bố tham chiếu nào, máy khách sẽ tạo tệp gói chứa toàn bộ lịch sử kho lưu trữ. Bất kỳ máy khách nào khác cũng dùng được tệp gói đó, nhưng cái giá rất lớn là lấy bất kỳ nhánh nào cũng gần như phải sao chép đầy đủ.

Đường cong cho thấy các tham chiếu được công bố ảnh hưởng thế nào đến kích thước tệp gói: từ toàn bộ lịch sử khi không công bố tham chiếu nào, qua mức chỉ công bố nhánh chính, đến chỉ các đối tượng mới khi công bố tất cả tham chiếu.

Công bố nhiều tham chiếu hơn giúp tạo tệp gói nhỏ hơn. Chỉ công bố một tham chiếu cũng có thể mang lại phần lớn mức giảm kích thước.

Theo mặc định, khi máy khách đẩy một nhánh tính năng mới, Pando chỉ công bố một nhánh chính được chỉ định làm cơ sở. Cách này giữ nguyên kích thước dữ liệu công bố dù kho lưu trữ có thêm nhánh, nhưng cũng đi kèm đánh đổi. Khi kỹ sư đẩy nhiều lần lên cùng một nhánh hoặc tạo nhánh tính năng dựa trên một nhánh tính năng khác, máy khách phải gửi lại mọi thứ nằm ngoài nhánh chính.

Vì vậy, các lần đẩy liên tiếp lên nhánh có dung lượng lớn hơn, có thể chứa đối tượng dư thừa và tiêu tốn thêm băng thông của máy khách. Trong kho mã hợp nhất của chúng tôi, tệp gói tạo theo cách này có kích thước trung vị dưới 20 KiB và phân vị thứ 99 dưới 3 MiB. Với chúng tôi, đây là đánh đổi hợp lý vì bên đọc đã có lịch sử nhánh chính được công bố có thể lấy nhánh chỉ bằng tệp gói đó.

Sau khi chọn tập hợp tham chiếu để công bố cho máy khách, Pando chấp nhận lần đẩy và cập nhật kho siêu dữ liệu. Pando chia việc cập nhật siêu dữ liệu thành phần đồng bộ và phần bất đồng bộ. Phần cập nhật siêu dữ liệu đồng bộ bảo đảm máy khách có thể lấy ngay commit vừa được cập nhật. Phần này ghi phiên bản tham chiếu mới và cập nhật ánh xạ con trỏ tham chiếu đến phiên bản đó, lưu đầu nhánh mới, các phụ thuộc dùng để thương lượng tệp gói và liên kết đến tệp gói vừa được đẩy.

Đồng thời, công việc bất đồng bộ, chẳng hạn lên lịch gộp nén, cũng được ghi nhận trong cùng giao dịch. Với những lần đẩy rất lớn, chỉ một phần các commit được ghi đồng bộ. Nhờ đó, số lần đọc và ghi vào kho siêu dữ liệu luôn là hằng số, bất kể kích thước lần đẩy.

Quan trọng là các máy chủ Pando có thể tiếp nhận độc lập những lần đẩy lên các tham chiếu không liên quan. Khóa duy nhất có tranh chấp là con trỏ tham chiếu, được chúng tôi bảo vệ bằng thao tác nguyên tử tương tự so sánh và hoán đổi. Chúng tôi có thể tăng thông lượng ghi bằng cách bổ sung máy chủ Pando, đơn vị mở rộng cho kho siêu dữ liệu hoặc kho blob.

Lấy và sao chép dữ liệu ở tốc độ đường truyền

Khi lấy dữ liệu, máy khách cung cấp cho máy chủ hai tập hợp ID commit, được gọi trong giao thức là have và want. Với máy chủ Git thông thường, máy chủ tính hiệu tập hợp giữa các đối tượng có thể truy tới từ commit cần lấy và từ commit đã có, nén phần chênh lệch rồi gửi kết quả cho máy khách. Dù máy chủ Git mã nguồn mở có nhiều loại chỉ mục cục bộ để tăng tốc thao tác này, việc tạo các tệp gói vẫn có thể tốn kém và thường khó tận dụng bộ nhớ đệm.

Thay vào đó, Pando dùng ánh xạ commit được ghi lại trong các lần đẩy để tra cứu các have và want của máy khách, quy chúng về tập hợp phiên bản tham chiếu chứa các commit đó. Mỗi phiên bản tham chiếu này liên kết tới một tệp gói mà Pando đã nhận trước đó. Máy chủ dùng thông tin này để xác định nhu cầu của máy khách và chọn các tệp gói bao phủ tập hợp đối tượng mà máy khách cần.

Planning fetches in Pando

Pando compares what a client has with what it wants, then selects the stored packfiles needed—including any missing branch dependencies.

Already heldFetchMissing branch dependency

Fetch 27 trunk packs.

Select a branch to include its trunk dependencies.

Pando ánh xạ các commit đã có và cần lấy trong giao thức Git thành một chuỗi phiên bản tham chiếu. Yêu cầu lấy một nhánh phụ đồng nghĩa với việc cần cả lịch sử trên nhánh chính đến các điểm mà nhánh phụ phụ thuộc vào.

Giao thức Git chỉ cho phép gửi một tệp gói trong phản hồi HTTP của một lần sao chép hoặc lấy dữ liệu. Tuy nhiên, một tính năng ít được chú ý của giao thức Git là packfile-uri cho phép máy chủ trả về liên kết để tải riêng các gói bổ sung. Pando chỉ dùng cơ chế này và gửi kèm một tệp gói rỗng ngay trong phản hồi. Để hỗ trợ tính năng này, OpenAI đã đóng góp một số cải tiến cho packfile-uri vào dự án gốc.

Các hệ thống OpenAI hoạt động ở nhiều khu vực địa lý, nên chúng tôi duy trì cụm Pando cùng bộ nhớ đệm cục bộ tại mỗi khu vực. Dịch vụ phân phối gói Express lưu đệm dữ liệu từ kho blob, còn một dịch vụ riêng lưu đệm dữ liệu từ kho siêu dữ liệu. Những bộ nhớ đệm này giúp Pando phục vụ yêu cầu theo ID commit hoặc một kiểu đọc tham chiếu đặc biệt hoàn toàn từ trạng thái đã lưu đệm trong cụm đó. Chúng tôi dùng băm nhất quán để định tuyến yêu cầu tệp gói đến máy chủ Express, với tỷ lệ truy xuất trúng bộ nhớ đệm trên 99%.

Hệ thống bộ nhớ đệm này cung cấp thông lượng cực cao, đủ để tận dụng hết băng thông mạng giữa máy khách và Pando. Với các URI tệp gói và bộ nhớ đệm cục bộ này, chúng tôi đã xác nhận rằng chỉ cần máy ảo tiêu chuẩn của nhà cung cấp đám mây, các thao tác sao chép và lấy dữ liệu đã có thể tận dụng hết đường truyền 10 Gbps.

Gộp nén và thời gian đến byte đầu tiên

Với hầu hết máy chủ Git, đóng gói lại và thu gom rác là cần thiết để duy trì hiệu năng khi kho lưu trữ phát triển. Cả hai bước đều thu hồi dung lượng, nhưng tiêu tốn nhiều tài nguyên tính toán và I/O.

Với kho lưu trữ thay đổi nhanh như kho mã hợp nhất của OpenAI, việc gửi một URI tệp gói riêng cho từng lần đẩy kể từ lần lấy dữ liệu gần nhất là không khả thi. Mỗi ngày, máy khách sẽ phải tải hàng chục nghìn tệp gói để cập nhật nhánh chính cho kịp.

Thay vào đó, Pando gộp các gói ở chế độ nền để giảm chi phí này, áp dụng thiết kế tương tự cơ chế gộp nén trong cây hợp nhất có cấu trúc nhật ký. Mỗi lần đẩy tạo ra một tệp gói mà Pando gọi là “L0”, đơn vị dữ liệu nhỏ nhất Pando có thể truyền. Theo thời gian, các tiến trình nền gom những tệp gói này thành các nhóm bao phủ một chuỗi phiên bản tham chiếu, với quy mô tăng theo cấp số nhân từ L0. Hiện kho mã hợp nhất của chúng tôi sử dụng các nhóm bao phủ tệp gói đến cấp L4.

Các nhóm bao phủ này phục vụ hai mục đích. Khi một nhóm bao phủ đủ đầy, tiến trình gộp nén sẽ nhận xử lý và tạo ra một tệp gói duy nhất. Việc sắp xếp lại đối tượng bên trong tệp gói giúp cải thiện tỷ lệ nén. Đồng thời, các nhóm này còn cải thiện hiệu năng thương lượng khi lấy dữ liệu bằng cách đóng vai trò danh sách nhảy trên các phiên bản tham chiếu. Bằng cách quét đồng thời các chỉ mục ở từng cấp, Pando có thể chọn tập hợp gói gửi cho máy khách trong thời gian logarit. Dù đã đóng gói lại hay chưa, các nhóm bao phủ tệp gói này giúp lập kế hoạch lấy dữ liệu hiệu quả hơn, giảm cả yêu cầu về thông lượng lẫn độ trễ đối với Pando.

Compaction in Pando

Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.

Compaction in progressPublished cover

Gộp nén kết hợp tệp gói từ nhiều phiên bản tham chiếu ở chế độ nền, giúp lấy dữ liệu hiệu quả hơn với ít tệp hơn.

Khác với nhiều máy chủ Git, tệp gói được giữ lại ở mọi cấp, không bị loại bỏ hay thu gom rác. Các gói ở mọi cấp được liên tục dùng để phục vụ máy khách, dựa trên các have và want mà máy khách gửi trong yêu cầu lấy dữ liệu. Với máy khách dùng Pando dưới dạng API, việc đọc một đối tượng bất kỳ từ tệp gói thường cần đọc ít dữ liệu hơn từ L0 so với L4, vì đối tượng ít có khả năng đã được nén hơn.

Gộp nén trong Pando vừa giảm đáng kể tổng lượng dữ liệu truyền cho máy khách, vừa giúp chi phí lập kế hoạch gói tăng chậm hơn tuyến tính so với mức độ máy khách bị tụt lại. Điều này đặc biệt quan trọng với các kho lưu trữ lớn và thay đổi nhanh, vốn được chúng tôi dự đoán sẽ trở nên phổ biến trong phát triển phần mềm.

Xây dựng Pando

Chúng tôi xây dựng Pando với Codex, từ khởi động dự án đến đưa vào vận hành thực tế, chỉ trong 10 tuần. Phần cốt lõi của Pando do một kỹ sư duy nhất thiết kế, các nhóm tác nhân lập trình và một nhóm nhỏ kỹ sư triển khai. Pando bắt đầu phục vụ lưu lượng thực tế vào tháng 4 năm nay và là một phần trong hệ thống công cụ quản lý mã nguồn lớn hơn được sử dụng tại OpenAI.

Chúng tôi đã sử dụng nhiều phương pháp khác nhau để kiểm chứng thiết kế của Pando. Chúng tôi áp dụng các nguyên tắc của mình về xây dựng nền tảng phần mềm an toàn hơn⁠. Vì đây là dự án xây dựng từ đầu, chúng tôi chọn lập trình Pando bằng Rust, không dùng các thành phần phụ thuộc kế thừa. Chúng tôi viết bộ kiểm thử tuân thủ để các tác nhân có thể đối chiếu thay đổi của mình với hành vi chuẩn của Git. Chúng tôi cũng phát triển các hệ thống điều khiển kiểm thử mô phỏng tất định để kiểm tra bất biến và thuộc tính của hệ thống, qua đó phát hiện và giảm thiểu những lỗi tranh chấp khó thấy cùng lỗi hồi quy trong thương lượng và lập kế hoạch gói.

Nhiều mô hình đã góp phần xây dựng hệ thống này. Khi có mô hình mới, chúng tôi dùng chúng để cải thiện hiệu năng và độ tin cậy của Pando. Dùng GPT‑6 Astra, các tác nhân viết mã Lean và TLA+ để hình thức hóa một “lý thuyết về Pando”, giúp chúng tôi tìm và sửa lỗi trong mã triển khai, đồng thời chứng minh các thuộc tính của hệ thống. Nhiều phiên làm việc liên tục kéo dài hàng trăm giờ để tìm và xử lý các trường hợp biên trong Pando. Chúng tôi cũng cho tác nhân hoạt động suốt ngày đêm để kiểm thử đối kháng Git và Pando bằng các mô hình có năng lực Daybreak.

Triển khai Pando

Ban đầu, chúng tôi triển khai Pando làm proxy chuyển tiếp cả thao tác đọc và ghi đến hệ thống quản lý mã nguồn hiện có. Nhờ đó, Pando và hệ thống cũ luôn đồng bộ, cho phép chuyển lưu lượng sang Pando an toàn mà không gián đoạn dịch vụ.

Trong thiết kế lưu đệm, Pando còn có thể loại bỏ yêu cầu trùng lặp và gộp yêu cầu trước khi chuyển tiếp. Điều này giúp các công cụ hiện có tiếp tục hoạt động ổn định khi chúng tôi lần lượt chuyển từng hệ thống sang Pando. Khi chuyển thêm khối lượng công việc sang Pando, các lần đẩy của chúng bổ sung dữ liệu vào bộ nhớ đệm Pando và cải thiện độ tin cậy của backend cũ.

Để tối ưu hiệu năng khi dùng Pando, chúng tôi bắt đầu phân phối bản Git công khai của mình tới máy của nhà phát triển và tích hợp vào ứng dụng ChatGPT cho máy tính. Chúng tôi dùng một lớp bao nội bộ quanh Git CLI để chuyển các máy khách sang Pando theo từng giai đoạn. Chúng tôi tạo thêm nhiều công cụ xử lý yêu cầu thay đổi, đánh giá mã và các API phục vụ tự động hóa. Cùng với lưu lượng Git, chúng tôi chuyển đổi hàng trăm tích hợp phụ thuộc vào API cũ, webhook và việc tải kho lưu trữ.

Chính Codex cũng giúp các nhà phát triển và các nhóm thực hiện chuyển đổi. Chúng tôi tạo các kỹ năng để chuẩn bị bản làm việc hiện có và hướng dẫn tác nhân sử dụng hệ thống mới. Nhờ đó, nhà phát triển chỉ cần yêu cầu Codex chuyển môi trường phát triển và dịch vụ của mình sang hệ thống mới.

Chuyển sang Pando đã cải thiện độ tin cậy khi hợp nhất mã và giảm gần 80% độ trễ đẩy trung vị.

Pando có thể phân phối một petabyte dữ liệu Git mỗi ngày chỉ với vài lõi CPU. Pando từng phân phối hơn 57 terabyte gói Git chỉ trong một giờ, với hơn 99% số byte đó được phục vụ từ bộ nhớ đệm trên đĩa cục bộ.

Tái sử dụng gói cho phép nhiều máy khách tải cùng lịch sử kho lưu trữ mà các nút phục vụ chỉ cần tính toán rất ít. Ví dụ này chỉ đề cập một kho lưu trữ, nhưng Pando được thiết kế để hỗ trợ nhiều kho hơn nữa. Khi nhu cầu tăng, chúng tôi có thể mở rộng độc lập khả năng phân phối dữ liệu và thương lượng Git.

Đóng góp trở lại cho dự án Git

Xây dựng Pando trên giao thức và phần mềm máy khách Git giúp chúng tôi phát hiện lỗi cùng các cơ hội cải thiện hiệu năng của chính Git.

Chẳng hạn, Pando cung cấp trực tiếp các gói có thể tái sử dụng cho máy khách, nên việc tải và lập chỉ mục các gói đó trở thành yếu tố cốt lõi để hệ thống chạy nhanh và đáng tin cậy. Công việc này làm lộ ra các trường hợp biên khi tải đồng thời, dọn dẹp sau lần truyền bị gián đoạn và tiếp tục những lượt tải thực ra đã hoàn tất. Chúng tôi sửa các luồng xử lý đó, thêm bước xác thực khi chấp nhận gói hiện có cho một lần lấy dữ liệu và đóng góp những thay đổi này vào dự án gốc.

Chúng tôi cũng cần biết thời gian và băng thông được dùng vào đâu, nên đã bổ sung theo vết lượt tải packfile-uri và số byte khi tạo gói. Nhờ vậy, dễ xác định hơn liệu một lần lấy dữ liệu chậm là do tạo gói hay truyền gói.

Trong bản Git của OpenAI⁠(mở trong cửa sổ mới), máy khách có thể tải và lập chỉ mục song song cho các gói độc lập. Chúng tôi bổ sung khái niệm “khai báo tường minh dữ liệu đã có” để máy chủ có thể công bố đầu nhánh hiện tại tách biệt với phần lịch sử mà máy khách đang đẩy có thể loại khỏi gói. Trên Pando, cơ chế này cho phép máy khách dùng --force-with-lease để kiểm tra nhánh tính năng chưa thay đổi và tạo gói chỉ dựa trên nhánh chính. Sau đó, bất kỳ ai có lịch sử nhánh chính đó đều có thể lấy nhánh bằng cùng một gói.

Một số cải tiến này đã có trong dự án Git gốc, còn nhiều cải tiến khác có trong kho lưu trữ công khai của chúng tôi. Chúng tôi tiếp tục hợp tác với cộng đồng Git để những cải tiến này hữu ích cả ngoài Pando. Bạn có thể theo dõi công việc đó trên trang theo dõi bản vá công khai⁠(mở trong cửa sổ mới) của chúng tôi.

Bước tiếp theo

Bằng cách xem lại những quan niệm thông thường về mở rộng kho lưu trữ Git, Pando giúp chúng tôi mở rộng kho mã hợp nhất mà không thay đổi quy trình Git các kỹ sư và tác nhân đang sử dụng. Trong kho mã hợp nhất của chúng tôi, Pando đã tăng khả năng xử lý song song, cải thiện độ tin cậy khi hợp nhất mã và rút ngắn thời gian công bố các thay đổi mã. Ngoài kho mã hợp nhất, chúng tôi cũng đang tìm hiểu các ứng dụng khác của Pando trong OpenAI, bao gồm những khối lượng công việc mà nhiều tác nhân cần cộng tác trên một kho lưu trữ Git dùng chung.

Khi kỹ sư và tác nhân tạo ra nhiều phần mềm hơn, ngày càng nhiều nhóm bên ngoài OpenAI sẽ gặp những giới hạn về quản lý mã nguồn mà chúng tôi từng gặp. Chúng tôi hiện đang tìm cách chia sẻ với người dùng công nghệ nền tảng làm nên Pando.

Pando được xây dựng dựa trên hai thập kỷ kinh nghiệm của những người sử dụng và vận hành kho lưu trữ Git. Chúng tôi sẽ tiếp tục phát triển Pando, đồng thời hợp tác với cộng đồng Git mã nguồn mở để chia sẻ mọi cải tiến có thể mang lại lợi ích rộng rãi hơn cho người dùng Git.

Tác giả

OpenAI