1. 애매모호한 트랜잭션의 개념

1-1. ACID의 의미

트랜잭션이 제공하는 안전성 보장(ACID) : 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability) 

원자성(Atomicity) : 여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶이고 트랜잭션은 완료(커밋)되거나 결함 발생 시 해당 트랜잭션의 모든 쓰기 작업이중단(어보트)되거나 둘 중 하나여야 한다.

일관성(Consistency)  : ACID에서의 일관성은 애플리케이션의 불변식 개념에 의존하고, 일관성을 유지하도록 트랜잭션을 올바르게 정의하는 것은 애플리케이션의 책임이다.

격리성(Isolation) : 동시에 실행되는 트랜잭션은 서로 격리된다. 오라클의 직렬성 격리는 스냅샷 격리를 구현한 것

지속성(Durability) : 트랜잭션이 성공적으로 커밋됐다면 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 보장

1-2. 단일 객체 연산과 다중 객체 연산

다중 객체 트랜잭션은 읽기 및 쓰기 연산들이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야 한다. 

관계형 데이터베이스에서는 TCP 연결 기반으로 BEGIN TRANSACTION문, COMMIT문 사이 모든것은 같은 트랜잭션에 속하는 것으로 여겨짐

다중 객체 트랜잭션의 필요성 

2. 완화된 격리 수준

2-1. 커밋 후 읽기

가장 기본적인 트랜잭션 격리로 두 가지를 보장해준다

  1. 데이터베이스에서 읽을 때 커밋된 데이터만 본게 된다 (더티 읽기가 없음)
  2. 데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다 (더티 쓰기가 없음)

2-2. 스냅샷 격리와 반복 읽기

트랜잭션은 특정 시점에 고정된 데이터베이스의 일관된 스냅샷만을 볼 수 있다

스냅샷 격리 구현은 커밋 후 읽기 격리처럼 전형적으로 더티쓰기를 방지하기 위해 쓰기 잠금을 사용한다

스냅샷 격리의 핵심 원리는 읽는 쪽에서 쓰는 쪽을 결코 차단히지 않고 쓰는 쪽에서 읽은 쪽을 결코 차단하지 않는다는 것 

다중 버전 동시성 제어(multi-version concurreny control, MVCC) : 데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 한다

반복 읽기(repeatable read) = 스냅샷 격리

2-3. 갱신 손실 방지

갱신 손실(lost update)애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 발생 가능

두 트랜젝션이 이 작없을 동시에 하면 두 번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경중 하나는 손실 될 수 있다

해결책

1. 원자적 쓰기 연산

2. 명시적인 잠금

3. 갱신 손실 자동 감지

4. compare-and-set

5. 충돌 해소와 복제

2-4. 쓰기 스큐와 팬텀

쓰기 스큐(write skew) : 두 트랜잭션이 동시에 같은 객체를 읽어서 두 개의 다른 객체를 갱신, 더티쓰기나 갱신 손실 이상 현상 발생

3. 직렬성(Serializability)

직렬성 격리는 여러 트랜잭션이 병렬로 실행되더라도 최종결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장

3-1. 실제적인 직렬 실행

동시성 문제를 해결하는 가장 간단한 방법은 동시성을 완전히 제거하는 것, 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행 

단일 스레드를 활용하기 위해서는 트랜잭션이 전통적인 현태와는 다르게 구조화 되어야 한다

현대의 스토어드 프로시저(stored procedure) 구현은 PL/SQL을 버리고 기존의 범용 프로그래밍 언어를 사용한다

단일 스레드 시스템의 처리량을 높이기 위해 파티셔닝 기법을 활용하면 된다

3-2. 2단계 잠금(2PL, 2-phase locking)

2단계 잠금(2PL)과 2단계 커밋2PC(2PC)는 아주 비슷하게 들리지만 완전히 다르다

2PL은 쓰기 트랜잭션이 다른 쓰기 트랜잭션 뿐만아니라 읽기 트랜잭션도 진행하지 못하게 막는다

2단계 잠금의 가장 큰 약점은 성능이다. 완화된 격리 수준을 쓸 때보다 트랜잭션 처리량과 질의 응답 시간이 크게 나빠진다

2단계 잠금이 서술 잠금 까지 포함하면 모든 형태의 쓰기 스큐를 막을 수 있고 격리 수준이 직렬성 격리가 된다

서술 잠금은 성능이 좋지 않아 대부분 2PL을 지원하는 데이터베이스는 서술 잠금을 간략하게 근사한 색인 범위 잠금, 다음 키 잠금을 구현

색인 범위 잠금 서술 잠금보다 정밀하지 않지만 오버헤드가 훨씬 더 낮기 때문에 좋은 타협안이 된다

3-3. 직렬성 스냅샷 격리(SSI, Serializable Snapshot Isolation)

직렬성 스냅샷 격리는 낙관적 동시성 제어 기법이다. 

예비 용량이 충분하고 트랜잭션 사이의 경쟁이 너무 심하지 않으면 낙관적 동시성 제어 기법은 비관적 동시성 제어보다 성능이 좋은 경향이 있다

SSI는 스냅샷 격리 위에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트시킬 트랜잭션을 결정하는 알고리즘을 추가한다 

2단계 잠금과 비교할 때 트랜잭션이 다른 트랜잭션을 기다리느라 차단되지 않기 때문에 읽기 작업이 많은 경우 성능이 더 뛰어나다

순차 실행과 비교할 때 단일 CPU 코어의 처리량에 제한되지 않는다

어보트 비율은 전체적인 성능에 큰 영향을 미친다

오랫동안 데이터를 읽고 쓰는 트랜잭션은 충돌 가능성이 높아 어보트되기 쉬우므로 읽기-쓰기 트랜잭션이 짧을 수록 유리

 

참고

'Books' 카테고리의 다른 글

[대규모 중심 어플케이션 설계] 05장. 복제  (1) 2025.01.15
더보기

복제란 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본을 유지한다는 의미 

 

데이터의 복제가 필요한 이유 

  • 지리적으로 사용자와 가깝게 데이터를 유지해 지연 시간을 줄인다.
  • 일부 노드에 장애가 발생해도 동작할 수 있도록 해 가용성을 높인다.
  • 읽기 질의를 제공하는 노드의 수를 확장해 읽기 처리량을 늘린다.

변경되는 데이터를 노드 간 복제하기 위한 대표적인 알고리즘

  • 단일 리더
  • 다중 리더
  • 리더 없는복제 방식

1. 리더와 팔로워

DB의 복사본을 저장하는 각 노드 : 복제 서버(replica)

모든 복제 서버에 데이터가 복제된 것을 보장하기 위해 일반적으로 리더 기반 복제를 활용

  • 복제 서버 중 하나를 리더(마스터, 프라이머리)로 지정 클라이언트로부터의 모든 쓰기 요청은 리더에게만 전달
  • 리더는 자신의 서버에 데이터를 기록하고 팔로워(슬레이브, 세컨더리) 복제 서버에 쓰기 요청을 전달
    • 클라이언트 관점에서 쓰기는 리더만 허용하므로 팔로워는 읽기 전용
    • 리더가 팔로워에게 쓰기 내용을 전달할 땐 복제 로그(replication log)나 변경 스트림(change stream)을 주로 활용
  • 리더기반 복제 모드는 여러 RDB에 내장되어 있고 분산 메시지 브로커에서도 사용

1-1. 동기식 대 비동기식 복제 

  • 동기식 복제의 장단점
    • 장점: 팔로워가 리더와 일관성 있게 최신 데이터 복사본을 가지는 것을 보장
    • 단점: 팔로워가 죽거나 네트워크 문제로 동기 팔로워가 응답하지 않을 경우 쓰기가 처리될 수 없음
  • 임의 한 노드의 장애가 전체 시스템을 멈추게 하므로 모든 팔로워가 동기식인 상황은 비현실적
  • 반동기식(semi-synchronouse)
    • 현실적으로 팔로워 하나는 동기식으로, 나머지는 비동기식으로 복제하는 방식을 사용
    • 적어도 두노드 (리더와 하나의 동기 팔로워)에 최신 복사본이 있는 것을 보장
  • 비동기식 복제의 장단점
    • 장점: 모든 팔로워가 잘못되더라도 리더가 쓰기 처리를 계속 할 수 있음 
    • 단점: 리더가 잘못되고 복구 불가능한 상황에서 팔로워에 아직 복제되지 않은 모든 쓰기는 유실 (지속성 보장 X)
  • 비동기식 복제는 내구성을 약화시키지만 많은 팔로워가 있거나 지리적으로 분산되었다면 비동기식 복제를 사용

1-2.  새로운 팔로워 추가 

  • 팔로워가 추가되어 기존 리더의 데이터를 복제할 때 가용성을 위해 중단없이 수행가능해야 한다. 그 과정을 다음과 같다.
    1. 전체 DB를 잠그지 않고, 리더 DB의 스냅샷을 일정시점에 가져온다.
    2. 스냅숏을 새로운 팔로워 노드에 복사한다.
    3. 팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경을 요청한다
    4. 팔로워가 스냅샷 이후 데이터 변경의 미처리분(backlog)을 모두 처리하면 따라잡았다고 말한다.

1-3. 노드 중단 처리

리더 기반 복제에서 고가용성 달성하는 방법

팔로워 장애 : 따라잡기 복구

각 팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관,  팔로워 장애는 쉽게 복구 가능

  • 보관된 로그에서 결함이 발생하기 전에 처리한 마지막 트랜잭션을 확인
  • 팔로워는 리더에 연결해 끊어진 동안 발생한 데이터 변경을 모두 요청

리더 장애 : 장애 복구

  • 장애 복구(failover)
    • 팔로워 중 하나를 새로운 리더로 승격해야 하고, 클라이언트는 새로운 리더로 쓰기를 전송하기 위해 재설정이 필요
    • 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작
  • 참고. 장애 복구 단계
  • 일부 운영팀은 소프트웨어가 자동 장애 복구를 지원하더라도 수동으로 장애 복구를 수행하는 방식이 선호
  • 노드 장애, 불안정한 네트워크, 지속성, 가용성, 지연 시간 등의 문제는 분산 시스템에서 발생하는 근본적인 문제

1-4. 복제 로그 구현

구문 기반 복제

  • 리더는 모든 쓰기 요청을 기록하고 쓰기를 실행한 다음 구문 로그를 팔로워에게 전송
  • RDB는 INSERT, UPDATE, DELETE 구문을 팔로워에게 전달하고 각 팔로워는 SQL 구문을 파싱하고 실행
  • 복제가 깨질 수 있는 다양한 사례
  • 대안 해결책
    • 리더가 구문을 기록할때 비결정적 함수의 고정 값을 반환하게끔 대처 
    • MySQL 5.1 이후 구문에 비결정성이 있는 경우 로우 기반 복제를 사용하도록 변경

쓰기 전 로그 배송

  • 일반적으로 저장소 엔진은 모든 쓰기에 대한 로그 기록
  • 리더는 디스크에 로그를 기록하는 일 외에도 팔로워에게 네트워크로 해당 로그를 전송
  • 가장 큰 단점은 로그가 제일 저수준의 데이터를 기술
  • 복제가 저장소 엔진과 밀접하게 결합되어 리더와 팔로워의 데이터베이스 소프트웨어 버전을 다르게 실행 할 수 없음

논리적(로우 기반) 로그 복제 

  • 복제 로그를 저장소 엔진 내부와 분리하기 위한 대안 하나는 복제와 저장소 엔진을 위해 다른 로그 형식을 사용하는 것
  • 논리적 로그(lgical log) : 저장소 엔진의 (물리적) 데이터 표현과 구별하기 위한 복제 로그 
    • RDB용 논리적 로그는 대개 로우 단위로 DB 테이블에 쓰기를 기술한 레코드 열
    • 여러 로우를 수정하는 트랜젝션은 여러 로그 레코드를 생성한 다음 트랜젝션이 커밋 되었음을 레코드에 표시
  • 논리적 로그를 저장소 엔진 내부와 분리했으므로 하위 호환성을 더 쉽게 유지 가능
  • 리더와 팔로워에서 다른 버전의 데이터베이스 소프트웨어나 저장소 엔진을 실행 가능

트리거 기반 복제

  • 데이터의 서브셋만 복제하거나 데이터베이스를 다른 종류의 데이터베이스로 복제해야 하거나 충돌해소로직이 필요 한 경우
    • 복제를 애플리케이션 층으로 옮김 
  • 트리거는 사용자 정의 애플리케이션 코드를 등록가능 
    • 데이터베이스 시스템에서 데이터 변경 시(쓰기 트랜젝션) 자동으로 실행
  • 일반적으로 트리거 기반 복제는 다른 방식에 비해 오버헤드가 있지만 유연성 때문에 유용한 매우 유용

2. 복제 지연 문제

최종적 일관성(Eventual Consistency) : 데이터베이스에 쓰기를 멈추고 잠시동안 기다리면 팔로워는 결국 따라잡고 리더와 일치

 

2-1. 자신이 쓴 내용 읽기

  • 자신이 쓴 내용을 바로 다시 읽기 했을 때 복제 서버엔 쓰기가 반영되지 않아 쓰기 전 데이터를 볼 가능성이 존재
  • 쓰기 후 읽기 일관성(자신의 쓰기 읽기 일관성) 은 항상 자신이 갱신한 내용에 대해서는 일관성을 보장
    • 단, 다른 사용자에 대해서는 일관성을 보장하지 않음
  • (참고) 쓰기 후 읽기 일관성 구현방법

2-2. 단조 읽기

  • 사용자가 시간이 거꾸로 흐르는 현상을 목격할 수 있음
    • 복제 서버 A, B 중 A서버에만 동기화가 되어있는 시점에 사용자가 처음엔 A 서버를 통해 데이터를 읽을땐 데이터가 반환되지만 그 다음에 B 서버를 통해 데이터를 읽으면 데이터가 반환되지 않음
  • 단조 읽기(monotonic read)는 이런 종류의 이상 현상이 발생하지 않음을 보장
    • 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되게끔 함 (다른 사용자는 다른 복제 서버에서 읽을 수 있음)

2-3. 일관된 순서로 읽기

  • 데이터가 서로 다른 파티션에 저장되고 파티션 리더가 다를 때 실제 시간상으론 A -> B 순으로 입력되었지만 팔로워들은 B -> A 순으로 복제가 될 가능성
  • Observer는 실제로 쓰여진 순서가 아닌 반대로 읽는다
  • 일관된 순서로 읽기(Consistent Prefix Read)는 이런 종류의 이상 현상이 발생하지 않음을 보장
    • 특정 순서로 쓰기가 발생하면 이 쓰기를 읽는 모든 사용자는 같은 순서로 쓰여진 내용을 보게 됨을 보장

2-4. 복제 지연을 위한 해결책 

  • 최종적 일관성으로 인한 복제 지연이 애플리케이션에 얼마나 영향을 끼치는지 파악할 필요가 있다
  • 지연에 대한 영향이 크다면 강력한 일관성을 보장할 수 있도록 시스템을 설계해야 한다
  •  애플리케이션에서 강력한 보장을 제공할 수도 있지만 애플리케이션에서 다루기엔 복잡하다
  • 트랜잭션은 이러한 문제에 대한 해결을 데이터베이스 단에서 보장해주지만 
  • 분산 데이터베이스로 전환되면서 많은 시스템이 트랜잭션을 포기했다
  • 트랜잭션이 성능과 가용성 측면에서 너무 비싸고, 확장 가능한 시스템에서는 어쩔 수 없이 최종적 일관성을 사용해야한다는 주장이 존재.

2. 다중 리더 복제

다중리더 설정 : 쓰기를 허용하는 노드를 하나 이상 두는 것으로 자연스럽게 확장

 

2-1. 다중 리더 복제의 사용 사례

1) 다중 데이터센터 운영

  • 각 데이터센터마다 리더가 존재하는 방식으로 다중 리더를 세팅
  • 데이터센터간에는 각 데이터센터의 리더가 다른 데이터센터의 리더에게 변경사항을 복제
  • 다중 리더 복제의 단점 : 동일한 데이터를 다른 두 개의 데이터 센터에서 변경할 경우 쓰기 충돌 해소 필요

2) 오프라인 작업을 하는 클라이언트

  • 모든 디바이스에는 리더처럼 동작하는 로컬 데이터베이스가 존재 (쓰기 요청을 받아야 함)
  • 디바이스의 인터넷이 연결되면 로컬에 변경된 데이터가 복제 서버로 동기화 된다.
  • 아키텍처 관점에서 보면 이 설정은 근본적으로 데이터 센터 간 다중 리더 복제와 동일하다.

2-2. 쓰기 충돌 다루기 

  • 다중 리더 복제의 가장 큰 문제 : 쓰기 충돌
    • 각 사용자의 변경을 로컬 리더에 적용하고 변경을 비동기로 복제시 출동을 감지
  1. 충돌 회피 : 충돌을 처리하는 제일 간단한 전략
  2. 일관된 상태 수렴 : 모든 복제 계획은 모든 복제 서버가 최종적으로는 동일하다라는 사실을 보장해야 한다 
  3. 사용자 정의 충돌 해소 로직 : 전체 트랜젝션이 아닌 개별 로우나 문서 수준에서 적용된다

2-3. 다중 리더 복제 토폴로지

  • 복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로를 말한다.
  • 리더가 둘 이상이라면 다양한 토폴로지가 가능하다.
    • 전체 연결 토폴로지는 모든 리더가 각자의 쓰기를 다른 모든 리더에서 전송한다
    • 원형 토폴로지는 각 노드가 하나의 노드로부터 쓰기를 받고, 이 쓰기를 다른 한 노드에 전달한다.
    • 별 모양 토폴로지는 지정된 루트 노드 하나가 다른 모든 노드에 쓰기를 전달한다. 트리로 일반화된다.

3. 리더가 없는 복제 

일부 데이터 저장소 시스템은 리더의 개념을 버리고 모든 복제 서버가 클라이언트로부터 쓰기를 직접 받을 수 있게 허용하는 접근 방식을 사용하기도 한다.

리더 없는 복제는 아마존이 내부 다이나모 시스템에서 사용한 후 다시 데이터베이스 아키텍처로 유행했다.

리악, 카산드라, 볼드모트는 다이나모에서 영감을 얻은 리더 없는 복제 모델의 오픈소스 데이터스토어이며 이런 종류의 데이터베이스를 다이나모 스타일이라고 부른다.

3-1. 노드가 다운됐을 때 데이터 베이스에 쓰기

  • 읽기 복구와 안티 엔트로피
    • 복제 계획은 최종적으로 모든 데이터가 모든 복제 서버에 복사됨을 보장해야 한다.
    • 다이나모 스타일은 누락된 쓰기 처리를 위해 두 가지 메커니즘을 주로 사용한다.
      • 읽기 복구
      • 안티 엔트로피 처리
    • 읽기 복구만 사용하는 경우 값을 읽을 때만 복구가 가능하므로 읽지 않는 값은 일부 복제본에 누락될 가능성이 존재한다.
  • 읽기와 쓰기를 위한 정족수
    • n개의 복제 서버가 있을 때 w개의 노드에 쓰기 성공이 보장되고 읽기는 r개 노드로 수행되는 경우에 w + r > n의 경우 항상 최신 값을 읽는 것을 기대한다.
    • 정족수(Quorum) 읽기와 쓰기인 경우 위 식을 보장한다. 

3-2. 정족수 일관성의 한계

  • 모든 읽기가 항상 최신값을 반환함을 보장하는 경우 흔히 Strong Consistency(강력한 일광성)을 보장한다고 한다.
  • 보통 w + r > n인 경우 강력한 일관성이 보장된다고 하지만 정족수 읽기와 쓰기인 경우 강력한 일관성을 보장하지 않는 에지 케이스가 존재한다.
  • 최신성 모니터링
    • 리더 기반 복제에서 데이터베이스는 일반적으로 복제 지연에 대한 지표를 노출한다 
    • 리더없는 복제 서비스에서 복제 서버의 오래됨을 측정하고 매개변수에 따라 오래된 값을 읽는 비율을 예측하는 연구가 있었다
    • 최종적 일관성은 의도적으로 모호한 보장이지만 운용성을 위해서는 최종적 을 정량화 할 수 있어야 한다

3-3. 느슨한 정족수와 암시된 핸드오프

  • 적절히 설정된 정족수가 있는 데이터베이스는 장애 복구 없이 개별 노드 장애를 용인한다
  • 노드가 n개 이상인 대규모 클러스터에서 특정 값을 위한 정족수가 충족되지 못하더라도 n에 포함되지 않은 다른 노드에 우선적으로 쓰기를 기록하는 방식을 느슨한 정족수라고 부른다
  • 느슨한 정족수는 쓰기 가용성을 높이는데 유용하지만 w + r > n인 경우에도 강력한 일관성을 보장하지 않는다
  • 느스한 정족수는 모든 일반적인 다이나모 수현에서 선택 사항이다

3-4. 동시 쓰기 감지

  • 다이나모 스타일 데이터베이스는 여러 클라이언트가 동시에 같은 키에 쓰는 것을 허용하므로 엄격한 정족수를 사용하더라도 충돌이 발생한다.
  • 동시 쓰기가 발생했을 때 네트워크 지연이나 부분적인 장애로 인해 쓰기 요청이 다른 노드에 다른 순서로 도착하여 일관성이 깨질 수 있다.
  • 최종적 일관성을 달성하기 위해 복제본들은 동일한 값이 돼야 한다
  • 쓰기 충돌 해소를 위한 기법
    • 최종 쓰기 승리(동시 쓰기 버리기)
      • 예전 값은 버리고 최신 값으로 덮어쓰는 방법이다. 
      • 어떤 쓰기가 최신인지 명확하게 결정할 수 있는 한 최종적 일관성이 보장된다.
      • 카산드라에서 유일하게 제공하는 충돌 해소 방법이고 리악에서는 선택적 기능이다.
      • LWW로 데이터베이스를 안전하게 사용하는 유일한 방법은 키를 한번만 쓰고 이후에는 불변 값으로 다루는 것이다.
    • 이전 발생 관계와 동시성 
      • 사실 작업이 다른 작업보다 먼저 발생하지 않으면 단순히 동시 작업이라 말한다
    • 동시에 쓴 값 병합
      • 형제 : 여러 작업이 동시에 발생한 경우 클라이언트가 합쳐 정리한 동시에 쓴 값 
      • 서버는 모든 키에 대해 버전을 기록하고 클라이언트가 키를 읽을 때 최신 버전뿐만 아니라 덮어쓰지 않은 모든 값을 병합하여 반환한다
      • 톰스톤 : 버전 값들을 병합할 때 상품이 제거되었음을 알 수 있게 남겨 둔 삭제 표시

참고

'Books' 카테고리의 다른 글

[대규모 중심 어플케이션 설계] 07장. 트랜잭션  (1) 2025.01.23