2 điểm bởi GN⁺ 2026-01-15 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 GraphQLdatabase 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đị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 APIdatabase 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ụ: MDEwOlJlcG9zaXRvcnkyMzI1Mjk4010: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

  • Ví dụ mã Python để trích xuất database ID từ node ID mới
    import base64, msgpack
    def node_id_to_database_id(node_id):
        prefix, encoded = node_id.split('_')
        packed = base64.b64decode(encoded)
        array = msgpack.unpackb(packed)
        return array[-1]
    
  • Có thể dùng cách này để trích xuất trực tiếp database ID của bình luận PR và giải quyết vấn đề liên kết URL
  • Hiện tại GitHub đang đồng thời duy trì hệ thống ID mới dựa trên MessagePackhệ thống cũ dựa trên chuỗi
  • Cấu trúc này cho thấy quá trình chuyển đổi nội bộ và nỗ lực duy trì khả năng tương thích của GitHub; các lập trình viên dùng API cần chú ý tới sự khác biệt giữa các định dạng ID

1 bình luận

 
GN⁺ 2026-01-15
Ý 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ới databaseId trong REST API
    Như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 databaseId của GraphQL API
    Tà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 đúng
    Phầ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

    • Các kiểu GraphQL của GitHub có những trường như permalink, url và interface UniformResourceLocatable, nên không cần tự dựng URL
    • Những cấu trúc nội bộ như vậy rất dễ hỏng theo thời gian
      Đó 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
    • Nếu muốn nhúng metadata vào định danh, tốt hơn nên mã hóa để người dùng không phụ thuộc vào cấu trúc nội bộ
      Cách này cũng thường được dùng trong pagination token
  • Những ID như 010:Repository2325298 có cấu trúc rất rõ ràng
    010 là enum loại, Repository là tên, 2325298 là DB ID
    Tức là một dạng length prefix. Repository có 10 ký tự, Tree có 4 ký tự

    • Gợi nhớ đến giao thức BitTorrent
    • Trông gần như một URN
  • 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

    • Nhưng URL issue/PR cũng không bất biến
      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

    • Tư duy “không cấp quyền cho người dùng, mà cấp cho team và người dùng là thành viên của team” hiệu quả hơn nhiều
      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

    • Có hai lý do
      1. để không làm lộ số lượng đối tượng ra bên ngoài
      2. để ngăn việc chỉ tăng ID lên là có thể duyệt toàn bộ đối tượng
        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
    • Nhưng natural key thì có thể thay đổi
      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