Post

<오라클 성능 고도화 원리와 해법1> Ch06-04 Prefetch

오라클 성능 고도화 원리와 해법1 - Ch06-04 Prefetch

Prefetch 라는 용어는 오라클에서 여러 가지 의미로 쓰인다. 예를 들어, 한 번의 Fetch Call 로 Array 크기만큼 여러 개 레코드를 가져오는 것을 ‘Row Prefetch’ 라는 용어로서 설명하는 문서들도 있다. 본 절에서 설명하는 Prefetch는 테이블 Prefetch와 인덱스 Prefetch를 지칭하는 것임을 먼저 밝힌다.

오라클을 포함한 모든 DBMS 는 디스크 블록을 읽을 때 곧이어 읽을 가능성이 높은 블록을 미리 읽어오는 Prefetch 기능을 제공한다. 디스크 I/O 가 비용이 크기 때문에 한 번의 I/O Call 을 통해 다량의 블록을 퍼 올릴 수 있으면 그만큼 성능 향상에 도움이 되기 때문이다. 앞 절에서 설명한 Multiblock I/O 도 Prefetch 기능 중 하나라고 할 수 있다. 지금부터 설명할 테이블 Prefetch, 인덱스 Prefetch 와 다른 점은 한 익스텐트에 속한 인접한 블록들을 Prefetch 한다는 점이다.

Prefetch 는 한 번에 여러 개 Single Block I/O 를 동시에 수행하는 것을 말한다. 앞에서 설명한 Multiblock I/O 는 한 번의 I/O Call 로써 서로 인접한(contiguous) 블록들을 같이 읽어 적재하는 것이라고 했는데, 지금 설명하는 테이블 Prefetch 와 인덱스 Prefetch 는 인접하지 않은(noncontiguous) 블록, 즉 서로 다른 익스텐트에 위치한 블록을 배치(Batch) 방식으로 미리 적재하는 것을 말한다. I/O Call 을 병렬 방식으로 동시에 여러 개 수행하는 것이므로 읽어야 할 블록들이 서로 다른 디스크 라이브에 위치한다면 Prefetch 에 의한 성능 향상은 더욱 배가된다.

이 기능은 곧 읽을 가능성이 높은 블록들을 미리 적재했을 때만 성능 향상에 도움을 준다. 미리 적재했는데 실제 사용으로 연결되지 못하면 버퍼 캐시 효율만 나빠진다. 오라클은 미리 적재했을 때 효과를 얻을 수 있는 오퍼레이션에만 이 기능을 적용하지만, 그럼에도 Prefetch 한 블록들이 실제 액세스로 연결되지 못한 채 캐시에서 밀려나는 비율이 높다면 더는 이 기능이 작동되지 못하도록 정지시켜버린다. Prefetch 된 블록들을 모니터링하는 기능은 CKPT 프로세스가 맡는다(SYS.X$KCBKPFS 뷰 참조).

앞으로 읽어야 할 블록들을 미리 적재하는 기능이므로 시스템 전반의 디스크 경합을 줄여주기보다, I/O 를 위한 시스템 Call 을 줄이고 개별 쿼리의 수행 속도를 향상시키는 데 주로 도움을 준다. 데이터 블록을 읽는 도중에 물리적인 디스크 I/O 가 필요할 때 서버 프로세스는 I/O 서브시스템에 I/O Call 을 발생시키고 잠시 대기 상태에 빠진다. 어차피 대기 상태에서 잠시 쉬어야 하므로, 곧이어 읽을 가능성이 높은 블록들을 버퍼 캐시에 미리 적재해 놓는다면 대기 이벤트 발생 횟수를 그만큼 줄일 수 있다.

(1) 인덱스 Prefetch

오라클 7.2 버전부터 사용되어온 기능이다. 브랜치 블록에서 앞으로 읽게 될 리프 블록 주소를 미리 얻을 수 있으므로 I/O Call 이 필요한 시점에 미리 캐싱해두는 것이 가능하다.

예를 들어, 그림 6-4 처럼 ① -> ② -> ③ -> ④ 순으로 Index Range Scan 을 진행한다고 하자. 2번 브랜치 블록을 읽고 5번 리프 블록을 읽으려는 시점에 5번 블록이 버퍼 캐시에 없으면 물리적인 디스크 I/O 가 필요하다. 이때 6번과 7번 블록까지 같이 적재해놓는다면, ④번 리프 블록 스캔이 진행하는 동안 디스크 I/O 때문에 대기할 가능성을 줄일 수 있다. 그림을 간단히 그렸지만 대개 하나의 브랜치 블록이 수십~수백개의 리프 블록을 가리킨다. Sequential 액세스 성능을 향상시키려고 Multiblock I/O 와 인덱스 Prefetch 같은 기능을 사용한다고 설명한 것을 상기하기 바란다.

인덱스 Prefetch 기능이 가장 효과적일 수 있는 상황은 Index Full Scan 이 일어날 때다. 부분 범위 처리 방식으로 중간에 멈추지만 않는다면 모든 인덱스 리프 블록을 읽게 되기 때문이다. 그런데 Index Full Scan 시 Prefetch 방식으로 I/O 하려면 리프 블록 위쪽에 있는 브랜치 블록들을 추가로 읽어야하기 때문에 Prefetch 하지 않을 때보다 I/O 가 약간 더 발생한다. (일반적인 Index Full Scan 시에는 가장 왼쪽 브랜치 블록만 읽는다.)

(2) 테이블 Prefetch

‘테이블 Lookup Prefetch’ 또는 ‘데이터 블록 Prefetch’ 라고도 한다. 인덱스를 경유해 테이블 레코드를 액세스하는 도중 디스크에서 캐시로 블록을 적재해야 하는 상황이 발생할 수 있는데, 그 때 다른 테이블 블록까지 미리 적재해두는 기능이다.

① -> ② -> ③ -> ④ 리프 블록에 있는 인덱스 레코드는 논리적인 순서를 따라 읽는다. 읽는 도중에 디스크 I/O 가 필요해지면 현재 읽던 리프 블록 내에서 앞으로 액세스해야 할 테이블 블록 주소 목록을 미리 취합할 수 있다. 예를 들어, 그림 6-5 처럼 인덱스를 스캔하면서 테이블 블록을 Random 액세스한다. 진행 순서는 ① -> ② -> ③ -> ④ -> ⑤ -> ⑥ -> ⑦ 이다. 5번 인덱스 리프 블록을 읽고 12번 테이블 블록을 읽으려는 시점에 12번 블록이 버퍼 캐시에 없으면 물리적인 디스크 I/O 가 필요하다. 이때 13, 15, 18번 블록까지 같이 적재해놓는다면 ⑤,⑥,⑦번 액세스 시에 디스크 I/O 때문에 대기하지 않아도 된다. 그림을 간단히 그렸지만 하나의 리프 블록이 수십~수백개의 테이블 레코드를 가리키고 있다.

Random 액세스 성능을 향상시키려고 버퍼 Pinning 과 테이블 Prefetch 같은 기능을 사용한다고 설명한 것을 상기하기 바란다. 버퍼 Pinning 은 Random 액세스에 의한 논리적 블록 요청 횟수를 감소시키고, 테이블 Prefetch 는 디스크 I/O 에 의한 대기 횟수를 감소시킨다.

이 기능은 인덱스 클러스터링 팩터가 나쁠 때 특히 효과를 발휘한다. 클러스터링 팩터가 나쁘면 논리적 I/O 가 증가할 뿐 아니라 디스크 I/O 도 많이 발생하기 때문이다.

DB2, SQL Server 등도 데이터 블록 Prefetch 기능을 갖지만 작동 방식은 조금씩 다르다. DB2를 예로 들면, 그림 6-5 에서 4~11번까지 리프 블록을 먼저 스캔하고 거기서 얻은 결과 집합을 rowid 순으로 정렬한 후에 테이블을 액세스한다. 앞으로 액세스하게 될 테이블 블록 순으로 정렬한 상태에서 액세스하기 때문에 각 테이블 블록을 한 번씩만 액세스하게 된다. 오라클에서 클러스터링 팩터가 높은 인덱스를 이용할 때 테이블 Random 액세스를 최소화할 수 있다고 했는데, 그 원리를 이용한 액세스 방식이라고 할 수 있다. 즉, 인덱스에서 읽은 집합을 실시간으로 정렬해 클러스터링 팩터를 좋게 만든 상태에서 테이블을 액세스하는 것으로 이해하면 된다. 한 번 액세스해 그곳에 저장된 레코드를 모두 읽어들임으로써 Random 액세스의 비효율을 완전히 제거할 수 있다.

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