- MySQL 8.0.34의 기본 격리 수준인 Repeatable Read는 단일 정상 노드에서도 ANSI SQL 및 Adya의 PL-2.99 기대와 맞지 않는 트랜잭션 일관성 위반을 보임
- Elle의 list-append 검사기, targeted workload, LazyFS를 조합해 MySQL 8.0.34, MariaDB 10.11.3, binlog 복제 클러스터, AWS RDS MySQL Multi-AZ DB Cluster를 함께 검증함
- Kleppmann의 2014년 Hermitage 결과처럼 G2-item, G-single, lost update가 재현됐고, 내부 일관성 위반과 non-repeatable read, Monotonic Atomic View 위반도 관찰됨
- 단일 MySQL의 Read Uncommitted, Read Committed, Serializable은 각각 PL-1, PL-2, PL-3에 부합해 보였지만, AWS RDS MySQL 클러스터는 Serializable에서도 G2-item과 G-single을 보임
- ANSI 또는 PL-2.99 수준의 Repeatable Read가 필요하다면 MySQL Repeatable Read만 믿기 어렵고, Serializable이나
SELECT... FOR UPDATE같은 명시적 잠금이 필요함
평가 대상과 범위
- MySQL은 널리 쓰이는 관계형 데이터베이스이며, 이 분석에서 “MySQL”은 기본 스토리지 엔진인 InnoDB를 사용하는 MySQL을 뜻함
- 초점은 단일 서버 MySQL이지만, binlog 복제를 쓰는 단일 쓰기 primary와 읽기 전용 secondary 클러스터도 함께 다룸
- 테스트 대상은 다음과 같음
- MySQL 8.0.34
- MariaDB 10.11.3
- Debian Bookworm
- AWS RDS Cluster의 “Multi-AZ DB Cluster” 프로필
- 작업은 보상 없이 독립적으로 수행됐고, Jepsen ethics policy에 따라 진행됨
SQL 격리 수준과 Repeatable Read의 기준
- ANSI SQL은 Read Uncommitted, Read Committed, Repeatable Read, Serializable을 P1 dirty read, P2 non-repeatable read, P3 phantom 가능 여부로 정의함
- 1995년 Berenson 등은 A Critique of ANSI SQL Isolation Levels에서 ANSI 정의의 모호성과 불완전성을 비판함
- P1, P2, P3는 해석 여지가 있음
- P0 dirty write 같은 중요한 현상이 빠져 있음
- P3는 predicate에 영향을 주는 insert만 금지하고 update나 delete는 다루지 않음
- Atul Adya의 1999년 논문은 트랜잭션 간 의존성 그래프를 기반으로 구현 독립적인 격리 수준을 정의함
- PL-1은 G0 write cycle을 금지함
- PL-2는 G0와 G1을 금지함
- PL-2.99는 G0, G1, G2-item을 금지하며 Repeatable Read에 대응함
- PL-3은 G0, G1, G2를 금지하며 Serializable에 대응함
- Jepsen은 일반적으로 Adya의 형식을 사용해 트랜잭션 기록과 이상 현상을 판별함
MySQL 문서와 Repeatable Read의 충돌
- MySQL 문서는 InnoDB가 SQL:1992 표준의 네 격리 수준을 모두 제공한다고 설명함
- 기본 격리 수준인 Repeatable Read는 같은 트랜잭션 안의 consistent read가 첫 읽기에서 설정된 스냅샷을 읽는다고 설명함
- consistent read 문서도 첫 읽기 시점의 timepoint에 따라 데이터베이스를 본다고 설명함
- 하지만 같은 문서의 주석은 스냅샷이
SELECT에는 적용되지만 DML 문장에는 반드시 적용되지 않으며, 다른 트랜잭션이 commit한 row를DELETE나UPDATE가 건드릴 수 있다고 말함 - 이 주석은 ANSI SQL과 MySQL 참조 매뉴얼이
SELECT도 DML로 본다는 점과 충돌하고, Repeatable Read에서 쓰기가 읽을 수 없던 row에 영향을 줄 수 있다는 혼란을 만듦
테스트 설계
- MySQL용 테스트 스위트는 Jepsen testing library 0.3.4 기반으로 작성됨
- 클라이언트는
mysql-connector-jJDBC 어댑터를 사용함 - 테스트에는 process pause, crash, network partition, fsync되지 않은 디스크 쓰기 손실 같은 fault injection이 포함됨
- 다만 이번 분석의 거의 모든 발견은 정상 상태의 단일 MySQL 노드에서 발생함
-
Elle list-append workload
- 핵심 workload는 Elle의 list-append checker를 사용함
- Elle는 트랜잭션 사이의 write-write, write-read, read-write 의존성을 추론하고, 의존성 그래프의 cycle로 특정 격리 수준 위반을 입증함
- list-append workload는 primary key로 식별되는 여러 list에 대해 read와 append로 구성된 랜덤 트랜잭션을 수행함
- list는 쉼표로 구분된 값을 담는
text필드로 인코딩하고, append는 SQLCONCAT으로 처리함 - 최근 개선으로 Elle는 다음을 더 잘 탐지함
- 읽히지 않은 append element에 대한 ww/rw 의존성 추론
- P4 lost update 명시 탐지
- real-time edge와 process edge를 포함한 복잡한 cycle 탐색
-
Targeted workload
- Non-repeatable read workload는
people테이블의 한 row를 대상으로 함 - 한 계열의 트랜잭션은
name만 update하고, 다른 계열은name을 읽고gender를 update한 뒤 다시name을 읽음 - 두 읽기 사이에
name이 바뀌면 Repeatable Read 위반임 - Monotonic Atomic View workload는 두 row의
value를 사용함 - writer는 row 0의
value를 증가시킨 뒤 row 1을 증가시킴 - reader는 row 0을 읽고 row 1의
noop을 update한 뒤 row 1과 row 0을 읽음 - 한 트랜잭션의 일부 효과를 봤다면 모든 효과를 봐야 함
- Non-repeatable read workload는
-
LazyFS
- LazyFS는 fsync되지 않은 쓰기 손실을 시뮬레이션하는 FUSE 파일시스템임
- MySQL process를 죽이고 LazyFS 캐시를 버린 뒤 MySQL을 재시작하는 방식으로 테스트함
- 이 보고서는 LazyFS가 포함된 첫 공개 Jepsen 보고서임
MySQL Repeatable Read에서 발견된 이상 현상
-
G2-item
- Adya의 PL-2.99 Repeatable Read는 predicate를 포함하지 않는 write-write, write-read, read-write 의존성 cycle인 G2-item을 금지함
- MySQL Repeatable Read는 단일 정상 노드에서도 G2-item을 반복적으로 허용함
- Kleppmann이 2014년 Hermitage에서 보고한 동작이 MySQL 8.0.34에서도 계속 발생함
- 예시 테스트는 40초 동안 214개 cycle을 보였음
- 이 동작은 PL-2.99 Repeatable Read에서는 금지되지만, ANSI SQL의 P2 정의가 같은 row를 두 번 읽는 경우만 다루기 때문에 ANSI 정의상 해석 여지가 남음
-
G-single과 read skew
- MySQL Repeatable Read는 G-single도 보임
- G-single은 write-write, write-read, read-write edge로 구성되지만 read-write edge가 서로 인접하지 않는 cycle임
- Kleppmann이 2014년 보고한 read skew가 MySQL 8.0.34에서도 확인됨
- 60초 append 테스트에서는 초당 약 140트랜잭션에서 G-single 244건과 G2-item 305건이 나타남
- append 테스트는 predicate 연산을 사용하지 않으므로 모두 Repeatable Read 위반으로 분류됨
-
Lost update
- P4 lost update는 두 트랜잭션이 같은 key의 같은 version을 읽고 둘 다 update하는 G-single의 특수 사례임
- Snapshot Isolation과 PL-2.99 Repeatable Read는 lost update를 금지함
- MySQL Repeatable Read는 단일 정상 노드에서도 lost update를 반복적으로 허용함
- 한 테스트에서 9,048개 성공 트랜잭션 중 새 checker는 198개의 lost update 사례에 관여한 446개 트랜잭션을 찾음
- 이 중 cycle로 드러난 사례는 47개뿐임
- 값을 읽은 뒤 쓰는 패턴은 MySQL Repeatable Read에서 안전하지 않음
- 객체를 읽어 메모리에서 수정한 뒤 다시 저장하는 표준 ORM 패턴에서는 commit된 변경이 조용히 사라질 수 있음
- 사용자는 명시적 잠금을 직접 사용해야 함
-
Non-repeatable read와 내부 일관성 위반
- MySQL Repeatable Read는 단일 정상 노드에서도 내부 일관성 위반을 보임
- 같은 테스트 run에서 9,048개 commit 트랜잭션 중 126개가 내부 일관성 오류를 보임
- 예시에서는 한 트랜잭션이 key를
nil로 읽고 값 하나를 append한 뒤 같은 key를 다시 읽었을 때, 다른 세 값이 추가된 상태를 관찰함 - 또 다른 예시에서는 key 1096을
[1 2 3]으로 읽고7을 append한 뒤 다시 읽었을 때[1 2 3 4 5 6 7]을 관찰함 - targeted workload에서는 한 Repeatable Read 트랜잭션 안에서
name을"pebble"로 읽고,gender를"femme"로 update한 뒤, 같은name을 다시 읽자"moss"가 나옴 - 이런 동작은 ANSI SQL의 non-repeatable read 정의와 MySQL 문서의 “첫 읽기에서 설정된 스냅샷” 설명에 어긋남
-
Monotonic Atomic View 위반
- Monotonic Atomic View는 한 트랜잭션의 어떤 효과를 본 트랜잭션이 그 트랜잭션의 모든 효과를 봐야 한다는 성질임
- MySQL Repeatable Read는 정상 단일 노드에서도 이를 반복적으로 위반함
- workload에서 writer는 row 0을 증가시킨 뒤 row 1을 증가시킴
- reader는 row 0에서 이전 값
0을 본 뒤, row 1에서 writer의 증가분1을 보고, 다시 row 0에서 여전히0을 봄 - 이는 row 1의 효과는 봤지만 row 0의 효과는 보지 못한 비단조 읽기이며, 일반적인 스냅샷 동작과 맞지 않음
AWS RDS MySQL Serializable의 이상 현상
- AWS RDS MySQL 클러스터는 “Serializable” 격리 수준에서도 Serializability를 반복적으로 위반함
- 기본 권장 production 프로필의 RDS MySQL 클러스터에서 append 테스트는 G2-item과 G-single anomaly를 보임
- 관찰된 anomaly는 한 트랜잭션의 효과를 본 트랜잭션의 앞선 의존성을 다른 트랜잭션이 놓치는 형태였음
- 이 anomaly는 G-single이자 G2-item으로 분류되며, Snapshot Isolation, Repeatable Read, Serializability를 모두 위반함
replica_preserve_commit_order관련 설정이 의심 요인으로 남아 있음- MySQL 8.0.27 이상은
replica_preserve_commit_order=ON이 기본값임 - RDS 기본 파라미터는 여전히
replica_preserve_commit_order=OFF에 해당하는 설정을 선택함 - RDS parameter group에서는 이 설정의 예전 이름인
slave_preserve_commit_order를 사용함 - 로컬 테스트 클러스터에 이 설정을 적용하면 유사한 G-single과 G2-item이 관찰됨
- MySQL 8.0.27 이상은
정상적으로 보인 부분과 LazyFS 결과
- MySQL 8.0.34의 Read Uncommitted, Read Committed, Serializable은 각각 PL-1, PL-2, PL-3을 만족하는 것으로 보임
- 이 결과는 단일 노드와 binlog 복제를 사용하는 작은 read-only replica 클러스터 모두에서 관찰됨
- process pause, crash, network partition에서도 해당 결과가 유지됨
- LazyFS fault injection은 MySQL 기본 설정에서 문제를 찾지 못함
innodb_flush_log_at_trx_commit=1기본값에서는 process crash와 fsync되지 않은 데이터 손실 이후에도 commit된 트랜잭션 손실이 나타나지 않음innodb_flush_log_at_trx_commit=0으로 바꾸면 MySQL이 몇 초마다 한 번만 fsync했고, 데이터 손실이 관찰됨
MySQL Repeatable Read의 실제 성격
- MySQL Repeatable Read는 PL-2.99 Repeatable Read를 만족하지 않음
- G2-item과 write skew를 보임
- Snapshot Isolation도 만족하지 않음
- G-single, read skew, lost update를 보임
- cursor stability도 만족하지 않음
- lost update가 발생함
- Read Atomic, Causal Consistency, Consistent View, Prefix Consistency, Parallel Snapshot Isolation도 배제됨
- 내부 일관성 위반이 관찰됨
- MySQL Repeatable Read는 Read Committed보다는 다소 강해 보임
- G0 dirty write, G1a aborted read, G1b intermediate read, G1c cyclic information flow는 관찰되지 않음
- 일부 읽기의 repeatability가 Read Committed보다 강한 성질을 제공함
- 다만 MySQL Repeatable Read가 정확히 어떤 consistency model인지 명확하지 않고, 공식적인 성질 정의도 없음
문서와 커뮤니티 이해의 불일치
- MySQL 커뮤니티에서는 Repeatable Read 동작이 충분히 이해되지 않은 상태임
- 여러 글은 MySQL Repeatable Read가 lost update를 막는다고 믿지만, 다른 글들은 막지 못한다고 보고 명시적 잠금 사용을 권함
- 여러 인터넷 자료는 MySQL Repeatable Read가 실제로 repeatable하다고 말하지만, Jepsen의 테스트는 그렇지 않은 사례를 보임
- MySQL과 MariaDB 문서도 Repeatable Read가 같은 트랜잭션 안에서 같은 스냅샷을 읽는다고 설명함
- MySQL consistent read 문서의 한 문장은 이 설명과 충돌하는 동작을 암시하지만, 해당 내용은 문서 안에 묻혀 있음
권고 사항
- MySQL이 현재 동작을 유지한다면 “Repeatable Read”가 실제로 어떤 consistency model을 제공하는지 명확히 문서화해야 함
- 다른 선택지는 현재 동작을 버그로 다루고 수정하는 것임
- MySQL과 다른 벤더가 PL-2.99 Repeatable Read 제공을 약속한다면 Jepsen은 이를 환영한다고 밝힘
- PL-2.99 또는 ANSI Repeatable Read가 필요한 사용자는 MySQL Repeatable Read에 주의해야 함
- 실무 대안은 다음과 같음
- MySQL의 Serializable 격리 수준 사용
READ COMMITTED에서SELECT ... FOR UPDATE같은 잠금 기법으로 읽기 강화
RDS 사용자를 위한 권고
- AWS RDS MySQL 클러스터는 “Serializable”에서 read skew와 G2-item을 보임
- Serializability에 의존하는 사용자는 RDS parameter group에서
slave_preserve_commit_order를ON으로 설정해야 함 - AWS는 기본값을 바꾸거나, RDS MySQL의 known limitations 문서에 허용되는 Serializability 위반을 명확히 설명해야 한다는 제안이 나옴
향후 작업과 표준화 요청
- MySQL binlog replication은 취약해 보였음
- 로컬 Jepsen 테스트에서 replication이 멈추는 여러 상황이 관찰됨
- AWS RDS MySQL replication은 몇 분의 테스트만으로 완전히 깨질 수 있었고, primary에서 성공한
CREATE DATABASE가 secondary에 나타나지 않는 상황이 1시간 동안 회복되지 않음
- secondary를 primary로 승격하거나 ring, star 같은 복제 topology는 탐색하지 않음
- predicate safety를 평가하기 위한 더 일반적인 predicate test 연구가 진행 중임
- ANSI SQL 격리 수준 정의는 Berenson 등이 모호성과 불완전성을 지적한 지 28년이 지나고 7번의 ANSI·ISO 개정이 있었지만 바뀌지 않음
- ISO/IEC 9075-2가 내부 anomaly, lost update, dirty write 같은 현상을 명확히 다룰 수 있도록 더 형식적이고 이식 가능한 격리 수준 정의가 필요함