Lore: Epic Games mở mã nguồn VCS sinh ra để thay Git ở thế giới binary khổng lồ

Lore: Epic Games mở mã nguồn VCS sinh ra để thay Git ở thế giới binary khổng lồ

Mục lục

Repo game không giống repo web. Trong engine cỡ Unreal, phần lớn dung lượng là asset binary: texture, mesh, world partition nặng hàng trăm MB đến vài GB, đổi liên tục bởi cả nghìn artist lẫn developer. Đẩy vào Git là chuỗi ngày đau khổ: Git LFS là thứ bolt-on, artist export texture 2 GB là cả pipeline kéo full file, dedup binary bị giới hạn bởi pack-delta heuristic, partial clone cộng sparse checkout còn experimental, mất mạng là fail.

Epic ngồi đúng vị trí chịu bài toán này lâu nhất: họ tự viết VCS cho UEFN, rồi mở mã nguồn dưới tên Lore, MIT, gần 4.9k stars, pre-1.0. Bài này mình đọc kỹ system design doc rồi mổ xẻ theo góc nhìn backend engineer: vì sao revision chỉ là blob 320 byte, chunking FastCDC hoạt động ra sao, và mô hình centralized multi-tenant trông thế nào. Pre-1.0 nên format còn đổi được, phần dưới là chân dung kiến trúc, không phải lời hứa API.

Vì sao một hãng game phải viết cả VCS mới

Doc khai motivation thẳng: workload Lore nhắm có ba tính chất mà không hệ thống phổ dụng nào giữ trọn. Content-agnostic: repo trộn source, config, build artifact, binary blobs, không shape nào chiếm ưu thế. Large mọi trục: triệu file, file dải terabyte, triệu revision, hàng trăm branch, nghìn user đồng thời, trăm repo chung một backend. Centrally coordinated: cần một nguồn truth cho access control, durability, conflict resolution, nhưng developer vẫn offline làm việc được.

So với prior art: Git mạnh về object store content-addressed và commit graph; Perforce chuẩn centralized với file-level locking; Sapling giải scale monorepo theo hướng text. Nhưng Git xử file nhiều GB qua LFS như thứ gắn ngoài, partial clone cộng sparse còn experimental, dedup binary bị giới hạn bởi pack delta, multi-tenant bỏ cho infra ngoài. Perforce dùng MD5 làm integrity hash, delta-encoded RCS cho text, full-file cho binary, protocol proprietary. Sapling sinh cho history text-shape, không nhắm repo binary chiếm chủ đạo. Khoảng trống Lore lấp là phần giao: content-addressed integrity, centralized server-of-record, sparse và lazy fetch mọi granularity, dedup mức fragment, multi-tenant trong thiết kế, format lẫn protocol công khai dưới MIT.

Kiến trúc: tách storage khỏi version control

Lore là hai hệ thống xếp chồng. Storage subsystem là store content-addressed, partition-based, kèm access boundary nghiêm ngặt; version control subsystem dựng revision, branch, merge, staging lên primitives của storage. Version control chỉ là một consumer của storage API, không phải lớp đặc quyền: storage subsystem tự nó là public API dùng độc lập, làm blob store đa tenant có dedup cho build cache mà không cần biết revision là gì.

Bản đồ hai tầng

graph TD
    Tool["CLI / IDE / App"] --> API["C library + bindings (Go, Python, JS, C#)"]
    API --> VC["Version control subsystem"]
    VC --> SS["Storage subsystem"]
    subgraph Store["Bên trong storage subsystem"]
        Imm["Immutable store: fragment theo (hash, context)"]
        Mut["Mutable store: branch pointer, name-to-ID"]
    end
    SS --> Imm
    SS --> Mut
    Imm --> Back["Local packfiles / S3 / ReplicatedStore"]
    Mut --> MBack["Local file / DynamoDB"]

Phân đôi hai store là nền tảng của thiết kế. Immutable store chứa mọi byte từng ghi, định danh bằng chính BLAKE3 hash 32 byte: nhanh hơn SHA-256 trên input dài, cấu trúc Merkle bên trong cho hash song song multi-core, collision phải tới 2^128 fragment mới thành chuyện. Mutable store là key-value nhỏ giữ branch latest pointer, name-to-ID, catalog: nơi duy nhất chứa sự biến đổi, mọi contention dồn vào đây, API chỉ bốn op load, store, cas, list.

Địa chỉ fragment là cặp (hash, context): 32 byte cộng 16 byte thành 48 byte. Hash trả lời đây là bytes gì; context trả lời thuộc entity nào, với file là file ID ổn định để theo move, copy và obliteration scope. Partition, ID 16 byte, mới là access boundary: hash là danh tính nội dung, context là danh tính entity, partition là quyền truy cập.

Revision 320 byte và Merkle tree đúc sẵn

Revision là snapshot đóng băng của toàn cây, lưu như fragment 320 byte chỉ chứa hash của mọi thứ: magic number và format version, revision number tăng dọc first-parent chain, hash parent, một parent cho revision thường, hai cho merge, hash của Merkle tree, của metadata, của link list, và repository ID cho merge cross-repository. 320 byte có chủ đích: load một revision là đúng một round trip.

Cây là chuỗi node block: node 96 byte, block chứa 512 node cộng header 128 byte, thành 49.280 byte, vừa khít một fragment. Node mô tả một file hay directory: flags, file mode, index 32-bit trỏ parent/child/sibling, name reference offset và length vào name table riêng của block kèm hash 64-bit tên lowercase, file size, content address. Fixed-size là đòn bẩy: block mmap thẳng từ đĩa vào RAM, dùng không parse không copy, đi giữa node chỉ là index lookup.

Block sharing cho structural dedup miễn phí: hai revision khác vài file share mọi block không đổi, chi phí revision mới tỷ lệ với số block bị sửa. Chuỗi chống giả mạo vì hash revision state gồm hash parent: sửa một byte revision cũ là chain đứt. Branch chỉ là pointer mutable trong mutable store, không có branch object: tạo branch là thêm entry name-to-ID và khởi tạo latest pointer, không sinh revision mới, doc gọi là free branching.

Chunking: FastCDC, fixed-size và cái giá của canonicality

File lớn hơn ngưỡng bị băm thành chunk, mỗi chunk là một fragment độc lập. Lore có hai chiến lược chunking; chọn cái nào là quyết định của application gọi storage API.

Hai chiến lược, hai bộ trade-off

graph TD
    F["File 8 GB (uasset)"] --> C1["Chunk 1 (~64 KiB, FastCDC)"]
    F --> C2["Chunk 2 (~64 KiB)"]
    F --> CN["Chunk N (<= 256 KiB)"]
    C1 --> H1["BLAKE3 hash"]
    C2 --> H2["BLAKE3 hash"]
    CN --> HN["BLAKE3 hash"]
    H1 --> L["Fragment list: (hash, offset), giữ thứ tự"]
    H2 --> L
    HN --> L
    L --> LR["List vượt 256 KiB? Tự chunk thành cây fragment list"]

FastCDC là content-defined chunking: rolling hash quét file, boundary rơi nơi hash khớp magic pattern. Tham số mặc định: trung bình 64 KiB, sàn 32 KiB, trần 256 KiB. Boundary theo content nên chèn đoạn ở đầu file chỉ dịch boundary phía sau đúng một lượng, chunk vùng không đổi giữ nguyên hash: sửa một byte giữa file 8 GB chỉ upload lại vài chunk.

Nhưng boundary phụ thuộc lịch sử chunking: re-chunk từ đầu có thể sinh boundary mới cả ở vùng không đổi, kéo cascade chunk phá dedup, nên implementation dùng temporal-coherence, reuse boundary cũ ở vùng bytes không đổi. Cứu được dedup nhưng giết canonicality: với CDC, address(A) khác address(B) không suy ra content(A) khác content(B), cùng content có thể có nhiều address. Fixed-size thì boundary theo offset thuần túy, canonical tuyệt đối, một content một address; giá là chèn một byte dịch mọi boundary phía sau. Lore để app tự chọn chiến lược; trần 256 KiB cũng là fragment-size threshold của protocol.

Với file multi-terabyte, danh sách fragment reference phình tới hàng trăm MB. Lore dùng recursive fragmentation: list vượt ngưỡng thì tự chunk thành fragment có flag đánh dấu là list, file thành cây fragment list, mỗi tầng address và dedup độc lập. Mỗi reference ghi hash của chunk và byte offset trong content ráp lại; list giữ thứ tự theo offset nên index được, binary search O(log n). Đọc một byte range chỉ fetch fragment overlap: sparse read scale theo range, không theo file size.

Nén tách khỏi addressing: Zstd per-fragment, address là hash payload chưa nén, đổi codec không đổi address. Đây là chỗ Lore khác Git: Git hash object kèm header kiểu blob \0 nên SHA-1 của file không phải hash raw bytes; Lore hash đúng raw payload, ai cầm raw bytes cũng tính được address bằng b3sum.

Sparse là mặc định, binary-first là bản chất

Lore thiết kế cho trường hợp toàn cây không bao giờ materialize trên máy ai. Clone mặc định sparse: khai báo subset qua view file .lore/view, inbound filter; path ngoài view không được materialize khi sync, switch, restore. Chiều ngược, ignore file .loreignore là outbound filter: path khớp rule không stage, không commit. FilterMode: op trên committed state chỉ consult view, op trên working tree consult cả hai, nên ignore mới không truy tố ngược file đã commit.

Sparse chỉ có nghĩa khi fetch lazy: load một revision chỉ walk phần cây view yêu cầu, kéo fragment backing phần đó, còn lại nằm nguyên trên remote và edge cache; local cache là LRU với budget do user đặt. Fetch byte range 4 MiB từ file nhiều GB không kéo cả file.

Binary-first nghĩa là storage và transport không làm gì với content ngoài vận chuyển bytes: không CRLF translation, không encoding inference, không clean/smudge trong data path. Text diff, three-way merge nằm ở version control layer, app tự do thay default. Với content không merge được theo nghĩa nào, world state của engine, scene serialize: file-level locking với ba op acquire, release, query, push sửa file đang lock từ client khác bị server từ chối trước khi visible.

Centralized nhưng offline-capable: push hai phase

Centralized vì access control, durability và conflict resolution cần một điểm quyết định duy nhất, nhưng không đồng nghĩa always-online: staging, commit, branch, switch, diff chạy hết trên local mutable store và fragment cache, đó là chỗ cho phép hệ vừa tập trung vừa offline được.

Push hai phase. Phase 1 upload fragment: client liệt kê fragment mà revision mới và ancestor chưa có trên remote tham chiếu, query cái nào tồn tại rồi upload phần thiếu, song song, out-of-order, resumable. Phase 2 advance latest pointer: khi mọi fragment durable, client gửi conditional put set latest = H_new nhưng chỉ nếu đang bằng H_old. Đó là compare-and-swap, serialization point duy nhất: hai client push cùng branch xếp hàng ở đây.

Push hai phase, một CAS duy nhất

sequenceDiagram
    participant C as Lore client
    participant S as Lore server
    C->>S: Query fragment nào còn thiếu
    S-->>C: Còn N fragment
    loop Upload song song, resumable
        C->>S: Put từng fragment (Zstd, không thứ tự)
    end
    C->>S: MutableCas: latest = H_new nếu đang bằng H_old
    alt CAS thắng
        S-->>C: OK, branch đã nhảy tới H_new
    else CAS thua vì branch đã dịch
        S-->>C: Conflict
        C->>C: Sync + merge local (merge revision, hai parent)
        C->>S: Retry push
    end

CAS thua là có client khác push trước: client sync, merge local rồi push lại, merge revision hai parent. Push opt-in server-side fast-forward merge: server tự tạo merge revision với parent_self là remote latest, parent_other là revision tới, rồi advance pointer, xong một round trip; conflict thật trả về client. Fragments-first làm push atomic: đứt giữa hai phase thì fragment nằm trong immutable store nhưng không pointer trỏ tới, readers vẫn thấy latest cũ.

Multi-tenant: partition là biên giới, hash không phải giấy thông hành

Phần đáng đọc nhất với dân backend là multi-tenancy: nhiều repo không liên quan share một backend, giả định tenant không tin nhau. Partition 16 byte là access boundary, server derive từ session đã xác thực chứ không phải client tự khai. Session gắn partition A không đọc được fragment partition B, kể cả khi hai partition chứa đúng cùng bytes.

Content addressing khiến hai client tạo cùng bytes ra cùng hash; nếu hash tồn tại đồng nghĩa đọc được bytes thì knows-the-hash attack thành công, tenant chỉ cần học hash từ log, build artifact hay dependency dùng chung là đọc trộm được content. Phòng thủ ở protocol: Put luôn đòi bytes, server không đăng ký chỉ từ hash knowledge dù bytes đã nằm sẵn. Copy là shortcut cross-partition duy nhất: session có quyền cả X lẫn Y thì nhờ server copy registration không truyền lại bytes, server kiểm tra cả hai authorization. Dedup nằm dưới access model.

Còn side-channel discipline: existence query chỉ chạy trong partition của session, trả bốn giá trị FoundInContext, Found (bytes có nhưng khác context), NotFound, Unknown. Lookup gần như constant-time dù content có hay không, để timing không lộ existence; error code theo precedence invalid command, not authorized, slow down, failed, not found. Auth là JWT trên QUIC binary, ALPN lore-storage/0.4, và gRPC; mỗi connection QUIC tới 8 stream, hai stream ưu tiên control, command pipeline, reply out-of-order.

Backend replaceable và scale theo chiều ngang

Storage subsystem chốt bằng hai trait ImmutableStore và MutableStore. Immutable: local file store, fragment trong packfile append-only với index mmappable; AWS S3, mỗi fragment một object key bằng hash; ReplicatedStore ghép store với peer replica, đọc miss kéo warm fragment từ peer lân cận, write propagate ngay. Mutable: local file với filesystem locking cho cas, và DynamoDB, conditional write map thẳng lên cas. Layout điển hình: local cache, network replica, rồi S3 durable sau một interface; backend mới chỉ là implement hai trait.

Scale nhờ read path stateless: lookup đưa bytes cho address A trong partition P, không session state, embarrassingly parallel, server nào trả cũng cùng bytes vì content-addressed; fragment write không contend vì address do bytes quyết định. Contention chỉ ở mutable store, sizing nó là quyết định capacity hệ trọng nhất; tiering hot/warm/cold với edge phục vụ 90% hoặc hơn traffic fragment không đụng upstream. Quá tải thì op trả SlowDown, client exponential backoff, tải cao làm chậm chứ không trả lời sai. Open problems ghi hash-based sharding: định tuyến bằng tail bytes của hash để cùng hash về cùng shard, front bytes lo fan-out ổ đĩa.

Obliteration, xóa theo yêu cầu pháp lý hay secret commit nhầm, xóa payload nhưng giữ address, hai phase PayloadObliterating rồi PayloadObliterated, crash-safe; phạm vi là context tức file ID, chi phí tỷ lệ với số fragment của file. Non-goals khai minh bạch: không peer-to-peer decentralization, không chống server gian lận.

Verdict: Lore có giết Git không

Không, và đó không phải tham vọng của nó. Git cho workflow phân tán, code-heavy vẫn vô đối. Lore sinh ra để chiếm chỗ Perforce ở thế giới centralized, binary-heavy: nhận server-of-record và file-level lock, thay MD5 bằng BLAKE3, delta-encode bằng content addressing, online-required bằng offline-capable, closed-everything bằng MIT và spec công khai từng byte. So với Git thuần, Lore vượt ở chỗ Git cố tình không làm: file nhiều GB là công dân hạng nhất, clone sparse mặc định, multi-tenant trong storage layer.

Điểm mạnh thật của thiết kế: phân tách hai subsystem rạch ròi, một serialization point duy nhất dễ reasoning, hash raw bytes bằng b3sum cực đúng cho hệ sinh thái, chunking, fragment list và sparse read gắn thành một mạch.

Chỗ chưa xong phải nói thẳng: pre-1.0, rebase, cherry-pick, squash mới ở mức data model; roadmap còn fork copy-on-read, branch-aware locking, multiple concurrent stages, hash sharding, desktop client. UEFN chạy Lore nội bộ nhưng bản open source chưa nói chuyện được với UEFN vì compression proprietary, Epic đang chuyển sang open compression.

Với dân backend, Lore đáng giá ở bài học thiết kế: hai store tách immutable khỏi mutable, tách addressing khỏi compression, một CAS làm điểm serialize duy nhất, hash raw bytes để tool nào cũng tính được address. Nếu repo asset vượt vài chục GB mà đang chạy Git cộng LFS, hoặc đang trả license Perforce đắt đỏ, thử Lore ở quy mô nhỏ, Go SDK lore-go có sẵn; production lớn chờ bản 1.0 và tooling lớn lên đã.

Nguồn tham khảo

Chia sẻ :

Bài viết liên quan

API Filtering: gọi dữ liệu như dân sành cà phê

API Filtering: gọi dữ liệu như dân sành cà phê

Năm đầu đi làm, mình từng viết một endpoint GET /api/menus trả về… toàn bộ menu. 200 items mỗi lần gọi. JSON nặng 1.2MB. Frontend chỉ cần tên + giá của 10 món đang active. Mình nhớ câu đầu tiên anh lead nói: “Mày đang gửi cả cái kho hàng cho người ta chỉ cần xem menu đấy à?”

Đọc thêm
Golang tắt Nagle's Algorithm mặc định: Cú lừa hiệu năng trên mạng chập chờn

Golang tắt Nagle's Algorithm mặc định: Cú lừa hiệu năng trên mạng chập chờn

Chào các bạn, hôm nay mình muốn chia sẻ với các bạn một câu chuyện dở khóc dở cười mà mình và nhiều anh em làm hệ thống đã từng nếm trải.

Đọc thêm
Dùng Golang Duy Trì Sự Sống: Hệ Thống Incident Management Tự Động Cho Bệnh Tiểu Đường Type 1

Dùng Golang Duy Trì Sự Sống: Hệ Thống Incident Management Tự Động Cho Bệnh Tiểu Đường Type 1

Bia hơi hoặc một đĩa salad ngỡ như lành mạnh chứa bao nhiêu gram carbohydrate? Với đại đa số mọi người, câu trả lời thường là “ai mà quan tâm”, ngoại trừ những người đang theo chế độ ăn kiêng khắt khe. Nhưng đối với khoảng 8 triệu người mắc bệnh Tiểu đường Type 1 (T1D) trên thế giới, đây là câu hỏi sống còn diễn ra mỗi ngày.

Đọc thêm