제프슨: Datomic Pro 1.0.7075 분석
(jepsen.io)- Datomic Pro 1.0.7075는 테스트에서 트랜잭션 간 안전성이 문서상 주장보다 강해 보였지만, 트랜잭션 내부 의미론은 일반적인 직렬 실행 모델과 크게 달랐음
- 모든 테스트 히스토리는 Serializable로 보였고, 단일 peer 세션은 Strong Session Serializable, 쓰기와
d/sync읽기는 Strong Serializable에 가까웠음 - Datomic의 add, retract, transaction function은 트랜잭션 안에서 순서대로 누적 실행되지 않고, 각 함수가 시작 시점 DB 상태만 보는 방식으로 동작함
- 같은 트랜잭션에서
approve와deny처럼 독립적으로는 안전한 transaction function을 조합하면, 합성 결과가 불변식 위반을 만들 수 있음 - 여러 transaction function을 한 트랜잭션에 넣을 때는 read set과 write set 관계를 확인하고, entity predicate, attribute predicate, entity spec 같은 명시적 제약을 함께 써야 함
Datomic Pro의 모델과 아키텍처
- Datomic은 시간 개념을 명시적으로 모델링하는 Entity-Attribute-Value OLTP 데이터베이스임
- 특정 시점의 DB 상태는
[entity, attribute, value]형태의 datom 집합으로 표현됨 - 각 datom에는 어떤 트랜잭션이 추가하거나 철회했는지도 보존됨
- 전체 datom은
[entity, attribute, value, transaction, asserted-or-retracted?]형태의 5-튜플임
- 특정 시점의 DB 상태는
- Datomic은 temporal database라서 현재뿐 아니라 과거의 논리 시각이나 wall-clock 시각 기준 스냅샷을 요청할 수 있음
- 전체 히스토리 뷰로 과거에 특정 사실이 존재했는지도 질의할 수 있음
- 질의 방식으로 Datalog 스타일 API, 그래프 순회 API, ODM 스타일
Entity타입을 제공함
- Datomic Pro는 사용자가 직접 운영할 수 있는 버전이고, Datomic Cloud는 AWS에서 동작하며 아키텍처가 일부 다름
- Datomic Pro는 여러 구성요소가 협력하는 구조임
- Transactor는 쓰기 트랜잭션 실행, 인덱스 유지, 스토리지 기록을 담당함
- Peer는 JVM 라이브러리를 내장한 두꺼운 클라이언트로, 트랜잭션 제출, 스토리지 대상 읽기 질의, 캐시를 수행함
- 다른 언어 애플리케이션을 위해 thin client와 peer server 기반 client-server 모델도 제공함
- 내부적으로 Datomic은 각 트랜잭션을 시간순 log에 append하고, entity·attribute·value·time 조합에 따라 정렬된 4개 인덱스를 유지함
- log와 인덱스는 Cassandra나 DynamoDB 같은 스토리지에 영속적이고 불변인 트리로 저장됨
- 트리 노드가 불변이어서 backing storage는 eventual consistency만 보장해도 됨
- 커밋 시 transactor가 새 불변 트리 노드를 저장한 뒤 루트 포인터를 compare-and-set(CaS)로 전진시키며, 이 CaS에는 Sequential consistency가 필요함
- Sequential CaS는 트랜잭션의 전역 순서를 보장하지만, 쓰기 처리량을 단일 transactor 속도에 묶음
- Datomic은 보통 한 번에 하나의 active transactor만 두고, 장애 허용을 위해 여러 transactor를 배포함
- peer는 스토리지와 transactor에 직접 연결하고, 각자 단조 증가하는 루트 포인터 복사본을 유지함
- 읽기는 불변 트리 노드를 캐시할 수 있어 peer 수를 늘리면 거의 선형적인 읽기 확장이 가능함
Datomic의 트랜잭션 모델
- Datomic은 일반적인 OLTP 데이터베이스처럼 interactive transaction을 제공하지 않음
- 트랜잭션을 시작하고, 연산 결과를 받은 뒤, 다음 연산을 제출하고, 마지막에 커밋하는 모델이 아님
- stored procedure와 비슷한 transaction function은 있지만, 호출자에게 임의 값을 반환할 수 없음
- 읽기와 쓰기 경로는 엄격히 분리됨
db는 peer가 알고 있는 최신 DB 상태를 반환함d/sync는 모든 peer 기준 최신 상태나 특정 시각 이후 상태를 얻기 위해 transactor와 동기화함d/as-of는 과거 시점의 DB 상태를 얻음- DB 상태는 불변이라 같은 상태에 대한 여러 질의는 정확히 같은 논리 시각에서 실행됨
- 쓰기 트랜잭션은 ordered list 형태의 operation으로 표현됨
- 예시는
:db/add,:db/retract,db/cas, 사용자 정의 transaction function 호출을 포함함 - transaction function은 트랜잭션 시작 시점의 DB 상태와 인자를 받아 새 operation 집합을 반환함
- 함수 호출은 assertion과 retraction만 남을 때까지 재귀적으로 확장됨
- 예시는
- transaction function은 내부에서 읽기를 수행해 조건부 쓰기를 결정할 수 있지만, 읽기 결과나 임의 정보를
transact호출자에게 직접 반환하지 않음transact는 트랜잭션 직전 DB 상태, 트랜잭션 결과 DB 상태, 확장된 datom 집합을 반환함- 호출자는 pre-state와 post-state를 사용해 조건부 쓰기가 일어났는지 판단할 수 있음
- Datomic은 저렴하고 전달 가능한 DB 스냅샷을 중심으로 문제를 풀도록 설계된 모델임
- Nubank는 Datomic의 현 개발사로, 금융 서비스를 약 9,400만 사용자에게 제공하고 하루 평균 23억 사용자 트랜잭션을 처리함
- Nubank의 거의 모든 제품은 Datomic을 system of record로 사용함
일관성 주장과 테스트 설계
- Datomic 문서는 ACID 트랜잭션을 주장하며, 트랜잭션이 단일 atomic write로 스토리지에 기록되고 각 peer가 특정 시점까지 완료된 모든 트랜잭션을 총순서로 관찰한다고 봄
- 클라이언트 acknowledgement 전에 트랜잭션은 durable storage에 flush됨
- 2024년 1월 초 분석 시작 시점의 문서는 쓰기 트랜잭션이 Serializable이라고 비공식적으로 주장했음
- 문서는 Datomic을 “single-writer” 시스템이라고도 했지만, Jepsen은 이 설명이 두 가지 이유로 부정확하다고 봄
- 장애 허용을 위해 여러 transactor를 운영할 수 있고, 장애 감지기가 완벽할 수 없으므로 여러 transactor가 동시에 active라고 판단하는 창이 생길 수 있음
- 단일 transactor만 있더라도 네트워크 지연으로 스토리지 메시지가 다른 transactor 메시지와 interleave될 수 있음
- Datomic의 안전성은 “single-writer” 논리가 아니라 스토리지 CaS operation의 Sequential consistency에서 나온다고 평가됨
- 여러 concurrent transactor가 있어도 CaS가 안전성을 제공해야 함
- 테스트는 Jepsen testing library로 작성한 Datomic test suite를 사용함
- Datomic Pro 1.0.7075를 Debian Bookworm 노드 클러스터에 설치함
- 스토리지는 AWS DynamoDB table을 사용함
- 두 노드는 transactor를 실행하고, 나머지 노드는 peer를 실행함
- peer는 Datomic peer library를 사용하는 작은 Clojure 프로그램이었고, 테스트 연산을 위한 HTTP API를 노출함
- 테스트는
d/db를 사용하는 stale read 가능 모드와d/sync로 최신성을 보장하는 모드를 모두 실행함
- 테스트는
- fault injection은 transactor와 peer 모두에 적용됨
- 프로세스 pause, crash, clock error를 주입함
- transactor·peer 사이, 노드·스토리지 사이 네트워크 파티션을 만들었음
- Datomic garbage collection도 요청함
- transactor는 스토리지와 안정적 연결을 유지하지 못하면 스스로 종료됨
- AWS 밖 노드에서 기본 5초 timeout을 쓰면 정상 네트워크 변동으로도 몇 분마다 종료됐음
- EC2 테스트 환경에서도 1초 timeout에서는 10~20분마다 종료됨
- Datomic은 operator가 supervisor daemon으로 transactor를 재시작할 것을 권장하며, 테스트는
Restart=on-failure설정의 systemd service를 사용함
네 가지 워크로드
-
List Append
- List Append 워크로드는 Elle transaction checker와 함께 사용됨
- 논리적으로 정수 원소 리스트들을 다루며, 각 리스트는 정수 primary key로 식별됨
- 클라이언트는 리스트 읽기 또는 고유 원소 append로 구성된 무작위 트랜잭션을 수행함
- Elle는 aborted read, intermediate read, internal consistency 위반, 원소 순서 불일치를 검사하고 dependency graph cycle을 찾아 일관성 모델 위반을 판정함
- Datomic에서 리스트는 하나의 entity와 두 속성으로 인코딩됨
append/key는 primary key 역할을 함append/elements는 다중값 속성으로 리스트의 정수 원소를 저장함- 다중값 속성은 순서 없는 set이므로, Jepsen은 각 datom의 transaction timestamp로 원소를 정렬해 Elle가 필요한 순서를 얻음
- mixed read-write transaction이 없다는 제약은 transaction function과 pre-state 계산으로 우회함
- 쓰기는 transaction function으로 수행함
transact가 반환한 pre-state를 이용해 트랜잭션 내부 읽기가 무엇을 봤을지 계산함- 같은 함수를
transact에서 한 번, peer에서 pre-state 기반 읽기 보완용으로 한 번 실행함
-
List Append with CaS
- List Append with CaS 워크로드는
db/cas패턴을 사용함 - 사용자는
d/db로 현재 상태를 읽고, 예를 들어 값이 4일 때만 5로 바꾸는[:db/cas 123 :counter/value 4 5]를 제출할 수 있음 - 모든 쓰기에
db/cas를 사용하면 logical “user transaction” 위에 ad hoc Snapshot Isolation을 만들 수 있음 - 이 워크로드는 리스트를 다중값이 아니라 single-valued comma-separated string으로 저장함
- 트랜잭션 시작 시 읽고, 로컬에서 읽기·쓰기를 적용한 뒤, 읽은 뒤 변경되지 않았음을 보장하는 CaS 트랜잭션을 구성함
- List Append with CaS 워크로드는
-
Internal
- Internal 워크로드는 트랜잭션 내부 일관성을 직접 측정함
- 같은 entity attribute에 1을 assert한 뒤 2를 assert하는 경우
- 같은 트랜잭션에서 fact를 assert하고 retract하는 경우
- 값을 assert한 뒤 CaS로 바꾸려는 경우
- 1→2, 2→3 같은 여러 CaS를 수행하는 경우
- entity 생성 뒤 lookup ref로 수정하는 경우
- transaction function으로 값을 두 번 increment하려는 경우를 포함함
-
Grant
- Grant 워크로드는 transaction function이 함수 불변식을 보존하는지 확인함
- grant는
created-at,approved-at,denied-at세 속성을 가진 단일 entity로 인코딩됨 - grant는 동시에 approved와 denied 상태가 되면 안 됨
approve와deny함수는 먼저 grant가 이미 approved 또는 denied인지 검사하고, 필요하면 abort함- 여러 transaction boundary 조합에서 grant가 동시에 approved와 denied가 되는지 검사함
테스트 결과: 트랜잭션 간 안전성은 강하게 보임
- Jepsen은 Datomic의 핵심 안전성 주장에 반하는 동작을 찾지 못함
- 트랜잭션은 total order로 적용된 것처럼 보였음
- 그 순서는 각 peer에서의 local operation order와 일치함
- 쓰기 트랜잭션만으로 제한한 히스토리와, 읽기에서
(d/sync conn)을 사용한 히스토리는 real-time order와 일치했음- Jepsen은 이를 Strict Serializable로 보인다고 봄
- 단일 peer에 session을 묶어 해석하면 Datomic은 Strong Session Serializability를 보장하는 것으로 보임
- 트랜잭션 히스토리는 어떤 total order로 실행된 히스토리와 구별되지 않음
- 그 order는 각 peer에서 관찰된 순서와 일치함
d/db는 비동기로 갱신되는 DB 복사본을 반환하므로 stale read가 가능함- Datomic 문서도 peer read가 최근 커밋된 트랜잭션 일부를 관찰하지 못할 수 있음을 명시함
d/sync는 transactor와 동기화해 stale read를 방지함
- 실험적 검증에는 한계가 남아 있음
- 버그의 존재는 증명할 수 있지만 부재는 증명할 수 없음
- Datomic이 의존하는 스토리지 시스템의 correctness error는 Datomic 보장 위반으로 이어질 수 있음
- DynamoDB 위 Datomic은 DynamoDB의 compare-and-set operation만큼 안전함
트랜잭션 내부 의미론: 순서가 아니라 동시성
- 대부분의 데이터베이스와 주요 트랜잭션 격리 형식화는 트랜잭션 내부에서 직렬 실행 의미론을 제공함
set x = 1; read x;라면 read는 일반적으로1을 봄- Adya, Cerone·Bernardi·Gotsman, Crooks·Alvisi·Pu·Clement 등의 형식화는 트랜잭션 내부 operation 순서와 “이전 write를 이후 read가 관찰한다”는 성질을 명시함
- Datomic의 transaction request는 ordered list지만, 실행은 그 순서를 보존하지 않음
- add, retract, transaction function은 서로 동시에 실행되는 것처럼 동작함
- transaction function은 항상 트랜잭션 시작 시점 DB 상태를 관찰함
- 이전 assertion, retraction, transaction function의 효과를 보지 않음
- 현재 값이
0인 entity에 같은 CaS를 두 번 넣으면, Datomic에서는 둘 다 시작 상태0을 보고 성공함
[[:db/cas 123 :internal/value 0 1]
[:db/cas 123 :internal/value 0 1]]
- 직렬 모델이라면 첫 CaS가 값을
1로 바꾸고 두 번째 CaS는 실패해야 함- Datomic에서는 두 CaS가 중복 assertion을 만들고 최종 값은
1이 됨
- Datomic에서는 두 CaS가 중복 assertion을 만들고 최종 값은
- 두 번의 increment transaction function도 직렬 모델과 다른 결과를 냄
[['internal/increment "x"]
['internal/increment "x"]]
- 시작 값이
0이면 직렬 모델의 결과는2임 - Datomic에서는 두 함수가 모두 시작 상태
0을 보고 최종 값은1이 됨 - transaction function은 앞선 assertion도 보지 않음
[[:db/add id-of-x :internal/value 1]
['internal/increment "x"]]
- Datomic에서는 최종 값이
2가 아니라1이 됨 - lookup ref도 트랜잭션 시작 시점 DB 상태를 사용함
- 같은 트랜잭션에서 entity를 추가한 뒤 lookup ref로 그 entity를 참조할 수 없음
- 이 경우
Unable to resolve entity오류로 abort됨
충돌 감지와 pseudo write skew
- Datomic은 같은 트랜잭션 안에서 단일 cardinality 속성에 서로 다른 값을 assert하면
:db.error/datoms-conflict로 abort함- 시작 값
0에 대해 값2를 assert하고, 동시에 increment function이 값1assertion을 만들면 충돌함 - 이 충돌 감지는 transaction function의 잘못된 합성에서 생기는 많은 놀라운 결과를 막을 가능성이 있음
- 시작 값
- write set이 서로 다른
[entity, attribute]쌍이면 충돌 감지만으로 불변식을 지키기 어려움- grant 워크로드가 이 상황을 보임
approve와deny는 각각 grant가 아직 approved도 denied도 아닌지 확인한 뒤 서로 다른 속성을 추가함
- 서로 다른 트랜잭션에서
approve와deny를 호출하면 Datomic의 Serializable 트랜잭션이 불변식을 보장함- 하지만 같은 트랜잭션 안에서 둘을 함께 호출하면 둘 다 시작 상태를 보고 성공함
[['grant/approve id]
['grant/deny id]]
- 결과 grant는
approved-at과denied-at을 모두 갖게 됨- “grant는 동시에 approved와 denied가 되면 안 된다”는 불변식이 깨짐
- Datomic의 in-transaction conflict checker는 두 함수가 서로 다른 속성에 assertion을 만들었기 때문에 이를 막지 않음
- 이 현상은 Berenson 등의 Write Skew와 유사함
- 두 함수가 서로의 효과를 보지 못해 read-write anti-dependency cycle이 생김
- transaction function을 트랜잭션처럼 본다면 Repeatable Read와 Serializability가 금지하는 G2-item anomaly와 유사함
- Datomic과 Nubank는 이 동작을 버그가 아니라 Datomic의 기대 동작으로 봄
- Nubank는 Datomic의 concurrent intra-transaction semantics를 유지할 계획임
entity predicate로 불변식 보강
- Datomic은 타입, uniqueness, 특정 attribute predicate, entity predicate 같은 제약 메커니즘을 제공함
- entity predicate는 모든 트랜잭션 효과가 적용된 candidate DB 상태와 entity ID를 받아, 커밋 허용 여부를
true또는false로 반환함 - 이름은 entity predicate지만 전체 DB 상태에 접근할 수 있어 특정 entity를 넘어선 전역 제약도 표현할 수 있음
- entity predicate는 모든 트랜잭션 효과가 적용된 candidate DB 상태와 entity ID를 받아, 커밋 허용 여부를
- grant 예제에서는
valid-grant?predicate로approved-at과denied-at이 동시에 존재하지 않도록 만들 수 있음
(defn valid-grant?
[db eid]
(let [{:grant/keys [approved-at denied-at]}
(d/pull db '[:grant/approved-at
:grant/denied-at]
eid)]
(not (and approved-at denied-at))))
- schema에는 entity spec을 추가해 이 predicate를 참조함
- entity spec과 연결된 entity predicate는 모든 트랜잭션에 자동 적용되지 않음
- Datomic은 entity spec 적용 여부를 domain decision으로 보고, 각 트랜잭션이 명시적으로 요청해야 한다는 입장임
approve와deny함수는 속성을 추가한 뒤:db/ensurevirtual datom으로 entity spec 적용을 요청할 수 있음
(defn approve
[db id]
[[:db/add id :grant/approved-at (Date.)]
[:db/add id :db/ensure :grant/valid?]])
- 이 entity spec을 사용하면 같은 트랜잭션에서 approve와 deny를 함께 시도할 때 entity predicate 오류가 발생하고 불변식이 보존됨
- 오류에는
:db.error/entity-pred,:db.error/pred-return false가 포함됨
- 오류에는
문서 변경과 사용자 권고
- Datomic은 Jepsen과 협업 후 문서를 크게 수정함
- transaction safety documentation은 Datomic이 실제로 제공한다고 보는 더 강한 안전성을 반영함
- 전역 Serializability, peer별 monotonicity, 쓰기 또는
sync를 사용한 읽기에 대한 Strict Serializability를 명시함 - “single-writer” 논거는 안전성 문서에서 제거됨
- transaction syntax and semantics 문서는 transaction request 구조, map form과 transaction function 확장 규칙, transaction 적용 과정을 포괄적으로 다룸
- transaction functions 문서도 수정됨
- consistency를 보장하는 여러 메커니즘, 함수 생성과 호출, 내장 함수 동작을 설명함
- transaction function이 “atomically analyze and transform database values”할 수 있다거나 “atomic read-modify-write processing”을 보장한다는 표현은 제거됨
- Datomic은
d/transact에 전달되는 자료구조를 앞으로 “transaction”이 아니라 transaction request로 부르려 함- 그 요소는 “statements”나 “operations” 대신 “data”로 부르려 함
[:db/add ...]와[:db/retract ...]는 각각 assertion request와 retraction request임- 이는 실제 assertion datom과 transaction request 안의 불완전한 assertion request를 구분하는 데 도움이 됨
- 사용자에게 필요한 주의점은 명확함
- Datomic의 트랜잭션 간 Serializability는 신뢰할 수 있음
- 트랜잭션 내부의 concurrent execution semantics는 흔치 않은 선택이므로, 여러 transaction function을 같은 트랜잭션에서 호출할 때 주의해야 함
- 특히 read set이 겹치고 write set이 분리된 경우를 조심해야 함
- 여러 increment가 조용히 하나의 update로 collapse될 수 있음
- attribute predicate와 entity spec을 사용할 수 있지만, entity spec은 필요한 모든 트랜잭션에서 명시적으로 요청해야 함
- 운영 측면에서도 transactor 재시작과 네트워크 변동을 고려해야 함
- Datomic transactor는 몇 분 동안 스토리지와 통신하지 못하면 스스로 종료됨
- Jepsen은 네트워크 변동에 더 강하도록 transactor에 retry loop를 추가할 것을 권고함
한계와 향후 연구 질문
- 이번 테스트에는 평가 범위 밖 항목이 남아 있음
- excision과 historical query는 평가하지 않았음
- Datomic client library도 조사하지 않았지만, Jepsen은 그 동작이 테스트용 peer와 유사할 가능성이 있다고 봄
- 스토리지 엔진은 DynamoDB 하나만 사용함
- Datomic Cloud도 평가하지 않았고, Datomic Cloud는 약간 다른 아키텍처를 사용함
- Jepsen은 트랜잭션 간 Serializability와 트랜잭션 내부 concurrent semantics를 함께 제공하는 시스템이나 형식화를 거의 알지 못한다고 밝힘
- Datomic의 모델은 여러 연구 질문을 만듦
- Datomic transaction을 전통적 transaction의 dual이나 “co-transaction” 모델로 볼 수 있는지
- 이런 모델의 장단점을 static analysis, runtime check, API extension으로 완화할 수 있는지
- 실제 사용자가 불변식을 깨는 transaction을 작성할 가능성이 얼마나 되는지
- 비교 대상으로 temporal Datalog 연구 프로젝트인 Alvaro’s Dedalus와 Fauna가 언급됨
- Dedalus도 Datomic처럼 transaction이 “all at once” 일어남
- Fauna는 temporal database이면서 Strong Serializability까지 지원하고, Datomic과 달리 트랜잭션 내부에서 serial execution과 incremental side effect를 제공하는 것으로 보임
- Datomic의 end-of-transaction conflict checker와 Snapshot Isolation의 first-committer-wins 규칙 사이의 유사성도 연구 기회로 남아 있음
- Snapshot Isolation 문헌 중 어떤 부분을 Datomic에 적용할 수 있는지
- Datomic transaction 내부의 cycle이 어떤 anti-dependency edge로 표현되는지
- lost update, Fractured Read, read-only transaction anomaly, Long Fork 같은 현상에 대응되는 내부 의미론상의 analogue가 있는지 질문이 남음
- CALM theorem과의 연결도 추가로 살펴볼 수 있음
- 논리적으로 monotonic한 transaction function은 같은 Datomic transaction 안에서 안전하게 결합될 수 있는지
- negation 없는 Datalog 프로그램이 이 실행 모델에서도 안전한지 연구할 수 있음
댓글과 토론
Hacker News 의견들
-
이 작업이 진행되는 걸 옆에서 지켜봤는데, 논의 과정을 보는 게 정말 흥미로웠음
Jepsen이 치명적인 버그를 찾지 못한 것도 놀라웠고, 문서와 의도된 특이 동작을 명확히 한 것만으로도 매우 유용한 결과였음
Datomic으로 은행을 운영하고 있다는 점을 생각하면, 신뢰를 쌓는 훈련으로 충분히 가치가 있었음- Rich Hickey가 설계한 데이터베이스라는 점을 생각하면 결과가 그리 놀랍지는 않음
정말 훌륭한 글이고, 스스로 꽤 똑똑하다고 느낄 때마다 Jepsen 분석을 읽으면 겸손해지기 좋음 - 혹시 어느 은행인지 물어봐도 될까요?
- “Jepsen이 치명적인 버그를 찾지 못한 것도 놀라웠다”는 부분에 대해, 보고서에는 “버그의 존재는 증명할 수 있지만, 버그의 부재는 증명할 수 없다”고 되어 있음
- 은행을 그 위에서 운영하기 전에 이런 검증을 직접 해보지는 않았나요?
- Rich Hickey가 설계한 데이터베이스라는 점을 생각하면 결과가 그리 놀랍지는 않음
-
Jepsen 보고서를 깊게 읽어본 건 이번이 처음인데, Datomic의 트랜잭션 내부 동작을 명확히 설명한 부분이 마음에 들었음
Datomic 트랜잭션과 SQL 데이터베이스 트랜잭션의 차이를 내가 얼마나 이해하지 못했는지도 깨달았음
특히 “Datomic은 예전에는d/transact에 전달되는 자료구조를 ‘transaction’이라고 부르고, 그 요소를 ‘statements’나 ‘operations’라고 불렀다. 앞으로는 이 구조를 ‘transaction request’라고, 그 요소를 ‘data’라고 부르려 한다”는 문단이 눈에 띔
이게datomic.api네임스페이스의 d/transact-async와 관련 기능에는 어떤 의미인지 궁금함
거의 1년 동안 Datomic을 쓰지 않았는데, 많은 게 바뀐 것처럼 보임- Jepsen 테스트 결과로 Datomic 소프트웨어에는 변경이 필요 없었음
datomic.api의 모든 기능은 그대로임
- Jepsen 테스트 결과로 Datomic 소프트웨어에는 변경이 필요 없었음
-
정말 좋은 데이터베이스에 대한 훌륭하고 자세한 보고서임
문서가 명확해지고 갱신되는 것도 매우 반가움
덧붙이면 Apple이 FoundationDB에 대한 Jepsen 분석 비용을 내줬으면 정말 좋겠음
Aphyr가 “그들의 테스트가 아마 더 낫다”고 말한 건 알지만, Jepsen이 실제로 FoundationDB에서 문제를 찾지 못한다면 또 하나의 훌륭한 데이터베이스라는 강한 근거가 될 것임- Aphyr의 생계를 뺏고 싶은 마음은 전혀 없지만, 충분히 동기 있는 기여자가 Jepsen 테스트를 직접 만드는 게 특별히 불가능한 이유가 있는지, 아니면 “GoFundMe 비슷한 방식”으로도 감당하기 어려울 만큼 비싼지 궁금함
이 분야를 잘 아는 건 아니지만, “$foo가 비용을 내줬으면”이라는 말을 보면 귀가 솔깃함
자본은 넘쳐나는데 Apple이 뭔가 해주길 기다리는 건 내 경험상 오래 걸림
- Aphyr의 생계를 뺏고 싶은 마음은 전혀 없지만, 충분히 동기 있는 기여자가 Jepsen 테스트를 직접 만드는 게 특별히 불가능한 이유가 있는지, 아니면 “GoFundMe 비슷한 방식”으로도 감당하기 어려울 만큼 비싼지 궁금함
-
Jepsen이 불변 조건 위반으로 이어지는 명확한 상황을 찾아냈는데, Datomic 쪽 대응은 문서 명확화뿐인 것처럼 보여서 인상적이었음
결국 Datomic 팀은 이런 위반이 발생한다는 걸 받아들이지만 신경 쓰지 않는다는 뜻인가?
글에서는 “Datomic 관점에서 grant 작업부하의 불변 조건 위반은 사용자 오류다. 트랜잭션 함수는 순서대로 원자적으로 실행되지 않는다. 트랜잭션 안의 다른 연산이 그 사전조건을 무효화할 수 있다면, 트랜잭션 함수에서 사전조건을 확인하는 것은 안전하지 않다”고 함- Jepsen이 확인했듯이, Datomic의 불변 조건 강제 메커니즘은 설계대로 동작함
사용자 관점에서 이것이 무슨 의미인지 보려면 다음과 같은 트랜잭션 의사 데이터를 생각해볼 수 있음
[ [Stu favorite-number 41] ;; maybe more stuff [Stu favorite-number 42] ]
이를 연산적으로 읽으면 트랜잭션 초반에는 내가 41을 좋아했고, 나중에는 42를 좋아하게 된 것처럼 보임
트랜잭션 종료 후 관찰자는 내가 42만 좋아한다고 보길 기대할 것이고, 어떤 조건에서 41을 볼 수 있는지를 걱정해야 함
이런 트랜잭션 내부 의미론의 연산적 해석은 여러 데이터베이스에서 흔하지만, 트랜잭션 안에 여러 시점이 존재한다고 가정함
Datomic에는 그런 시점이 없고 원하지도 않으며, “트랜잭션 중간에” 무슨 일이 있었는지 걱정하지 않아도 되는 점을 선호함
Datomic에서 트랜잭션의 모든 사실은 같은 시점에 발생하므로, 이 트랜잭션은 내가 두 숫자를 동시에 좋아하기 시작했다고 말함
Datomic 트랜잭션을 여러 연산의 조합으로 잘못 읽으면 당연히 온갖 “불변 조건 이상”을 찾을 수 있음
반대로 SQL 트랜잭션에 Datomic 모델을 잘못 덧씌워도 “불변 조건 이상”을 찾을 수 있음
이런 오해 가능성 때문에 좋은 문서가 필요했고, Jepsen과 함께 문서를 개선해 [1] 부주의한 표현을 다듬고 오해를 줄이려 했음
이 특정 오해를 직접 다루는 기술 노트도 추가함 [2]
[1] https://docs.datomic.com/transactions/transactions.html#tran...
[2] https://docs.datomic.com/tech-notes/comparison-with-updating... - 요약하면 “잠재적 함정이지만 문서와 일관되고, 설계대로 동작한다”에 가까움
실제로 중요한지는 사용자가 어떤 불변 조건을 보존하려고 트랜잭션 함수를 작성하면서, 그 함수가 동시에가 아니라 순차 실행될 때만 불변 조건을 지키는지에 달려 있음
Datomic의 입장, 혹은 Datomic 쪽에서 끼어들어 말해주면 좋겠지만, 사용자가 그런 트랜잭션 함수를 그리 자주 쓰지는 않는다는 것임
이 입장은 방어 가능함. 문서에는 트랜잭션 함수가 서로를 관찰하는 게 아니라 트랜잭션 시작 상태를 관찰한다고 명시되어 있었기 때문임
반면 문서에는 트랜잭션 함수가 불변 조건을 보존하는 데 쓰일 수 있음을 암시하는 표현도 있었음: “[txn fns] can atomically analyze and transform database values. You can use them to ensure atomic read-modify-update processing, and integrity constraints...”
이 표현과 거의 모든 다른 직렬화 가능 데이터베이스가 트랜잭션 내부 순차 의미론을 사용한다는 사실 때문에, 보고서에서 이 이슈에 많은 지면을 썼음
복잡한 질문이라 명확한 답은 없고, 일반 데이터베이스 커뮤니티와 특히 Datomic 사용자들이 이 의미론을 어떻게 받아들이는지 듣고 싶음 - 글의 3.1절 끝에서 이미 답을 준다고 봄: “이 동작은 놀라울 수 있지만, 대체로 Datomic 문서와 일관된다. Nubank는 이 동작을 바꿀 계획이 없으며, 우리는 이를 버그로 보지 않는다”
“불변 조건 위반으로 이어지는 상황”이라고 하면 Datomic의 버그처럼 들리지만, 이건 그런 게 아님
Datomic이 트랜잭션을 처리하는 방식을 이해하고 그에 맞춰 코드를 작성해야 함
Nubank와는 관련이 없지만, Datomic을 범용 데이터베이스로 쓰면서 이것이 문제가 된 상황은 겪지 못했음 - 일부 관계형 데이터베이스에서 방금 선택한 값에 의존해 갱신하려면 SELECT ... FOR UPDATE가 필요하다는 걸 알아야 하는 상황과 비슷하게 들림
- Jepsen이 확인했듯이, Datomic의 불변 조건 강제 메커니즘은 설계대로 동작함
-
모르는 사람을 위해 덧붙이면, Jepsen이라는 이름은 “call me maybe”를 부른 Carly Rae Jepsen에서 따온 말장난임
분산 시스템 연구 프로젝트 이름으로는 완벽하다고 봄- 프로젝트가 오래됐다고는 생각했지만, 노래가 그렇게 오래됐다는 건 몰랐음
첫 커밋은 2013년임
https://github.com/jepsen-io/jepsen/tree/4b112e7046a20efa80a...
노래는 2011년에 나왔고, 시간이 정말 빠름
- 프로젝트가 오래됐다고는 생각했지만, 노래가 그렇게 오래됐다는 건 몰랐음
-
Datomic을 실전에서 오래 써보지는 않았는데, 워낙 특이해서 이 중 놀랄 만한 게 있나 싶음
Datomic 트랜잭션은 기본적으로 배치에 가깝고, 항상 단일 스레드라고 생각했기 때문에 경쟁 조건이 많지 않은 건 당연해 보임
설계상 느리고 안전한 쪽임- “같은 트랜잭션에서 x를 두 번 증가시키면 x+2가 아니라 x+1이 된다”는 예시는 꽤 중요해 보임
상당히 조심해야 할 것 같음 - Datomic은 트랜잭션 로그를 언젠가 압축하나요?
- “같은 트랜잭션에서 x를 두 번 증가시키면 x+2가 아니라 x+1이 된다”는 예시는 꽤 중요해 보임
-
Kyle 고마워요
우리 문서가 불충분했다는 점은 분명함
Rich와 함께 Datomic의 트랜잭션 모델에 대해 더 명확하고 포괄적인 문서를 쓰려고 했음
흔한 오해를 미리 막을 수 있길 바라며, 모든 피드백을 환영함
https://docs.datomic.com/transactions/model.html -
Datomic의 데이터 모델은 트리플 저장소나 RDF에 익숙하면 꽤 직관적임
그런데 문서나 온라인 논의에서는 이런 유사성이 자주 언급되지 않음
사람들이 그런 개념에 익숙하지 않아서인지, 아니면 시맨틱 웹 관련 연상을 방해 요소로 보는 건지, 혹은 내가 놓친 근본적인 차이가 있는 건지 궁금함 -
이 분석을 정말 기다리고 있었음
최근 Datomic 비슷한 데이터 저장소를 직접 만들고 있어서 유용할 것 같고, 지금 읽는 중임
MongoDB 분석도 재미있었고, Redis, RethinkDB 등 다른 분석도 꼭 보면 좋음
언젠가 rqlite/dqlite나 turso/libsql에 대한 분석도 있으면 좋겠음- rqlite 만든 사람임
2년 전에 rqlite에 대한 Jepsen 스타일 분석이 있었음 [1]
https://www.philipotoole.com/testing-rqlite-read-consistency...
보고서 자체는 여기 있음: https://github.com/wildarch/jepsen.rqlite/blob/main/doc/blog...
[1] https://www.rqlite.io
- rqlite 만든 사람임