[DB] 트랜잭션과 로깅 원리

DB TRANSACTION

개념 : All or Nothing!

  • A transaction is a logical, atomic unit of work that contains one or more SQL statements. (Oracle docs)

  • DB의 상태변경을 위해서 수행하는 논리적인 작업 단위를 구성하는 일련의 연산들의 집합. (one or more SQL statements 에서 알 수 있듯이 작업 단위는 CRUD로 대표되는 질의어 한 문장이 아니다. )

  • 전체 작업이 모두 완료되거나, 정상 처리 안될 경우에는 아무것도 실행되지 않는 처음 상태로 돌아가야 한다.

특징 : ACID

  • Atomic 원자성
    • DBMS는 완료되지 않은 트랜잭션의 중간 상태를 반영해서는 안된다
    • All or Nothing!
  • Consistency 일관성
    • 성공적으로 수행된 트랜잭션은 정당한 데이터만을 데이터베이스에 반영해야 한다.
    • 이게 무슨 뜻이냐면 DB 스키마에 지정된 기본키, 외래키 제약같은 명시적인 무결성 제약 조건 뿐만 아니라 계좌 이체와 같이 계좌 잔고의 합이 이체 전후와 같아야 한다는 비명시적인 조건까지 모두 지키는 것을 말한다
  • Isolation 독립성
    • 각각의 트랜잭션은 다른 트랜잭션의 수행에 영향을 받지 않고 독립적으로 수행되어야 한다
    • 다른 트랜잭션 작업이 트랜잭션 도중 끼지 못한다는 것을 의미한다
    • 이것이 보장되지 않으면 원래 상태로 트랜잭션이 돌아가지 못한다
    • 독립성을 보장하기 위한 가장 쉬운 방법은 순차적으로 수행하는 것이지만, 다양한 locking 프로토콜로 독립성을 보장하고 있다. (후술)
  • Durability 지속성
    • 트랜잭션에 의한 변경은 향후 어떤 장애가 발생되더라도 보존되어야 한다

트랜잭션 지원을 위한 DBMS 구조

버퍼 관리자

  • DB는 크게 질의 처리기, 저장 시스템으로 나눌 수 있다. (MySql의 경우 저장 시스템을 InnoDB, MyISAM으로 선택 가능)
  • DBMS는 데이터를 고정 길이 페이지로 저장하고, 페이지 단위로 입출력이 이루어진다. 이 때 전체 데이터베이스의 일부분을 메인 메모리에 유지해서 성능 향상을 꾀하는데, 이것들을 관리하는 모듈을 페이지 버퍼, 또는 버퍼 관리자라고 부른다.

  • 버퍼 관리 정책에 따라 트랜잭션의 UNDO와 REDO 복구가 요구되거나, 그렇지 않게 된다.

버퍼관리정책

UNDO

  • 트랜잭션이 정상적으로 종료되지 않을 경우, 변경된 페이지가 원상복구 되어야 한다.
  • 이것을 undo라 한다.
  • 두 가지 UNDO 방식이 있는데
    • STEAL : 수정된 페이지를 언제든 디스크에 쓴다
    • ¬STEAL : 수정된 페이지를 트랜잭션 종료 시점까지 버퍼에 유지하다가 마지막에 디스크에 쓴다
  • 두번째 정책은 굉장히 큰 메모리 버퍼가 필요하다
  • 따라서 거의 모든 DBMS가 첫번째 정책을 따른다
    • 근데 이건 언제든 트랜잭션 도중에 바뀐 데이터를 디스크에 쓸 수 있다는 뜻이잖아?
    • 따라서 UNDO 로깅과 복구가 필연적으로 필요하다

REDO

  • 커밋한 트랜잭션의 수정은 어떠한 경우에도 유지되어야 한다
  • 이미 커밋한 트랜잭션의 수정을 재반영하는 복구 작업을 REDO 복구라 한다.
  • UNDO와 마찬가지로 버퍼 정책에 따라 영향을 받는다
  • 여기도 정책이 두 가지가 있는데
    • FORCE : 수정했던 모든 페이지를 트랜잭션 커밋 시점에 반영
      • REDO 복구가 필요없다. 이미 디스크에 반영이 되니까.
    • ¬FORCE : 수정했던 페이지를 커밋 시점에 디스크에 반영 X.
      • 디스크에 쓰지 않을 뿐 로그는 남긴다.
      • 반영되지 않았기 때문에 무조건 반드시 REDO 복구가 필요하다

정리

  • 거의 모든 DBMS는 REDO는 STEAL, UNDO는 ¬STEAL 정책을 택하고 있다
  • 따라서 UNDO 복구, REDO 복구가 모두 필요하다

트랜잭션 복구 기법

로그

  • UNDO 복구, REDO 복구를 위해 가장 널리 쓰이는 방법은 log 기법이다
  • 데이터베이스의 모든 갱신 작업을 기록한다
  • 안정적으로 저장하기 위해 DBMS 자체적으로 여러 벌의 로그를 유지하기 하지만, 보통은 로그 하나다.
  • 로그는 append 방식으로 쭉 덧붙여지며, 각 로그 레코드는 고유한 식별자를 가진다

  • 물리적, 논리적 로깅으로 나눌 수 있다

로그 방식

대부분의 DBMS 제품들은 물리적 로깅, 물리전 전이 로깅, 논리적 전이 로깅을 혼합해서 사용하고 있음

물리적 로깅

  • DBMS에서 가장 많이 쓰이는 로깅 방식
  • 로그 레코드는 갱신 전, 갱신 후 이미지를 모두 가지고 있고 UNDO 때는 이전 이미지를 쓰고 REDO 때는 이후 이미지를 쓴다. 가장 간단쓰.
  • 페이지 수준이나 레코드 수준에서 이뤄지기도 한다

물리적 전이 로깅

  • 이전, 이후 이미지를 모두 기록하지 않고, XOR 차이점을 기록한다

논리적 전이 로깅

  • 물리적인 로깅이 결과값을 기록한다면 논리적 전이 로깅은 오퍼레이션을 기록한다
  • 예를 들어, a = a + 1과 같은 연산을 로깅할 때 물리적인 로깅은 이전값 0, 이후값 1을 로깅하지만 논리적 전이 로깅은 연산 자체를 기록한다

로깅 규칙

밑의 두 가지 조건은 반드시 무조건 하늘이 두 쪽이 나도 지켜져야 한다.

WAL : Write Ahead Logging

해당 업데이트가 DB에 써지기 전 반드시 먼저 UNDO 정보가 로깅되야 한다. 그래야 어떠한 경우에도 UNDO 복구를 수행할 수 있다.

REDO도 마찬가지!

트랜잭션이 정상 종료되기 위해서는 반드시 먼저 REDO 정보가 로깅되야 한다. 적어도 커밋 시점에 REDO 정보가 로깅되어야 REDO 복구를 수행할 수 있다.

그럼 어떻게 로깅을 수행하는가?

  • DBMS는 로그 레코드를 위한 별도의 로그 버퍼를 둠.
  • 이 로그 버퍼를 통해 로그 파일에 입출력함.
  • 성능을 위해 로그 버퍼에 로그 레코드를 모았다가 블록 단위로 로그 파일에 출력.

  • 로그 버퍼에 유지된 레코드는 다음과 같은 시점에 파일에 쓰이게 된다(write)
    • 어떤 트랜잭션이 커밋을 요청한 경우
    • WAL을 해야하는 경우
    • 로그 버퍼가 모두 소진된 경우
    • DBMS가 내부적으로 필요로 하는 경우
  • 보통 첫 번째와 두 번째의 경우에 로깅이 수행되겠지?

로깅은 느려터진 fsync가 두 번이나 수행되어야 해서 느리다

  • DBMS는 로그 레코드를 안전하게 쓰기 위해 write 호출 외에 fsync 함수를 호출한다고 한다. 이 fsync 함수 호출이 OS에서 매우 느린 연산 축에 속하고, 커밋 되려면 아까 말했듯이 로그가 로그 파일에 써져야 하기 때문에 fsync 함수 호출이 종료될 때까지 대기해야 한다.
  • 더 자세히 보면, 이게 왜 느린지 알 수 있다.
    • 로그 레코드 쓰고 fsync 함수 실행
    • 로그 헤더 업데이트 후 fsync 함수 실행
  • 이렇게 두 번 씩이나 느린 fsync 함수를 수행하는 이유는 어떤 버퍼 프레임부터 디스크에 쓰게 될 지는 OS 맘이고, 순서대로 디스크에 써진다는 보장이 없기 때문이다. 헤더 업데이트가 필연적으로 수행이 되어야만 하는 한계가 있다.
  • 대부분의 커밋 연산은 이 로그 레코드를 로그 파일에 쓰고, fsync 함수를 실행하는 시간이라고 보면 된다.

  • 수천, 수만건의 커밋이 발생한다면? 이런 느려터진 디스크 입출력 과정이 수없이 많이 진행되어야 한다.

그래서 로깅을 빠르게는 못하나?

고속버스처럼 모아서 처리하자! - 그룹 커밋

각 트랜잭션의 커밋 요구를 개별 처리 대신 한꺼번에 모아서 처리한다.

  • 수천 내지 수만 TPS 요청이 있다고 가정하면, 한 트랜잭션이 커밋할 때 조금만 기다리면 다른 트랜잭션이 커밋을 요청할 것이다.
  • 개별적으로 처리하지 말고 고속버스에 사람들 태우는 것처럼 모아다가 한 번에 처리하면 디스크 출력 횟수를 줄일 수 있다.
  • 프로그래밍 세계의 모든 것은 trade-off… 이 방식은 각 트랜잭션의 응답 시간을 희생하고 시스템 전체의 처리량을 높이는 방식이다.
  • 동시에 요청되는 커밋 횟수가 많을 수록 효율이 높아진다.
  • 핵심은 당연히 적절한 그룹 커밋 대기 시간을 정하는 것이다. 너무 짧으면 효율이 떨어지고 너무 길면 응답 시간이 느려진다.

지속성을 포기하자! - fsync를 버린다

속도 저하와 관련된 만악의 근원인 fsync를 포기하고 다른 방식으로 로깅한다.

  • 우리가 쓸 myql의 InnoDB의 경우 엄격한 fsync 방식 대신 다른 방식의 로깅 방식으로 조정할 수 있다
  • 비동기 커밋이라는 것도 있는데, 로그 버퍼에 로그 레코드를 쓰고 바로 커밋을 완료하는 방식이다. 이러면 로그 파일이 써지기도 전에 커밋이 완료되겠지?
  • 당연히 비동기 커밋을 쓰게 되면 데이터 유실이 발생한다. 근데, 이미 트랜잭션 수행 중 UNDO 로깅은 시행이 된 상태라서 rollback은 가능하다.
  • 민감한 데이터에는 쓰면 안되고, 일부 데이터 유실을 감당할 수 있는 서비스라면 고려해볼만 하다.

로그로 어떻게 복구가 이루어지나?

트랜잭션 철회의 경우

  • 사용자 요청 또는 오류 발생 등으로 시스템이 트랜잭션을 철회하는 경우

  • 로그 역방향 탐색 => 해당 트랜잭션의 UNDO 복구가 필요한 로그 찾음 => UNDO 수행 => 다시 UNDO 못하도록 보상 로그 레코드(Compensation Log Record)라 하는 REDO 전용 로그 생성
  • 보상 로그 레코드는 UNDO 로그의 이전 로그를 가리키게 하여 재차 UNDO 발생되는 일이 없도록 함

장애로 인한 재시작의 경우

  • 소프트웨어나 하드웨어 문제로 데이터베이스 시스템이 재시작 복구하는 경우

  • 크게 3단계로 복구가 이루어지게 된다

    • 로그 분석 단계 : 무엇을 어디부터 복구할 것인가?
      • 마지막 체크포인트부터 최근 로그까지 탐색하면서 어디부터 시스템이 복구 시작을 해야하는지, 어느 트랜잭션을 복구해야 하는지를 알아낸다
    • REDO 복구 단계
      • 복구를 시작해야하는 지점부터 장애 발생 시점까지 REDO가 필요한 모든 로그를 REDO 복구
      • 모든 트랜잭션에 대해 (심지어 실패한 트랜잭션이라 하더라도) REDO 복구
      • 이렇게 우선 해놓고 이후 단계인 REDO 복구를 하면 장애 발생 이전 시점과 데이터가 같아진다
    • UNDO 복구 단계
      • 로그를 최신 시점부터 역방향 탐색하며 UNDO 복구가 필요한 로그들에 대해 UNDO 복구 수행
      • 이 단계의 UNDO 복구를 개별 트랜잭션과 구분하기 위해 GLOBAL UNDO라고 한다.

커밋 중 오류가 발생한다면?

네이버 D2 트랜잭션 상태 다이어그램

  • ALL or NOTHING!
  • 커밋이 완료된 것이 아니라면, 수행되지 않은 것과 같이 취급.
  • 일단 커밋이 되면 partially commited 상태가 되고, 별 다른 오류가 없으면 committed가 됨
  • 하지만 그렇지 않다면? 어림도 없지. 바로 failed 상태를 거쳐서 aborted 된다. rollback 되겠지?

참고

Naver D2 - DBMS는 어떻게 트랜잭션을 관리할까?

(학습 용도로 위 문서를 재구성하며 스스로 이해할 수 있도록 정리했습니다.)

Oracle docs - Introduction to transactions

 Date: August 22, 2019
 Tags:  DB

Previous:
⏪ [DB] DB의 클러스터링 인덱스 구조, 정규화

Next:
[JS] 함수형 프로그래밍 입문 #4 ⏩