- 복식부기용 OLTP 데이터베이스 TigerBeetle은 안전성과 속도를 앞세우며, Jepsen은 0.16.11~0.16.30 계열을 3~6대 Debian 클러스터에서 장애 주입과 함께 검증함
- 테스트는 명시적 타임스탬프 순서와 문서 기반 단일 스레드 상태 머신 모델을 결합해 Strong Serializability와 계정·전송·쿼리 의미론을 함께 확인함
- 주요 안전성 버그는 다중 필터 쿼리 결과 누락과 Java 클라이언트 헤더 타임스탬프 오류였으며, 0.16.26 이상에서는 여러 장애 조합에서도 Strong Serializability 주장과 일치하는 결과가 관찰됨
- 가용성 측면에서는 클라이언트 무한 재시도, 세션 eviction 시 프로세스 크래시, 단일 노드 장애 지연 급증, 디스크 비트플립·업그레이드 중 서버 panic, 단일 노드 디스크 손실 복구 경로 부재가 드러남
- TigerBeetle 0.16.43은 보고된 이슈 대부분을 반영해 단일 노드 장애 지연 완화와
tigerbeetle recover를 포함하며, 운영자는 0.16.43 업그레이드와 0.16.26 이상 전환 시 릴리스 노트 확인이 필요함
TigerBeetle의 설계와 테스트 범위
- TigerBeetle은 복식부기용 OLTP 데이터베이스로, 임의의 행·객체·그래프·blob 대신 계정(accounts)과 전송(transfers)만 저장함
- Viewstamped Replication(VR)을 기반으로 Strong Serializable 일관성을 제공한다고 약속하며, 금융 거래·재고·티켓팅·유틸리티 계량 같은 모델에 맞춰 설계됨
- 고경합·고처리량 워크로드에 맞춰 모든 쓰기를 primary VR 노드의 단일 코어로 통과시키며, scale-out보다 scale-up에 초점을 둠
- 성능을 위해 배치 처리, I/O 병렬화, 고정 스키마, 고정 크기·캐시 정렬 데이터 구조를 사용함
- 장애 모델은 메모리, 프로세스, 시계, 스토리지, 네트워크를 명시적으로 다룸
- 프로세스는 멈추거나 크래시할 수 있음
- 시계는 앞뒤로 점프할 수 있음
- 디스크는 완전 실패뿐 아니라 부분 쓰기 손상과 데이터 오염도 겪을 수 있음
- 네트워크는 지연, 드롭, 중복, 오배송, 메시지 손상을 일으킬 수 있음
- TigerBeetle은 결정론적 시뮬레이션 테스트를 사용하며, VOPR 테스트는 클러스터 전체와 시계·디스크·네트워크 인터페이스를 시뮬레이션함
데이터 모델과 요청 의미론
- 데이터 모델은 accounts와 transfers 두 종류의 레코드로 구성됨
- 계정은 사용자 지정 128비트
id,ledger,flags,timestamp,code,user_data_32,user_data_64,user_data_128등을 가짐 - 전송은
debit_account_id,credit_account_id,amount,ledger,flags, 사용자 지정 필드 등을 포함하는 불변 레코드임
- 계정은 사용자 지정 128비트
- 전송은 단일 단계로 즉시 post될 수 있고, pending과 post/void로 나뉘는 2단계 전송도 가능함
- pending transfer는 debit·credit 계정의 용량을 예약함
- 이후 pending amount 이하를 post하거나 void할 수 있음
timeout필드로 자동 만료를 제어함
- 계정은
closed플래그와 네 개의 잔액 필드를 제외하면 불변이고, 전송은 항상 불변임- 전송을 변경하거나 되돌리려면 보상 전송을 새로 만들어야 함
- 요청은 단일 종류의 논리 연산을 나타내며, 보통 최대 8190개 이벤트 배치를 포함함
create_accounts,create_transfers는 쓰기 요청임lookup_accounts,lookup_transfers,query_accounts,query_transfers,get_account_transfers,get_account_balances는 읽기 요청임
- 각 요청은 데이터베이스 관점에서 하나의 트랜잭션이지만, 커밋된 요청 안의 일부 이벤트는 논리적으로 실패해 오류 코드를 반환할 수 있음
- 이벤트 간 조건부 원자성이 필요하면 chain을 사용해 같은 chain 안의 이벤트가 모두 성공하거나 모두 실패하게 함
Jepsen 테스트 방식
- Jepsen 테스트 스위트는 Jepsen testing library를 사용해 속성 기반 테스트와 장애 주입을 결합함
- 테스트 대상은 TigerBeetle 0.16.11부터 0.16.30까지이며, 여러 개발 빌드도 포함됨
- 클러스터는 3~6대 Debian 노드로 구성됨
- LXC 컨테이너와 EC2 VM 양쪽에서 실행됨
- TigerBeetle의 공식 클라이언트는 모든 노드에 연결하는 smart client라서 동시성 오류를 숨길 수 있음
- Jepsen은 일반 smart-client 동작도 테스트함
- 동시에 각 클라이언트를 단일 노드에 제한하는 방식도 사용함
- 검증기는 두 단계로 동작함
- 성공한 요청의 실행 타임스탬프를 읽고, 실패·타임아웃된 쓰기는 이후 관찰된 효과에서 타임스탬프를 추론함
- 추론된 타임스탬프 순서대로 문서 기반 TigerBeetle 상태 머신 모델을 실행해 결과와 오류 코드를 검증함
- 상태 머신 모델은 1,600줄 이상의 Clojure로 작성됐으며, 계정·전송 맵, 인덱스, transient error, 내부 통계, 시계 흐름 등을 포함함
- 중복 ID, 비단조 타임스탬프, 잔액 제약, 호환되지 않는 플래그, chain의 speculative execution과 rollback 등을 처리함
- 고성능 영속 자료구조 라이브러리 Bifurcan을 사용함
장애 주입과 파일 손상 테스트
- Jepsen은 프로세스
SIGKILL,SIGSTOP, 여러 형태의 네트워크 파티션, 밀리초부터 수백 초까지의 시계 변경, 빠른 시계 왕복 변경을 주입함 - 테스트 중 여러 버전으로 노드 업그레이드도 수행함
- 새 파일 손상 nemesis로 다양한 스토리지 장애를 만들었음
- 랜덤 비트 flip으로 우주선 간섭 같은 손상을 시뮬레이션함
- 파일 청크를 다른 청크로 대체해 misdirected write를 시뮬레이션함
- 파일 청크 스냅샷을 나중에 복원해 lost write를 시뮬레이션함
- TigerBeetle 노드는 단일 데이터 파일을 가지며, 파일은 예측 가능한 offset의 zone으로 나뉨
- WAL header, superblock zone의 중복 copy 등 특정 zone만 손상시키는 테스트를 수행함
- 여러 zone이나 전체 파일을 손상시키는 테스트도 포함됨
- “helical” disk fault는 모든 노드의 파일을 손상시키되 각 노드마다 다른 청크를 손상시키는 방식임
- TigerBeetle의 최신 replica 파일 배치가 일반적으로 bit-for-bit 동일하기 때문에, 단일 레코드가 모든 replica에서 복구 불가능하게 손상되는 상황을 피하려는 목적임
- WAL의 head는 노드별 위치가 다를 수 있어 예외가 됨
발견된 안전성 문제
- 0.16.13에서
query_accounts,query_transfers,get_account_transfers응답이 일부 또는 전체 결과를 누락하는 문제가 자주 발생함- 누락된 결과는 항상 응답 끝부분이었고, 응답은 올바른 결과의 prefix였음
- 단일 필터 쿼리에서는 나타나지 않았고,
ledger와code같은 다중 필터 조합에서 발생함 - 원인은 여러 인덱스 간 zig-zag merge join의 bounds check 버그였음
- #2544로 추적됐고 0.16.17에서 수정됨
- 0.16.13에 Jepsen 테스트 지원용으로 추가된 Java 클라이언트 헤더 API가 잘못되거나 중복된 실행 타임스탬프를 반환함
- 원인은 Java 클라이언트의 mutable singleton 응답 객체
Batch.EMPTY였음 - 성공 응답이 빈 batch로 표현되면서 여러 응답이 같은 객체의 header를 덮어씀
- #2495로 수정됐고 0.16.14에 반영됨
- 실제 데이터 정합성에는 영향을 주지 않고 Java 클라이언트 header API의 요청 타임스탬프에만 영향을 줌
- 원인은 Java 클라이언트의 mutable singleton 응답 객체
- 0.16.26 이상에서 관찰된 결과는 TigerBeetle의 Strong Serializability 주장과 일치함
- 프로세스 pause, crash, 네트워크 partition, clock error, disk corruption, upgrade 조합에서도 해당 속성이 유지됨
클라이언트와 요청 처리 문제
- TigerBeetle 문서는 요청이 timeout되지 않고 클라이언트가 응답을 받을 때까지 계속 retry한다고 설명함
- Java 비동기 메서드는
CompletableFuture를 반환하며.get(timeout, timeUnit)이나.orTimeout(...)같은 timeout API를 사용할 수 있음 - .NET 클라이언트의
Task도 timeout 기반Wait()를 제공함
- Java 비동기 메서드는
- 무한 retry는 definite error와 indefinite error를 모두 감출 수 있음
- 예를 들어 TCP 연결이
ECONNREFUSED로 실패하면 해당 원 요청은 실행될 수 없다는 definite failure임 - 하지만 클라이언트가 이를 호출자에게 알리지 않고 내부 retry만 계속하면, 호출자 관점에서는 timeout이나 중단 같은 indefinite failure가 됨
- 예를 들어 TCP 연결이
- 이 문제는 #206에서 논의 중이며, 보고서 기준으로 unresolved임
- Jepsen은 definite error와 indefinite error를 1급으로 표현하고 호출자에게 반환할 것을 권고함
- 자동 retry는 유지할 수 있지만 설정 가능해야 하며, 연결 개시와 응답 대기 최대 시간을 옵션으로 둘 것을 권고함
- 0.16.11 Java 클라이언트는 timeout 처리를 위해 동기 호출 스레드를 interrupt하거나 비동기 호출 후 client를 close할 때 JVM 전체가 segfault되는 문제가 있었음
- 원인은 request data structure의 unset field였음
- 클라이언트가 request 생성과 제출 사이에 닫히면 Zig 기본값인
0xaaa...주소를 역참조함 - #2435로 수정됐고 0.16.12에 반영됨
- 공식 클라이언트는 서버가 session eviction을 알릴 때 프로세스 전체를 crash시켰음
- TigerBeetle은 기본적으로 concurrent session을 64개로 제한함
- 서버보다 새로운 클라이언트 버전을 사용할 때도 eviction이 발생함
- #2484 이후 0.16.13부터는 eviction 시 프로세스 crash 대신 호출자에게 error를 반환함
단일 노드 장애 시 지연 급증
- 단일 노드 장애에서 client latency가 3~5자릿수까지 증가하는 사례가 반복됨
- 5노드 클러스터에서 단일 노드를 죽이면 minimum latency가 1ms 미만에서 10초로 증가함
- 3노드 클러스터에서 한 노드를 죽인 테스트에서는 1~50ms였던 latency가 요청당 약 100초까지 증가하고, 노드 재시작 전까지 거의 1000초 동안 지속됨
- 원인은 TigerBeetle의 prepare 전파 방식과 관련됨
- 전통적인 VR은 primary가 모든 secondary에 prepare를 보내고 ack를 직접 받음
- TigerBeetle은 노드를 ring으로 배치하고 primary가 다음 secondary에 prepare를 보내면, 각 secondary가 다음 노드로 전달함
- 이 방식은 노드 하나의 bandwidth 요구를 줄이지만, ring의 다음
f개 replica 중 하나가 실패하면 commit이 막힐 수 있음
- 이 문제는 #2739로 추적됨
- 0.16.30은 prepare 메시지 절반을 ring 반대 방향으로 보내는 방식으로 완화함
- 일부 prepare가 장애 노드를 우회할 수 있음
- Jepsen 테스트에서 100초대 latency가 1~30초 수준으로 줄어듦
- 0.16.43은 추가 성능 개선을 포함함
- 노드가 ring 양방향으로 replicate함
- ring topology가 동적으로 바뀌며, 클러스터가 네트워크 조건과 장애에 따라 노드 순서를 조정함
디스크 손상과 서버 crash
- 0.16.20에서 superblock, WAL, grid zone의 단일 비트 손상이 startup crash를 유발하는 경우가 있었음
- 로그는
panic: reached unreachable code를 출력하고 종료됨 - 원인은 sector padding 검사 버그였음
- 로그는
- TigerBeetle checksum은 청크의 데이터는 덮지만 padding은 제외함
- padding의 0 비트가 1로 바뀌면 checksum은 통과함
- 이후 padding이 여전히 0인지 확인하는 assertion이 실패해 서버가 crash함
- padding 손상은 safety를 해치지 않으며, 다시 0으로 만들거나 다른 replica에서 복구할 수 있음
- VOPR은 기존에 sector 전체를 손상시켰기 때문에 이 버그를 찾지 못함
- sector 손상은 checksum 실패와 repair 경로를 유발했고, padding assertion까지 도달하지 않았음
- TigerBeetle은 #2681에서 VOPR에 single-byte error를 추가함
- 0.16.26부터 padding 손상 sector를 crash 대신 repair함
- superblock copy number 비트플립도 같은 panic을 일으킬 수 있었음
- superblock 네 copy는 각기 다른 2바이트
copy번호를 가지며, checksum은 이 번호를 건너뜀 - 디스크에서 손상된 copy number가 메모리로 읽힌 뒤 write 시 0~3 범위 assertion을 실패시킴
- 0.16.26에서 copy number를 reset하는 방식으로 해결됨
- superblock 네 copy는 각기 다른 2바이트
업그레이드 관련 문제
- 0.16.25 이하에서 0.16.26 이상으로 업그레이드할 때
panic: checkpoint divergedcrash가 반복 관찰됨- 원인은 0.16.26의
CheckpointState구조 변경이었음 - 새 버전은 released blocks 집합을 포함했지만, 이전 버전 호환 상태 전송 과정에서 이 정보가 비어 있을 수 있었음
- 이후 노드가 0.16.26으로 재시작하면 다른 replica가 아는 released blocks를 잃은 상태가 될 수 있음
- assertion이 divergence를 감지하고 crash해 클라이언트가 불일치 데이터를 관찰하지 못하게 함
- 원인은 0.16.26의
- 이 문제는 #2745로 changelog에 문서화됨
- TigerBeetle은 패치된 0.16.26을 릴리스하지 않음
- 운영자는 0.16.26 이상으로 업그레이드하기 전 client를 멈추고 replica catch-up을 기다려야 함
- 0.16.16에서 0.16.28로 여러 upgrade를 약 20초 안에 연속 수행하거나 upgrade 중 노드가 pause/crash될 때
release_transitionassertion failure가 발생함- 실행 중인 노드는 새 binary를
memfd로 열고exec()로 바꾸지만, 그 사이 디스크의 binary가 더 새 버전으로 교체될 수 있음 - 코드가 디스크의 version header까지 현재 실행 버전과 같다고 assert하면서 실패함
- #2758로 0.16.29에서 assertion을 warning으로 바꿈
- 실행 중인 노드는 새 binary를
- 0.16.26에서 0.16.27로 업그레이드할 때 deprecated message type 때문에
panic: switch on corrupt value가 발생함- 새 노드의
switch문에 오래된 메시지 타입 case가 없어 crash함 - #2763로 0.16.29에서 deprecated message type을 다시 case에 넣고 무시하도록 수정함
- 새 노드의
단일 노드 디스크 손실 복구
- TigerBeetle은 파일 손상에 강하지만, 디스크 실패·화재·EBS volume error·운영자 실수 등으로 노드의 데이터 파일 전체가 없어지거나 복구 불가능하게 손상될 수 있음
- 보고서 시점의 문서에는 실패 노드 교체 방법이 없었고, undocumented recovery 절차로
tigerbeetle format을 실행해 빈 데이터 파일로 초기화한 뒤 repair를 기대하는 방식이 있었음 - Jepsen은 reformat이 대부분 동작하지만 안전하지 않을 수 있음을 확인함
- 3노드 중 2노드에 committed operation
op가 있고 그중 하나를 reformat하면,op를 관찰하지 못한 2/3 majority가 view change를 수행해 operation이 손실될 수 있음 - 실제 테스트에서 acknowledged transfer 5개가 손실된 run이 있었음
- upgrade 중 newer binary로 format된 노드가 cluster version transition을 완료하기 전 startup crash하는 사례도 있었음
- 3노드 중 2노드에 committed operation
- 이 문제는 #2767로 추적됨
- 이후 TigerBeetle 0.16.43은 catastrophic data loss를 겪은 노드를 복구하는
tigerbeetle recover명령을 포함함
Jepsen의 결론과 권고
- 발견된 안전성 문제는 두 가지임
- 0.16.17 이전의 다중 필터 쿼리 결과 누락
- Jepsen 테스트용 Java 클라이언트 디버깅 API의 잘못된·중복 타임스탬프
- crash 문제는 총 7개였음
- Java 클라이언트 2개: uninitialized memory access, eviction 시 프로세스 crash
- 서버 5개: 디스크 손상 관련 panic 2개, 업그레이드 관련 panic 3개
- #2745는 문서화됐고, 나머지 crash는 0.16.29까지 해결됨
- 0.16.43은 보고서의 이슈 중 하나를 제외한 나머지를 해결함
- unresolved 항목은 client request가 설계상 계속 retry되는 문제임
- 사용자 권고는 명확함
- 0.16.43으로 업그레이드할 것
- 0.16.26으로 또는 그 이후 버전으로 넘어갈 때 release note를 확인할 것
- 테스트 환경에서 단일 노드 장애를 시뮬레이션하고 애플리케이션이 상승한 latency에 어떻게 반응하는지 측정할 것
- TigerBeetle의 아키텍처는 sound해 보이며, VR·flexible quorum·protocol-aware recovery 통합이 Strong Serializability 핵심 불변식을 훼손하지 않은 것으로 관찰됨
- 다만 Jepsen 검증은 실험적 접근이므로, 버그의 존재는 증명할 수 있지만 부재는 증명할 수 없음