관리 메뉴

HAMA 블로그

트랜잭션 인사이드 본문

RDBMS (PostgreSQL)

트랜잭션 인사이드

[하마] 이승현 (wowlsh93@gmail.com) 2015. 7. 1. 11:19

http://helloworld.naver.com/helloworld/textyle/407507 펌


NHN Business Platform 서비스플랫폼개발센터 오이석

트랜잭션 관리는 DBMS가 제공하는 여러 기능 중에서 가장 중요하고 기본적인 것 중의 하나로, DBMS 사용자들에게는 공기와 같은 존재입니다. 이 글에서는 우리가 트랜잭션을 커밋하거나 철회했을 때 어떤 일이 일어나는지, 어떻게 DBMS가 트랜잭션을 복구하는지에 대해서 알아보려고 합니다. 어떤 원리로 트랜잭션 관리라는 매직이 이루어지는지 살펴봅시다.

트랜잭션은 무엇인가?

잘 알려진 내용이라 진부한 측면이 있지만, 먼저 트랜잭션이 무엇인지 정의부터 살펴보자. 하나의 논리적 작업 단위를 구성하는 일련의 연산들의 집합을 트랜잭션이라고 한다. 트랜잭션의 예로 계좌 간의 자금 이체가 많이 언급된다. 한 계좌에서 10만 원을 인출하여 다른 계좌로 10만 원 입금하는 이체 작업은 전체 작업이 정상적으로 완료되거나, 만약 정상적으로 처리될 수 없는 경우에는 아무 것도 실행되지 않은 처음 상태로 되돌려져야 한다. 이러한 트랜잭션은 다양한 데이터 항목들을 액세스하고 갱신하는 프로그램 수행의 단위가 된다. 흔히 트랜잭션은 ACID 성질이라고 하는 다음의 네 가지 성질로 설명된다.

  • Atomicity(원자성): 이체 과정 중에 트랜잭션이 실패하게 되어 예금이 사라지는 경우가 발생해서는 안 되기 때문에 DBMS는 완료되지 않은 트랜잭션의 중간 상태를 데이터베이스에 반영해서는 안 된다. 즉, 트랜잭션의 모든 연산들이 정상적으로 수행 완료되거나 아니면 전혀 어떠한 연산도 수행되지 않은 상태를 보장해야 한다. atomicity는 쉽게 'all or nothing' 특성으로 설명된다.
  • Consistency(일관성): 고립된 트랜잭션의 수행이 데이터베이스의 일관성을 보존해야 한다. 즉, 성공적으로 수행된 트랜잭션은 정당한 데이터들만을 데이터베이스에 반영해야 한다. 트랜잭션의 수행을 데이터베이스 상태 간의 전이(transition)로 봤을 때, 트랜잭션 수행 전후의 데이터베이스 상태는 각각 일관성이 보장되는 서로 다른 상태가 된다. 트랜잭션 수행이 보존해야 할 일관성은 기본 키, 외래 키 제약과 같은 명시적인 무결성 제약 조건들뿐만 아니라, 자금 이체 예에서 두 계좌 잔고의 합은 이체 전후가 같아야 한다는 사항과 같은 비명시적인 일관성 조건들도 있다.
  • Isolation(독립성): 여러 트랜잭션이 동시에 수행되더라도 각각의 트랜잭션은 다른 트랜잭션의 수행에 영향을 받지 않고 독립적으로 수행되어야 한다. 즉, 한 트랜잭션의 중간 결과가 다른 트랜잭션에게는 숨겨져야 한다는 의미인데, 이러한 isolation 성질이 보장되지 않으면 트랜잭션이 원래 상태로 되돌아갈 수 없게 된다. Isolation 성질을 보장할 수 있는 가장 쉬운 방법은 모든 트랜잭션을 순차적으로 수행하는 것이다. 하지만 병렬적 수행의 장점을 얻기 위해서 DBMS는 병렬적으로 수행하면서도 일렬(serial) 수행과 같은 결과를 보장할 수 있는 방식을 제공하고 있다.
  • Durability(지속성): 트랜잭션이 성공적으로 완료되어 커밋되고 나면, 해당 트랜잭션에 의한 모든 변경은 향후에 어떤 소프트웨어나 하드웨어 장애가 발생되더라도 보존되어야 한다.

트랜잭션은 다음의 <표 1>과 같이 세 가지 중 하나의 형태로 종료된다. 문제 없이 정상적으로 수행된 경우에는 커밋을 통해서 종료될 것이고, 잘못된 입력이 주어졌거나 일관성 제약 조건을 위배한다거나 하는 상황이 발생되거나 사용자의 요청에 의하여 철회되는 경우가 있으며, 타임 아웃이나 교착 상태 등과 같이 시스템이 감지하는 문제로 인하여 DBMS가 철회하는 경우가 있다.

표 1 트랜잭션의 세 가지 가능한 결과 형태(Gray 외, 1981)

BEGIN

READ

WRITE

READ

WRITE

COMMIT

BEGIN

READ

WRITE

READ

ABORT

BEGIN

READ

WRITE

READ

ç SYSTEM ABORTS TRANSACTION

이 외에도 트랜잭션은 각종 시스템 고장으로 인하여 영향을 받을 수 있으며, DBMS는 이와 같은 상황에서 트랜잭션을 관리해야 한다.

트랜잭션 관리를 위한 DBMS의 전략

트랜잭션 관리를 위한 DBMS의 전략을 이해하기 위해서는 우선 DBMS의 개략적인 구조와 버퍼 관리자 및 트랜잭션 관리와 연관된 버퍼 관리 정책에 대한 이해가 필요하다.

데이터베이스 시스템은 보통 비휘발성 저장 장치인 디스크에 데이터를 저장하며 전체 데이터베이스의 일부분을 메인 메모리에 유지한다. DBMS는 데이터를 고정 길이의 페이지(page)로 저장하며, 디스크에서 읽거나 쓸 때에 페이지 단위로 입출력이 이루어진다. 메인 메모리에 유지하는 페이지들을 관리하는 모듈을 보통 페이지 버퍼(page buffer) 관리자 또는 버퍼 관리자라고 부르는데, DBMS의 많은 주요 모듈 중에서 매우 중요한 모듈 중의 하나이다. DBMS는 각 제품마다 구조가 다르기는 하지만, <그림 1>과 같이 크게 질의 처리기(Query Processor)와 저장 시스템(Storage System)으로 나눠볼 수 있다. MySQL의 경우에는 InnoDB, MyISAM 등과 같이 여러 하부 저장 시스템을 선택할 수 있는데, 이와 같은 모델은 상부의 질의 처리기와 하부의 저장 시스템 간의 명확하게 구분되는 계층(layered) 구조에 해당한다. CUBRID 역시 질의 처리기와 저장 시스템 두 개의 구성 요소로 이루어져 있으며, 질의 처리기와 저장 시스템이 좀 더 밀접하게 연결되어 있다.

343d976a40006a2bc3d59f9243069fc1.png

그림 1 DBMS의 개략적인 구조

DBMS의 많은 구성 요소 중에서 굳이 버퍼 관리자를 소개한 이유는 버퍼 관리 정책이 트랜잭션 관리에 매우 중요한 결정을 가져오기 때문이다. 버퍼 관리 정책에 따라서 트랜잭션의 UNDO 복구와 REDO 복구가 요구되거나 그렇지 않게 된다. 이 부분에 대해서 하나씩 살펴보자.

UNDO는 왜 필요할까?

오퍼레이션 수행 중에 수정된 페이지들이 버퍼 관리자의 버퍼 교체 알고리즘에 따라서 디스크에 출력될 수 있다. 버퍼 교체는 전적으로 버퍼의 상태에 따라서 결정되며, 일관성 관점에서 봤을 때는 임의의 방식으로 일어나게 된다. 즉 아직 완료되지 않은 트랜잭션이 수정한 페이지들도 디스크에 출력될 수 있으므로, 만약 해당 트랜잭션이 어떤 이유든 정상적으로 종료될 수 없게 되면 트랜잭션이 변경한 페이지들은 원상 복구되어야 한다. 이러한 복구를 UNDO라고 한다. 만약 버퍼 관리자가 트랜잭션 종료 전에는 어떤 경우에도 수정된 페이지들을 디스크에 쓰지 않는다면, UNDO 오퍼레이션은 메모리 버퍼에 대해서만 이루어지면 되는 식으로 매우 간단해질 수 있다. 이 부분은 매력적이지만 이 정책은 매우 큰 크기의 메모리 버퍼가 필요하다는 문제점을 가지고 있다. 수정된 페이지를 디스크에 쓰는 시점을 기준으로 다음과 같은 두 개의 정책으로 나누어 볼 수 있다.

  • STEAL: 수정된 페이지를 언제든지 디스크에 쓸 수 있는 정책
  • ¬STEAL: 수정된 페이지들을 최소한 트랜잭션 종료 시점(EOT, End of Transaction)까지는 버퍼에 유지하는 정책

STEAL 정책은 수정된 페이지가 어떠한 시점에도 디스크에 써질 수 있기 때문에 필연적으로 UNDO 로깅과 복구를 수반하는데, 거의 모든 DBMS가 채택하는 버퍼 관리 정책이다.

REDO는 왜 필요할까?

이제는 UNDO 복구의 반대 개념인 REDO 복구에 대해서 알아볼 것인데, 앞서 설명한 바와 같이 커밋한 트랜잭션의 수정은 어떤 경우에도 유지(durability)되어야 한다. 이미 커밋한 트랜잭션의 수정을 재반영하는 복구 작업을 REDO 복구라고 하는데, REDO 복구 역시 UNDO 복구와 마찬가지로 버퍼 관리 정책에 영향을 받는다. 트랜잭션이 종료되는 시점에 해당 트랜잭션이 수정한 페이지들을 디스크에도 쓸 것인가 여부로 두 가지 정책이 구분된다.

  • FORCE: 수정했던 모든 페이지를 트랜잭션 커밋 시점에 디스크에 반영하는 정책
  • ¬FORCE: 수정했던 페이지를 트랜잭션 커밋 시점에 디스크에 반영하지 않는 정책

여기서 주의 깊게 봐야 할 부분은 ¬FORCE 정책이 수정했던 페이지(데이터)를 디스크에 반영하지 않는다는 점이지 커밋 시점에 어떠한 것도 쓰지 않는다는 것은 아니다. 어떤 일들을 했었다고 하는 로그는 기록하게 되는데 이 부분은 아래에서 자세히 설명한다.

FORCE 정책을 따르면 트랜잭션이 커밋되면 수정되었던 페이지들이 이미 디스크 상의 데이터베이스에 반영되었으므로 REDO 복구가 필요 없게 된다. 반면에 ¬FORCE 정책을 따른다면 커밋한 트랜잭션의 내용이 디스크 상의 데이터베이스 상에 반영되어 있지 않을 수 있기 때문에 반드시 REDO 복구가 필요하게 된다. 사실 FORCE 정책을 따르더라도 데이터베이스 백업으로부터의 복구, 즉 미디어(media) 복구 시에는 REDO 복구가 요구된다. 거의 모든 DBMS가 채택하는 정책은 ¬FORCE 정책이다.

정리해보면 DBMS는 버퍼 관리 정책으로 STEAL과 ¬FORCE 정책을 채택하고 있어, 이로 인해서 UNDO 복구와 REDO 복구가 모두 필요하게 된다. 

트랜잭션 관리

지금까지 설명한 UNDO 복구와 REDO 복구를 위해서 가장 널리 쓰이는 구조는 로그(log)이다. Shadow paging(nilavalagan, 2009)이라고 불리는 복구 기법도 존재하지만, 여기서는 보편적으로 사용되는 로그 기법에 대해서만 설명하기로 한다.

로그

로그는 로그 레코드의 연속이며 데이터베이스의 모든 갱신 작업을 기록한다. 로그는 이론적으로는 안정적 저장 매체(stable storage)에 기록된다고 하는데, 안정적 저장 매체는 어떤 경우에도 절대로 손실이 발생하지 않는 이른바 이상적인 매체이다. 바꿔 말하면 현실 상에서는 존재하지 않는다고 봐야 하는데, RAID 등 인프라 시스템의 도움 외에도 DBMS 자체적으로 여러 벌의 로그를 유지하는 등 안정적 저장 매체처럼 동작하게 하는 기법을 사용하기도 한다. 하지만 대부분 DBMS는 성능 상의 이유로 하나의 로그를 유지한다.

로그는 덧붙이는(append) 방식으로 기록되며, 각 로그 레코드는 고유의 식별자를 가진다. 로그 레코드의 식별자를 LSN(Log Sequence Number) 혹은 LSA(Log Sequence Address)라고 부른다. 로그는 항상 뒤에 덧붙이는 방식으로 쓰이기 때문에, 로그 식별자는 단조 증가하는 성질을 가진다. 로그 데이터는 기록할 오브젝트의 타입에 따라서 물리적/논리적 로깅으로 분류할 수 있고, 데이터베이스의 상태 또는 변화를 야기한 전이를 기록하느냐에 따라서 분류할 수 있다.

표 2 로그 데이터 분류(Haerder & Reuter, 1983)

 

State

Transition

Logical

-

액션(DML문, DDL문)

Physical

이전 이미지

이후 이미지

XOR 차이

물리적인 상태 로깅(physical state logging)

이 방법은 DBMS에서 가장 널리 쓰이는 기본적인 로깅 방법인데 이에 해당하는 로그 레코드는 갱신 이전 이미지와 이후 이미지를 모두 다 가지고 있으며, UNDO 복구 때에는 이전 이미지로 현재 이미지를 대체하며, REDO 복구 때에는 이후 이미지를 반영하는 방식으로 복구가 이루어진다. 결국 이런 복구 작업은 이전 이미지 혹은 이후 이미지로 단순히 대체하는 작업으로 이해하면 된다. 예를 들어, UPDATE 문장에 대한 로깅은 수정 이전 이미지(즉, 수정 전 레코드 이미지)와 이후 이미지(새로 갱신하는 레코드 이미지)를 모두 기록하고, UNDO 시에는 수정 이전 이미지로 대체하는 식으로, REDO가 필요한 경우에는 수정 이후 이미지를 반영하는 식으로 이루어진다. 물리적 상태 로깅은 때로는 페이지 수준(예를 들어, 인덱스나 데이터 파일의 헤더 페이지의 변경 로깅)에서 이루어지기도 하고, 레코드 수준에서 이루어지기도 한다.

물리적인 전이 로깅(physical transition logging)

이 방법은 페이지 혹은 레코드에 대해서 이전 및 이후 이미지를 모두 기록하기 보다는 XOR 차이점을 기록하는 방식으로 이루어진다. 복구 시점에서 로그 레코드에 기록된 XOR 이미지와 레코드 이미지를 이용하여 UNDO 복구와 REDO 복구를 수행하게 된다.

논리적인 전이 로깅(logical transition logging)

이 방법은 오퍼레이션 로깅(operation logging)으로도 불리는데, 물리적인 로깅이 결과 값을 기록하는 방식이라면 논리적인 로깅은 어떤 일을 했었는가를 기록하는 방식이다. 예를 들어, a = a + 1과 같은 연산을 로깅할 때 이전 값 0, 이후 값 1을 물리적으로 기록할 수도 있고, a = a + 1 이라는 연산 그 자체를 기록할 수 있다. 이러한 논리적인 로그에 대한 복구 작업은 REDO를 위하여 로그 레코드에 기록된 오퍼레이션을 재수행하거나, UNDO를 위하여 역 오퍼레이션을 수행하는 방식으로 이루어진다.

이런 논리적인 전이 로깅은 로그 레코드의 크기를 크게 줄여준다는 장점이 있다. 하지만 더 중요한 점은 물리적으로 복구하기 쉽지 않은 자료 구조에 대한 로깅을 쉽게 해준다는 점이다. 예를 들어, 인덱스 구조로 많이 사용되는 B+-tree 또는 B-tree 는 split, merge 와 같은 SMO(Structure Modification Operation)를 통해서 레코드의 위치가 계속 변경되기 때문에 로깅 시점과 복구 시점의 데이터 물리적 위치가 같다는 점이 보장되지 않기 때문에(페이지 내의 위치가 다를 수도 있고, 심지어 다른 페이지에 위치할 수도 있다), 물리적인 로그를 통해서 복구하기가 쉽지 않지만, 논리적인 로그를 통해서 보다 쉽게 복구할 수 있다. 즉, 인덱스에 키 값 k와 포인터 p가 저장되었다는 논리 로그에 대한 REDO 복구는 인덱스에 (k, p)를 다시 삽입하는 작업이면 충분하고, UNDO 복구는 (k, p)를 인덱스에서 제거하는 작업을 수행하면 된다.

DBMS 제품들은 위에서 살펴본 물리적인 상태 로깅, 물리적인 전이 로깅, 논리적인 전이 로깅 방법(그 외에도 물리-논리(physiological) - 수정한 페이지를 물리적으로 식별할 수 있지만 해당 페이지 내에서는 논리적으로 기록되는 - 로깅 기법도 존재한다) 중에 하나만을 선택하여 사용하는 것이 아니라 이들을 적절하게 혼용한다. CUBRID도 각각의 로깅 기법이 유리한 경우에 대해서 물리적인 로깅과 논리적인 로깅을 함께 사용하고 있다. 위에서 설명한 3개의 그룹으로 나눠볼 수 있는 로그 레코드가 DBMS 내에 실제로 몇 종류나 필요할까 하는 궁금증이 드는데, DBMS마다 다르지만 CUBRID의 경우에는 UNDO 로그, REDO 로그, 커밋 로그 같은 것을 포함하여 약 40여 종류의 로그 레코드가 존재한다. 같은 로그 레코드라고 하더라도 자료 구조마다 복구 연산이 다르기 때문에 DBMS가 가지고 있는 복구 연산(함수)은 로그 레코드 종류보다는 훨씬 많이 필요한데, CUBRID는 현재 약 100여 개의 복구 연산을 가지고 있다.

여기서 한 가지 더 얘기할 사항은 로그를 통한 UNDO 복구, REDO 복구는 멱등성(idempotent)을 가져야 한다는 점이다. 멱등성은 여러 번 수행하더라도 한 번 수행한 결과와 같아야 한다는 것을 의미하는데, 물리적인 로그를 통한 복구는 자연스럽게 멱등성이 보장되지만, 논리적인 로그를 통한 복구는 그렇게 간단하지 않다. 예를 들어, a++ 연산을 여러 번 반복해서 복구하게 되면 정확하지 않게 복구될 수 있게 되는데, 이런 일을 방지하기 위해서 한 번 수행한 복구 연산을 또 다시 수행하지 않도록 해야 한다. 이를 어떻게 해결하는지는 이후에 살펴보기로 한다.

로그는 어떻게 쓸까?

로그는 로그 타입에 관계없이 다음의 규칙에 따라 써진다.

  • 해당 업데이트가 데이터베이스에 써지기 전에 먼저 관련된 UNDO 정보가 로그에 써져야 한다. 이 원칙을 WAL(Write Ahead Logging)이라고 부른다. 어떤 경우에도 UNDO 복구가 되기 위해서는 반드시 WAL 규칙이 준수되어야 한다.

  • 트랜잭션이 정상적으로 종료 처리되기 위해서는 먼저 REDO 정보가 로그에 써져야 한다. 역시 어떤 경우에도 REDO 복구를 할 수 있기 위해서는 REDO 로그가 적어도 커밋 시점에는 써져야 한다.

DBMS는 로그 레코드를 위한 별도의 버퍼를 유지하는데, 이를 로그 버퍼라고 한다. 로그 버퍼 관리는 DBMS마다 서로 다른 방식으로 구현하는데 로그 버퍼를 통해서 로그 파일에 입출력한다는 점은 같다. 성능을 위해서 로그 버퍼에 로그 레코드를 모았다가 블록 단위로 로그 파일에 출력한다.

트랜잭션들이 동시에 수행을 하면서 각각의 연산에 대해서 로그 레코드를 생성하게 되는데, 이들은 로그 버퍼에 유지되게 되고 몇몇 시점에 로그 파일에 써지게 된다. 로그 버퍼에 유지된 로그 레코드는 (1) 어떤 트랜잭션이 커밋을 요청한 경우, (2) WAL을 해야 하는 경우, (3) 로그 버퍼가 다 소진된 경우, (4) DBMS가 내부적으로 필요로 하는 경우(예를 들어, 체크 포인트(checkpoint) 연산, 로그 관리 연산 등)에 로그 파일에 출력된다. 대부분의 경우는 (1)과 (2)의 경우로 발생되며 (3), (4)로 인해서 수행되는 경우도 있다. 로그 버퍼는 상대적으로 작기(대개 수MB에서 수십MB 수준) 때문에 긴(long) 트랜잭션이 수행 중인 경우에는 로그 버퍼가 소진될 수 있다.

어떤 트랜잭션이 커밋을 요청하는 경우에는 해당 트랜잭션의 마지막 로그 레코드까지 출력하면 되는데, <그림 2>에서 트랜잭션 T1이 커밋할 때 LSN3까지의 로그 레코드가 로그 파일에 써져야 한다.

5205aed6cb95030dbd71df8b2a05a419.png

그림 2 로그 버퍼의 예

로그를 쓰는 일은 왜 느릴까?

로그 레코드가 손실되는 경우가 발생되면 데이터베이스가 완전히 복구될 수 없기 때문에 로그 레코드를 안전하게 쓰는 것이 필요한데, DBMS는 최대한 안전하게 로그를 쓰기 위해서 write 함수(내지는 writev 함수) 호출 외에 fsync 함수(fsync(2) - Linux man page, 2013)를 호출한다. fsync 함수 호출이 디스크에 물리적으로 써지는 것까지 보장하면 좋겠지만 리눅스를 포함한 대부분의 운영 체제는 그렇게까지 보장하지는 않는다. fsync 함수 호출은 매우 느린 연산이고, 커밋을 위해서는 해당 트랜잭션의 로그가 로그 파일에 써져야 하기 때문에 커밋을 하려는 트랜잭션은 fsync 함수 호출이 종료되기를 대기해야 한다.

더 자세히 보면, 로그 버퍼를 쓸 때에는 로그 헤더 정보와 로그 레코드를 써야 하는데, 로그 레코드를 먼저 쓰고 fsync 함수를 실행하고, 로그 헤더를 업데이트한 후에 다시 fsync 함수를 실행해야 한다. 로그는 끝에 추가되는 방식으로 써지기 때문에 이와 같이 하지 않으면 로그 레코드나 헤더가 온전하지 않은 상태로 기록될 수 있게 된다. fsync를 처리할 때 어떤 버퍼 프레임부터 디스크에 쓰게 될지는 DBMS 입장에서는 알 수 없고 DBMS가 원하는 순서대로 디스크에 반영하도록 할 방법도 없기 때문에, 이와 같이 여러 단계를 거쳐 로그를 쓰게 된다.

하지만 로그 버퍼를 로그 파일에 쓸 때에 한 번에 한 페이지만 쓰는 것이 아니라 여러 페이지를 쓰는 경우가 대부분인데, 이런 경우의 로그 쓰기 작업은 더 복잡하게 이루어진다. 예를 들어, 로그 버퍼 Bi부터 Bk까지 출력하는 경우에, 먼저 Bj부터 Bk까지 로그 파일에 쓰고 fsync 함수를 실행하고, 첫 번째 로그 버퍼인 Bi를 쓴 후 다시 fsync 함수를 실행하고 나서, 비로소 로그 헤더를 업데이트하는 절차로 이루어진다.

2b978007f0168bc97f8568300c1a47c9.png

그림 3 로그 버퍼를 로그 파일에 쓰는 순서

대부분의 커밋 연산이 소모하는 시간은 로그 레코드를 로그 파일에 쓰고, fsync 함수를 실행하는 시간이라고 보면 된다. 정확성을 위해서는 fsync 함수를 여러 차례 호출해야 하는데, 일 초에도 수천, 수만 트랜잭션이 커밋을 요청하는 상황을 생각해보면 로깅을 위해서 얼마나 많은 디스크 출력이 있어야 하는지 쉽게 이해할 수 있을 것이다.

로그 쓰기 작업, 즉 커밋 오퍼레이션의 성능을 높일 방법이 없을까?

성능을 위한 몇 가지 기법이 있는데, 먼저 그룹 커밋(group commit)부터 알아보자. 그룹 커밋은 각각의 트랜잭션의 커밋 요구를 개별적으로 처리하기 보다는 모아서 한꺼번에 처리하는 방식이다. 수천 내지는 수만 TPS 수준의 요청이 있다고 했을 때, 한 트랜잭션이 커밋할 때 잠시만 기다리면 다른 트랜잭션들이 역시 커밋을 요청할 것이고, 이들의 요청을 한 번에 처리하게 되면 디스크 출력 횟수를 줄일 수 있으므로 이로 인해 성능을 높일 수 있게 된다. 즉, 그룹 커밋은 여전히 정확성을 보장하면서 각 트랜잭션의 응답 시간(response time)은 약간 희생시키는 경우가 발생되더라도 시스템 전체의 처리량(throughput)을 높이자는 의도이다. 쉽게 생각해서 개별적으로 승용차를 이용하는 방식과 고속 버스를 이용하는 방식을 연상하면 된다. 고속 버스의 경우 정해진 출발 시각까지 대기해야 하지만, 한 번에 이동하는 승객이 많기 때문에 효율은 높다.

그룹 커밋은 (한계 시점 이전까지는) 동시에 요청되는 커밋 요구가 많을수록 그 효율이 높아지는데, 적절한 그룹 커밋 대기 시간을 정하는 것이 시스템 성능에 매우 중요하다. 너무 짧으면 효율이 떨어지게 되고, 너무 길면 응답 시간이 느려지고 효율은 더 이상 높아지지 못하게 된다. 최적의 값은 워크로드 패턴에 따라서 다르며 대게 짧게는 몇 밀리초(ms)에서 길게는 수백 밀리초(ms)까지 설정하기도 하는데, 시스템의 부하에 따라서 시스템이 적응적(adaptive)으로 자동 조정(Helland 외, 1988)하기도 한다.

성능을 위해서 지속성을 살짝 포기할 수는 없을까?

커밋 성능을 극대화하기 위해서 지속성(durability)을 일부 포기하는 방식도 있다. DBMS 제품에 따라서는 매 커밋마다 정확하게 로그를 쓰고 fsync 함수를 실행하는 것이 아니라 보다 나은 성능을 위해 좀 더 느슨하게 로그를 쓰는 옵션을 제공하기도 한다. 예를 들어, InnoDB는 innodb_flush_log_at_trx_commit 파라미터(InnoDB Startup Options and System Variables, 2013)의 설정을 통해서 로그를 쓰는 방식을 조정할 수 있다.

또한, 응용이나 시스템의 성격에 따라서는 비동기 커밋(asynchronous commit) 방식을 사용하기도 하는데, 비동기 커밋은 로그 버퍼에 로그 레코드를 쓰고 곧바로 커밋을 완료하는 방식이다. 즉, 로그 파일에 로그가 써질 때까지 대기하지 않고 커밋을 하게 된다. 로그 레코드는 로그 쓰기 스레드(내지는 프로세스)가 이후에(대개는 매우 짧은 시간 내에 곧바로) 비동기적으로 쓰게 되는데, 트랜잭션의 로그가 미처 써지기 전에 시스템에 장애가 발생되면 해당 트랜잭션은 이미 커밋을 완료했지만 손실되게 된다. 비동기 커밋 방식으로 인해 발생할 수 있는 데이터의 손실은 커밋한 트랜잭션이 변경한 데이터의 유실(loss)이며, 이 때도 데이터의 일관성은 보장된다. 최근의 몇몇 트랜잭션의 커밋 로그가 유실된 것이기 때문에 복구 시점에 DBMS는 마치 해당 트랜잭션이 커밋을 하지 않은 것으로 간주하여 이를 철회(rollback)시키게 된다. 철회를 위한 UNDO 로그는 이미 트랜잭션 수행 중에 WAL 원칙에 따라서 트랜잭션 로그에 써져 있기 때문에, 커밋되지 않은 데이터가 데이터베이스에 반영되지는 않는다. 비동기 커밋은 성능 향상 효과가 크기는 하지만, 손실이 발생할 수 있으므로 응용의 성격에 따라서 신중하게 선택해야 할 필요가 있다. 동시에 커밋 요청이 매우 많이 요구되며 데이터의 일부 유실을 감내할 수 있는 응용 환경에서는 적용을 고려해볼 수 있을 것이다. CUBRID도 그룹 커밋과 비동기 커밋 방식을 모두 제공하고 있고, 설정을 통해서 쉽게 적용할 수 있다.

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

이제 그러면 로그를 통해서 어떻게 복구가 이루어지는지 알아보자. 복구에는 두 가지 종류가 있는데, 사용자의 요청 또는 오류 발생 등으로 인해서 시스템이 트랜잭션을 철회하는 경우와 소프트웨어 문제나 하드웨어 문제 등으로 인해서 장애가 발생하고 데이터베이스 시스템이 재시작 복구(restart recovery)하는 경우가 있다.

트랜잭션 철회는 어떻게?

트랜잭션을 철회하는 경우는 시스템은 정상적으로 동작하고 있는 중이며 특정 트랜잭션만 철회하는 경우인데, 이 때 트랜잭션의 철회는 다음과 같이 이루어진다. 먼저 로그를 역방향으로 탐색하면서 해당 트랜잭션의 UNDO 복구가 필요한 로그를 찾아서 이에 해당하는 UNDO 연산을 수행한다. 역방향으로 로그를 탐색하면서 트랜잭션 수행 순서의 역순으로 UNDO를 수행해야 정확하게 UNDO가 이루어질 수 있다.

UNDO를 수행하고 나면 해당 UNDO 작업에 대한 보상 로그 레코드(CLR, Compensation Log Record)라고 하는 REDO 전용 로그를 쓰게 되는데, UNDO를 하고 난 이후에 다시 UNDO를 해서 복구가 잘못 이루어지지 않도록 하기 위함이다. CLR은 이전 로그 레코드 위치를 UNDO 로그의 이전 로그를 가리키도록 하여 이후에는 한 번 UNDO된 로그를 다시 접근하여 재차 UNDO하게 되는 일이 발생되지 않도록 해준다. 이전 로그를 계속 탐색하면서 해당 트랜잭션의 시작 로그까지 도달하면 해당 트랜잭션의 철회 복구가 완료된 것이다.

장애로 인해 재시작되면 어떻게 복구가 되나?

장애 발생 이후 데이터베이스가 재시작 복구하는 경우에는 크게 3 단계로 복구가 이루어진다.

1 단계는 로그 분석 단계로, 마지막 체크포인트(checkpoint) 시점부터 최근 로그(EOL, End of Log)까지 로그를 탐색하면서 어디서부터 시스템이 복구를 시작해야 하는지, 어느 트랜잭션들을 복구해야 하는지 등등을 알아내는 단계이다.

2 단계는 REDO 복구 단계로 복구를 시작해야 하는 시점부터 장애 발생 직전 시점까지 REDO가 필요한 모든 로그를 REDO 복구를 하는 단계이다. 이 단계에서는 심지어 실패한 트랜잭션의 REDO 로그조차도 REDO를 하게 되는데, 언뜻 보면 불필요한 것으로 생각되지만 이렇게 하면 이후의 복구 단계를 매우 간단하게 하는 효과를 가져다 준다. 이 단계에서는 모든 트랜잭션에 대해서 REDO 복구만 한다는 점이 중요한데, 이러한 REDO 복구가 완료된 시점의 데이터베이스 상태는 장애 발생 시점의 상태와 같게 된다. 이전 상황을 그대로 재현하여 복원한다는 의미로 이 REDO 복구에서 이루어지는 작업을 repeating history(Mohan 외, 1992)라고 부른다.

마지막 3 단계는 UNDO 복구 단계로 로그를 최신 시점부터 다시 역방향으로 탐색하면서 UNDO 복구가 필요한 로그들에 대해서 UNDO 복구를 수행한다. 여기서 수행하는 UNDO는 결국 위에서 설명한 트랜잭션 철회 시에 수행하는 UNDO와 같은 방식으로, repeating history를 통해 데이터베이스 상태를 장애 시점까지 복원해두고 UNDO 복구를 여러 트랜잭션의 철회로 간단하게 해결할 수 있다. 한 트랜잭션만 철회시키는 것이 아니라 여러 트랜잭션을 철회시킨다는 차이점만 존재한다. 이 단계의 UNDO 복구를 개별 트랜잭션의 UNDO와 구별하여 Global UNDO라고도 부른다.

2880ae634676944afdd10bed6a86954f.png

그림 4 재시작 복구 단계와 로그 접근 방향

로그를 통한 복구 과정 중에 특정 로그가 UNDO 내지는 REDO 복구가 필요한 것인지를 판단해야 할 필요가 있다. 이미 로그가 반영되었다면 그 로그에 대한 복구 연산은 필요치 않은데 이는 어떻게 해결할까? 앞서 설명한 바와 같이 모든 로그에는 LSN이라고 하는 식별자가 있는데, 데이터베이스의 모든 페이지는 page LSN을 가지고 있다. 이 page LSN은 페이지가 갱신될 때마다 해당 로그의 LSN으로 갱신된다. 즉, 모든 페이지는 해당 페이지를 마지막으로 갱신한 로그의 식별자를 포함하고 있으므로, 로그를 적용해야 할지 여부는 해당 로그의 LSN과 page LSN을 비교함으로써 판단할 수 있다. Page LSN이 어떤 로그의 LSN보다 예전 것이라면 해당 페이지는 반드시 해당 로그로 복구되어야 한다는 것을 의미하며, 반대로 page LSN이 해당 로그의 LSN과 같거나 더 최신의 값을 가지고 있다면 이 페이지는 해당 로그보다 나중에 쓰인 로그로 이미 갱신되었다는 것을 의미하므로 복구가 필요치 않다는 것을 의미한다. CUBRID는 page LSN으로 페이지 시작 부분의 8바이트의 공간을 사용하므로, 기본 16KB의 페이지를 사용하는 경우 실제 데이터가 저장되는 공간은 page LSN을 위한 공간을 제외한 16376바이트가 된다.

e9bfe03d5dbbde428c1522181eba2dfc.png

그림 6 데이터베이스의 페이지 구성

백업을 이용한 미디어 복구는 어떻게?

디스크 미디어(media)의 문제가 생겼을 때 수행하는 미디어 복구, 일명 아카이브(archive) 복구가 있는데, 이는 데이터베이스의 백업으로부터 복구를 하는 것을 의미한다. 데이터베이스 백업 기법에는 여러 가지가 있는데, 데이터베이스가 수행 도중에 트랜잭션들의 수행을 방해하지 않고 현재 스냅샷(snapshot)을 그대로 복사하는 퍼지(fuzzy) 백업이 CUBRID를 포함한 상용 DBMS가 사용하는 기법이다.

트랜잭션이 수행하고 있는 도중에 데이터베이스 이미지를 복사하는 것이기 때문에 미처 커밋하지 못한 일부 트랜잭션의 이미지가 복사될 수도 있고, 커밋한 트랜잭션의 데이터가 아직 반영되지 못한 채로 복사가 될 수도 있다. 이렇게 퍼지하게 복사한 데이터베이스 백업으로 어떻게 복원(restore)를 할까? 역시나 답은 로그에 있다. 미디어 복구 시에는 데이터베이스 백업과 (이에 포함된) 로그, 혹시 남아 있다면 장애 시점의 로그까지 활용하여 복구를 하게 되는데, (아주 간략하게 설명하면) 데이터베이스 백업은 데이터베이스 파일을 복사한 것이므로 이를 새로 복사해둔 후 데이터베이스를 재시작한다고 생각하면, 미디어 복구 문제는 위에서 설명한 장애 발생 이후에 재시작 복구 작업과 결국 같은 문제가 된다. 결국 로그를 읽어서 퍼지하게 복사했던 데이터베이스 이미지에서 아직 미처 반영되지 못한 커밋했던 트랜잭션들을 다시 REDO해 주고, 결국 커밋 레코드가 포함되지 않은 트랜잭션들은 UNDO해 주면 된다. 이러한 미디어 복구 시점의 재시작 복구를 특별히 roll-forward 복구라고 부르기도 한다.

미디어 장애가 발생했을 때 마지막 데이터베이스 백업 이후의 모든 로그가 남아 있다면 장애 시점까지 손실 없이 데이터베이스를 복원할 수 있다. 불행히도 백업 이후의 일부 로그가 유실되었다면 최소한 백업 시점의 일관성이 유지되는 데이터베이스 시점까지는 복원이 가능하다. 미디어 복구를 이용하여 특정 시점으로 데이터베이스를 복원하는 것도 가능한데, 이는 roll-forward 과정을 현재 시점까지 전체를 수행하는 것이 아니라 DBA가 원하는 특정 시점까지만 수행하면 된다.

커밋을 하면 어떤 일이 일어나나?

커밋을 하면 어떤 일이?

커밋 트리거 혹은 지연된(deferred) 트리거가 정의되어 있다면 해당 트리거가 수행된다. 또한, 트랜잭션 수행 도중에 생성했던 커서(질의 결과 집합)를 정리하게 된다. SQL 표준은 커서를 선언할 때 트랜잭션 커밋 이후에도 커서를 계속 유지하고 결과를 볼 수 있도록 하는 Holdable Cursor(Cursor (databases), 2013)로 선언한 커서만을 유지하고 그 외의 커서는 모두 해제할 것으로 정의하고 있는데, 반면 JDBC에서는 기본으로 Holdable Cursor로 정의하고 있다. 이는 JDBC의 기본 동작이 자동 커밋이기 때문에 사용자의 편의성을 고려한 결정인데, 바깥쪽(outer) 커서에서 얻어온 결과를 기반으로 중첩 루프의 안쪽(inner)에서 다른 질의를 하는 경우에 전체를 묶어서 트랜잭션 처리를 하지 않으면 안쪽 질의가 커밋되는 순간에 바깥쪽 커서마저 닫히게 되는 상황이 발생된다. Holdable Cursor가 지원되면 안쪽에서 커밋을 하더라도 바깥쪽 커서가 계속 유지되기 때문에 트랜잭션 처리를 하지 않아도 된다.

DBMS는 트랜잭션을 수행하는 과정 중의 일부 내부 연산들(예를 들어, 리소스 반환과 같은 물리적인 연산)을 커밋이나 롤백과 같은 트랜잭션 종료 시점까지 지연시키는 경우가 있는데, 이런 연기(postpone)된 연산들이 포함되어 있었다면 커밋 시점에 수행된다. 장애 대비를 위하여 데이터베이스의 복제(replication)를 적용하고 있다면, 복제를 위한 로그를 쓰는 것과 같은 작업을 수행한다. 복제는 DBMS마다 구현 전략이 상이하기 때문에 일반적으로 설명하기는 어려운 측면이 있는데, CUBRID는 트랜잭션 로그를 기반으로 하는 복제를 사용하며 트랜잭션 커밋 시점에 복제를 위한 로그를 쓴다. 트랜잭션을 수행하는 과정 중에 획득한 모든 락(lock)을 해제하고, 트랜잭션이 최종적으로 커밋했다는 로그를 쓴 후에 마지막으로 트랜잭션이 가지고 있던 메모리, 트랜잭션 식별자 등과 같은 리소스들을 반환하고 비로소 트랜잭션이 종료하게 된다.

커밋을 하다가 오류가 발생되면?

응용 프로그램의 커밋 요청으로 위에서 설명한 단계들을 수행하는 과정 중에 오류가 발생되면 어떻게 될까? "끝나기 전까지는 끝난 것이 아니다."라는 말이 가장 적절한 설명이 될 것 같다. 트랜잭션이 커밋이 완료된 것이 아니라면 그것은 수행되지 않은 것과 같게 취급된다. <그림 7>에서 사용자의 커밋 요청이 오면 일단 해당 트랜잭션의 상태는 'partially committed' 상태가 된다. 문제 없이 커밋할 수 있으면 'committed' 상태로 완료되지만, 그렇지 않은 경우에는 다른 오류 발생과 마찬가지로 'failed' 상태를 거쳐 결국 'aborted' 상태에 다다르게 된다.

9bfdf8875b6a11f4c3274e0677d46596.png

그림 7 트랜잭션 상태 다이어그램(Silberschatz 외, 2010)

 

마치며

지금까지 DBMS가 어떻게 트랜잭션을 관리하는가라는 주제로 트랜잭션 관리의 주요 원리들을 간략하게 살펴보았다. DBMS의 트랜잭션 관리는 워낙 방대하고 깊숙한 주제라 본 글에서는 주요 개념들을 개괄적으로 다루었는데, 이 부분에 대해서 좀 더 관심이 있으면 참고 문헌에 수록된 자료들을 살펴보면 된다. 데이터베이스 시스템 전반에 대한 이해가 필요하다면 "Database System Concepts"(Silberschatz 외, 2010)를, 트랜잭션 복구와 관련하여 좀 더 궁금하면 "Recovery Mechanisms in Database Systems"(Kumar & Hsu, 1998)를, 트랜잭션 처리 전반에 대해 구현 수준까지 깊게 이해하고 싶다면 "Transaction Processing: Concepts and Techniques"(Gray & Reuter, 1993)를 추천한다.

참고 자료

[1] "Cursor (databases)." (2013). http://en.wikipedia.org/wiki/Cursor_(databases)#.22WITH_HOLD.22에서 검색됨

[2] "fsync(2) - Linux man page." (2013). http://linux.die.net/man/2/fsync에서 검색됨

[3] "InnoDB Startup Options and System Variables." (2013). http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_flush_log_at_trx_commit에서 검색됨

[4] Gray, J., & Reuter, A. (1993). "Transaction Processing: Concepts and Techniques." Morgan Kaufmann Publishers.

[5] Gray, J., Mcjones, P., Blasgen, M., Lindsay, B., Lorie, R., Price, T. 외. (June, 1981). The Recovery Manager of the System R Database Manager. "Computing Surveys, Vol. 13, No.2", 228.

[6] Haerder, T., & Reuter, A. (December, 1983). Principles of Transaction-Oriented Database Recovery. "ACM Computing Survey, Vol. 15, No. 4".

[7] Helland, P., Sammer, H., Lyon, J., Carr, R., Garrett, P., & Reuter, A. (1988). "Group Commit Timers and High-Volume Transaction Systems." Tandem Technical Report 88.1.

[8] Kumar, V., & Hsu, M. (1998). "Recovery Mechanisms in Database Systems." Prentice Hall PTR.

[9] Mohan, C., Haderle, D., Lindsay, B., Pirahesh, H., & Schwarz, P. (March, 1992). ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging. "ACM Transactions on Database Systems, Vol. 17", 94-162.

[10] nilavalagan. (2009). "Shadow Paging Recovery Technique." https://www.classle.net/book/shadow-paging-recovery-technique에서 검색됨

[11] SilberschatzAbraham, KorthF.Henry, & SudarshanS. (2010). "Database System Concepts." McGraw-Hill.

'RDBMS (PostgreSQL)' 카테고리의 다른 글

[DB/분산] 초보자를 위한 CAP 이론  (1) 2016.04.29
JDBC 트랜잭션  (0) 2015.07.30
PostgreSQL 조인  (0) 2015.05.13
Efficient Use of PostgreSQL Indexes  (0) 2015.05.13
PostgreSQL 날짜&시간 사용하기  (0) 2015.05.13
Comments