Post

<오라클 성능 고도화 원리와 해법1> Ch01-03 버퍼 Lock

오라클 성능 고도화 원리와 해법1 - Ch01 오라클 아키텍처 - 03 버퍼 Lock

(1) 버퍼 Lock이란?

DB 버퍼 캐시 내에서 버퍼 블록을 찾았으면 가급적 빨리 쥐고 있던 래치를 해제해야 한다. 그러지 않으면 하나의 cache buffers chains 래치에 여러 개의 해시 체인이 달렸으므로 래치에 대한 경합 발생 가능성이 증가하게 된다. 그런데 그 버퍼에 먼저 접근한 선행 프로세스가 아직 버퍼를 사용 중이라면 어떻게 해야할까? 데이터 정합성(Intergrity)에 대한 고민이 또 생긴다.

아주 짧은 순간일지라도 두 개 이상의 프로세스가 동시에 버퍼 내용을 읽고 쓴다면 문 제가 생길 수 있는 것이다. 이를 막기 위해 캐시된 버퍼 블록을 읽거나 변경하려는 프로 세스는 먼저 버퍼 헤더로부터 버퍼 Lock을 획득해야 한다. 버퍼 Lock을 획득했다면 래치를 곧바로 해제한다.

버퍼 내용을 읽기만 할 때는 Share 모드, 변경할 때는 Exclusive 모드로 Lock을 설정한다. 액세스를 직렬화하기 위한 메커니즘이므로 당연히 Exclusive 모드 Lock은 한 시점에 하나의 프로세스만 얻을 수 있다. select문이더라도 블록 클린아웃 (Cheanout)이 필요할 때는 버퍼 내용을 변경하는 작업이므로 Exclusive 모드 Lock을 요구한다.

만약 해시 체인 래치를 획득하고 목적한 버퍼를 찾았는데 다른 프로세스가 버퍼 Lock을 Exclusive 모드로 점유한 채 내용을 갱신 중이라면 래치를 쥔 채 기다릴 수는 없다. 그럴 때는 버퍼 헤더에 있는 버퍼 Lock 대기자 목록(Waiter List)에 자신을 등록하고 일단 래치는 해제한다. 이때, 버퍼 Lock 대기자 목록에 등록돼 있는 동안 buffer busy waits 대기 이벤트가 발생한다.

대기자 목록에서 기다리다가 버퍼 Lock이 해제되면 버퍼 Lock을 획득하고, 원했던 작업을 진행한다. 목적한 읽기/쓰기 작업을 완료하면 버퍼 헤더에서 버퍼 Lock을 해제해야 하는데, 이때도 버퍼 헤더를 액세스하려는 다른 프로세스와 충돌이 생길 수 있으므로 해당 버퍼가 속한 체인 래치를 다시 한번 획득한다. 버퍼 Lock을 해제하고 래치를 해제해야 비로소 버퍼 블록 읽기가 완료된다. 읽으려는 블록이 버퍼 캐시에 없을 때는 디스크 I/O까지 수반되므로 하나의 블록 읽기가 얼마나 고비용인지 실감케 한다.

(2) 버퍼 핸들

버퍼 Lock을 설정하는 것은 자신이 현재 그 버퍼를 사용 중임을 표시해 두는 것으로서, 그 버퍼 헤더에 Pin을 걸었다고도 표현한다. 버퍼 Lock을 다른 말로 버퍼 Pin 이라고 표현하기도 하며, 앞에서 말한 Pinned 버퍼가 여기에 해당한다. 변경 시에는 하나의 프로세스만 Pin을 설정할 수 있지만 읽기작업을 위해서라면 여러 개 프로세스가 동시에 Pin을 설정할 수 있다.

버퍼 헤더에 Pin을 설정하려고 사용하는 오브젝트를 ‘버퍼 핸들(Buffer Handle)’이라고 부르며, 버퍼 핸들을 얻어 버퍼 헤더에 있는 소유자 목록(Holder List)에 연결시키는 방식으로 Pin을 설정한다. 버퍼 핸들도 공유된 리소스이므로 버퍼핸들을 얻으려면 또 다른 래치가 필요해지는데, 바로 cache buffer handles 래치가 그것이다. 버퍼를 Pin하는 오퍼레이션이 많을수록 오히려 cache buffer handles 래치가 경합지점이 될 것이므로 오라클은 각 프로세스마다 _db_handles_cached 개수만큼의 버퍼 핸들을 미리 할당해 주며, 기본 값은 5개다. 각 세션은 이를 캐싱하고 있다가 버퍼를 Pin 할 때마다 사용하며, 그 이상의 버퍼 핸들이 필요할 때만 cache buffer handles 래치를 얻고 추가로 버퍼 핸들을 할당받는다.

(3) 버퍼 Lock의 필요성

사용자 데이터를 변경할 때는 DML Lock을 통해 보호하도록 돼 있는데, 그것을 담는 블록에 또 다른 Lock을 획득해야 하는 이유가 궁금할 수 있다. 이유는, 오라클이 하나의 레코드를 갱신하더라도 블록 단위로 I/O를 수행하기 때문이다. 예를 들어, 블록 안에 저장된 10개의 레코드를 읽는 짧은 순간 동안 다른 프로세스에 의해 변경이 발생하면 잘못된 결과를 얻게 된다.

그리고 값을 변경하기 전에 레코드에 로우 단위 Lock을 설정하는 일 자체도 레코드의 속성을 변경하는 작업이므로 두 개의 프로세스가 동시에 로우 단위 Lock을 설정하려고 시도한다면 (대상 로우가 서로 다르더라도) 문제가 된다. 블록 헤더 내용을 변경하는 작업도 동시에 일어날 수 있는데, 이런 동시 액세스가 실제로 발생한다면 Lost Update 문제가 생겨 블록 자체의 정합성(Intergrity)이 깨지게 된다. 따라서 블록 자체로의 진입을 직렬화해야 하는 것이다.

(4) 버퍼 Pinning

버퍼 Pin과 관련해서 꼭 알아야 할 기능이 있는데, ‘버퍼 Pinning’이 그것이다. 이는 버퍼를 읽고 나서 버퍼 Pin을 즉각 해제하지 않고 데이터베이스 Call이 진행되는 동안 유지하는 기능을 말한다. 같은 블록을 반복적으로 읽을 때 버퍼 Pinning을 통해 래치 획득 과정을 생략한다면 논리적인 블록 읽기(Logical Reads) 횟수를 획기적으로 줄일 수 있다. 모든 버퍼 블록을 이 방식으로 읽는 것은 아니며, 같은 블록을 재방문할 가능성이 큰 몇몇 오퍼레이션을 수행할 때만 사용한다.

버퍼 Pinning은 하나의 데이터베이스 Call(Parse Call, Execute Call, Fetch Call) 내에서만 유효하다. 즉, Call이 끝나고 사용자에게 결과를 반환하고 나면 Pin은 해제되어야 한다. 따라서 첫 번째 Fetch Call에서 Pin된 블록은 두 번째 Fetch Call에서 다시 래치 획득 과정을 거쳐 Pin 되어야 한다.

전통적으로 버퍼 Pinning이 적용되던 지점은 인덱스를 스캔하면서 테이블을 액세스할 때의 인덱스 리프 블록이다. Index Range Scan 하면서 인덱스와 테이블 블록을 교차 방문할 때 블록 I/O를 체크해보면, 테이블 블록에 대한 I/O만 계속 증가하는 이유가 여기에 있다.

그리고 인덱스를 경유해 테이블을 액세스할 때 인덱스 클러스터링 팩터가 좋다면(=인덱스 레코드가 가리키는 테이블 rowid 정렬 순서가 인덱스 키 값 정렬 순서와 거의 일치한다면) 같은 테이블 블록을 반복 액세스할 가능성이 그만큼 커진다. 그래서 오라클 8i부터, 인덱스로부터 액세스되는 하나의 테이블 블록을 Pinning하기 시작했다. 실제 클러스터링 팩터가 좋은 인덱스를 경유해 테이블을 액세스해 보면, 논리적 블록 읽기 횟수가 매우 적게 나타나는 것을 관찰할 수 있다.

이처럼 오라클은 버전이 올라갈수록 버퍼 Pinning을 적용하는 지점을 점차 확대시켜 나가고 있다.

버퍼 Lock은 매우 Internal 한 메커니즘이어서 이해하기 쉽지 않을 뿐 아니라, 굳이 몰라도 상관없는 내용들이다. 다만, 버퍼 Pinning을 통한 블록 I/O 감소효과는 SQL을 튜닝하는 데 있어 워낙 중요한 내용이므로 이 원리만큼은 기억할 필요가 있다. 인덱스를 통해 얼마 안 되는 테이블 레코드를 읽는데도 성능이 매우 안 좋을 때가 있는가 하면 다량의 데이터를 읽는데도 빠를 때가 있는데, 인덱스 클러스터링 팩터와 관련이 있고 그 이면에는 버퍼 Pinning 원리가 숨어있다.

This post is licensed under CC BY 4.0 by the author.