대부분의 데이터베이스는 성능과 정확성 사이의 절충을 위해 여러 트랜잭션 격리 수준을 제공하지만, 약한 격리는 미묘한 버그를 낳을 수 있다. CockroachDB는 기본값으로 SERIALIZABLE 격리를 제공해 애플리케이션이 기대하는 일관성을 보장한다.
URL: https://www.cockroachlabs.com/blog/acid-rain/
Title: Real transactions are serializable
대부분의 데이터베이스는 여러 트랜잭션 격리 수준 중 하나를 선택할 수 있게 하며, 이는 정확성과 성능 사이의 절충을 제공합니다. 하지만 그 성능은 대가를 치릅니다. 개발자가 트랜잭션 간 상호작용을 꼼꼼히 공부하지 않으면 미묘한 버그를 도입할 위험이 있기 때문입니다. CockroachDB는 애플리케이션이 항상 기대하는 데이터를 보도록 기본값으로 강한(“SERIALIZABLE”) 격리를 제공합니다. 이 글에서는 이것이 무엇을 의미하는지, 그리고 불충분한 격리가 실제 애플리케이션에 어떤 영향을 미치는지 설명하겠습니다.
SQL 표준은 네 가지 격리 수준을 정의합니다:
SERIALIZABLE
REPEATABLE READ
READ COMMITTED
READ UNCOMMITTED
SERIALIZABLE 트랜잭션은 마치 한 번에 하나의 트랜잭션만 실행되는 것처럼 동작합니다. 다른 격리 수준은 SQL 표준이 완곡하게 “세 가지 현상(the three phenomena)”이라고 부르는 것을 허용합니다. 즉 더티 리드(dirty reads), 반복 불가능 읽기(non-repeatable reads), 팬텀 리드(phantom reads)입니다. 후속 연구에서는 추가적인 “현상”과 격리 수준이 식별되었습니다.
현대 연구에서는 이러한 “현상”을 더 흔히 “이상 현상(anomalies)”이라고 부르며, 더 노골적으로는 “거짓말(lies)”이라고도 합니다. SERIALIZABLE이 아닌 격리 수준을 사용한다는 것은, 올바른 답을 내는 대신 더 빠르기를 바라는 마음으로 데이터베이스가 잘못된 답을 반환하도록 허용하는 것과 같습니다. SQL 표준은 이것이 위험하다는 점을 인식하고 SERIALIZABLE이 기본 격리 수준이어야 한다고 요구합니다. 더 약한 격리 수준은 이러한 이상 현상을 감내할 수 있는 애플리케이션을 위한 잠재적 최적화로 제공됩니다.
대부분의 데이터베이스는 SERIALIZABLE이 기본값이어야 한다는 명세를 무시하고, 기본값으로 더 약한 READ COMMITTED 또는 REPEATABLE READ 격리 수준을 선택해 안전성보다 성능을 우선합니다. 더 우려스러운 점은 일부 데이터베이스(Oracle, 그리고 9.1 이전의 PostgreSQL 포함)는 직렬화 가능한(serializable) 트랜잭션 구현 자체를 제공하지 않는다는 것입니다. Oracle의 SERIALIZABLE 격리 수준 구현은 실제로 “스냅샷 격리(snapshot isolation)”라는 더 약한 모드입니다.
스냅샷 격리는 SQL 언어의 초기 표준화 이후에 개발되었지만, 성능과 일관성의 균형이 좋기 때문에 여러 데이터베이스 시스템에서 구현되었습니다. 이는 READ COMMITTED보다 강하지만 SERIALIZABLE보다 약합니다. REPEATABLE READ와 유사하지만 정확히 동등하지는 않습니다(REPEATABLE READ는 팬텀 리드를 허용하지만 쓰기 스큐(write skew)를 방지하는 반면, 스냅샷 격리는 그 반대가 참입니다). 스냅샷 격리를 구현한 데이터베이스들은 이를 SQL 표준의 네 가지 수준에 어떻게 끼워 넣을지 서로 다른 결정을 내렸습니다. Oracle은 가장 공격적인 입장을 취해 자신들의 스냅샷 구현을 SERIALIZABLE이라고 부릅니다. CockroachDB와 Microsoft SQL Server는 보수적으로 SNAPSHOT을 별도의 다섯 번째 격리 수준으로 취급합니다. PostgreSQL(9.1 이후)은 그 중간쯤으로, REPEATABLE READ 대신 스냅샷 격리를 사용합니다.
더 약한 격리를 기본값으로 삼는 데이터베이스에서는 직렬화 모드가 덜 사용되기 때문에, 테스트나 최적화가 덜 되어 있는 경우가 많습니다. 예를 들어 PostgreSQL은 직렬화 트랜잭션 간 충돌을 추적하기 위해 고정 크기 메모리 풀을 사용하는데, 부하가 크면 이 풀이 고갈될 수 있습니다.
대부분의 데이터베이스 벤더는 더 강한 트랜잭션 격리를, 예외적으로 높은 일관성이 필요한 애플리케이션만 켜는 ‘특이한 옵션’ 정도로 취급합니다. 하지만 대부분의 애플리케이션은 더 빠르지만 안전하지 않은 약한 격리 모드에서도 동작할 것으로 기대됩니다. 이러한 문제 접근은 거꾸로 되어 있으며, 애플리케이션을 다양한 미묘한 버그에 노출시킵니다. Cockroach Labs에서는 트랜잭션 이상 현상에 대해 생각하는 것을 настолько 좋아해서 회의실 이름을 전부 그것들로 지을 정도지만, SERIALIZABLE 대신 SNAPSHOT 격리를 선택하는 것이 언제 ‘안전하면서도’ ‘유익한지’를 자신 있게 조언하기는 어렵습니다. 우리의 철학은 그 반대가 아니라, 안전을 출발점으로 삼고 성능을 향해 나아가는 편이 더 낫다는 것입니다.
스탠퍼드의 최근 연구에서는 약한 격리가 실제 버그로 이어지는 정도를 탐구했습니다. Todd Warszawski와 Peter Bailis는 12개의 전자상거래(eCommerce) 애플리케이션을 조사해 트랜잭션과 관련된 버그 22개를 발견했으며, 그중 5개는 더 높은 격리 수준에서 실행했다면 피할 수 있었을 버그였습니다. 이들 버그 중 상당수는 악용하기 쉬웠고 직접적인 재무적 영향을 가졌습니다. 예를 들어 테스트한 애플리케이션 5개에서는, 다른 브라우저 탭에서 결제(check out)하는 동시에 장바구니에 상품을 추가하면 그 상품이 무료로 주문에 포함되는 결과가 발생할 수 있었습니다. 연구진은 이러한 취약점을 반자동 방식으로 식별하는 도구를 개발했으며, 이를 통해 유사한 공격(연구진이 “ACIDRain”이라고 명명)이 더 널리 발생할 수 있는 길을 열었습니다.
기본값이 약한 트랜잭션 격리인 대부분의 데이터베이스는 우회책을 제공합니다. 예를 들어 SELECT 문에 붙는(비표준) FOR UPDATE, LOCK IN SHARE MODE 같은 수정자(modifier)가 있습니다. 올바르게 사용하면 이런 수정자는 약한 격리 수준에서도 트랜잭션을 안전하게 만들 수 있습니다. 하지만 이를 잘못 사용하기 쉽고, 일관되게 사용하더라도 이러한 확장 기능은 SERIALIZABLE 모드의 단점 대부분을 초래합니다(사실 READ COMMITTED 트랜잭션에서 SELECT FOR UPDATE를 과도하게 사용하면, 직렬화 가능성이 공유 락(shared lock)만 요구하는 경우에도 배타 락(exclusive lock)을 사용하기 때문에 SERIALIZABLE 트랜잭션보다 성능이 더 나쁠 수 있습니다). ACIDRain 연구는 이 기법의 한계를 보여줍니다. SELECT FOR UPDATE 기능을 사용하려고 시도한 애플리케이션 중 올바르게 사용한 것은 3개 중 1개뿐이었고, 나머지는 취약한 상태로 남아 있었습니다.
약한 격리 수준의 사용을 장려하는 데이터베이스는 데이터의 안전성보다 성능을 우선시해 왔고, 그 결과 여러분은 트랜잭션 간 미묘한 상호작용을 연구하고 오류가 나기 쉬운 우회책을 구현해야 했습니다. CockroachDB는 기본값으로 SERIALIZABLE 트랜잭션을 제공해, 트랜잭션 데이터베이스에 기대하는 일관성을 항상 볼 수 있도록 보장합니다.
분산 트랜잭션이 취향에 맞나요? 우리 엔지니어링 팀은 채용 중입니다! 채용 공고는 여기에서 확인하세요.
_일러스트: _Lisk Feng