캐시 퓨전(Cache Fusion)은 오라클(Oracle) Real Application Clusters(RAC)에서 두 인스턴스 간에 데이터 블록을 전송하는 기술로, RAC의 가장 핵심적이고 중요한 기능입니다.
개요
오라클 공식 문서에 따르면, RAC 클러스터의 각 인스턴스는 자체적인 로컬 버퍼 캐시를 보유하고 있으며, 여기서 캐싱 작업이 수행됩니다. 하지만 여러 사용자가 서로 다른 노드에 접속한 상태에서는 다른 인스턴스가 소유한 데이터 블록에 접근하거나 잠금(lock)을 설정해야 하는 경우가 자주 발생합니다.
이런 상황에서 요청 인스턴스(requesting instance)는 해당 블록을 보유한 인스턴스(holding instance)에게 데이터 블록을 요청하고, 인터커넥트(interconnect) 메커니즘을 통해 블록을 전달받아 접근하게 됩니다. 바로 이 개념이 캐시 퓨전(Cache Fusion)입니다.
단일 인스턴스 환경의 동작 방식
캐시 퓨전을 본격적으로 살펴보기 전에, RAC가 아닌 일반 단일 인스턴스 데이터베이스에서 데이터 블록 요청이 발생했을 때 어떻게 동작하는지 먼저 이해할 필요가 있습니다.
다음은 단일 인스턴스 환경에서의 4단계 트랜잭션 처리 과정입니다.
- 사용자가 최근에 수정된 블록을 읽을 때, 해당 블록 안에 활성 트랜잭션이 존재할 수 있습니다.
- 사용자 프로세스는 언두(Undo) 세그먼트 헤더를 읽어 해당 트랜잭션이 커밋되었는지 여부를 판단해야 합니다.
- 트랜잭션이 아직 커밋되지 않았다면, 프로세스는 블록 내 데이터와 언두 세그먼트에 저장된 데이터를 활용해 버퍼 캐시에 일관된 읽기(Consistent Read, CR) 버전의 블록을 생성합니다.
- 언두 세그먼트가 트랜잭션이 커밋되었음을 보여주면, 프로세스는 해당 블록을 다시 확인하여 블록을 정리(clean out)하고 변경 사항에 대한 리두(Redo)를 생성해야 합니다.
이제 같은 시나리오를 두 인스턴스로 구성된 RAC 클러스터, 즉 캐시 퓨전 환경에서 어떻게 처리되는지 살펴보겠습니다.
캐시 퓨전(Cache Fusion)이란?
RAC 환경에서는 두 개 이상의 인스턴스가 동일한 스토리지(예: ASM)에 위치한 동일한 데이터베이스 파일에 접근합니다. 각 인스턴스는 고유한 SGA와 백그라운드 프로세스를 가지며, 이는 곧 각 인스턴스마다 독립적인 버퍼 캐시를 보유한다는 의미입니다.
이러한 버퍼 캐시들은 인스턴스 수준에서는 개별적으로 동작하지만, 데이터베이스 수준에서는 하나의 엔티티인 글로벌 캐시(Global Cache)로 융합되어 인스턴스 간에 데이터 블록을 공유합니다. 이것이 바로 캐시 퓨전입니다.
캐시 퓨전은 고속 IPC 인터커넥트를 활용해 클러스터 내 인스턴스들 사이에서 캐시 대 캐시(cache-to-cache) 방식으로 데이터 블록을 전송합니다. 이러한 블록 전송(block shipping) 방식은 디스크 I/O를 제거하고 읽기/쓰기 동시성을 크게 최적화합니다.
이러한 이해를 바탕으로 인스턴스 간 블록 전송을 담당하는 글로벌 캐시 서비스(GCS, Global Cache Service)를 살펴볼 차례입니다.
GCS의 주요 백그라운드 프로세스
- 글로벌 캐시 서비스 프로세스(LMSn)
- 글로벌 인큐 서비스 데몬(LMD)
데이터 블록 리소스와 모드(Mode)
백그라운드 프로세스를 자세히 알아보기에 앞서, 오라클이 데이터 블록을 어떻게 다루고 관리하는지 이해해야 합니다.
오라클은 데이터 블록을 하나의 리소스(resource)로 취급하며, 각 리소스는 서로 다른 모드로 유지될 수 있습니다. 이 모드 체계는 데이터 무결성을 유지하는 중요한 메커니즘으로, 리소스 보유자가 데이터를 수정하려는지 아니면 단순히 읽으려는지에 따라 세 가지 유형으로 분류됩니다.
- Null(N) 모드: 주로 플레이스홀더(placeholder) 역할로 유지되는 모드입니다.
- Shared(S) 모드: 다른 세션이 데이터 블록을 수정하지 않으며, 동시 공유 접근을 허용하는 모드입니다.
- Exclusive(X) 모드: 보유 프로세스에게 배타적 접근 권한을 부여합니다. 다른 프로세스는 해당 리소스에 쓸 수 없으며, 일관된 읽기(CR) 블록만 가질 수 있습니다.
글로벌 캐시 서비스 프로세스(LMSn)
인스턴스로부터 블록 요청이 들어오면, GCS는 메모리에 블록 복사본을 유지하면서 다른 인스턴스로의 블록 전송을 조율합니다. 이때 만들어지는 각 복사본을 과거 이미지(Past Image, PI)라고 부릅니다. 블록이 더티(dirty) 상태에서 요청된 횟수에 따라 하나의 데이터 블록에 대해 여러 개의 PI가 존재할 수도 있습니다.
참고: 데이터 블록을 읽으려면 반드시 일관된 상태에서 읽어야 하며, 다른 트랜잭션이 변경 중인 내용을 그대로 읽는 것은 허용되지 않습니다.
글로벌 인큐 서비스 데몬(LMD)
글로벌 인큐 서비스(GES, Global Enqueue Service)는 오라클의 모든 인큐(enqueue) 메커니즘 상태를 추적합니다. GES는 딕셔너리 캐시 잠금, 라이브러리 캐시 잠금, 그리고 트랜잭션에 대해 동시성 제어를 수행하며, 이 작업은 둘 이상의 인스턴스가 접근하는 리소스에 대해 이루어집니다. GES는 데이터 파일과 컨트롤 파일에 대한 접근은 제어하지만, 데이터 블록 자체는 제어하지 않습니다.
GES가 관리하는 주요 리소스는 다음과 같습니다.
- 트랜잭션 잠금(Transaction Locks): 트랜잭션이 변경 작업(insert, update 등)을 시작할 때 배타적(exclusive) 모드로 획득되며, 트랜잭션이 커밋되거나 롤백될 때까지 유지됩니다.
- 라이브러리 캐시 잠금(Library Cache Locks): SQL, DML, DDL, PL/SQL 또는 Java 구문을 파싱하거나 컴파일하는 과정에서 데이터베이스 객체(테이블, 뷰, 패키지, 패키지 본문 등)가 참조될 때, 해당 구문을 처리하는 프로세스가 적절한 모드로 획득합니다.
- 딕셔너리 캐시 잠금(Dictionary Cache Locks): 클러스터 데이터베이스 모드에서 글로벌 인큐가 사용됩니다. 클러스터 내 모든 오라클 인스턴스는 동일한 데이터 딕셔너리 구조를 공유합니다.
- 테이블 잠금(Table Locks): 테이블 전체를 보호하는 GES 잠금입니다. 트랜잭션이 테이블을 수정할 때 획득하며, Null(N), Row Share(RS), Row Exclusive(RX), Share(S), Share Row Exclusive(SRX), Exclusive(X) 등 여러 모드로 유지될 수 있습니다.
과거 이미지(PI)와 일관된 읽기(CR) 이미지
본격적인 시나리오에 들어가기 전에, 과거 이미지(PI)와 일관된 읽기(CR) 이미지의 개념을 먼저 이해해야 합니다.
과거 이미지(Past Image, PI)
PI 개념은 RAC 환경에만 특화된 것입니다. 어떤 인스턴스가 데이터 블록에 대해 배타적 잠금을 보유하고 갱신 작업을 수행 중이라고 가정해 보겠습니다. 이때 RAC 내 다른 인스턴스가 해당 블록을 필요로 하면, 블록을 보유한 인스턴스는 블록을 디스크에 기록하는 대신 자신의 버퍼 캐시에 PI(과거 이미지)를 보관한 채 블록을 요청 인스턴스로 전송할 수 있습니다. 즉, PI란 블록이 디스크에 기록되기 이전 상태의 데이터 블록 복사본입니다.
일관된 읽기 이미지(CR Image)
특정 블록을 트랜잭션 A1이 접근하거나 수정하고 있는 도중에, 동시에 다른 트랜잭션 A2가 같은 블록에 접근하거나 읽으려는 상황을 생각해 볼 수 있습니다. A1이 아직 커밋되지 않았다면, A2는 블록의 일관된 읽기(수정되지 않은) 복사본이 필요합니다. 이 CR 복사본은 해당 블록의 UNDO 데이터를 이용해 생성됩니다.
캐시 퓨전의 세 가지 시나리오
캐시 퓨전에는 세 가지 서로 다른 시나리오가 존재합니다.
- 읽기-읽기(Read-Read) 시나리오
- 읽기-쓰기(Read-Write) 시나리오
- 쓰기-쓰기(Write-Write) 시나리오
읽기-읽기(Read-Read) 시나리오
이 시나리오는 치명적이지 않은(non-critical) 경우입니다. 블록을 요청하는 인스턴스와 블록을 보유한 인스턴스 모두 읽기 트랜잭션을 수행 중이므로 배타적 잠금이 발생하지 않습니다. 인스턴스 B가 GCS에 읽기 블록을 요청하면, GCS는 인스턴스 A가 소유한 블록의 가용성을 확인하고 공유(shared) 잠금을 획득합니다. 이후 GCS는 인스턴스 A에게 요청된 블록을 인스턴스 B로 전송하도록 지시합니다.
읽기-쓰기(Read-Write) 시나리오
이 시나리오는 충돌(contention)이 발생할 수 있는 중요한 경우입니다.
인스턴스 A가 데이터 블록을 갱신하는 중이므로 배타적 잠금을 보유하고 있습니다. 잠시 후 인스턴스 B가 GCS에 동일한 데이터 블록에 대한 읽기 요청을 보냅니다.
GCS는 확인 결과 인스턴스 A가 해당 블록에 대해 배타적 잠금을 획득한 것을 발견하고, 인스턴스 A에게 블록 해제를 요청합니다. 그러면 인스턴스 A는 자신의 버퍼 캐시에 CR 이미지를 생성하고, GCS에 이를 인스턴스 B로 전송하도록 통지합니다.
CR 이미지 생성에 GCS가 관여하며, 이를 요청 인스턴스로 전송하는 과정에서 바로 캐시 퓨전이 작동하는 것입니다.
쓰기-쓰기(Write-Write) 시나리오
인스턴스 A와 인스턴스 B가 모두 하나의 데이터 블록에 대해 배타적 잠금을 획득하려고 시도하는 상황입니다.
인스턴스 B가 GCS에 블록 요청을 보내면, GCS는 가용성을 확인한 후 인스턴스 A가 이미 잠금을 보유하고 있음을 파악합니다. 따라서 GCS는 인스턴스 A에게 인스턴스 B를 위해 블록을 해제하도록 요청합니다. 이후 인스턴스 A는 자신의 현재 블록에 대한 PI를 버퍼에 생성하고, 리두 엔트리를 기록한 뒤, GCS에 블록을 인스턴스 B로 전송하도록 통지합니다.
이제 인스턴스 B는 해당 블록을 받아 평소처럼 변경 작업을 수행합니다.
CR과 PI의 핵심 차이점
PI와 CR 이미지의 차이에 대해 마지막으로 정리하면 다음과 같습니다.
CR 이미지는 읽기-쓰기 유형의 경합을 피하기 위해 전송됩니다. 요청 인스턴스는 쓰기 작업을 수행하려는 것이 아니므로 블록에 대한 배타적 잠금이 필요 없고, 따라서 읽기 작업에는 블록의 CR 이미지만으로도 충분합니다.
반면 쓰기-쓰기 경합 상황에서는 요청 인스턴스 역시 데이터 블록에 대한 배타적 잠금을 획득해야 합니다. 쓰기 작업을 위한 잠금을 획득하려면 CR 이미지가 아닌 실제 블록이 필요합니다. 따라서 블록을 보유한 인스턴스는 실제 블록을 전송하되, 해당 블록이 디스크에 기록될 때까지는 블록의 PI를 보관할 책임이 있습니다.
이렇게 함으로써 만약 인스턴스 장애나 크래시(crash)가 발생하더라도, 오라클은 RAC 인스턴스들에 분산된 PI를 활용해 블록을 복구할 수 있습니다. 블록이 한 번 디스크에 기록되면 크래시 발생 시 복구가 더 이상 필요 없으므로, 관련된 PI는 폐기될 수 있습니다.