- Trong quá trình sử dụng GitHub API, đã phát sinh sự cố khiến tính năng tạo liên kết cho bình luận PR không hoạt động do ID không khớp
- Kết quả điều tra cho thấy GitHub đồng thời sử dụng hai hệ thống ID: node ID của GraphQL và database ID của REST API
- Sau khi giải mã base64 node ID, xác nhận rằng 32 bit thấp chứa database ID, nên có thể chuyển đổi bằng phép toán bitmask đơn giản
- Phân tích thêm cho thấy GitHub đang dùng lẫn định dạng ID mới dựa trên MessagePack và định dạng cũ dựa trên chuỗi
- Cấu trúc này cho thấy tính hai mặt trong hệ thống nhận diện đối tượng nội bộ của GitHub, và lập trình viên cần cẩn trọng khi tích hợp API
Phát hiện hệ thống ID kép của GitHub
- Trong lúc phát triển tính năng của công cụ review mã bằng AI của Greptile, đã phát sinh lỗi khiến liên kết tới bình luận PR trên GitHub không hoạt động
- Đã gắn ID bình luận đã lưu vào URL, nhưng khi bấm vào thì không chuyển tới trang GitHub
- Sau khi kiểm tra tài liệu GitHub, phát hiện node ID của GraphQL API và database ID của REST API tồn tại như hai hệ thống khác nhau
- Ví dụ node ID:
PRRC_kwDOL4aMSs6Tkzl8
- Ví dụ database ID:
2475899260
- node ID là chuỗi mã hóa base64 dùng để nhận diện đối tượng trên toàn GitHub, còn database ID được dùng làm định danh URL dạng số nguyên
Phân tích quan hệ giữa node ID và database ID
- Khi so sánh node ID và database ID của nhiều bình luận PR, xác nhận rằng hai giá trị này cùng tăng theo một khoảng nhất định
- Khi giải mã phần base64 của node ID, thu được một số nguyên 96 bit, và 32 bit thấp của giá trị này trùng với database ID
- Ví dụ:
PRRC_kwDOL4aMSs6Tkzl8 → 32 bit thấp = 2475899260
- Có thể trích xuất database ID bằng phép toán bitmask đơn giản
- Chuyển đổi bằng biểu thức dạng
decoded & ((1 << 32) - 1)
Định dạng ID cũ của GitHub
- Khi giải mã node ID của kho lưu trữ cũ (
torvalds/linux), xuất hiện một chuỗi ở định dạng khác
- Ví dụ:
MDEwOlJlcG9zaXRvcnkyMzI1Mjk4 → 010:Repository2325298
- Định dạng này có cấu trúc
[số loại đối tượng]:[tên đối tượng][Database ID], là định danh tường minh dựa trên chuỗi
- Với đối tượng tree, nó có dạng
04:Tree2325298:7201bfb9..., bao gồm ID của repository và giá trị SHA
- GitHub đang đồng thời sử dụng định dạng cũ và định dạng mới, và định dạng thay đổi tùy theo loại đối tượng cũng như thời điểm được tạo
Cấu trúc của định dạng node ID mới
- Hướng dẫn migration GraphQL của GitHub nêu rõ rằng node ID nên được xem là chuỗi opaque, nhưng thực tế vẫn có cấu trúc nội bộ
- Sau khi giải mã base64 và unpack bằng MessagePack, dữ liệu xuất hiện dưới dạng mảng
- Ví dụ:
[0, 47954445, 2475899260]
- Thành phần của mảng
- Phần tử đầu tiên (0): được suy đoán là mã nhận diện phiên bản
- Phần tử thứ hai (47954445): database ID của repository
- Phần tử thứ ba (2475899260): database ID của đối tượng
- Độ dài mảng thay đổi theo loại đối tượng; commit chứa SHA, còn repository chỉ chứa hai phần tử
Ứng dụng thực tế và kết luận
1 bình luận
Ý kiến trên Hacker News
GitHub global node ID mới nhất có thể bị ép sử dụng qua header
'X-Github-Next-Global-ID'ID được cấu thành từ tiền tố loại của đối tượng và payload msgpack được mã hóa base64
Ví dụ, ID người dùng của tôi
"U_kgDOAAhEkg"được giải mã thành[0, 541842], khớp vớidatabaseIdtrong REST APINhưng không nên phụ thuộc vào kiểu triển khai nội bộ như thế này; tốt hơn là truy vấn trực tiếp trường
databaseIdcủa GraphQL APITài liệu liên quan: Hướng dẫn di chuyển GraphQL global node ID, Thông tin người dùng GitHub của tôi, Ví dụ giải mã bằng CyberChef, Triển khai GitHub ETag
Tôi cho rằng việc giải mã theo cách này là mong manh
Global node ID của GraphQL vốn phải là opaque
Nhiều kiểu dữ liệu của GitHub (như PullRequest) có cung cấp trường
databaseId, nên dùng cái đó mới đúngPhần lớn GraphQL API mã hóa base64 tên kiểu và DB ID, nhưng không có gì đảm bảo quy ước này sẽ luôn được giữ nguyên
Tham khảo: Tài liệu đối tượng PullRequest, Đặc tả GraphQL global ID
permalink,urlvà interfaceUniformResourceLocatable, nên không cần tự dựng URLĐó là lý do API cung cấp permalink. ID hay mẫu liên kết đều có thể thay đổi bất cứ lúc nào
Cách này cũng thường được dùng trong pagination token
Những ID như
010:Repository2325298có cấu trúc rất rõ ràng010là enum loại,Repositorylà tên,2325298là DB IDTức là một dạng length prefix. Repository có 10 ký tự, Tree có 4 ký tự
Opus 4.5 biết mẹo giải mã GitHub ID này và tự động viết mã giải mã
Điều tác giả phát hiện về mặt kỹ thuật là đúng, nhưng không được tài liệu hóa và không được hỗ trợ
GitHub trước đây cũng từng âm thầm thay đổi cấu trúc nội bộ của node ID
Chỉ cần họ thêm trường vào mảng MessagePack, đổi cách mã hóa, mã hóa kín, hoặc chuyển sang UUID
thì mọi hệ thống phụ thuộc vào cấu trúc nội bộ này sẽ hỏng ngay lập tức
Những định danh GitHub mà tôi lưu một cách tường minh chỉ ở mức khóa URL bất biến (số issue/PR hoặc commit hash)
ID comment thì cứ nhét nguyên vào một JSON blob
Không cần cố chuẩn hóa mọi thứ. JSON đủ nhanh rồi
Trừ khi bạn cần truy vấn chéo ở cấp độ comment, còn không thì gần như chẳng bao giờ thành vấn đề hiệu năng
Nếu repository đổi tên hoặc được chuyển sang tổ chức khác thì URL sẽ đổi
API v3 ngày xưa không có ID, nên nếu ai đó đổi username hoặc tên repository thì rất khó lần ra họ là ai
Vì thế tôi đã tự triển khai hệ thống quản lý quyền sở hữu theo team
Terraform provider không ổn lắm, nên lúc offboarding thường xuyên xảy ra chuyện kiểu “người quản trị duy nhất đã nghỉ việc”
Mọi repository đều do team sở hữu, và quyền truy cập cũng chỉ được cấp theo team
Kiểu kiểm soát truy cập dựa trên team này không chỉ hữu ích với GitHub mà còn với các hệ thống khác
Đây là ví dụ điển hình của Định luật Hyrum — khi mọi người bắt đầu phụ thuộc vào hành vi không được tài liệu hóa thì sớm muộn gì cũng sẽ vỡ
Trong thiết kế cơ sở dữ liệu, người ta thường cung cấp ra bên ngoài natural key opaque, còn bên trong thì dùng ID số nguyên tăng dần
Tuy nhiên, nếu dùng ID tổng hợp thì các vấn đề này giảm đi.
Ví dụ, nếu ID repository chứa cả object ID bên trong, thì việc tăng ID cũng chỉ khám phá được các đối tượng trong cùng repository
Nếu trộn thêm entropy hoặc timestamp thì gần như không thể bị lạm dụng
Vì vậy, an toàn hơn là công khai surrogate key vô nghĩa
Ví dụ, YouTube có thể dùng số thứ tự nội bộ, nhưng ra bên ngoài lại cung cấp ID dạng mã không mang ý nghĩa
Giờ thì tôi đã hiểu vì sao đội GitHub trong vài năm gần đây lại mở rộng mạnh hỗ trợ sharded/multi-database cho Rails