직렬성이란?

  • 직렬성 격리는 가장 강력한 격리 수준

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

  • 즉, 데이터베이스에서 발생할 수 있는 모든 경쟁조건을 막음

직렬성의 구현

실제적인 직렬 실행

  • 말 그대로 한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하는 방식

  • 어쩌면 단순하지만, 2007년이 되어서야 단일 스레드에서 직렬 실행하는 것이 가능해짐

    • 과거엔 높은 성능을 위해선 다중 스레드 동시성이 필수적인 것으로 여겨졌기 때문
    • 하지만, 램 가격이 저렴해지면서 많은 사용 사례에서 활성화된 데이터셋을 전체 메모리에 유지할 수 있게 됨
    • OLTP 트랜잭션은 보통 짧고 실행하는 읽기와 쓰기 개수가 적음, 반대로 오래 실행되는 분석질의는 읽기 전용이라서 직렬 실행 루프 밖에서 일관된 스냅숏을 사용해 실행할 수 있음
  • Redis, 데이토믹 등에서 이와 같은 방법을 사용

스토어드 프로지어 안에 트랜잭션을 캡슐화

  • 데이터베이스 초창기엔 트랜잭션이 사용자 활동의 전체 흐름을 포함할 수 있게 하려고 했음

  • 이를테면 항공권 예약의 여러 단계를 하나의 트랜잭션으로 처리되고 원자적으로 커밋된다면 깔끔할 수 있을 것이라고 생각한 것

  • 하지만, 이는 사용자 입력을 기다려야한다는 의미이며, 이는 데이터베이스가 대부분 유휴 상태로 기다려야 한다는 의미이며, 매우 많은 동시 실행 트랜잭션을 지원해야 한다는 의미임

  • 따라서, 누구나 알듯 요즘엔 애플리케이션 코드와 데이터베이스 서버 사이에서 질의와 결과를 주고받는 식으로 작동하고 있음

  • 하지만, 이런 트랜잭션은 애플리케이션과 데이터베이스 사이의 네트워크 통신에 많은 시간을 소비함

  • 즉, 데이터베이스에서 동시성을 허용하지 않는다면, 매우 낮은 처리량만이 달성될 것이며, 쓸만한 성능을 얻기 위해선 여러 트랜잭션이 동시에 실행될 수 있어야 함

  • 따러서 단일 스레드에서 트랜잭션을 순차적으로 처리하는 시스템들은 상호작용하는 다중 구문 트랜잭션을 허용하지 않으며, 대신 스토어드 프로시저 형태의 기능을 제공함

  • 스토어드 프로시저는 트랜잭션에 필요한 데이터는 모두 메모리에 있고, 스토어드 프로시저는 네트워크나 디스크 I/O 대기 없이 매우 빨리 실행된다고 가정

  • 스토어드 프로시저의 단점

    • 데이터베이스마다 스토어드 프로시저용 언어가 있고, 이러한 언어들은 범용 프로그래밍 언어보다 기능적으로 부족함
    • 데이터베이스에서 실행되는 코드는 관리(디버깅/버전관리/배포)가 어려움
    • 잘못 작성된 스토어드 프로시저가 데이터베이스 서버의 성능을 망가트릴 수 있음(이는 애플리케이션 서버에 미치는 영향보다 훨씬 큼)

파티셔닝

  • 직렬성의 구현은 단일 장비에 있는 단일 CPU코어 속도로 트랜잭션 처리량이 제한됨

  • 읽기 트랜잭션은 스냅숏 격리를 사용해 다른 곳에서 실행될 수 있지만, 쓰기 처리량이 높은 애플리케이션에서는 단일 스레드 트랜잭션 처리가 병목지점으로 작용될 수 있음

  • 이를 해결하기 위해 각 CPU 코어에 각자의 파티션을 할당해서 트랜잭션 처리량을 CPU 코어 개수에 맞춰 선형적으로 확장할 수 있음

  • 파티셔닝의 단점

    • 여러 파티션에 접근해야 하는 트랜잭션이 있다면, 데이터베이스는 해당 트랜잭션이 접근하는 모든 파티션에 걸쳐서 코디네이션해야함(잠금, 해제)

    • 트랜잭션이 단일 파티션에서 실행될 수 있는지 여부는 애플리케이션에서 사용되는 데이터 구조에 매우 크게 의존

      • 단순한 키-값 데이터는 쉽게 파티셔닝되지만, 여러 보조 색인이 있는 데이터는 파티셔닝 방식을 사용하지 않는 편이 좋음

실제적인 직렬 실행은 언제 도입해야 할까?

  • 모든 트랜잭션이 작고 빠를 때(하나의 트랜잭션이 모든 트랜잭션 처리를 지연시킬 수 있음)

  • 활성화된 데이터셋이 메모리에 적재될 수 있는 경우(디스크 I/O가 발생한다면 매우 느려짐)

  • 쓰기 처리량이 단일 CPU 코어에서 처리될 수 있을만큼 낮아야 함(그렇지 않다면, 파티셔닝을 이용할 수 있어야 함)

2단계 잠금(2PL)

  • 2PL(two-phase locking)

  • 약 30년 동안 데이터베이스에서 직렬성을 구현하는 데 널리 쓰인 방식

  • 잠금의 중요 개념은 트랜잭션이 동시에 같은 객체에 쓰려고 하면 잠금은 나중에 쓰는 쪽이 먼저 쓰는 쪽에서 트랜잭션을 완료할때까지 기다리도록 하는 것.

  • 2PL에서는 쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있지만, 누군가 어떤 객체에 쓰려고(변경, 삭제)하면 독점적인 접근을 부여함

  • 스냅숏 격리는 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않지만, 2PL은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막으며, 그 역도 성립함

  • 구현

    • 잠금을 공유 모드와 독점 모드로 나누어 사용

    • 읽기 시에는 공유 모드로 잠금을 획득, 쓰기 시에는 독점 모드로 잠금을 획득

    • 트랜잭션이 종료(커밋, 어보트)될 때까지 잠금을 획득

    • 트랜잭션이 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드 함

    • 공유 모드가 필요한 이유는 어떤 트랜잭션에서 읽기를 하고 있을 때 해당 객체가 변경이 되면 안되기 때문. 즉, 공유 모드 잠금을 획득했다면 다른 트랜잭션에서는 공유 모드 잠금을 획득할 수 있지만, 독점 모드 잠금은 획득할 수 없다는 의미

  • 단점

    • 2PL의 가장 큰 약점은 성능임

    • 잠금을 획득하고 해제하는 오버헤드도 있지만, 더 중요한 원인은 동시성이 줄어들기 때문임

  • 서술 잠금

    • 앞에서 봤던 쓰기 스큐를 유발하는 팬텀 문제 즉, 한 트랜잭션이 다른 트랜잭션의 검색 질의 결과를 바꿔버리는 문제를 예를 들어 보자

    • 회의실 예약에 있어 한 트랜잭션이 특정 시간 범위 내에 있는 회의실 예약을 검색했다면, 다른 트랜잭션은 같은 시간 범위 내에서 동일한 회의실을 예약하거나 갱신할 수 없음

    • 이는 개념상으로 서술 잠금이 필요함

 SELECT * FROM bookings
 WHERE room_id = 123 AND
 end_time > '2018-01-01 12:00' AND
 start_time < '2018-01-01 13:00';
  • 색인 범위 잠금

    • 하지만 서술 잠금은 조건에 부합하는 잠금을 확인하는 데 오랜 시간이 걸림

    • 따라서 2PL을 지원하는 데이터베이스는 실제로는 색인 범위 잠금을 구현함

    • 일종의 서술 잠금을 간략하게 근사한 것으로, 조건을 색인 범위로 간략하게 계산해 잠금을 실행함

    • 범위 잠금과 대치되는 적합한 색인이 없다면 테이블 전체를 공유 잠금으로 잡음

직렬성 스냅숏 격리(SSI)

  • 2008년에 처음 등장

  • 단일 노드 데이터베이스(PostgreSQL 9.1 이상)와 분산 데이터베이스(파운데이션DB) 모두에서 사용

  • 완전한 직렬성을 제공하며, 스냅숏 격리에 비해 약간의 성능 손해를 가짐

비관적 동시성 제어 vs 낙관적 동시성 제어

  • 비관적 동시성 제어

    • 2PL에서 사용한 방식
    • 다중 스레드 프로그래밍에서의 상호 배제와 비슷
  • 낙관적 동시성 제어

    • 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 계속 진행하도록 하는 것

    • 최종적으로 트랜잭션이 커밋되어야 할 때 데이터베이스는 나쁜 상황이 발생했는지(격리가 위반되었는지)를 확인하며, 위반됐다면 어보트시킴

    • 하지만, 경쟁이 심하면 어보트시켜야 할 트랜잭션 비율이 높아져 성능이 저하됨

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

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

뒤쳐진 전제에 기반한 결정

  • 앞선 스냅숏 격리에서 쓰기 스큐를 설명할 때 반복되는 패턴이 있음

  • 트랜잭션이 데이터베이스에서 데이터를 읽고 그 질의 결과를 기반으로 어떤 동작을 취할지 결정함. 하지만 스냅숏 격리하에서는 트랜잭션이 커밋되는 시점에 질의의 결과가 더 이상 최신이 아닐 수 있음

  • 직렬성 격리를 제공하려면 데이터베이스는 트랜잭션이 뒤처진 전제를 기반으로 동작하는 상황을 감지하고 그런 상황에서는 트랜잭션을 어보트시켜야 함

    • 이를 위해선 두 가지 상황을 고려해야 함

    • 오래된 MVCC 객체를 읽었는지 감지

    • 과거의 읽기에 영향을 미치는 쓰기 감지(읽은 후에 쓰기가 실행되는지)

오래된 MVCC 읽기 감지

  • 스냅숏 격리에서는 트랜잭션이 MVCC 데이터베이스의 일관된 스냅숏에서 읽으면 스냅숏 생성 시점에 다른 트랜잭션이 썼지만 아직 커밋되지 않은 데이터는 무시함

  • 하지만 이와 같은 방식을 사용하면, 위 그림에서 처럼 오류가 발생함

  • 이를 위해서는 오래된 읽기(이전 버전의 MVCC 스냅숏)가 감지되었을 때 데이터베이스는 무시된 쓰기 중에 커밋된 것이 있는지 확인해야함

    • 커밋을 기다리는 이유는 위 그림에서 트랜잭션 43이 읽기 전용이라면 쓰기 스큐의 위험이 없으므로 어보트 시킬 필요가 없기 때문임

과거의 읽기에 영향을 미치는 쓰기 감지

  • 데이터를 읽은 후 다른 트랜잭션에서 그 데이터를 변경할 경우도 고려대상임
  • 위 그림에서 SSL 잠금은 다른 트랜잭션을 차단하지 않음
  • 데이터베이스는 색인 항목 1234를 사용해 트랜잭션 42와 43이 이 데이터를 읽었다는 사실을 기록(커밋되거나 어보트될때까지 유지)
  • 이후, 트랜잭션이 데이터베이스에 쓸 때 영향받는 데이터를 최근에 읽은 트랜잭션이 있는지 색인에서 확인하며, 트랜잭션이 읽은 데이터가 더이상 최신이 아니라면 트랜잭션에게 알림
  • 쓰기 잠금과 비슷하지만, 읽는 쪽에서 키밋될 때까지 차단하지 않음 즉, 트랜잭션 42는 어보트되지 않으며, 트랜잭션 43은 충돌되는 쓰기가 커밋됐으므로 어보트됨

0개의 댓글