Object-Storage-Native WAL
“S3가 인터넷의 malloc이라면, conditional write는 그 위에 포인터 자료구조를 만드는 도구다.”
핵심 개념
Write-Ahead Log(WAL)는 전통적으로 로컬 디스크의 fsync에 durability를 의존해왔다. Kafka도, PostgreSQL도, RocksDB도 마찬가지다. 그래서 로그를 운영한다는 것은 곧 디스크 fleet을 운영한다는 것이었고, 디스크가 죽으므로 복제가 필요하고, 복제가 있으므로 리더 선출과 ISR과 rebalance가 필요했다.
Object-Storage-Native WAL은 이 계층을 통째로 들어낸다. durability를 오브젝트 스토리지(S3/GCS)에 위임하고, 로그 서버는 stateless 프로세스가 되어 오브젝트 스토리지를 겨누기만 한다. 이 접근을 실용적으로 만든 결정적 사건이 2024년 11월 S3의 conditional write 지원이다.
왜 지금 가능해졌나 — conditional write
S3에서 eventual → strong consistency 전환 이후 가장 실질적인 변화. writer가 두 가지를 명시할 수 있다:
| 헤더 | 의미 | 메모리 대응물 |
|---|---|---|
If-None-Match: * | 그 키가 존재하지 않을 때만 쓴다 | 원자적 할당 + 초기화 |
If-Match: <etag> | 현재 내용이 정확히 이 digest일 때만 덮어쓴다 | compare-and-swap |
이 두 프리미티브만 있으면 오브젝트 스토리지 위에 lock-free 자료구조를 직접 구성할 수 있다. 외부 조율 서비스(ZooKeeper, etcd, DynamoDB 락)가 필요 없다.
Jeff Bezos가 S3를 명세할 때 원했던 것이 **“인터넷을 위한 malloc”**이었다는 관점을 취하면 자연스럽다 — S3 키는 불투명한 가변 크기 메모리 영역을 가리키는 불투명한 포인터이고, conditional write는 그 포인터를 원자적으로 조작하는 명령어다.
30년 된 알고리즘의 재사용
Chroma의 wal3는 이 위에 교과서적인 lock-free 단일 연결 리스트 큐를 그대로 올렸다.
| 메모리 버전 | 오브젝트 스토리지 버전 |
|---|---|
노드 할당 (malloc) | fragment 파일을 if-none-match로 PUT |
| head 포인터 로드 | manifest 파일 GET |
| head CAS | manifest를 if-match로 PUT |
| 락 없이 동시 prepend | 락 없이 동시 append |
“억지가 아니다 — 메모리에 적용되는 같은 알고리즘이 오브젝트 스토리지에 적용될 수 있다.”
확장성: 컬렉션당 하나의 로그
중앙 집중 로그(단일 Kafka 클러스터)는 새 고객을 받는 능력을 본질적으로 제한한다. 오브젝트 스토리지 기반 로그는 disjoint prefix에 로그를 다중화함으로써 이 제약을 벗어난다.
scalable commutativity rule — 두 연산이 데이터를 공유하지 않으면 확장 가능한 구현이 존재한다. 오브젝트 스토리지에서는 컬렉션이 서로 다른 prefix를 가질 때 이 규칙이 자연스럽게 구현으로 번역된다. 각 컬렉션이 자기 전용 로그 = 자기 read/write 처리량 스케일링 단위를 갖는다.
Kafka에서는 확장 단위가 topic인데 topic이 대체로 클러스터에 묶여 있어, 실무에서는 topic 마이그레이션·topic 관리·일반 확장성을 다루는 자명하지 않은 shim 레이어를 위에 얹게 된다.
검증 레이어 — setsum
오브젝트 스토리지 WAL의 진짜 위험은 durability가 아니라 garbage collection이다.
불변·append-only 스토리지 기반 시스템에서 데이터를 지우는 곳이 정의상 하나뿐이면, 모든 의도적 데이터 손실을 한 곳에 집중시킨 것이고 거기가 데이터 손실 버그가 사는 곳이다.
wal3의 답은 setsum — 결합적·교환적(order-agnostic) 체크섬:
일반 체크섬: fn checksum(String) -> Digest
setsum: fn setsum([String]) -> Digest + digest에 대한 multi-set 연산
union(digest({A}), digest({B})) == digest({A, B}) 가 성립하되 계산 복잡도가 다르다. 집합의 addition/union/removal/difference를 digest에 대해 상수 시간에 수행한다.
로그의 단조성(쓰기는 항상 끝, GC는 항상 앞)과 결합하면 불변식이 나온다:
digest(GC된 prefix) ⊎ digest(라이브 데이터) == digest(전체 이력)
이 등식이 매 append·trim 후 O(1)로 유지되고, 깨지면 즉시 버그다. 실제로 idempotent GC 버그를 잡았다 — A, B, C에서 A, B를 두 번 GC하는 것은 허용되어야 하는데 setsum 구성이 그것을 깔끔히 탐지하지 못해 합이 맞지 않았다.
독립 구현 검증: 표준화된 setsum이 있으면 서로 다른 바이너리·릴리스·구현으로 같은 로그의 digest를 계산해 일치를 확인할 수 있다. 서로 다른 구현이 같은 버그를 보일 가능성은 극도로 낮다.
”시스템은 두 번 만들어져야 한다”
wal3가 남긴 가장 일반화 가능한 원칙:
최고의 시스템은 두 번 만들어져야 함을 인식하고 — 기능을 위해 한 번, 검증을 위해 한 번 — 그것을 설계 안에 인코딩한다.
프로그래머에게 시스템의 현실은 픽션이다. 볼 수 없고 관측성 메커니즘을 통해서만 본다. 지표·로그·trace는 시스템을 닮았지만 결정적으로 시스템 자체가 아니다. wal3는 사실상 세 시스템을 만들었다 — 데이터 로그, 그 digest, 그리고 둘의 공존을 보여주는 관측성. 셋이 얽혀서 steady state에서 시스템이 자기 검증적이 된다.
대가는 명확하다: 서로 얽힌 같은 코드의 두 병렬 구현을 개발해야 하고 한쪽 변경이 다른 쪽에 반영되어야 한다. 엔지니어링 규율이 필요하다.
언제 쓰고 언제 안 쓰는가
쓸 때
- 테넌트/컬렉션 수가 확장 축일 때 — 로그 하나당 관리 비용이 거의 0이어야 하는 경우
- 운영 인력이 얇을 때 — 관리할 디스크·persistent volume·리더 선출이 없다
- durability를 하드웨어가 아니라 운영자 실수 방지로 프레이밍할 때 (11 nines보다 실수가 훨씬 가능성 높다). 오브젝트 스토리지에 베팅하고 delete 경로만 신중히 감사하면 된다
- 인프라를 소유하고 싶을 때 — 코드를 채택하면 소유하게 된다. 소유하지 않으면 소유한 시스템과 그 위에 만드는 시스템 사이의 impedance mismatch를 우회하느라 타협한다
안 쓸 때 / 대안
- 레이턴시가 마이크로초 단위로 중요할 때 — 오브젝트 스토리지 PUT은 수십~수백 ms
- Kafka 프로토콜 호환이 요구사항일 때 — wal3는 prefix 아래 로그에 대한
read/append/trim만 지원한다. 이 단순함이 구현 면에서 유리하게 작용했지만, 그 제약이 유스케이스에 영향을 주는지 신중히 평가해야 한다 - WarpStream — 오브젝트 스토리지 위의 disk-less Kafka 호환 구현으로 가장 직접 비교 가능하다. Kafka를 충실히 재현했지만 오픈소스가 아니다
- CDC — SQL DB 위 CDC로 쓰기 이력을 roll up할 수 있지만 컬렉션당 독립 스트림이 필요하고, 대상 프리미티브에 CDC 시스템이 없으면 처음부터 만들어야 한다
비교
| 축 | Kafka | WarpStream | wal3 |
|---|---|---|---|
| durability 원천 | 로컬 디스크 + 복제 | 오브젝트 스토리지 | 오브젝트 스토리지 |
| 확장 단위 | topic (클러스터에 묶임) | topic | prefix = 컬렉션 |
| 운영 부담 | 디스크 fleet | 낮음 | 낮음 (stateless) |
| 오픈소스 | ✅ | ❌ | ✅ (Rust) |
| API 표면 | 큼 | Kafka 호환 | read/append/trim |
| 내장 검증 | — | — | setsum 불변식 |
연관 개념
- Object Storage Evolution — conditional write, S3 Express One Zone 등 오브젝트 스토리지 자체의 진화
- Database Concurrency Control — CAS, lock-free, MVCC와의 관계
- Silent Failures and Data Integrity — 조용히 틀리는 시스템을 어떻게 잡는가
- Data Sketches — setsum 계열 상수 시간 집계 스케치
- Idempotency — idempotent GC와 exactly-once
- Disaggregated OLTP Architecture — 스토리지/컴퓨트 분리라는 같은 방향의 움직임
- Distributed Systems Reliability
- Real-Time Stream Processing
Source: wal3 - A Write-Ahead Log for Chroma Built on Object Storage