ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [데이터 중심 애플리케이션] 트랜잭션
    Software Development/Database 2021. 11. 7. 19:39

    트랜잭션은 데이터베이스에 접속하는 애플리케이션에서 프로그래밍 모델을 단순화하려는 목적으로 만든 것이다.

     

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

    트랜잭션은 이점과 한계가 있다. 

    ACID의 의미

    ACID를 준수한다? 모호할 수 있다. 그저 마케팅 용어로 사용된다.

    ACID를 따르지 않는 시스템을 BASE라고 한다. 가용성, 유연한 상태, 최종적 일관성을 가진다.

    원자성

    오류가 생겼을 때 트랜잭션을 어보트하고 해당 트랜잭션에서 기록한 모든 내용을 취소하는 능력.

    일관성

    항상 진실이어야 하는, 데이터에 관한 어떤 선언이 있다는 것.

    일관성은 애플리케이션의 불변식 개념에 의존, 일관성은 애플리케이션이 정의하는 것이지 데이터베이스가 보장하는 것이 아니다.

    일관성은 애플리케이션의 속성이다.

    격리성

    동시에 실행되는 트랜잭션은 서로 격리된다. 트랜잭션은 다른 트랜잭션을 방해할 수 없다.

    지속성

    트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되지 않는다는 것을 보장.

    단일 노드 데이터베이스에서 지속성은 비휘발성 저장소에 기록됐다는 것을 의미. 쓰기 전 로그나 비슷한 수단을 동반하기도 한다.

    단일 객체 연산과 다중 객체 연산

    단일 객체 쓰기

    단일 객체 연산은 여러 클라이언트에서 동시에 같은 객체에 쓰려고 할 때 갱신 손실을 방지한다. 그러나 일반적인 의미의 트랜잭션이 아니다. 원자성은 장애 복구용 로그를 써서 복구.

    격리성은 객체에 잠금을 사용해 구현.

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

    분산 데이터스토어는 다중 객체 트랜잭션 지원을 포기. 그러나 근본적으로 트랜잭션을 막는 것은 아니다.

    다중 객체 트랜잭션은 참조가 유효한 상태로 유지되도록 보장.

    비정규화된 데이터가 동기화가 깨지는 것을 방지.

    보조 색인이 있으면 값이 변경될 때 마다 색인도 갱신이 필요하다. 트랜잭션의 관점에서 색인은 서로 다른 데이터베이스 객체다. 트랜잭션 격리성이 없다면 어떤 색인에서는 레코드가 보이고 다른 색인은 갱신되지 않아서 레코드가 보이지 않을 수 있다.

    오류와 어보트 처리

    ACID 데이터베이스는 오류가 생기면 어보트되고 안전하게 재시도할 수 있다.

    모든 시스템이 이 철학을 따르는 것은 아니다. 리더 없는 복제

    어보트의 취지는 안전하게 재시도를 할 수 있게 하는 데 있다.

    그러나 어보트는 완벽하지 않다.

    • 트랜잭션이 성공했지만 서버가 클라이언트에게 커밋 성공을 알리는 도중 네트워크가 끊겼을 때 재시도하면 트랜잭션이 두번 실행.
    • 오류 과부하 때문이라면 트랜잭션 재시도는 문제를 개선하는 게 아니라 악화시킬 수 있다.
    • 일시적인 오류만 재시도할 가치가 있으며 영구적인 오류는 재시도해도 아무 소용이 없다.
    • 트랜잭션이 데이터베이스 외부에 부수효과가 있다면 부수 효과가 실행될 수 있다. 이메일을 보낸다면 이메일을 계속 재전송할 수 있다.

    완화된 격리 수준

    동시성 문제는 트랜잭션이 다른 트랜잭션에서 동시에 변경한 데이터를 읽거나 두 트랜잭션이 동시에 같은 데이터를 변경하려고 할 때만 나타난다.

    테스트로 발견하기 어렵다. 매우 드물게 발생하기 때문이다. 

    커밋 후 읽기

    두 가지를 보장한다.

    1. 데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다(더티 읽기가 없음).

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

    더티 읽기 방지

    트랜잭션이 데이터베이스에 데이터를 썼지만 아직 커밋되거나 어보트되지 않았다고 하자. 다른 트랜잭션에서 커밋되지 않은 데이터를 볼 수 있다면 이를 더티 읽기라 부른다.

    커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 읽기는를 막아햐 한다. 트랜잭션이 쓴 내용은 커밋된 후에야 다른 트랜잭션에게 보인다는 뜻이다.

    더티 읽기를 막는게 유용한 이유

    • 트랜잭션이 여러 객체를 갱신하는데 더티 읽기가 생기면 다른 트랜잭션이 일부는 갱신된 값을, 일부는 갱신되지 않은 값을 볼 수 있다.
    • 트랜잭션이 어보트되면 그때까지 쓴 내용은 모두 롤백돼야 한다.

    더티 쓰기 방지

    두 트랜잭션이 데이터베이스에 있는 동일한 객체를 동시에 갱신하려고 하면 무슨 일이 생길까?

    먼저 쓴 내용은이 아직 커밋되지 않은 트랜잭션에서 쓴 것이고 나중에 실행된 쓰기 작업이 커밋되지 않은 값을 덮어쓰는 것을 더티 쓰기라고 부른다.

    보통 먼저 쓴 트랜잭션이 커밋되거나 어보트될 때까지 두 번째 쓰기를 지연시키는 방법을 사용한다.

    커밋 후 읽기 구현

    로우 수준 잠금을 사용해 더티 쓰기 방지. 

    스냅숏 격리와 반복 읽기

    각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 읽는다. 스냅숏 격리는 백업이나 분석처럼 실행하는 데 오래 걸리며 일기만 실행하는 질의에 요긴하다.

    스냅숏 격리 구현

    더티 쓰기를 방지하기 위해 쓰기 잠금을 사용한다. 쓰기를 실행하는 트랜잭션은 같은 객체에 쓰느 다른 트랜잭션의 진행을 차단할 수 있다는 뜻이다. 그러나 읽을 때는 아무 잠금도 필요 없다.

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

    쓰기 작업이 일상적으로 처리되는 것과 동시에 일관성 있는 스냅숏에 대해 오래 실행되는 읽기 작업을 처리할 수 있다.

    스냅숏 격리를 구현하기 위해 데이터베이스는 더티 읽기를 막는 데 쓰는 메커니즘을 일반화한 방법을 사용.

    데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 한다. 데이터베이스가 객체의 여러 버전을 함께 유지하므로 이 기법은 다중 버전 동시성 제어라고 한다.

     

    데이터베이스가 스냅숏 격리가 아니라 커밋 후 읽기 격리만 제공할 필요가 있다면 갹체마다 버전 두개씩만 유지하면 충분하다. 커밋된 버전과 덮어 쓰여졌지만 아직 커밋되지 않은 버전이다. 

    일관된 스냅숏을 보는 가시성 규칙

    트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정. 

    아래의 조건이 모두 참이면 객체를 볼 수 있다.

    • 읽기를 실행하는 트랜잭션이 시작한 시점에서 읽기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태였다.
    • 일긱 대상 객체가 삭제된 것으로 표시되지 않았다. 또는 삭제된 것으로 표시됐지만 읽기를 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 아직 커밋되지 않았다.

    색인과 스냅숏 격리

    단순하게 색인이 객체의 모든 버전을 가리키게 색인 질의가 현재 트랜잭션에서 볼 수 없는 버전을 걸러내게 하는 것.

    반복 읽기와 혼란스러운 이름

    스냅숏 격리는 읽기 전용 트랜잭션에 유용.

    갱신 손실 방지

    갱신 손실 문제는 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 발생할 수 있다. 두 트랜잭션이 이 작업을 동시에 하면 두 번재 쓰기 작업이 첫 번째  변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있다.

    원자적 쓰기 연산

    원자적 갱신 연산을 애플리케이션 코드에서 read-modify-write 주기를 구현할 필요를 없애 준다. 

    명시적인 잠금

    여러 플레이어가 동시에 같은 물체를 옮길 수 있는 다중플레이어 게임을 생각해 보자. 이 경우 원자적 연산이 충분하지 않을 수 있다.

    두 플레이어가 동시에 같은 물체를 움직일 수 없도록 잠금을 사용.

    갱신 손실 자동 감지

    병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트시키고 read-modify-write 주기를 재시도하도록 강제.

    데이터베이스가 이 확인을 스냅숏 격리와 결합해 효율적으로 수행할 수 있다.

    Compare-and-set

    값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용. 현재 값이 이전에 읽은 값과 다르면 갱신은 반영되지 않고 read-modify-write 주기를 재시도.

    두 명의 사용자가 동시에 같은 위키 페이지를 갱신하지 못하도록 이런 방법을 시도할 수 있다.

    충돌 해소와 복제

    복제가 적용된 데이터베이스에서 갱신 손실을 막는 것은 다른 차원의 문제다. 여러 노드에 복사본이 많기 때문에 데이터가 다른 노드들에서 동시에 변경될 수 있다.

    복제가 허용된 데이터베이스에서 흔히 쓰는 방법은 쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전을 생성하는 것을 허용하고 사후에 애플리케이션 코드가 특별한 데이터 구조를 사용해 충돌을 해소하고 이 버전들을 병합.

    쓰기 스큐와 팬텀

    의사들이 병원에서 교대로 서는 호출 대기를 관리하는 애플리케이션을 있다면, 병원은 한 시점에 여러 의사가 호출 대기 상태에 잇게 하려고 하지만 최소 한 명의 의사는 반드시 호출 대기를 해야 한다.

    A와 B가 호출 대기를 하는데, 동시에 호출 대기 상태를 끄면, 데이터베이스에서 스냅숏 격리를 사용하므로 두 트랜잭션은 다음 단계로 이동한다. 트랜잭션은 모두 커밋됐지만, 호출 대기하는 의사가 없다. 최소 한 명의 의사가 호출 대기해야 한다는 요구사항을 위반했다.

    쓰기 스큐를 특징짓기

    이런 이상 현상을 쓰기 스큐라고 한다. 두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티 쓰기도 갱신 손실도 아니다. 충돌이 발생한 것 처럼 보이진 않지만 경쟁 조건에 있는 것이다. 두 트랜잭션이 한 번에 하나씩 실행됐다면 두 번쨰 의사는 호출 대기를 끄는게 방지됐을 것이다. 쓰기 스큐는 두 트랜잭션이 같은 객체들을 읽어서 그중 일부를 갱신할 때 나타날 수 있다. 

    • 회의실 예약 시스템
    • 다중플레이어 게임
    • 사용자명 획득
      • 같은 사용자명을 동시에 생성할 경우, 스냅숏 격리에 안전하지 않다. 유일성 제약 조건이 해결책이다.
    • 이중 사용 방지
      • 쓰기 스큐가 발생하면 동시에 삽입된 두 개의 지불 항목이 모여서 잔고가 음수가 되게 하는 일이 생길 수 있지만 어떤 트랜잭션도 다른 쪽에 알려주지 못한다.

    직렬성

    직렬성 격리는 가장 강력한 수준이라고 여겨진다. 여러 트랜잭션이 병렬로 실행되도라도 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장한다.

    실제적인 직렬 실행

    동시성을 완전히 제거하여 동시성 문제를 피한다. 단일 스레드에서 한 번에 트랜잭션 하나만 처리.

    2007년경에 단일 스레드루프에서 트랜잭션을 실행하는 게 실현 가능하다고 결론이 났다. 높은 성능을 위해 다중 스레드 동시성이 필수적인 것으로 여겨졌다. 단일 스레드 실행이 가능한 이유는 다음과 같은 변화 때문이다.

    • 램 가격이 저렴해졌다. 데이터셋 전체를 메모리에 유지할 수 있다. 트랜잭션이 접근해야 하는 데이터가 메모리에 있다면 데이터를 디스크에 읽어 오기를 기다려야 할 때보다 트랜잭션이 훨씬 빠르다.
    • OLTP 트랜잭션은 짧고 실행하는 읽기와 쓰기의 개수가 적다. 오래 실행되는 분석 질의는 읽기 전용이라 직렬 실행 루프 밖에서 일관된 스냅숏을 사용해 실행할 수 있다.

    레디스, 데이토믹 등은 트랜잭션을 순차적으로 실행하는 방법이 구현되어 있다. 단일 스레드로 실행되도록 설계된 시스템이 동시성을 지원하는 시스템보다 성능이 나을 때도 있다. 잠금을 코디네이션하는 오버헤드를 피할 수 있다. 대신 처리량은 CPU 코어 하나의 처리량으로 제한된다. 

    트랜잭션을 스토어드 프로시저 안에 캡슐화하기

    트랜잭션 코드 전체를 스토어드 프로지서 형태로 데이터베이스에 미리 제출. 트랜잭션에 필요한 데이터는 메모리에 있기 때문에 스토어드 프로시저는 네트워크가 디스크 I/O 대기 없이 매우 빨리 실행.

    스토어드 프로시저 장단점

    데이터베이스 벤더마다 제각각이다.

    데이터베이스에서 실행되는 코드는 관리가 어렵다. 디버깅하기 어렵고 버전 관리 및 배포가 불편하며 테스트하기도 까다롭고 모니터링용 지표 수집 시스템과 통합하기도 어렵다.

    데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감하다.

    위 문제는 범용 프로그래밍 언어롤 사용하여 대체했다. 모든 트랜잭션은 단일 스레드에서 실행.

    파티셔닝

    순차적 실행은 동시성 제어가 간단하지만 처리량이 단일 장비에 있는 단일 CPU 코더의 속도로 제한된다.

    파티셔닝 하면 단일 파티션 트랜잭션보다 코디네이션 때문에 엄청나게 느려진다. 초당 1000개 처리한다(볼트DB의 경우).  장비 추가해도 소용 없다. 키-값 데이터는 흔히 파티셔닝 가능하다. 보조 색인이 있는 데이터는 여러 파티션에 걸친 코디네이션이 많이 필요하다.

    2단계 잠금

    스냅숏 격리는 읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 읽는 쪽을 막지 않는 것이다. 2단계 잠금(2PL)은 직렬성을 제공하므로 갱신 손실과 쓰기 스큐를 포함한 모든 경쟁 조건으로부터 보호.

    쓰기 트랜잭션은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막고 역도 성립한다.

    2단계 잠금 구현

    교착 상태가 생길 수 있기 때문에, 트랜잭션 중 하나를 어보트시켜서 다른 트랜잭션들이 진행할 수 있도록 한다. 어보트는 재시도

    2단계 잠금의 성능

    성능이 약점이다. 그래서 잘 안썼다. 동시성이 줄어들기 때문이다. 

    서술 잠금

    공유/독점 잠금(2단계 잠금) 비슷하게 동작하지만, 검색 조건에 부합하는 모든 객체에 속하도록 한다.

    그러나 잘 동작하지 않는다. 진행중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸린다.

    직렬성 스냅숏 격리(SSI)

    위의 내용대로라면, 동시성과 경쟁 조건은 트레이프 오프 관계일까?

    직렬성 스냅숏 격리는 완전한 직렬성을 제공하지만 스냅숏 격리에 비해 약간의 성능 손해만 있다. 2008년에 등장.

    단일 데이터베이스, 분산 데이터베이스 모두에서 사용된다.

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

    2단계 잠금은 비관적 동시성 제어 메커니즘이다. 다중 스레드 프로그래밍에서 자료구조 보호를 위해 사용되는 상호 배제와 비슷하다.

    직렬 실행은 어떤 면에서 극단적으로 비관적이다. 

    직렬성 스냅숏 격리는 낙관적 동시성 제어 기법이다. 트랜잭션이 커밋을 원할 때 데이터베이스는 나쁜 상황이 발생했는지 확인한다. 만약 그렇다면 재시도 한다. 직렬로 실행된 트랜잭션만 커밋이 허용된다.

    예비 용량이 충분하고 트랜잭션 사이의 경쟁이 너무 심하지 않으면 낙관적 동시성 제어 기법은 성능이 좋은 경향이 있다. 경쟁은 가환 원자적 연산을 써서 줄일 수 있다. SSI는 스냅숏 격리 위에 쓰기 작업 사이의 직렬성 충돌을 감지하고 어보트시킬 트랜잭션을 결정하는 알고리즘을 추가한다.

    뒤처진 전제에 기반한 결정

    트랜잭션에서 실행하는 질의와 쓰기는 인과적 의존성이 있을 수 있다. 직렬성 격리를 제공하려면 데이터베이스는 트랜잭션이 뒤처진 전제를 기반으로 동작하는 상황을 감지하고 그런 상황에서는 트랜잭션을 어보트 시켜야 한다.

    오래된 MVCC 읽기 감지하기

    불필요한 어보트를 피해서, 일관된 스냅숏에서 읽으며 오래 실행되는 작업을 지원하는 스냅숏 격리의 특성을 유지한다.

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

    트랜잭션이 데이터베이스에 쓸 때 영향받는 데이터를 최근에 읽는 트랜잭션이 이쓴ㄴ지 색인에서 확인해야 한다.

    직렬성 스냅숏 격리의 성능

    트랜잭션이 다른 트랜잭션들이 잡고 있는 잠금을 기다리느라 차단될 필요가 없다는 것이다. 스냅숏 격리하에서와 마찬가지로 쓰는 쪽은 읽는 쪽을 막지 않고 읽는 쪽도 쓰는 쪽을 막지 않는다. 

     

     

     

     

     

    댓글

Designed by Tistory.