1P by GN⁺ | ★ favorite | 댓글 1개
  • Graft는 모든 클라이언트에 전체 변경 로그를 보내는 대신, 물리 복제의 단순성과 논리 복제의 효율을 결합하려는 오픈소스 트랜잭션 스토리지 엔진
  • 고정 크기 Page로 구성된 Volume을 Snapshot 단위로 다루며, 서버는 실제 데이터가 아니라 변경된 페이지 인덱스의 압축 비트셋인 graft를 내려줌
  • 클라이언트는 graft를 보고 필요한 페이지만 가져오며, Leap 기반 프리패칭, 도메인별 프리패칭, 전체 변경분을 가져오는 선제 가져오기를 선택할 수 있음
  • 객체 스토리지와 엣지 서버를 활용해 브라우저, 모바일 앱, 서버리스 함수, 임베디드 환경 같은 제약 환경에서도 부분 복제를 목표로 함
  • 일관성 모델은 Serializable Snapshot Isolation이며, 오래된 Snapshot 기반 커밋은 거부하고 클라이언트가 reset/replay, merge, Volume fork 중 하나로 처리함

Graft가 풀려는 복제 문제

  • 부분 복제는 필요한 데이터만 동기화하면 쉬워 보이지만, 실제 설계에서는 복제 방식마다 뚜렷한 대가가 있음
    • 논리 복제는 모든 변경을 정밀하게 추적하지만 강한 일관성을 복잡하게 만듦
    • 물리 복제는 그 복잡성을 피하지만, 나중에 버릴 변경까지 모두 동기화해야 함
  • Graft는 지연 동기화, 부분 복제, 강한 일관성, 수평 확장성, 객체 스토리지 내구성을 목표로 만든 오픈소스 트랜잭션 스토리지 엔진임
  • 출발점은 SQLSync에서의 경험임
    • SQLSync는 SQLite 위에 구축된 프론트엔드 최적화 데이터베이스 스택이며, Git과 분산 시스템 아이디어를 동기화 엔진에 사용함
    • SQLSync는 전체 변경 로그를 모든 클라이언트에 복제하는 구조라 서버에서는 괜찮아도 엣지와 브라우저 환경에는 맞지 않음
  • Graft의 목표는 클라이언트가 원하는 속도로 동기화하고, 필요한 것만 가져오며, 엣지와 오프라인 기기에서도 임의 데이터를 강한 일관성으로 복제하는 것임

전체 복제와 스키마 인식 diff 사이의 설계

  • 기존 해법은 크게 두 갈래로 나뉨
    • 전체 복제: 전체 데이터셋을 각 클라이언트에 동기화하므로 서버리스 함수나 웹앱 같은 제약 환경에 실용적이지 않음
    • 스키마 인식 diff: CDC나 CRDT처럼 행 또는 필드 수준의 논리 변경을 추적하지만, 애플리케이션과 깊게 통합해야 하고 임의 데이터에는 일반화하기 어려움
  • Graft는 전체 복제처럼 스키마에 무관함
    • 저장 데이터의 종류를 알거나 신경 쓰지 않고, 바이트가 담긴 페이지를 복제함
  • 동시에 논리 복제처럼 마지막 동기화 이후 무엇이 바뀌었는지에 대한 압축된 설명을 클라이언트에 전달함
  • 핵심 추상화는 Volume
    • Volume은 고정 크기 Page의 희소하고 정렬된 컬렉션임
    • 클라이언트는 특정 Snapshot에서 트랜잭션 API로 Volume을 읽고 씀
    • 내부적으로 Graft는 필요한 것만 저장·복제하며, 객체 스토리지를 내구성 있고 확장 가능한 백엔드로 사용함

지연 동기화: 클라이언트가 원하는 시점에 따라잡기

  • Graft는 엣지 클라이언트가 가끔 깨어나고, 네트워크가 불안정하며, 실행 시간이 짧은 환경을 전제로 설계됨
  • 지속적인 복제에 의존하지 않고, 클라이언트가 언제 동기화할지 직접 선택함
  • 동기화는 “마지막 Snapshot 이후 무엇이 바뀌었는가”라는 질문에서 시작함
  • 서버는 실제 데이터를 보내지 않고, 변경된 페이지 인덱스의 압축 비트셋인 graft 를 응답함
    • graft는 기존 Snapshot에 새 변경을 붙이는 안내 역할을 함
    • 클라이언트는 어떤 페이지를 재사용할 수 있고, 어떤 페이지를 필요할 때 가져와야 하는지 알 수 있음
  • graft는 데이터가 아니라 변경 메타데이터이므로, 무엇을 언제 가져올지에 대한 제어권이 클라이언트에 남음

부분 복제와 프리패칭

  • 브라우저 탭, 모바일 앱, 서버리스 함수에서는 일부 쿼리를 처리하기 위해 전체 데이터셋을 내려받기 어려움
  • 클라이언트는 graft를 받은 뒤 어떤 페이지가 여전히 유효하고 어떤 페이지를 가져와야 하는지 판단함
  • 필요한 페이지만 선택적으로 가져오므로, 실제로 사용할 데이터만 복제할 수 있음
  • Graft는 페이지 접근 지연을 줄이기 위해 여러 프리패칭 방식을 지원함
    • 일반 목적 프리패칭: Leap 알고리듬 기반 내장 프리패처가 접근 패턴을 식별해 향후 페이지 접근을 예측함
    • 도메인별 프리패칭: 애플리케이션이 사용자 프로필처럼 자주 조회되는 데이터에 대한 지식을 활용해 관련 페이지를 미리 가져올 수 있음
    • 선제 가져오기: 필요하면 모든 변경분을 가져와 사실상 전체 복제로 되돌릴 수 있으며, 서버 측 Graft 워크로드에 특히 유용함
  • 페이지는 객체 스토리지에 직접 호스팅되므로 내구성과 확장성을 갖춘 복제 기반으로 쓰임

엣지 배치와 임베디드 클라이언트

  • Graft의 엣지 복제는 어떤 데이터를 동기화할지뿐 아니라, 필요한 위치에 데이터를 두는 것까지 목표로 함
  • 페이지는 객체 스토리지에서 글로벌 엣지 서버 플릿을 통해 제공됨
    • 자주 접근되는 hot page는 클라이언트 가까이에 캐시될 수 있음
    • 전 세계 사용자 위치와 관계없이 낮은 지연과 높은 응답성을 목표로 함
  • Graft 클라이언트는 가볍고 임베드되도록 설계됨
    • 의존성이 적고 런타임이 작음
    • 브라우저, 기기, 모바일 앱, 서버리스 함수 같은 환경에 통합할 수 있음
  • 엣지 캐싱은 일관성과 충돌 처리 문제를 만들기 때문에, Graft는 강한 일관성 모델을 함께 제공함

일관성 모델과 충돌 처리

  • Graft는 Serializable Snapshot Isolation을 일관성 모델로 사용함
  • 클라이언트는 특정 Snapshot에서 격리되고 일관된 데이터 뷰를 얻으며, 읽기는 서로 방해하지 않고 동시에 진행될 수 있음
  • 쓰기는 엄격하게 직렬화되어 모든 트랜잭션에 전역적으로 일관된 순서가 생김
  • 오프라인 우선과 지연 복제 특성 때문에, 클라이언트가 오래된 Snapshot을 바탕으로 커밋을 시도할 수 있음
    • 이런 커밋을 무조건 수락하면 strict serializability가 깨짐
    • Graft는 해당 커밋을 안전하게 거부하고, 클라이언트가 처리 방식을 선택하게 함
  • 클라이언트의 일반적인 선택지는 세 가지임
    • Reset and replay: 최신 Snapshot을 가져오고 로컬 트랜잭션을 다시 적용한 뒤 재시도함
      • 전역 데이터는 strict serializable 상태를 유지함
      • 로컬에서는 Optimistic Snapshot Isolation을 경험하며, 읽기는 내부적으로 일관된 Snapshot을 보지만 커밋 거부 시 해당 Snapshot이 버려질 수 있음
    • Merge: 로컬 상태를 서버의 최신 Snapshot과 병합함
      • 이 경우 전역 일관성 모델이 snapshot isolation으로 낮아질 수 있음
    • Volume fork: 영구적으로 새 Volume을 만들어 분리함
      • 전역 serializability를 유지함

만들 수 있는 애플리케이션

  • 오프라인 우선 앱: 노트, 작업 관리, CRUD 앱처럼 부분적으로 오프라인에서 동작하는 앱에서 Graft가 동기화를 담당할 수 있음
    • 충돌 핸들러와 결합하면 임의 데이터 위에 멀티플레이어 기능도 가능함
  • 크로스 플랫폼 데이터: 모바일 플랫폼, 기기, 웹에서 데이터를 공유하고 벤더 종속을 줄일 수 있음
  • 상태 없는 읽기 복제본: 로컬 상태 없이 데이터베이스 복제본을 띄우고 최신 Snapshot 메타데이터를 가져온 뒤 바로 쿼리를 실행할 수 있음
    • 전체 데이터를 내려받거나 로그를 재생할 필요가 없음
  • 임의 데이터 복제: Graft는 페이지 복제에 집중하므로 페이지 내부 데이터 형식에는 관여하지 않음

SQLite 확장 libgraft

  • 현재 Graft를 쓰기 가장 쉬운 방법은 네이티브 SQLite 확장인 libgraft
  • libgraft는 SQLite가 동작하는 곳에서 쓸 수 있고, 클라이언트가 실제로 사용하는 데이터베이스 일부만 복제함
  • SQLite VFS를 구현해 데이터베이스 읽기와 쓰기를 가로챔
  • SQLite가 WAL mode에서 제공하는 것과 같은 트랜잭션 및 동시성 의미론을 제공함
  • 제공 기능은 다음과 같음
    • 객체 스토리지와의 비동기 복제
    • 엣지와 기기에서의 지연 부분 복제
    • Serializable Snapshot Isolation
    • 특정 시점 복원
  • 문서는 GitHub의 SQLite 문서에서 볼 수 있음

참여와 관리형 서비스 계획

  • Graft는 GitHub에서 공개 개발됨
  • 이슈, 토론, Pull Request를 받을 수 있으며 contribution guide를 제공함
  • 대화 채널로 Discord와 이메일이 제공됨
  • Graft Managed Service 출시도 계획되어 있으며, 대기 목록 가입 링크가 제공됨

로드맵

  • Graft는 1년간의 연구, 여러 번의 반복, 한 번의 큰 방향 전환을 거쳤지만 아직 남은 작업이 많음
  • 계획된 항목은 다음과 같음
    • WebAssembly 지원: 브라우저에서 Graft를 사용할 수 있게 하며, SQLite 공식 Wasm 빌드, wa-sqlite, sql.js 지원을 목표로 함
    • Graft와 SQLSync 통합: Wasm 지원 뒤 SQLSync의 mutation, rebase, query subscription 계층을 분리해 Graft 복제 데이터베이스 위에 올리는 계획임
    • 클라이언트 라이브러리 확대: Python, JavaScript, Go, Java용 네이티브 Graft 클라이언트 래퍼를 원함
    • 저지연 쓰기: 현재 push 작업은 객체 스토리지에 완전히 커밋될 때까지 블록됨
      • S3 express zone 실험
      • 객체 스토리지 앞단에 저지연 내구성 합의 그룹을 두는 방식
    • 가비지 컬렉션, 체크포인팅, 컴팩션: 쿼리 성능 극대화, 낭비 공간 최소화, 영구 삭제를 위해 필요함
    • 인증과 권한 부여: 관리형 서비스 계정부터 Volume 읽기/쓰기 세밀 권한까지 포함하는 넓은 작업임
    • Volume forking: 서비스는 Segment 참조를 새 Volume에 복사해 zero-copy fork를 수행할 수 있지만, 로컬 fork는 현재 모든 페이지를 복사해야 함
    • 충돌 처리: 내장 충돌 해결 전략과 확장 지점을 제공할 계획이며, 초기 전략은 겹치지 않는 트랜잭션을 자동 병합하는 것임

SQLite 복제 해법과의 비교

  • 비교 정보는 문서와 블로그 글에서 모은 것이며, 완전히 정확하지 않을 수 있다는 단서가 붙어 있음
  • mvSQLite

    • mvSQLite는 SQLite 페이지를 FoundationDB에 직접 저장하는 커스텀 VFS 계층을 구현함
    • Graft와 mvSQLite는 페이지 수준 버전 관리로 지연 fetch와 부분 데이터베이스 뷰를 가능하게 하는 점이 비슷함
    • 차이는 저장 위치와 페이지 변경 추적 방식임
      • mvSQLite는 FoundationDB에 의존하고 모든 노드가 클러스터에 직접 접근해야 함
      • Graft의 Splinter 기반 changeset은 자체 포함형이라 배포하기 쉽고, 변경 페이지 버전을 알기 위해 FoundationDB를 직접 질의할 필요가 없음
  • Litestream

    • Litestream은 SQLite WAL frame을 객체 스토리지에 계속 복제하는 스트리밍 백업 솔루션임
    • Graft는 커스텀 VFS로 SQLite 커밋 과정에 직접 통합되어 지연 부분 복제와 분산 쓰기를 가능하게 함
    • 둘 다 페이지를 객체 스토리지에 복제하고 특정 시점 복원을 지원함
  • cr-sqlite

    • cr-sqlite는 테이블을 CRDT로 바꿔 논리적 행 수준 복제를 가능하게 하는 SQLite 확장임
    • 자동 충돌 해결을 제공하지만 스키마 인식과 애플리케이션 수준 통합이 필요함
    • Graft는 스키마에 무관하고 임의 SQLite 확장 및 커스텀 데이터 구조와 호환되지만, 전역 serializability를 위해 애플리케이션이 충돌 해결을 명시적으로 처리해야 함
  • Cloudflare Durable Objects with SQLite Storage

    • Durable Objects와 SQLite를 결합하면 비즈니스 로직으로 감싼 강한 일관성과 높은 내구성의 데이터베이스를 Cloudflare 엣지 네트워크에 둘 수 있음
    • 내부적으로 SQLite WAL을 객체 스토리지에 복제하고 주기적으로 체크포인트하는 점에서 Litestream과 비슷함
    • Graft는 복제를 1급 기능으로 노출하고 엣지와의 효율적 복제를 목표로 함
  • Cloudflare D1

    • Cloudflare D1은 HTTP API로 접근하는 관리형 SQLite 데이터베이스임
    • Graft는 데이터를 클라이언트 애플리케이션에 임베드해 엣지로 직접 복제하는 분산형 모델임
  • Turso & libSQL

    • TursolibSQL을 통해 관리형 SQLite 데이터베이스와 임베디드 복제본을 제공함
    • Graft는 부분 복제와 임의의 스키마 무관 데이터 구조 지원으로 차별화됨
    • Graft 백엔드 서비스는 페이지 수준에서 동작하며 트랜잭션 수명주기 전체를 클라이언트에 맡김
  • rqlite & dqlite

    • rqlitedqlite는 Raft 기반 합의와 네트워크 프로토콜을 통해 SQLite를 여러 서버에 분산함
    • 이들은 서로 연결을 유지하는 상태 있는 노드 집합을 동기화하는 데 초점을 둠
    • Graft는 객체 스토리지 위에 구축된 상태 없는 시스템이며, 엣지와 데이터를 주고받도록 설계됨
  • Verneuil

    • Verneuil은 객체 스토리지를 통해 SQLite Snapshot을 읽기 복제본에 비동기 복제하며 신뢰성을 우선함
    • 복제 지연이나 신선도를 최소화하는 메커니즘은 명시적으로 피함
    • Graft는 다중 작성자 분산 데이터베이스에 더 가깝게 동작하며, 선택적 실시간 부분 복제를 강조함

댓글과 토론

Hacker News 의견들
  • 일관성 모델이 잘 이해되지 않음
    Graft 클라이언트는 로컬에서 커밋한 뒤 비동기로 원격 커밋을 시도한다고 되어 있고, 전역적으로는 엄격한 직렬 가능성(Strict Serializability)을 보장하므로 같은 스냅샷 기반의 동시 커밋 중 하나는 성공하고 하나는 실패한다고 함
    그런데 API에는 commit(VolumeId, ClientId, Snapshot LSN, page_count, segments)라는 단일 커밋 연산만 있음
    클라이언트가 로컬에서 성공한 커밋 이후, 그 커밋의 비동기 전파가 실패했고 그 위에 쌓은 작업을 롤백해야 한다는 사실을 어떻게 알 수 있는지 불명확함
    보통 커밋은 해당 트랜잭션이나 상태가 유효하다는 보장을 뜻하는데, 여기서는 로컬 커밋이 미래 어느 시점에 무효화될 수 있어 “비동기 검증된 커밋”과 전혀 다른 개념처럼 보임

    • 맞음, 조금 헷갈릴 수 있음. 블로그 글의 Consistency 섹션에서 설명해 보았음: https://sqlsync.dev/posts/stop-syncing-everything/#consisten...
      핵심은 시스템이 오프라인 쓰기를 지원한다면, 그 쓰기를 수행하는 클라이언트는 정의상 범용적인 엄격한 직렬 가능성을 가질 수 없다는 것임
      트랜잭션이 나중에 동기화될 때 더는 유효하지 않을 수 있다는 가정 아래 존재해야 함
      Graft는 서버 측 커밋을 엄격하게 직렬화하는 강한 기반을 제공하되, 로컬 쓰기를 어떻게 처리할지는 클라이언트가 선택하게 함
      클라이언트는 오프라인이면 모든 로컬 쓰기를 거부하고 온라인이 될 때까지 기다리거나, 온라인 복귀 시 최신 스냅샷 위에 로컬 쓰기를 리베이스해 “낙관적 스냅샷 격리”처럼 동작하게 하거나, 원격 변경과 로컬 변경을 병합할 수 있음
      저장하는 자료구조에 따라 달라지겠지만, 예를 들어 충돌 없는 복제 자료형(CRDT)을 저장하면 잘 맞을 수 있음
      그 밖에 Volume을 완전히 포크해 사용자가 수동 병합하게 하거나, 로컬 변경을 모두 버릴 수도 있음
      목표는 엣지 네이티브 시스템을 만들 수 있는 구성 요소를 제공하는 것이고, 애플리케이션에 필요한 로컬 쓰기 의미론을 강하게 정해 두고 싶지는 않음
    • 기본적으로 단일 마스터 비동기 복제이고, SQLite의 저널 모드에서만 동작하는 것으로 보임
      마스터가 SQLite 저널을 모두 커밋 이력으로 저장하고, 팔로워에게 보내 재생하게 하는 구조임
  • Graft 만든 사람임. 좋은 댓글, 별, 응원 모두 정말 고맙고, 다시 공개적으로 개발하게 되어 기분이 좋음
    오늘 저녁은 억지로라도 로그오프하려고 함. 내일 아침에 다시 와서 추가 질문에 답하겠음
    막 Washington, DC에 도착해 Antithesis BugBash[1]에 참석하려는 중이고, 시차 적응을 미리 안 해두면 후회할 것 같음
    이번 주 Washington 근처에 있거나 컨퍼런스에 온다면 만나고 싶으니 hello [at] orbitinghail [dotdev]로 연락해 주면 됨
    [1]: https://bugbash.antithesis.com/

    • 공유해줘서 고맙고 정말 멋져 보임. 특히 그래픽이 훌륭하다고 꼭 말하고 싶음
      직접 만들었는지 도움을 받았는지는 모르겠지만, 페이지나 데이터베이스 같은 기술 개념을 설명하는 그림을 만들기 어려운데 여기 그림들은 요점을 잘 전달함
    • 오늘도 Graft에 대한 관심이 이어져서 신남. 대화를 계속하고 싶다면 Discord [1]에 참여하거나 GitHub [2]에 토론을 올려도 됨
      앞으로 며칠 동안 Antithesis BugBash [3]에서 결정적 시뮬레이션 테스트(DST)를 좋아하는 사람들과 이야기할 예정임
      근처에 있다면 직접 만나게 연락해 주면 좋겠음
      [1]: https://discord.gg/etFk2N9nzC
      [2]: https://github.com/orbitinghail/graft
      [3]: https://bugbash.antithesis.com/
    • 훌륭한 작업임. 약간 엉뚱한 질문이지만, 페이지의 아름다운 다이어그램을 어떤 도구로 만들었는지 궁금함
      예: https://sqlsync.dev/_astro/pull_changes.DjOYfgBf_2biXxv.webp
  • 제대로 이해했다면 일관성 모델은 본질적으로 git 같음
    로컬 사본이 있고 거기에 변경을 만든 뒤, “push” 시점에 충돌이 나면 “rebase”나 “merge”를 하는 식임
    문제는 충돌을 깔끔하게 감지할 방법이 없다는 점임. 문서는 변경된 페이지를 말하지만, 페이지 변경은 충돌의 좋은 지표가 아님
    읽기 충돌 때문에 충돌이 생길 수 있음. 예를 들어 고객 ID 갱신은 UPDATE Customers SET id='bar' WHERE id='foo'; UPDATE Orders SET customerId='bar' WHERE customerId='foo'이고, 고객 구매 추가는 SELECT id FROM Customers WHERE email="blah"; INSERT INTO Orders(customerId, ...) VALUES("foo", ...);일 수 있음
    갱신 작업이 먼저 커밋되고 Orders 테이블 페이지가 가득 차 있어 삽입 시 새 페이지가 할당된다면, 두 작업은 페이지 충돌이 없지만 결과는 잘못됨
    이를 고치려면 쓰기가 발생한 트랜잭션 동안 읽은 페이지를 추적해야 하는데, 갱신 컬럼이 인덱스에 없어서 테이블 스캔이 필요하면 쉽게 전체 테이블이 될 수 있음

    • git에서도 리베이스는 물론 안전한 연산이 아니고, 병합도 휴리스틱이라 충돌이나 조용한 오병합이 생길 수 있음
      간단한 예시는 여기 있음: https://www.caktusgroup.com/blog/2018/03/19/when-clean-merge...
    • 뒤쪽에서 이 부분을 다룸
      변경이 이미 무효가 된 스냅샷을 기반으로 해서 엄격한 직렬 가능성이 불가능하다면, 재생을 하거나 로컬 트랜잭션은 내구성이 없지만 시스템 전체로는 직렬 가능성을 되찾을 수 있고, 병합을 선택하면 스냅샷 격리로 낮아짐
      로컬의 아직 동기화되지 않은 트랜잭션이 페이지 읽기 집합을 유지하고 그쪽에서 충돌을 찾는다면 이 방식은 타당해 보임
  • “클라이언트가 graft를 가져온 뒤 정확히 무엇이 바뀌었는지 알 수 있고, 그 정보를 이용해 어떤 페이지가 여전히 유효하고 어떤 페이지를 가져와야 하는지 정확히 판단할 수 있다”는 부분이 Cloud-Backed SQLite의 매니페스트와 어떻게 비교되는지 궁금함: https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki
    변경된 페이지를 보내는 점에서 설계가 비슷하지만, 서버 쪽 계산이 필요 없다는 점은 큰 장점으로 보임

    • 언급해줘서 고마움. Cloud-Backed SQLite(CBS)는 훌륭한 프로젝트이고, 어쩌면 더 중요하게는 Graft보다 훨씬 성숙함
      CBS는 지적한 대로 매니페스트와 블록을 사용함. 읽기 쪽에서는 매니페스트를 가져와 재사용 가능한 블록과 새로 가져와야 하는 블록을 알 수 있으니 그 관점에서는 매우 비슷함
      쓰기 계층은 꽤 다름. CBS는 클라이언트가 블록을 직접 쓰지만, Graft는 중간의 PageStore를 활용해 영속성을 처리함
      중간자를 쓰는 첫 번째 장점은 PageStore가 여러 Volume의 변경을 S3의 더 큰 세그먼트로 모을 수 있고, 곧 세그먼트를 시간에 따라 압축·최적화해 질의 성능을 개선하고 tombstone을 제거할 수 있다는 것임
      두 번째 장점은 Graft에 꽤 고유한데, 작성된 페이지가 MetaStore 커밋을 통해 LSN에 고정되기 전까지 “떠 있는” 상태라는 점임
      쓰기 동시성이 커질 때 중요함. 클라이언트 커밋이 마지막 스냅샷 기반이 아니라서 거부되면, 최신 스냅샷 위에 로컬 변경을 리베이스할 수 있음
      이때 Graft 모델은 이전 커밋 시도에서 올린 페이지의 임의 부분집합을 새 커밋에 재사용할 수 있게 해주며, 최선의 경우 추가 페이지 업로드를 완전히 없앨 수 있음
      겹치지 않는 작업부하에서 동시성을 크게 개선할 수 있을지 실험해 보고 싶음
      세 번째 장점은 권한임. Graft를 배포하면 PageStore와 MetaStore에서 세분화된 쓰기 권한을 강제할 수 있음
      반면 CBS는 클라이언트가 블롭 저장소에 직접 접근해야 함. 서버 측 배포에서는 괜찮을 수 있지만, 애플리케이션 안에 복제본을 넣고 싶은 엣지·기기 사용 사례에는 맞지 않음
      매니페스트 측면에서도 CBS에서는 클라이언트가 최신 매니페스트만 가져오면 되는 것은 맞지만, 많은 클라이언트와 높은 변경량으로 확장하면 Graft의 압축 비트셋 방식이 클라이언트가 가져와야 하는 데이터를 크게 줄여 줌
      메타데이터에 대해 스냅샷이 아니라 로그를 가져오는 것처럼 생각하면 됨
      그리고 사소한 취향이지만 Rust를 정말 좋아함
    • 이건 못 봤는데 정말 멋짐
      최근 gcloud에서 소규모 서버리스 데이터베이스를 찾고 있었는데, 이것이 실제 Google Cloud 제품들보다 나을 수도 있겠음
      Cloud Firestore가 당연한 선택처럼 보였지만, 개발과 CI 환경에 널리 깔린 기존 gcloud 자격 증명으로 동작하게 하는 방법을 못 찾았음. 실력 문제일 수도 있음
  • 이런 범용 해법은 늘 좀 혼란스럽게 느껴짐. 분산 데이터 사용은 본질적으로 결함과 특정 한계를 가지므로, 자기 사용 사례에 가장 가까운 해법을 골라야 함
    Graft의 아이디어는 좋지만 곧바로 몇 가지 복잡함을 만남. 예를 들어 네트워크가 불안정한 엣지 기기라면, 클라이언트가 필요한 순간에 필요한 페이지를 가져올 수 없을 수 있음
    오히려 클라이언트에게 정말 필요한 것은 접근이 필요할 때 기다리지 않도록 가능한 순간마다 모든 데이터를 가져오는 것임
    하지만 데이터가 많으면 영원히 걸릴 수 있음. 그래서 애플리케이션이 어떤 데이터를 요청하는지 더 똑똑하게 알아야 하고, 우선순위나 데이터 클래스를 만들어 가장 필요한 것을 먼저 가져와야 함
    이게 SQLite용으로 만들어졌다는 점도 같은 결론을 강화함. 분산 관계형 데이터베이스는 보통 나쁜 아이디어인데, 사람들이 대개 ACID 비슷한 것을 원하지만 분산 방식으로는 강제할 수 없기 때문임
    그래서 CRDT 같은 것이 존재함. 모든 “사본”을 독립적인 것으로 취급해, 같은 것을 여러 곳에 안정적으로 분산할 수 있다는 허구를 깨는 방식임
    관계형 데이터베이스가 복제에 잘 맞지 않는 이유도 여기서 드러남. 전체 관계형 데이터베이스를 CRDT로 복제하려면 셀마다 CRDT 하나가 필요하고, 데이터가 불어나게 됨

  • 비슷한 도구 링크 모음이 천천히 늘고 있어서 도움이 될까 하여 공유함
    https://tinybase.org/
    https://www.evolu.dev/
    https://replicache.dev/
    https://fireproof.storage/
    https://vlcn.io/
    https://www.instantdb.com/
    https://loro.dev/
    https://electric-sql.com/
    https://docs.y-sweet.dev/
    https://syncedstore.org/docs/
    https://collabs.readthedocs.io/en/latest/
    https://remotestorage.io/
    https://rxdb.info/offline-first.html
    https://github.com/siriusastrebe/jsynchronous
    https://www.powersync.com/
    https://pouchdb.com/
    https://jazz.tools/
    https://www.triplit.dev/
    https://automerge.org/
    https://www.dxos.org/
    언젠가 전부 써 보거나, 써 본 사람이 정리한 글을 읽어 보고 싶음

    • 왜 이렇게 많은지 궁금함. 아무도 실제로 필요로 하지 않는 것을 계속 재발명하는 건가 싶음
      개인적으로는 pouchdb, automerge, loro, 그리고 sqlsync까지 네 개를 이미 방문해 봄
      업무에서 다루는 기존 아키텍처에 이런 도구를 끼워 넣으려 했지만 아무것도 잘 맞지 않았음
      추측하자면 이런 해법들은 추상화 계층이 완전히 잘못됐음. 만든 사람들은 이것이 엄청난 혁신일 거라 생각하지만, 현실에서는 REST API와 클라이언트 쪽 영속 저장소면 99%는 충분함
      브라우저의 서비스 워커와 모바일 앱이 있으면 데이터 저장소를 두는 데 별 문제가 없음
      구체적인 부분 업데이트를 보내기보다 서버에서 전체 상태를 다시 불러오는 편이 사용자에게 설명하기도 쉽고 구현하기도 쉬움
      감사 로그를 조금 둔 마지막 쓰기 승리도 애플리케이션의 99.9%에는 충분하고 사람들에게 설명하기도 아주 쉬움
      반대로 데이터베이스 데이터의 충돌 병합은 설명도 구현도 쉽지 않음
      REST 요청 대신 전체 데이터베이스를 동기화하면, 서버 측 감사 로그가 조작되지 않게 만드는 것도 쉽지 않음
      이런 “동기화 데이터베이스” 접근은 10쪽짜리 에세이를 써야 하는 사람에게 좋다는 이유로 LaTeX 사용을 제안하는 느낌과 정확히 같음
    • “요즘 거의 모든 문제는 동기화 문제다”
      https://news.ycombinator.com/item?id=43434239
    • 아주 관련 있는 블로그 글을 썼음
      https://marcoapp.io/blog/offline-first-landscape
  • 정말 흥미로운 프로젝트이고 읽을거리도 좋았음. 많이 배웠음
    원격 메모리 선가져오기를 예측하는 데 쓰는 Leap 알고리즘(https://www.usenix.org/system/files/atc20-maruf.pdf)을 읽다가 꽤 깊이 빠져드는 중임
    libgraft의 SQLite 통합에 집중해서 Turso 등과 비교하기 쉽지만, 작성자가 이를 더 일반적이고 저수준의 분산 저장소 문제로 접근한 점이 좋음
    실제로 견고하다는 것이 입증된다면 SQLite 말고도 훨씬 많은 곳에 쓰일 수 있어 보임
    동시에 구체적 경험이 이끌지 않는 “저수준 범용 해법”은 종종 위험함
    작성자의 SQLSync 경험과 첫날부터 Graft를 SQLite에 적용한 점이, 범용 해법에 도전할 자격을 준다고 느껴짐
    특히 조정 책임을 애플리케이션·클라이언트 계층으로 옮긴 접근이 마음에 듦. 조정은 트레이드오프가 많은 영역이라, 애플리케이션이 어떻게 처리할지 깊이 고민하도록 요구하는 게 맞아 보임
    여기 질문 중 상당수는 기존 SQLite 복제 시스템과 비교를 요청하는데, 글 하단에 이 주제를 잘 다룬 섹션이 있음: https://sqlsync.dev/posts/stop-syncing-everything/#compariso...

    • 고마움. Graft를 SQLite/SQLSync 밖의 해법에 적용하는 것을 탐색하는 데 매우 기대하고 관심이 큼
      그래서 더 일반적으로 만들기로 한 것이 주요 동기였음
      지적한 대로 먼저 사용 사례를 개발하는 데 시간을 쓰고, 거기서 거꾸로 일반 해법으로 나아간 게 다행임
      반대로 갔다면 보지 못했을 실수를 많이 했음
      그리고 “Leap” 토끼굴에는 정말 꽤 깊이 빠졌고, 재미있는 주제임
  • 구현 세부사항까지 보지는 않아서 거기에는 말하지 않겠지만, 이건 정말 풀어야 할 중요한 문제임
    오래전부터 앱이 클라이언트와 서버 사이에서 변경사항을 동기화하는 데 쓸 수 있는 범용 동기화 계층을 원했고 필요했음
    앱 개발자가 별도 작업 없이 데이터베이스가 URL을 가리키게만 하면 동기화를 전부 처리해 주면 좋겠음
    그러면 앱 개발자는 노력 없이 개인 동기화를 지원할 수 있고, 사용자는 동기화만을 위해 구독료를 내지 않아도 됨
    NAS에서 동기화 서버를 돌리면 모든 앱이 그쪽과 데이터를 동기화하면 됨
    물론 앱 개발자들은 구독이 안정적인 수입원이기 때문에 원하지 않겠지만, 사용자들이 요구해 주면 좋겠음

    • 10년쯤 전에는 CouchDB가 이 문제의 답이었음
      2025년에 상태가 어떤지는 모르겠지만, 당시에는 예를 들어 비행기 판매용 태블릿이 지상에 돌아와 온라인이 되면 중앙 데이터베이스와 동기화하는 용도로 쓰였음
    • 고마움. 같은 방식으로 세상을 보고 있어 반가움
      나도 항상 위에 단순한 일관성 모델을 올릴 수 있는 범용 동기화 계층을 원했음
      Graft를 오픈소스로 만든 것도 바로 그걸 가능하게 하려는 것임
      어디든 배포하면 되고, 사용 사례를 더 잘 지원할 수 있도록 알려주면 좋겠음
  • 인상적임. VFS를 쓰는 건 정말 재미있는 “해킹”임
    우리가 만드는 노트·작업용 오프라인 우선 IDE [1]에서도 자체 동기화 엔진을 개발했는데, 자료구조는 아웃라이너 작업을 지원하기 위한 트리, 실제로는 그래프임
    충돌 해결은 항상 어려운 문제이고, 특히 트리에서는 여러 오프라인 사용자가 낙관적으로 로컬 변경을 커밋하면 전역 병합 후 유효하지 않은 트리 상태가 될 수 있음
    우리는 클라이언트가 온라인이 되어 다른 트리 삽입 델타를 받으면 트리 삽입을 롤백하는 방식으로 처리함
    SQLSync 설명을 보면 이를 처리하는 가장 좋은 방법은 최신 스냅샷을 가져온 뒤 재생하는 것처럼 이해됨
    하지만 전체 스냅샷을 가져오는 건 꽤 비싸게 들릴 듯함
    들어오는 델타 수가 아주 많을 때만 그렇게 하는 휴리스틱은 있지만, 대부분의 오프라인 동기화에서는 필요 없었음
    SQLSync가 이 스냅샷을 어떻게 정의하는지 궁금함
    노트 앱 예시로 말하면, 전이 포함 같은 그래프 기능 때문에 “노트” 하나의 스냅샷을 가질 수는 없음
    SQLSync에는 “reset and replay”를 위해 모든 상태를 보내지 않아도 되는 영리한 방법이 있는지 궁금함
    [1] https://thymer.com

    • 고마움. SQLite VFS 작성은 정말 좋아함
      SQLSync에서 큰 문제는 정확히 말한 것처럼 재생을 위해 전체 스냅샷을 가져와야 한다는 점이었음
      이것이 Graft 설계에서 “부분적” 측면을 넣은 핵심 동기임
      클라이언트는 들어오는 스냅샷 중 자신이 아직 갖고 있지 않고, 자기 트랜잭션의 읽기·쓰기 집합과 겹치는 부분만 가져오면 됨
      따라서 SQLSync가 Graft 기반이 되면, reset and replay를 위해 모든 상태를 내려받는 일을 자주 피할 수 있음
      클라이언트가 스냅샷 격리로 완화해도 괜찮다면, 특히 쓰기 스큐(Write Skew)[1]를 경험할 수 있다는 뜻이고, 로컬 트랜잭션의 읽기·쓰기 집합이 스냅샷 변경 집합과 교차하지 않으면 Graft가 자동 병합을 수행할 수 있음
      [1]: https://jepsen.io/consistency/phenomena/a5b
  • 흥미로워 보임. 머릿속에 넣기 매우 어려운 문제이고, 이걸 다루는 사람들은 확실히 분야를 앞으로 밀고 있음
    연결성이 매우 불확실한 React Native 앱 중 하나에서 한번 시도해 볼까 생각 중임

    • 연결성이 매우 불확실한 React Native 앱에서 써 보려 한다면, 비슷한 것들도 살펴볼 만함. 강한 의견은 없고, 이 분야를 조금 조사해 봐서 공유함
      https://rxdb.info
      https://www.powersync.com
      https://electric-sql.com
      https://dexie.org
      https://localfirstweb.dev도 좋은 링크임
    • 작업하기 재미있는 프로젝트였고, 이 토끼굴이 얼마나 깊은지 보게 되어 기대됨
      실제로 해보면 어땠는지 듣고 싶음
      아직 모바일에서 SQLite 확장을 돌려 보지는 못해서, 그쪽 도움은 매우 고마울 것 같음
      다른 아키텍처로 컴파일해야 할 수는 있어도 “그냥 동작”하길 바라고 있음