본문 바로가기

DB 공부

Buffer Pool Manager - 1

반응형

DBMS는 디스크에 있는 파일에 직접 접근할 수가 없습니다. 무조건 메모리에 올린 다음에 접근이 일어나야 합니다.

디스크 I/O는 정말 느립니다. 그래서 디스크에 존재하는 파일들을 얼마나 효율적으로 메모리에 올리고 내리는지는 성능에 있어서 중요해집니다. 이런 이유 때문에 DBMS는 OS에게 이러한 작업을 맡기지 않고 직접하려고 하죠.

이러한 작업을 효율적으로 만들기 위해 신경 쓸 부분을 두 가지로 볼 수 있습니다.

먼저 같이 사용되는 비율이 높은 페이지들은 디스크 상에서 서로의 거리가 가깝게 저장하도록 하는 것입니다. 같이 사용되는 페이지들이 가깝게 위치한다면 sequential access의 이점을 늘릴 수 있는 것이죠. 이런 식으로 효율성을 늘리는 것을 spatial control이라고 합니다.

다른 방법으로는 페이지들을 읽거나 쓰는 타이밍을 언제로 할지를 잘 결정하는 것입니다. 페이지를 디스크에 써야할 때 가까운 페이지라면 조금 기다렸다가 한 번에 쓰는게 이득일 수도 있습니다. 그리고 execution engine이 아직 페이지를 요구하지 않았지만 필요할 수도 있는 것을 미리 읽어올 수도 있습니다.

이런 읽고 쓰는 타이밍을 조절해서 디스크 I/O로 인한 손해를 줄이는 것을 temporal control이라고 합니다.

Buffer Pool

Buffer Pool 이라는 것은 메모리 상에 존재하는 페이지 배열입니다. 메모리 상에 있는 각 페이지는 frame이라고 부릅니다. DBMS가 특정 페이지를 Buffer Pool Manager에게 요구하면 해당 페이지를 메모리에 전재하는 frame에 올린 뒤에 해당 frame의 주소를 DBMS에게 알려줍니다.

일반적으로 데이터베이스의 파일은 buffer pool 보다 크기 때문에 페이지를 갖고오려는데 비어있는 frame이 없다면 사용이 끝난 frame을 비운 뒤에 페이지를 갖고와야 합니다.

이런 작업들을 위해 buffer poll manager는 여러 메타 데이터를 저장하고 있습니다. 특정 frame을 현재 사용하고 있는 스레드가 있다면 해당 frame은 비워지면 안되기 때문에 각 frame별로 이를 사용하고 있는 스레드 수를 pin count로 저장합니다.

그리고 page와 frame을 매핑시켜주는 page table도 들고 있습니다. 그리고 frame에 있는 페이지를 비울 때 디스크에 새로 써줘야되는지를 나타내는 dirty flag도 저장합니다. 그리고 로그를 위해서 어떤 스레드/쿼리가 해당 페이지를 수정했는지도 저장합니다.

이 외에도 여러 스레드가 frame과 메타데이터에 접근할 수 있기 때문에 동기화를 위한 정보들도 저장합니다.

위 그림에서 page 3는 pin된 상태이고 page2는 디스크에 쓰는 작업을 하고 있어서 다른 스레드가 접근을 못하도록 막고 있습니다.

Lock vs Latch

DBMS의 세계에선 lock이 OS의 세계의 lock과는 조금 다른 의미를 가집니다. OS의 세계에서 lock(mutex)는 코드, 데이터 등 실제 물리적 메모리 상에서 race condition이 발생하는 지역을 보호하기 위해서 사용되는 것을 말합니다.

DBMS에서 이를 뜻하는 말은 latch입니다. lock은 논리적인 동기화 장치라고 생각하면 될 거 같습니다. 이러한 구분을 하는 이유는 rollback 때문입니다.

DBMS는 트랜잭션이 atomic 하게 진행되는 것을 보장해야 됩니다. 그렇기 때문에 lock을 이용하는데 이 lock을 얻고 트랜잭션을 수행하던 중에 뭔가 문제가 발생하면 롤백을 할 수 있어야 합니다.

반면에 물리적 메모리나 디스크에 접근하는 작업의 경우 이런 롤백에 대한 요구가 없기 때문에 mutex로도 충분합니다. 그래서 용어를 구분합니다.

Optimization

 

OS Page Cache

보통 디스크 I/O는 OS에서 제공하는 API를 통해 이루어진다. 그리고 OS 또한 자체적으로 파일 시스템에 사용되는 캐시 시스템이 존재한다.

이 캐시는 buffer pool의 기능과 매우 유사한데 DBMS가 이를 직접 관리하려는 것은 OS랑은 다르게 뭘 하려는 것인지를 알고 있고 이에 맞춰서 최적화를 진행할 수 있기 때문이다.

그리고 직접 관리하기 때문에 디스크 I/O를 OS에게 요구할 때 보통은 캐싱하지 말라고도 한다.(O_DIRECT) OS의 캐시에도 저장되고 buffer pool에도 같은 내용이 저장될 것이기 때문이다. 주요 DB 중에서 OS page cache를 사용하는 것은 postgreSQL이 있다.

어쨌든 이제 OS에게 맡기지 않고 직접 buffer pool을 관리함으로 가능해지는 최적화 몇가지를 알아보자.

 

Allocation Type

Buffer pool을 관리하는 방식도 또 종류를 나눠서 부른다. Global Policy와 Local Policy가 있다.

글로벌은 단어에서 유추할 수 있듯이 모든 수행 시나리오를 고려하며 빨라질 수 있는 방식을 취한다. 말이 잘 이해가 안 될수도 있는데 이와 반대되는 Local Policy가 뭔지 보면 그나마 잡힌다.

Local Policy란 특정 쿼리, 트랜잭션만을 고려했을 때 빨라질 수 있는 방식을 취하는 것을 말한다. 만약에 특정 쿼리만을 빠르게 처리할 수 있는 방식이 전체적으로 봤을 땐 총 수행시간을 늘릴 수도 있다.

일반적으로는 둘 다 섞어서 사용한다.

 

Multiple Buffer Pool

최적화 방법 중 하나로 여러 개의 buffer pool을 둬서 사용하는 것이 있다. 당연히 여러개고 크면 좋지란 뭔 당연한 소리인가 싶을 수도 있다.

buffer pool을 여러개 둔다는 것이 어떤 의미냐면 테이블마다 전용 buffer pool을 가지거나 페이지의 타입(index, table) 별로 buffer pool을 둬서 각각의 buffer pool마다 local policy를 둬서 최적화를 진행한다는 의미이다.

테이블 별로 buffer pool을 뒀다고 해보자. A라는 테이블은 sequential scan이 주로 일어나는 테이블이고 B라는 테이블은 point query가 주로 일어나는 테이블이라면 각 테이블에게 전용 buffer pool을 할당해서 서로 다르게 관리한다면 최적화를 쉽게 달성할 수 있다는 것이다.

그리고 인덱스 페이지만 저장하는 buffer pool이 있다면 상위 인덱스에 해당하는 페이지를 최대한 남겨놓는 방식으로 최적화하거나도 가능하다.

또 다른 이점으로는 latch로 인한 지연이 적어진다는 것이다. 하나의 큰 buffer pool을 사용한다면 page table에 접근할 때마다 latch를 얻어야 하는데 buffer pool이 여러개라면 이를 분산시킬 수 있어서 성능이 좋아질 수 있다.

당연한 얘기지만 원하는 페이지를 원하는 buffer pool에 매핑시키는 방식으론 각 레코드에 매핑을 위한 새로운 값(object id)을 부여해서 바로 매핑시켜주는 것입니다. 오라클이나 SQLServer가 이런 방식을 이용합니다.

다른 방식으로는 그냥 page id를 키로 하는 해시테이블을 쓰는 겁니다. MySQL이 이를 사용한다고 합니다.

 

Pre-Fetching

Pre-fetching은 말 그대로 미리 가지고 오는 최적화를 말한다. 페이지 0부터 페이지 10까지 읽을 필요가 있는 쿼리를 수행한다고 치자. buffer pool의 크기가 3이라고 하자.

처음에 buffer pool이 비어 있다면 0을 들고오고 1을 들고 오고 2를 들고오고 차례대로 페이지를 디스크에서 가지고 올 것이다. 매번 페이지를 가지러 가면 10번의 디스크 I/O가 발생한다.

이러지 말고 페이지 0을 들고 올 때 페이지 1, 2를 같이 들고 와서 buffer pool을 채워버리자는 것이다. 이렇게 되면 0을 읽을 때 한번, 3을 읽을 때 한번, 6을 읽을 때 한번, 10을 읽을 때 한번 디스크 I/O가 발생해서 10번에서 4번으로 줄어든다.

그러나 이런 식으로 sequential scan의 pre-fetch는 OS도 하라고 부탁하면 잘 해준다. 애초에 파일 시스템과 메모리 간의 페이지 이동을 OS가 해줄 수도 있지만 일부러 DBMS가 하는 이유가 OS는 할 수 없는 것을 할 수 있기 떄문이라고 했다.

그런 경우를 알아보자.

위 그림과 같이 인덱스가 구성되어 있다고 치자. 그리고 val 값이 100부터 250까지의 튜플들을 갖고 오는 쿼리 Q1을 수행한다고 하자.

그러면 먼저 index-page0에 접근할 것이다. 그리고 index-page1이 접근할 것이다.

100부터 250이기 때문에 index-page3은 당연히 필요하고 해당 페이지에는 튜플이 100개밖에 없기 때문에 그 다음 리프노드가 필요하다.

이 때 필요한 페이지는 3번과 5번이다. OS는 이런 정보를 모르기 때문에 디스크 I/O가 4번 발생하겠지만 DBMS는 이를 알기 때문에 3을 가지고 올 때 5까지 가져오라고 할 수 있다. 디스크 I/O가 1번 줄어든 것이다.

 

Scan Sharing

Scan Sharing은 다른 쿼리의 결과를 공유해서 쓰자는 생각에서 나온 최적화 방식이다.

다른 쿼리가 사용하고 buffer pool에 남아 있는 페이지를 다시 쓰는 것과는 좀 다른 개념인데, 한 쿼리가 scan을 진행중인데 scan을 해야하는 다른 쿼리가 실행되면 이미 진행중이던 scan에 합류해서 같이 진행한다는 것이다.

이 기능은 SQL Server나 IBM DB2, Oracle에서 사용할 수 있다고 한다.

간단한 예시를 보자.

A 테이블의 val의 전체 합을 구하는 쿼리 Q1을 실행하고 있다. page0부터 page5까지 읽어야 한다. 2번 페이지까지 읽고 3번 페이지를 읽으려고 할 때 buffer pool이 꽉 차서 페이지 0과 페이지 3을 스왑했다. 이 때 val의 전체 평균을 구하는 쿼리 Q2가 실행된다.

이 때 scan sharing을 할 수 있다. Q1도 Q2도 어차피 3번부터 5번까지는 읽어야 한다. DBMS는 이를 눈치채고 Q2도 Q1이 읽고 있는 3번 페이지부터 읽게 해서 3번부터 5번까지 읽게 한다.

 

 

그 뒤, 다시 0번부터 2번까지를 Q2가 읽는다. 이런 방식의 최적화를 scan sharing이나 synchronized scans라고 부른다.

 

Buffer Pool Bypass

쿼리를 수행할 때 정말 임시적으로 필요한 데이터가 있다면 이런 데이터를 읽기 위해서 buffer pool을 비우고 싶지 않을 수도 있다. 예를 들어서 파일 전체의 sequential scan이나 join을 위해서 잠시 필요한 데이터라거나 하는 경우다.

이런 경우를 위해서 buffer pool 외에도 적은 양의 메모리를 둬서 캐싱 처리같은 것 없이 파일을 읽어들일 수 있는 임시 메모리를 두고 거기에 불러오는 것이 buffer pool bypass이다.

 

여기까지가 강의에서 소개한 최적화 방법이다.

반응형

'DB 공부' 카테고리의 다른 글

해시 테이블(Hash Table)  (0) 2021.08.26
Buffer Pool Manager - 2  (0) 2021.06.25
DBMS는 저장소를 어떻게 관리하는가? - 2  (2) 2021.06.08
DBMS는 저장소를 어떻게 관리하는가?  (0) 2021.06.02
SQL에서 NULL과의 연산  (0) 2021.05.21