본문 바로가기
WIKI 기술 지식 베이스

lakeFS 내부 동작(Internals)

원문 보기 위키 갱신

이 페이지는 lakeFS가 표면 아래에서 어떻게 동작하는지 설명해요. Git과 객체 스토리지 개념을 혼합한 데이터 모델로 시작해, 객체가 물리적으로 어떻게 저장되는지, 머지가 어떻게 동작하는지, 불변 커밋을 효율적으로 만드는 버저닝 internals, 그리고 레퍼런스의 일관성을 유지하는 메타데이터 스토어까지 데이터의 흐름을 따라가요. 위에서 아래로 읽어도 되고, 필요한 레이어로 바로 점프해도 돼요.

출처: 문서

본문

데이터 모델

lakeFS는 S3 같은 객체 스토리지의 개념과 Git의 개념을 혼합해요. 그래서 아래 용어 대부분은 둘 중 하나를 사용해 본 적이 있다면 익숙할 거예요.

lakeFS는 객체 스토리지 안의 객체를 관리하기 위한 인터페이스예요. 실제 데이터 자체는 lakeFS 안에 직접 저장되지 않고 기반이 되는 객체 스토리지에 저장되며, lakeFS는 그 객체들에 대한 포인터와 추가 메타데이터를 관리해요.

리포지토리

lakeFS에서 리포지토리는 관련된 객체의 집합, 즉 객체 컬렉션이에요. 많은 경우 표 형식 데이터의 다양한 포맷 테이블이나 JSON·로그 파일 같은 반구조화 데이터, 이미지·비디오·센서 데이터 같은 비구조화 객체의 집합을 나타내요. 리포지토리는 객체, 브랜치, 커밋을 하나로 묶는 논리적 네임스페이스로, Git의 리포지토리에 비유할 수 있어요.

리포지토리 이름은 소문자나 숫자로 시작해야 하고, 소문자, 숫자, 하이픈만 포함해야 하며, 3자에서 63자 사이여야 해요.

커밋

커밋을 사용하면 리포지토리를 특정 시점에서 볼 수 있고, 보는 데이터가 커밋 순간과 정확히 동일하다는 보장을 받아요. 커밋은 특정 시점의 리포지토리 전체 내용을 담은 불변 "체크포인트"예요.

각 커밋에는 커미터, 타임스탬프, 커밋 메시지 같은 메타데이터와, 원하는 대로 추가할 수 있는 임의의 키/값 쌍이 포함돼요.

커밋 식별

커밋은 커밋 ID로 식별되며, 커밋의 모든 내용의 다이제스트예요. 커밋 ID는 본래 길기 때문에 고유한 접두사로 축약할 수 있어요. 커밋은 ref라고 불리는 텍스트 정의로도 식별할 수 있고, ref의 예로는 태그, 브랜치 이름, 표현식이 있어요.

브랜치

lakeFS의 브랜치는 사용자가 리포지토리의 자신만의 격리된 뷰를 만들게 해 줘요. 한 브랜치의 변경은 다른 브랜치에는 나타나지 않고, 사용자는 한 브랜치의 변경을 가져다 머지로 다른 브랜치에 적용할 수 있어요.

제로카피 브랜칭

내부적으로 브랜치는 단순히 커밋에 대한 포인터와 커밋되지 않은 변경 집합이에요. 브랜치 생성은 제로카피 연산이에요: 데이터를 복제하는 대신 lakeFS는 브랜치를 위해 소스 커밋에 대한 포인터를 만들어요.

태그

태그는 특정 커밋에 의미 있는 이름을 부여해 사용자가 특정 릴리스, 실험, 버전을 사람이 읽기 쉬운 이름으로 참조하게 해 줘요. 예를 들어 v2.3은 릴리스를, dev-jane-before-v2.3-merge는 Jane의 개인 임시 시점을 표시하죠. 태그 이름은 Git ref 이름과 동일한 규칙을 따라요.

히스토리

브랜치의 히스토리는 브랜치 끝점(tip)부터 각 커밋의 첫 번째 부모까지의 커밋 목록이에요. 히스토리는 시간을 거슬러 거슬러 올라가요.

머지

머지는 한 브랜치의 변경을 다른 브랜치에 통합해요. 머지의 결과는 새 커밋이며, 목적지가 첫 번째 부모, 소스가 두 번째 부모가 돼요. 전체 삼방 머지(three-way merge) 알고리즘과 충돌 규칙은 아래의 머지 동작 방식 섹션을 참고하세요.

Ref 표현식

lakeFS는 ref를 만들기 위한 표현식을 지원해요. 이는 Git의 revisions와 유사하며, 그 섹션 끝의 모든 ~와 ^ 예시는 lakeFS에서도 변경 없이 동작해요.

  • 브랜치나 태그는 ref 표현식이에요.

  • <ref>가 ref 표현식이라면:

  • <ref>^는 첫 번째 부모를 가리키는 ref 표현식이에요.

  • <ref>^N은 N번째 부모를 가리켜요. 특히 <ref>^1은 <ref>^와 같아요.

  • <ref>~는 첫 번째 부모를 가리키므로 <ref>~는 <ref>^와 같아요.

  • <ref>~N은 N번째 부모를 가리키며 항상 첫 번째 부모를 따라가므로, <ref>~N은 캐럿이 N개 연달아 붙은 <ref>^^...^와 같아요.

lakeFS 고유 개념

**기반 스토리지(underlying storage)**는 lakeFS가 여러분의 객체와 일부 불변 메타데이터를 보관하는 객체 스토리지의 위치예요. lakeFS 리포지토리를 만들 때 스토리지 네임스페이스(storage namespace), 즉 리포지토리 데이터가 저장되는 기반 스토리지의 위치를 지정해요.

우리는 기반 스토리지를 **물리적(physical)**이라고 부르기도 하고, 객체 내용을 저장하는 데 쓰인 경로를 **물리적 경로(physical path)**라고 부르기도 해요. lakeFS가 객체를 기반 스토리지에 저장한 후에는 정리(cleanup) 중 완전히 제거하는 경우 외에는 절대 수정되지 않아요. lakeFS가 하는 일의 상당 부분은 lakeFS 경로가 객체 스토리지의 물리적 경로로 어떻게 번역되는지를 관리하는 것이고, 이 매핑은 일반적으로 단순하지 않아요. 많은 객체 스토리지와 달리 lakeFS는 여러 경로를 기반 스토리지의 동일한 객체로 매핑할 수 있고, 버전 간 변경되지 않은 객체에는 항상 그렇게 해요.

스토리지 네임스페이스 안에서 lakeFS는 data/ 프리픽스 아래에 객체를 저장해요. lakeFS Enterprise(v1.85.0부터)에서는 객체가 객체의 내부 ID에서 파생된 **샤드(shards)**와 **파티션(partitions)**으로 조직돼요. 결과 물리적 경로(data/<shard>/<sub-shard>/<partition>/<id>)는 lakeFS 경로와 아무 관계가 없어요. 객체를 고정적이고 안정적인 많은 수의 프리픽스에 흩어 두면 클라우드 객체 스토리지가 쓰기 버스트 동안 핫스팟을 피할 수 있어요.

lakefs 프로토콜 URI

lakeFS는 경로 URI에 특정 형식을 사용해요. URI lakefs://<REPO>/<REF>/<KEY>는 주어진 리포와 ref 표현식 아래 키에 대한 객체 경로예요. 이것은 경로 프리픽스와 전체 경로 모두에 사용돼요. 유사하게 lakefs://<REPO>/<REF>는 ref 표현식에서의 리포지토리를, lakefs://<REPO>는 리포를 식별해요.

lakeFS가 데이터를 어떻게 저장하나요

lakeFS는 데이터 버저닝 엔진이므로 동일한 객체의 여러 버전을 저장해야 해요. 그래서 효율적인 버저닝이 가능하도록 객체 스토리지에 객체를 저장해요. 이 때문에 데이터가 실제로 어디에 사는지 이해하기가 혼란스러울 수 있고, 이 섹션이 그것을 밝혀 줘요.

lakeFS는 리포지토리 데이터와 메타데이터를 리포지토리의 스토리지 네임스페이스, 즉 객체 스토리지의 전용 경로 아래에 저장해요. 그 네임스페이스를 나열하면 두 개의 프리픽스가 보여요:

aws s3 ls s3://<storage_namespace>/
                        PRE _lakefs/
                        PRE data/

lakeFS는 실제 사용자 데이터를 data/ 프리픽스 아래에 저장해요. _lakefs/ 프리픽스에는 커밋 메타데이터, 즉 범위(range)와 메타범위(meta-range) 파일과 lakeFS 내부 데이터가 들어 있어요. lakeFS는 불변 데이터를 관리하므로 객체를 논리적 이름 아래 저장하지 않아요. 그렇게 하면 덮어쓰여 불변성 보장이 깨질 수 있거든요. allstar_games_stats.csv라는 CSV 파일을 main 브랜치에 업로드하면 lakeFS는 data/ 프리픽스 아래에 무작위 물리적 주소를 생성하고 그곳에 업로드해요. 경로에서 객체로의 매핑은 업로드, 커밋, 머지에 따라 변하고, 객체를 업데이트하면 lakeFS는 다른 버전들을 보존하면서 그 버전을 위한 새 물리적 주소를 만들어요. lakeFS는 객체의 논리적 주소를 물리적 주소에 연결하고 그 관계를 커밋 메타데이터에 저장해요.

lakeFS는 객체 스토리지를 불변으로 사용하므로 업로드된 것은 절대 변경되거나 덮어쓰이지 않아요. lakeFS가 실제로 언제 어떻게 스토리지에서 데이터를 삭제하는지는 가비지 컬렉션 문서를 참고하세요. 데이터를 찾기 위해 lakeFS는 lakefs://my-repo/main/allstar_games_stats.csv 같은 논리적 주소, 즉 리포지토리와 브랜치를 나타내는 주소를 사용해요. KV 메타데이터 스토어를 통해 lakeFS는 먼저 주어진 브랜치에서 객체의 커밋되지 않은 버전을 찾고, 없으면 브랜치 헤드의 최신 커밋된 버전을 가져와요:

  • main 브랜치의 현재 스테이징 토큰 아래 KV 메타데이터 스토어에서, 객체의 커밋되지 않은 변경이 있으면 반환돼요.

  • 객체 스토리지의 _lakefs/ 프리픽스 아래에 저장된 브랜치의 헤드 메타범위와 범위에서, main의 최신 커밋에 저장되었던 대로의 객체 메타데이터를 반환해요.

반환되는 물리적 경로는 s3://<storage_namespace>/data/gp0n1l7d77pn0cke6jjg/cg6p50nd77pn0cke6jk0 형태예요. lakeFS의 동일한 객체는 존재하는 각 버전마다 하나씩 여러 물리적 주소를 가질 수 있어요.

객체 스토어에서 객체 위치 찾기

객체의 물리적 위치를 확인하는 한 가지 방법은 lakectl fs stat 명령이에요:

lakectl fs stat --pre-sign=false lakefs://my-repo/main/allstar_games_stats.csv
Path: allstar_games_stats.csv
Modified Time: 2024-08-02 10:13:33 -0400 EDT
Size: 0 bytes
Human Size: 0 B
Physical Address: s3://niro-test/repos/docs/data/data/geh1jurck6tfom0s1t8g/cqmej33ck6tfom0s1tvg
Checksum: d41d8cd98f00b204e9800998ecf8427e
Content-Type: application/octet-stream

lakeFS는 객체의 모든 버전을 보여줄 수 있어요. 예를 들어 3 버전 전의 dev 브랜치에서 객체의 물리적 위치를 보려면 dev~3 레퍼런스를 사용하세요:

lakectl fs stat lakefs://my-repo/dev~3/allstar_games_stats.csv
Path: allstar_games_stats.csv
Modified Time: 2024-08-02 10:11:49 -0400 EDT
Size: 916393 bytes
Human Size: 916.4 kB
Physical Address: s3://<storage_namespace>/data/data/geh1jurck6tfom0s1t8g/cqmei9bck6tfom0s1tt0
Checksum: 48e04a4c072acdcf932ee6c43f46ef14
Content-Type: application/octet-stream

이것은 어떤 lakeFS 레퍼런스 유형과도 동작해요. lakeFS가 데이터를 어떻게 저장하는지 더 알고 싶다면 이 블로그 포스트를 참고하세요.

머지 동작 방식

lakeFS의 머지 연산은 Git과 유사해요. 머지 소스(커밋 또는 레퍼런스)의 변경을 브랜치인 머지 대상에 통합해요.

lakeFS는 먼저 merge base, 즉 두 커밋의 가장 가까운 공통 조상을 찾고, 각 커밋에서 파일의 존재 여부와 식별자를 검토해 삼방 머지를 수행해요. 아래 표에서 "A", "B", "C"는 가능한 파일 내용, "X"는 없는 파일, "conflict"는 결과로만 나타나는 머지 실패예요.

In base In source In destination Result Comment
A A A A 변경 없는 파일
A B B B 양쪽에서 같은 방식으로 변경된 파일
A B C conflict 양쪽에서 다르게 변경된 파일
A A B B 한쪽 브랜치에서만 변경된 파일
A B A B 한쪽 브랜치에서만 변경된 파일
A X X X 양쪽에서 삭제된 파일
A B X conflict 한쪽에서 변경되고 다른 쪽에서 삭제된 파일
A X B conflict 한쪽에서 변경되고 다른 쪽에서 삭제된 파일
A A X X 한쪽에서 삭제된 파일
A X A X 한쪽에서 삭제된 파일

머지 전략

API와 lakectl은 선택적 strategy 플래그를 허용해요. source-wins는 충돌을 소스 객체 쪽으로, dest-wins는 대상 객체 쪽으로 해결해요:

예시

lakectl merge lakefs://example-repo/validated-data lakefs://example-repo/production --strategy source-wins

전략은 머지의 모든 충돌 객체에 적용되며 현재는 충돌을 개별적으로 다룰 수 없어요. 포맷에 구애받지 않는 시스템인 lakeFS는 파일 단위로 머지하고, 포맷별 또는 사용자 정의 충돌 처리 머지 전략은 로드맵에 있어요.

비동기 머지

lakeFS Enterprise에서 제공되는 기능으로, 비동기 머지 연산은 대형 리포지토리에서의 확장성을 개선해요. API는 즉시 태스크 ID를 반환하고, 머지는 백그라운드에서 실행되며, 클라이언트는 완료를 폴링해요. 성공 시 상태 응답에 머지 커밋 정보가 포함되고, 실패 시 오류와 상태 코드가 포함돼요. 동기 응답과 동일하므로 오래된 클라이언트도 계속 동작해요. lakectl과 lakeFS UI는 lakeFS Enterprise에 연결되면 비동기 머지를 자동으로 사용하고 폴링을 대신 처리해 줘요. 이것은 Async Commit and Merge 기능의 머지 쪽 파트너예요.

버저닝 내부(Versioning Internals)

lakeFS의 커밋은 불변이므로 불변 객체 스토리지에 저장하기 쉬워요. 오래된 커밋은 드물게 접근되는 반면 새 커밋은 자주 접근되므로 계층형 스토리지 접근이 잘 맞아요: 객체 스토리지가 진실의 원천이고, 로컬 디스크와 심지어 RAM이 더 자주 접근되는 커밋을 캐시하는 거죠. 커밋이 불변이므로 한 번 캐시되면 공간이 부족해질 때만 퇴출(evict)하면 되고, 복잡한 무효화는 필요 없어요.

커밋은 SSTable로 저장되며, RocksDB와 호환돼요. 세 가지 이유예요:

  • 현대 하드웨어에서 매우 높은 읽기 처리량. 설계 파트너 중 하나의 S3 인벤토리를 모델로 삼은 2억 객체 리포지토리를 나타내는 커밋으로, 초당 약 50만 건의 랜덤 GetObject 호출을 달성했어요. 매우 높은 처리량 대 비용 비율이죠.

  • 잘 알려진 저장 형식이라 SSTable이 비교적 쉽게 생성·소비돼요. 객체 스토리지에 저장하면 데이터 엔지니어링 도구가 분석과 분산 컴퓨팅에 접근할 수 있어 운영 데이터베이스에 묶어 두는 사일로 효과를 줄여요.

  • SSTable 형식은 키에 대한 델타 인코딩을 지원해, 많은 키가 공통 접두사를 공유하는 데이터 레이크에 공간 효율적이에요.

각 lakeFS 커밋은 그 커밋 시점의 리포지토리 전체 키스페이스를 이루는, 연속적이고 겹치지 않는 SSTable 집합으로 표현돼요.

SSTable 파일 형식 ("Graveler 파일")

lakeFS 메타데이터는 Graveler라 불리는 형식으로 인코딩돼요. 콘텐츠 어드레싱 가능한 키/값 쌍을 인코딩하는 표준화된 방식이에요:

Graveler 파일 형식

각 키/값 쌍(ValueRecord)은 key, identity, value로 구성돼요. 단순한 identity는 값의 바이트에 대한 sha256 해시 또는 값을 고유하게 식별하는 어떤 바이트 시퀀스가 될 수 있어요. Graveler의 관점에서 두 ValueRecord는 key와 identity 필드가 같으면 동일해요. Graveler 파일 자체도 콘텐츠 어드레싱 가능하므로, Git처럼 파일 이름이 identity이며 담고 있는 ValueRecord들의 identity에서 계산돼요:

valueRecordID = h(h(valueRecord.key) || h(valueRecord.Identity))

fileID = h(valueRecordID1 + … + valueRecordIDN)

일관된 키스페이스 뷰 구축

저장 형식에는 추가 요구 사항이 두 가지 있어요. 커밋 생성은 공간과 시간이 효율적이어야 해요. 10억 개 중 단일 객체 변경이 리포지토리 전체 스냅샷을 쓰지 않고, 변경 크기에 비례해 커밋이 유지되도록 변경되지 않은 데이터 파일을 재사용해야 하죠. 커밋 간 diff 역시 절대 크기가 아니라 차이 크기에 비례한 시간으로 실행되어야 해요.

이를 지원하기 위해 lakeFS는 콘텐츠 주소로 어드레싱되는 리프 노드(범위(range))와, 모든 범위를 담아 키스페이스의 완전한 일관된 뷰를 나타내는 특수 범위인 **메타범위(meta-range)**로 이루어진 2계층 머클 트리(Merkle tree)를 구축해요:

메타범위는 커밋을 이루는 범위들을 가리켜요

커밋 B가 커밋 A에서 파생되었고 범위 e-f의 파일만 변경했다고 가정하면, B는 수정된 키를 담고 있는 범위를 제외한 모든 범위를 재사용하고, 그 범위만 새 해시로 재생성해요. 그러면 새 메타범위가 만들어지는데, 그 해시가 담고 있는 모든 범위의 해시에서 파생되기 때문이에요. 대부분의 커밋이 공통 접두사를 공유하는 관련 객체를 변경한다고 가정하면 재사용 비율은 매우 높아요. 우리는 설계 파트너 두 곳의 S3 인벤토리로 이를 테스트했어요. 키스페이스를 시뮬레이션된 블록으로 파티셔닝하고 시간에 따른 변화를 측정했더니 일일 변화율이 약 5~20%였어요. 하루 20 커밋이라는 온건한 수준에서 커밋은 이전 커밋의 블록 중 최소 99%를 재사용할 것으로 예상돼요.

객체 스토리지에서는 범위와 메타범위가 데이터 객체와 함께 _lakefs/ 프리픽스 아래에 저장돼요:

<lakefs root>
    _lakefs/
        <range hash1>
        <range hash2>
        <range hashN>
        ...
        <metarange hash1>
        <metarange hash2>
        <metarange hashN>
        ...
    <data object hash1>
    <data object hash2>
    <data object hashN>
    ...

이 비교적 평평하고 고정된 2계층 구조는 트리 깊이에 실질적 제한을 두지 않아요. 메타범위가 다른 메타범위를 가리키게 해 트리를 재귀적으로 만들 수도 있지만, 단순함을 위해 lakeFS는 2계층으로 시작해요.

레퍼런스와 커밋되지 않은 메타데이터의 표현

lakeFS는 커밋된 데이터든 커밋되지 않은 데이터든 객체 데이터를 항상 사용자의 객체 스토어에 있는 스토리지 네임스페이스에 저장해요. 하지만 lakeFS 객체 메타데이터는 객체 스토어나 키-값 스토어 중 어느 쪽에도 저장될 수 있어요.

불변인 커밋된 메타데이터와 달리, 커밋되지 않은(즉 "스테이징된") 메타데이터는 빈번한 랜덤 쓰기를 겪는 매우 가변적인 것이에요. ref, 특히 브랜치도 마찬가지예요. 브랜치는 기반 커밋에 대한 포인터로 매 커밋이나 머지마다 수정되거든요. 두 메타데이터 유형 모두 가변적이고 강한 일관성 보장이 필요하며 장애 내성이 있어야 해요. main 브랜치의 현재 포인터를 사용할 수 없게 되면 시스템의 상당 부분이 사실상 중단되기 때문이에요. 다행히 이것은 커밋된 메타데이터보다 훨씬 작은 메타데이터 집합이고, 레퍼런스와 커밋되지 않은 메타데이터는 그 일관성 보장을 위해 키-값 스토어(지원 데이터베이스 참고)에 저장돼요.

메타데이터 스토어

버전 0.80.2부터 lakeFS는 모든 데이터베이스 연산을 PostgreSQL과의 강한 결합에서 벗어나 일반 키-값 스토어로 옮겼어요. SQL 데이터베이스는 분명한 장점이 있지만 Postgres와의 강한 결합이 사용자를 제한했기 때문에, 키-값 스토어 위의 lakeFS가 도입되었어요.

KV 스토어는 Get, Set, Compare-and-Set, Delete, Scan 메서드를 가진 일반 인터페이스를 구현해요. 각 항목은 [partition, key, value] 트리플렛으로 표현되며 세 필드 모두 일반 바이트 배열이에요. 그래서 사용 모듈이 각 필드의 형식에 대한 최대한의 유연성을 갖죠. 내부적으로 KV 구현은 데이터를 영속화하는 백킹 데이터베이스에 의존해요. lakeFS는 DynamoDB와 PostgreSQL용 드라이버를 함께 제공하고, 사용자와 기여자는 자신이 선택한 데이터베이스의 드라이버를 직접 개발할 수도 있어요. 실험용 인메모리 KV 스토어도 있지만 영속성은 없어요.

메타데이터 객체(리포지토리, 브랜치, 커밋, 태그, 커밋되지 않은 객체)를 저장하기 위해 lakeFS는 일반 KV 스토어 위에 이 객체들을 protobuf로 직렬화·역직렬화하는 또 다른 레이어를 구현해요. 이 레이어는 일반 인터페이스에만 의존하므로 어떤 스토어 구현이 쓰이든 관계없이 동작해요.

추가 읽기

더 깊은 설명은 KV 설계 문서를 참고하세요.

KV를 이용한 낙관적 락킹

SQL 데이터베이스와 키-값 스토어의 핵심 차이 하나는 자원을 잠그는 능력이에요. KV 인터페이스를 단순하고 백킹 데이터베이스 선택에 유연하게 유지하기 위해 lakeFS는 락킹을 지원하지 않기로 했고, 여기서 동시성 과제가 생겼어요. 흔한 lakeFS 흐름인 Commit을 생각해 봐요. 이 과정에서 여러 데이터베이스 연산이 수행돼요: 브랜치의 모든 관련 커밋되지 않은 객체를 수집해 커밋됨으로 표시하고, 새 커밋 객체를 만들고, 브랜치를 새 커밋을 가리키도록 업데이트하죠.

이 흐름은 동시 실행에 매우 민감해서, 두 커밋 흐름이 병렬로 실행되면 lakeFS가 정확성을 보장해야 해요. PostgreSQL 기반 lakeFS는 커밋 연산 전체 동안 브랜치를 단순히 잠갔어요. KV 스토어에서는 lakeFS 대신 낙관적 락킹(optimistic locking) 알고리즘을 구현해요. Compare-And-Set으로 커밋 시작 시점의 브랜치 상태를 기억하고, 마지막에 브랜치가 변경되지 않았을 때만 업데이트하는 거예요. 샘플링된 상태와 현재 상태가 다르면 다른 커밋이 진행 중이라는 뜻이므로 첫 커밋이 실패하고 나중 커밋에 완료 기회를 줘요:

  • 커밋 A가 스테이징 토큰을 tokenA로 설정하고 브랜치를 샘플링해요.

  • 커밋 B가 스테이징 토큰을 tokenB로 설정하고 브랜치를 샘플링해요.

  • 커밋 A가 끝나 브랜치 업데이트를 시도하지만 스테이징 토큰이 이제 tokenA가 아닌 tokenB라 실패해요.

  • 커밋 B가 끝나 브랜치를 업데이트해요. tokenB가 자신의 기대와 일치하거든요.

커밋이 시작되며 새 스테이징 토큰을 설정할 때, 이전 값은 브랜치의 아직 유효한 스테이징 토큰 목록(sealed tokens)에 추가돼요. 덕분에 실패한 커밋 때문에 스테이징 토큰이나 객체가 사라지는 일은 없어요.

트랜잭션과 원자적 업데이트

또 다른 차이는 PostgreSQL은 트랜잭션을 제공하지만 KV 스토어는 그렇지 않다는 점이에요. lakeFS는 이전에 여러 업데이트를 하나의 원자적 연산으로 묶어, 어느 지점에서든 실패하면 전체 연산이 롤백되게 했어요. 새 리포지토리 생성을 생각해 봐요. 리포지토리 객체 자체, 초기 브랜치, 브랜치가 가리키는 초기 커밋, 세 개의 객체가 있죠. SQL에서는 이것이 단일 트랜잭션이었고 어느 지점에서든 실패하면 깨끗하게 롤백됐어요.

트랜잭션이 없으면 중간 실패가 리포지토리를 초기 브랜치 없이 남겨 두면서도 여전히 접근 가능하게 만들 수 있어요. 이를 완화하기 위해 lakeFS는 리포지토리 관련 객체 전체를 담는 리포지토리별 파티션을 도입했어요. 파티션 키는 특정 리포지토리 인스턴스에서 파생되며, lakeFS는 먼저 그 파티션 키 아래에 커밋과 브랜치를 만든 뒤에야 리포지토리를 생성해요. 덕분에 세 객체가 모두 성공적으로 생성된 후에만 리포지토리와 그 객체들이 접근 가능해져요. 실패가 떠다니는(dangling) 객체를 남길 수는 있지만 일관성은 유지되고, 그런 객체의 수는 크지 않을 것으로 예상돼요.

더 알아보기 (Learn more)

공식 문서의 internals 페이지는 https://docs.lakefs.io/concepts/internals 에서 확인할 수 있어요.