Post

<오라클 성능 고도화 원리와 해법1> Ch06-06 RAC 캐시 퓨전

오라클 성능 고도화 원리와 해법1 - Ch06-06 RAC 캐시 퓨전

데이터베이스 동시 사용자가 많을 때 부하를 분산할 목적으로 시스템마다 다양한 데이터 분산 전략을 사용한다.

  1. 데이터베이스 서버간 복제
  2. 업무별 수직 분할
  3. 데이터 구분에 따른 수평 분할

1번은 여러 대의 데이터베이스 서버를 두고 각 서버에서 발생한 트랜잭션 데이터를 상호 복제하는 방식으로, 실시간 동기화가 필요할 때는 복제 과정에서 발생하는 부하 때문에 실제 부하 분산 효과를 얻기 어렵다.

2번은 업무 영역별(공통/접수/작업/AS, 공통/고객/청구 · 수납/상담 등)로 데이터베이스를 따로 두고 각각 다른 테이블을 관리하며, 다른 영역의 데이터는 분산 쿼리를 이용해 조회하는 방식이다. 분산 쿼리로 자주 액세스되는 공통 영역의 범위에 따라 성패가 좌우된다.

3번은 스키마는 같지만 데이터 구분에 따라 데이터베이스를 따로 가져가는 방식을 말한다. 예를 들어, 전국 중 · 고등학생의 수험 정보를 관리한다고 할 때, 테이블 구조는 같지만 서버를 분할해서 각 시도별로 학생들의 수험 정보를 관리하는 것이다. 분할된 데이터 간 의존성이 낮을 때(-> 같이 조회되는 일이 거의 없고, 릴레이션십을 갖는 테이블들도 같은 기준으로 분할 가능할 때) 성공적인 모델이지만 서버간 데이터 이동이 발생할 때 어떻게 처리할지에 대한 모델 관점에서의 방안 마련이 필요하다. 예를 들어, 부산 수험생이 서울로 이주하는 경우 데이터적으로 어떻게 처리할지에 대한 고민이 필요하다는 뜻이다.

물리적으로 분산시킨 데이터를 논리적으로 다시 통합해 하나의 뷰를 통해 액세스할 수 있도록 데이터베이스를 클러스터링하는 기법도 몇 가지 형태로 발전을 거듭해왔다. 몇 년 전부터는 데이터베이스를 다시 하나로 통합하고 이를 액세스하는 인스턴스를 여러 개 두는 공유 디스크(Shared Disk) 방식의 데이터베이스 클러스터링 기법이 도입되기 시작했다. 그 중 오라클 RAC 모델은 공유 디스크 방식에 기반을 두면서 인스턴스간에 버퍼 캐시를 공유(Shared Cache)하는 캐시 퓨전(Cache Fusion) 기술로 발전하였다. 하나의 데이터베이스를 여러 인스턴스가 공유하는 RAC 기본 아키텍처에 대해서는 1장에서 간단하게 살펴본 바 있다.

오라클 RAC는 캐시 퓨전 기술을 통해 현재 가장 진일보한 데이터베이스 클러스터링 기술로 평가받고 있을 뿐 아니라 가용성, 확장성, 부하 분산 측면에서 이미 성공적인 모델로서 자리를 굳혔다. 특히, 데이터를 하나의 데이터베이스에 통합 모델로 관리함으로써 높은 정합성을 유지할 수 있다는 것이 가장 큰 장점이라고 할 수 있다.

하지만 장점만 있는 것은 아니다. 튜닝이 잘 되지 않아 많은 블록 I/O를 일으키는 애플리케이션에서 RAC를 도입한다면 부하 분산은 커녕 단일 인스턴스 환경에서보다 더욱 심각한 성능 저하 현상을 겪게된다. 여러 인스턴스에 놓인 프로세스끼리 하나의 데이터를 동시에 읽고 쓰려는 경합이 심하게 발생하기 때문이다. 그런 동시 액세스를 직렬화하려면 추가적인 동기화 메커니즘이 필요하고, 이 때문에 새로운 성능 이슈들이 생긴다.(그림 6-7) 따라서 RAC 모델 특성상 발생하는 성능 문제들을 해결하려면 캐시 퓨전 프로세싱 원리를 이해할 필요가 있다.

RAC는 글로벌 캐시(Global Cache)라는 개념을 사용한다. 즉, 클러스터링된 모든 인스턴스 노드의 버퍼 캐시를 하나의 버퍼 캐시로 간주한다. 따라서 필요한 데이터 블록이 로컬 캐시(Local Cache)에 없더라도 다른 노드에 캐싱돼 있다면 디스크 I/O를 일으키지 않고 그것을 가져와 읽거나 쓸 수 있다.

모든 데이터 블록에 대해 마스터 노드가 각각 정해져 있고, 그 노드를 통해 글로벌 캐시에 캐싱돼 있는 블록의 상태와 Lock 정보를 관리한다. 마스터 노드는 각 블록 주소(DBA)의 해시값에 의해 인스턴스가 기동되는 시점에 동적으로 정해진다61).

  1. 오라클 버전에 따라 리소스 마스터 결정 알고리즘이 조금씩 바뀌고 있는데, 복잡한 세부 알고리즘을 일일이 이해할 필요는 없으며 기본 원리를 이해하는 것만으로 충분하다.

캐시 퓨전 원리를 간단히 설명하면, 읽고자 하는 블록이 로컬 캐시에 없을 때 마스터 노드에 전송 요청을 하고, 마스터 노드는 해당 블록을 캐싱하고 있는 노드에 메시지를 보내 그 블록을 요청했던 노드에 전송하도록 지시하는 방식이다. 만약 어느 노드에도 캐싱돼 있지 않다면 직접 디스크에서 읽도록 권한을 부여한다.

캐시 퓨전 메커니즘을 자세히 설명하기에 앞서 1장에서 설명한 ‘Current 블록’의 의미(아래)를 상기하기 바란다.

Current 블록은 디스크로부터 읽혀진 후 사용자의 갱신 사항이 반영된 최종 상태의 원본 블록을 말하며, CR 블록은 Current 블록에 대한 복사본이다. CR 블록은 여러 버전이 존재할 수 있지만 Current 블록은 오직 한 개뿐이다.

RAC 환경에서의 Current 블록은 Shared 모드 Current (이하 SCur)와 Exclusive 모드 Current(이하 SCur)로 나뉜다. SCur 상태의 블록은 동시에 여러 노드에 캐싱될 수 있지만 XCur 상태의 블록은 단 하나의 노드에만 존재할 수 있다. 이는 RAC 성능 문제를 이해하는 데 있어 매우 중요한 사실이다.

자주 읽히는 데이터 블록을 각 노드가 SCur 모드로 캐싱하고 있을 때가 가장 효율적인 상태가 된다. 하지만 그 중 한 노드가 XCur 모드로 업그레이드를 요청하는 순간 다른 노드에 캐싱돼 있던 SCur 블록들은 모두 Null 모드로 다운그레이드(downgrade)된다. 더는 쓸 수 없는 PI(Past Image) 블록이 되는 것이다.

캐시 퓨전 원리에 대해 좀 더 자세히 살펴보자. RAC 노드간 버퍼 캐시를 공유하면서 블록을 서로 주고받는 전송 메커니즘은 아래 5가지로 나눌 수 있다62).

  • 전송 없는 읽기: Read with No Transfer
  • 읽기/읽기 전송: Read to Read Transfer
  • 읽기/쓰기 전송: Read to Write Transfer
  • 쓰기/쓰기 전송: Write to Write Transfer
  • 쓰기/읽기 전송: Write to Read Transfer
  1. Oracle9i Real Application Cluster - Cache Fusion의 작동 원리와 활용(Oracle Korea megazine, 2003년 Spring, 이상범)에 설명된 내용을 재구성한 것임을 밝힌다.

(1) 전송 없는 읽기: Read with No Transfer

A 노드에서 K 블록을 읽으려는데, 현재 어떤 노드에도 캐싱돼 있지 않은 상태다. 현재 K 블록의 SCN은 123이라고 하자.

  1. K 블록을 읽으려는 A 노드는 그 블록의 리소스 마스터인 B 노드에게 전송 요청을 보낸다. 이때, gc cr request 이벤트에서 대기한다.
  2. B 노드는 현재 어떤 노드에도 K 블록을 캐싱하고 있지 않음을 확인하고, A 노드에게 데이터파일에서 직접 블록을 SCur 모드로 읽도록 권한을 부여(Grand)한다.
  3. A 노드는 디스크에서 블록을 읽어 로컬 캐시에 캐싱한다.

(2) 읽기/읽기 전송: Read to Read Transfer

A노드만 K블록을 SCur 모드로 캐싱한 상태에서 C노드가 같은 K블록을 SCur 모드로 읽으려고 한다.

  1. C노드는 리소스 마스터인 B노드에게 K블록에 대한 전송 요청을 보낸다. 이때, gc cr request 이벤트를 대기한다.
  2. B노드는 현재 K블록을 A노드가 캐싱하고 있음을 확인하고, C노드에 블록을 전송해주도록 A노드에게 지시한다.
  3. A노드는 C노드에게 블록을 전송해준다.
  4. C노드는 블록을 성공적으로 전송받아 SCur 모드로 캐싱하게 되었음을 알리려고 마스터 노드인 B에게 메시지를 보낸다.

(3) 읽기/쓰기 전송: Read to Write Transfer

A와 C노드 모두 K블록을 SCur 모드로 캐싱하고 있다. 이제 C노드가 K블록을 XCur 모드로 업그레이드(upgrade)하려고 한다. 물론 해당 블록을 갱신하려는 것이다.

  1. 마스터 노드인 B에게 K블록을 XCur 모드로 업그레이드하겠다고 요청한다.
  2. B노드는 현재 K블록을 A노드도 캐싱하고 있음을 확인하고 Null 모드로 다운그레이드(Downgrade) 하도록 지시한다.
  3. A노드는 C노드에게 Null 모드로 다운그레이드했음을 알려준다.
  4. C노드는 K블록을 XCur 모드로 업그레이드하고 그 결과를 마스터 노드인 B에게 알려준다. 이때 A노드에 캐싱돼 있던 블록이 Null 모드로 다운그레이드된 사실까지 함께 알려준다.

이제 C노드가 XCur 모드로 K블록을 얻고 변경을 가하므로 블록 SCN은 증가하게 된다. 123에서 154로 증가했다고 하자.

(4) 쓰기/쓰기 전송: Write to Write Transfer

현재 A노드는 K블록을 Null 모드로 갖고 있고, C노드는 XCur 모드로 갖고 있다. C노드가 갖고 있는 Current 버전의 SCN은 154로 증가되었고, 데이터 파일에 있는 블록 SCN은 아직 123으로 Dirty 버퍼 상태다. 이 때, A노드가 K블록을 XCur 모드로 읽으려고 한다. 물론 해당 블록을 갱신하려는 것이다.

  1. 마스터 노드인 B에게 K 블록을 XCur 모드로 요청한다.
  2. B 노드는 현재 K 블록을 C 노드가 XCur 모드로 캐싱하고 있음을 확인하고, A 노드에게 보내주도록 지시한다.
  3. C 노드는 A 노드에게 블록을 전송하고, 자신이 가지고 있던 블록은 Null 모드로 다운그레이드 한다. C 노드가 가지고 있던 XCur 블록은 아직 커밋되지 않아 Row Lock이 걸린 상태일 수 있다.
  4. A 노드는 K 블록을 XCur 모드로 캐싱하게 됐음을 B 노드에게 알려준다.

다른 인스턴스가 갱신 중인 블록을 읽고자 할 때 Row Lock이 해제될 때까지 기다리지 않고 Row Lock이 설정된 채로 블록을 주고 받는다는 것은 매우 중요한 사실이다. 게다가, 예전 OPS에서는 쓰기/쓰기 전송 상황에서 C 노드는 일단 블록을 디스크에 기록한다. 그러면 A 노드는 이를 디스크에서 읽는 방식을 사용했고 이처럼 디스크를 거쳐 블록을 주고 받는 과정을 핑(ping)이라고 불렀다. RAC에서는 디스크 동기화 없이 Row Lock이 설정된 채로 버퍼 캐시 간 블록 전송이 가능해진 것이다.

A 노드가 XCur 모드로 K 블록을 얻고 변경을 가하므로 블록 SCN은 증가하게 된다. 154에서 168로 증가했다고 하자.

(5) 쓰기/읽기 전송: Write to Read Transfer

현재 A 노드는 K 블록을 XCur 모드로 갖고 있고, C 노드는 Null 모드로 갖고 있다. A 노드가 갖고 있는 Current 버전의 SCN은 168로 증가되었고, 데이터 파일에 있는 블록 SCN은 여전히 123이므로 Dirty 버퍼 상태다. 이때, C 노드가 K 블록을 SCur 모드로 읽으려고 한다.

  1. 마스터 노드인 B에게 K블록을 SCur 모드로 요청한다.
  2. B노드는 현재 K블록을 A노드가 XCur 모드로 캐싱하고 있음을 확인하고, C노드에게 보내주도록 지시한다.
  3. A노드는 C노드에게 블록을 전송하고 자신이 갖고 있던 블록은 SCur 모드로 다운그레이드 한다.
  4. C노드는 K블록을 SCur 모드로 캐싱하게 됐음을 B노드에게 알려준다. 이때, A노드에 캐싱돼 있던 블록이 SCur 모드로 다운그레이드된 사실까지 함께 알려준다.

간단히 설명했지만 쓰기/읽기 캐시 퓨전에서는 3번과 4번 과정이 좀 더 복잡하다. K블록의 커밋 여부에 따라 다른데, 아직 커밋되지 않았다면 Current 블록을 전송하지 않고 계속 CR Copy를 만들어 전송한다. C노드는 읽기 작업을 원하는 것이므로 굳이 Current 블록을 보낼 필요는 없다. 현재 A노드에서 갱신이 진행 중이므로 Current 블록을 보내면 언젠가 다시 가져와야 하는 부담 때문에 그렇게 처리하는 것이다.

K블록이 커밋된 상태더라도 위에서 설명한 것처럼 (3,4번) 바로 Current 블록을 보내지는 않는다. 처음에는 CR Copy만을 전송하다가 일정 횟수 이상 요청이 반복적으로 들어오면 그때 Current 블록을 보내준다. Current 블록을 보내 주려면 자신의 XCur 모드를 SCur로 다운그레이드 해야 하는데, 곧이어 갱신이 다시 발생한다면 XCur 모드로 또 다시 업그레이드 하는 일이 발생하게 된다. 그 때는 SCur 블록을 가져갔던 다른 노드도 모두 Null 모드로 다운그레이드 해야 하므로 이런 일이 자주 발생한다면 RAC 부하가 증가하게 된다. 이런 부하 발생 가능성을 최소화하려고 일정 횟수만큼 CR Copy만을 보내주는 방식을 사용하는 것이다.

CR Copy를 보내주는 횟수는 _fairness_threshold 파라미터에 의해 결정되며, 기본 값은 4로 설정돼 있다. 커밋된 XCur 블록을 보유하고 있는 노드(그림 6-12에서는 A노드)는 블록 전송 요청을 받을 때마다 CR Copy를 만들어 전송하고 fairness_count 값을 1씩 증가시킨다. fairness_count 값이 4에도 달하면 Redo 로그 버퍼를 비우고 XCur를 SCur로 다운그레이드한다. 이제 SCur 상태이므로 이후에 읽기 요청을 보낸 노드는 곧바로 SCur 모드로 블록을 전송받게 된다.

주로 읽기 작업 위주로 수행한다면 _faires_threshold 파라미터를 낮게 설정하는 것이 성능 향상에 도움이 될 수 있다. 만약 0으로 설정하면 CR Copy 전송 없이 곧바로 SCur 모드로 다운그레이드하고 Current 블록을 전송하게 된다. XCur 모드를 SCur로 다운그레이드했다 가 다시 XCur로 업그레이드 할 가능성이 적다면 읽기 요청이 반복되는 블록들을 가급적 빨리 SCur 모드로 보내주는 것이 좋다. 아래 쿼리를 수행했을 때 다운그레이드 Ratio가 높다면, 결국에는 Current 모드로 공유할 수 밖에 없음에도 CR을 만들어 보내주느라고 시스템이 고생하고 있음을 의미한다.

쓰기/읽기 캐시 퓨전 원리를 이해한다면 RAC를 구성할 때 데이터를 가공하는 노드와 읽는 노드를 서로 분리하는 것이 성능에 얼마나 안 좋은 영향을 끼칠지 충분히 예상할 수 있다.

dynamic remastering
리소스 친화도(Resource Affinity)에 따라 마스터 노드가 동적으로 변경될 수 있다. 예를 들어, A 노드가 ownership을 갖는 리소스를 B 노드가 반복적으로 요청한다면 어느 순간부터 그 리소스에 대한 마스터 노드가 B로 바뀔 수 있는 것이다. 자주 사용하는 리소스에 대한 상태 정보를 자신이 직접 관리하므로 RAC 성능 향상에 도움을 준다.

지금까지 RAC 캐시 퓨전 메커니즘에 대해 설명했다. 캐시 퓨전 과정에서 생기는 성능 부하는 I/O 관련 부하와 같은 시각에서 바라봐야 한다. 블록에 대한 읽기 요청 횟수가 많으면, 디스크 I/O 관련 대기 이벤트가 증가하는 만큼 RAC 관련 이벤트도 같이 증가하는 것을 관찰할 수 있다. 따라서 해법도 같다. 블록 읽기 요청 횟수를 줄여 인터커넥트를 통한 데이터 전송량을 감소시키는 것이 가장 확실하고 근본적인 해결책이며, 이는 SQL 튜닝을 통해 달성할 수 있다.

필자가 최근 모공기관에서 SQL 튜닝을 했는데, 앞서 컨설팅했던 업체의 진단 내용은 인터커넥트 장비를 좀 더 빠른 것으로 교체하라는 것이었다. 그 조언에 따라 고객사도 장비 교체 계획을 갖고 있었다. 하지만 인덱스 조정과 SQL 튜닝을 통해 전반적인 블록 I/O가 획기적으로 감소했고, RAC 관련 대기 이벤트도 현저히 감소함에 따라 인터커넥트 장비 교체 계획은 없던 일이 되어버린 사례도 있었다.

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