MongoDB - 스토리지 엔진 (2)

Share

Last Updated on 3월 24, 2024 by Jade(정현호)

안녕하세요 
이번 포스팅에서는 MongoDB 의 스토리지 엔진에 대해서 전반적으로 내용을 살펴보려고 합니다. 

포스팅은 Real MongoDB 책 내용을 정리한 것이 주요 내용이며, 추가적으로 MongoDB Document 의 내용이 포함되어 있습니다. 
아래 글에서 이어지는 내용입니다.  

WiredTiger 스토리지 엔진

WiredTiger 스토리지 엔진은 Berkeley DB 개발자들에 의해서 개발된 임베디드 데이터베이스 엔진으로 2014년 12월 MongoDB로 인수되어 지금은 MongoDB의 기본 스토리지 엔진으로 사용되고 있습니다.

MongoDB은 WiredTiger 스토리지 엔진을 도입하기 하기전에는 MMAPv1 스토리지 엔진을 사용하였고 MMAPv1 스토리지 엔진은 동시성의 문제 그리고 캐시 기능 부재 로 인하여 범용적으로 사용하기에는 여러가지 문제점이 있었습니다.

그래서 MongoDB 에서는 WiredTiger 스토리지 엔진을 인수하여 MongoDB의 스토리지 엔진으로 내장하게 되었습니다.


WiredTiger 스토리지 엔진은 내부적인 잠금 경합 최소화(Lock-free algorithm) 을 위해서 "하자드 포인터(Hazzard-Pointer)" 나 "스킵 리스트(Skip-List)" 와 같은 새로운 기술을 채택하고 있습니다. 또한 최신의 RDBMS 가 가지고 있는 MVCC 기능이나 데이터 파일 압축, 암호화 기능을 모두 가지고 있습니다.

그래서 MongoDB는 WiredTiger 스토리지 엔진을 내장하게 되면서 상용 RDBMS가 가지고 있는 고급 기능들을 모두 지원하게 되었습니다.
             

공유 캐시

WiredTiger 스토리지 엔진을 사용하면서 공유 캐시의 최적화는 MongoDB 처리 성능에 있어서 매우 중요한 요소이며 그래서 공유 캐시의 사용량에 대한 내용은 MongoDB 모니터링을 하는데 있어서 필수적인 지표입니다.

db.serverStatus() 에서는 다양한 정보를 볼 수 있으며 그 중에서 아래 3개의 지표는 공유 캐시의 크기 사용 용량, 더티 페이지 크기에 대해서 확인할 수 있습니다.

Total Cache Bytes = 
db.serverStatus().wiredTiger.cache."maximum bytes configured"

Used Cache Bytes = 
db.serverStatus().wiredTiger.cache."bytes currently in the cache"

Dirtiy Cache Bytes = 
db.serverStatus().wiredTiger.cache."tracked dirty bytes in the cache"


MongoDB 3.4부터 WiredTiger의 공유 캐시 기본 크기는 다음 둘 중에 더 큰 사이즈를 사용합니다.
(RAM - 1GB)의 50%
또는
256MB

예를 들어 4GB RAM이 있는 시스템에서 WiredTiger 캐시는 1.5GB RAM 으로 설정됩니다
계산식 : 0.5 * (4GB - 1GB) = 1.5GB

다른 계산으로는 총 1.25GB RAM이 있는 시스템은 WiredTiger 캐시에 256MB를 할당합니다.
계산식인 (0.5 * (1.25GB - 1GB)) 을 하였을 경우 125MB 로써 256MB 보다 작기 때문입니다.

3.4 버전 부터 1GB 미만으로도 설정이 가능합니다.

wiredTiger:
     engineConfig:
         cacheSizeGB: 20


공유 캐시는 MongoDB 서버를 재시작 하지 않고도 크기를 조정할 수 있습니다. 하지만 공유 캐시의 크기를 조정하는 작업은 WiredTiger 스토리지 엔진의 많은 내부 동시 처리 작업들을 멈추고 크기를 변경하게 됨에 따라 쿼리의 처리량이 많거나 부하가 많을 때는 조정에 주의가 필요 합니다.

• 변경 명령어

db.adminCommand( { "setParameter": 1,
                   "wiredTigerEngineRuntimeConfig": "cache_size=265G"})


공유 캐시의 페이지는 매우 빈번하게 참조 되므로 B-Tree의 각 페이지에 접근하는 방법에 따라서 쿼리의 성능도 많은 영향을 받게 됩니다.


일반적인 RDBMS의 경우 디스크에 저장된 데이터 페이지(또는 블록) 를 그대로 공유 캐시에 저장합니다.

공유 캐시에 적재된 B-Tree 노드를 통해서 자식 노드를 찾아갈 때도 B-Tree 상의 주소를 사용합니다.
그래서 부모 노드를 통해서 자식 노드를 찾아가는 과정이 모두 테이블 스페이스와 데이터 페이지 번호를 통해서 게속 찾아가게 됩니다.

즉 트리를 찾아가는 과정에서 계속해서 페이지 주소와 실제 메모리상의 주소로 맵핑을 참조하게 됩니다.


MongoDB의 WiredTiger 스토리지 엔진은 디스크의 데이터 페이지를 공유 캐시 메모리에 적재하면서 메모리에 적합한 트리 형태로 재구성하면서 적재를 합니다.

그래서 WiredTiger 스토리지 엔진의 공유 캐시에서 메모리에 적재된 페이지를 찾아가는 과정을 모두 별도의 매핑 과정 없이 메모리 주소(C/C++ 의 포인터 주소)를 이용해서 바로 검색할 수 있기 때문에 매핑에 관한 정보 또는 관련 테이블에 대한 경합이나 오버헤드가 없습니다.


일반적인 RDBMS 에서는 하나의 데이터 페이지 내에 저장된 레코드들의 인덱스를 별도로 관리하고 이는 하나의 데이터 페이지에 저장된 수백건의 레코드를 처음부터 끝가지 스캔하지 않기 위해서 필요한데, MongoDB WiredTiger 스토리지 엔진은 디스크에 저장되는 데이터 페이지의 레코드 인덱스를 별도로 관리하지 않고 저장합니다.

그리고 데이터 페이지를 공유 캐시 메모리에 적재할 때 레코드 인덱스를 새롭게 생성해서 메모리에 적재를 하는 방법을 사용하고 있습니다.


MongoDB 의 WiredTiger 는 이렇게 디스크의 데이터 페이지를 메모리에 적재하는 과정에서 여러가지 변환 과정을 거치게 됩니다. 그에 따라서 공유 캐시로 읽어 들이는 과정이 기존의 RDBMS 보다는 느리게 처리가 됩니다. 

다만 한 번 공유 캐시 메모리에 적재된 데이터 페이지에서 필요한 레코드를 검색하고 변경하는 작업은 기존의 RDBMS 보다 훨씬 빠르고 효율적으로 작동합니다.


MongoDB의 WiredTiger 스토리지 엔진은 공유 캐시의 잠금 경합(Mutex Contention)을 최소화하기 위해 Lock-Free 알고리즘을 사용하고 있습니다.

일반적으로 Lock-Free 알고리즘은 잠금을 전혀 사용하지 않는 시스템을 의미하는 것이 아니라 잠금 경합을 최소화하는 알고리즘을 의미하며, WiredTiger 스토리지 엔진에서는 대표적으로 하자드 포인트스킵 리스트 자료 구조를 활용하게 됩니다.

             

하자드 포인트(Hazard Pointer)

사용자 쓰레드는 사용자의 쿼리를 처리하기 위해 WiredTiger 캐시를 참조하는 스레드이며, 이빅션 쓰레드(Eviction Thread) 는 캐시가 다른 데이터 페이지를 적재할 수 있도록 빈 공간을 만들어주는 역할을 담당하는 쓰레드입니다.


MongoDB의 WiredTiger 캐시의 데이터 페이지를 참조할 때, 먼저 하자드 포인터에 자신이 참조하는 페이지를 등록하게 됩니다.
그리고 사용자 쓰레드가 쿼리를 처리하는 동안 이빅션 쓰레드는 동시에 캐시에서 제거할 데이터 페이지를 선택하여 캐시에서 삭제하는 작업을 진행하게 됩니다.

이때 이빅션 쓰레드는 제거해도 될 페이지에 대해서 먼저 하자드 포인터에 등록되어 있는지를 확인합니다. 해당 페이지의 하자드 포인터의 등록 여부에 따라서 캐시에서 삭제 여부가 결정되게 됩니다.

하자드 포인트에 등록되어 있지 않다면 캐시에서 제거합니다.


WiredTiger 스토리지 엔진에서 사용할 수 있는 하자드 포인트의 최대 개수는 기본적으로 1,000개로 제한되어 있습니다. 포인터의 개수가 부족해서 처리량이 느려지거나 문제가 있다면, WiredTiger 스토리지 엔진 옵션을 변경하여 하자드 포인터의 최대 개수 변경을 고려할 수 있습니다.

Storage:
...
   engine: wiredTiger
   ...
   ...
   configString: "hazard_max=2000"


위와 같이 configString 을 사용하여 변경 할 수 있습니다.
            

스킵 리스트(Skip-List)

WiredTiger 스토리지 엔진에서 Lock-Free 를 구현하기 위한 또 다른 기술은 스킵 리스트 자료 구조입니다.

[그림1 : Linked list - wikipedia]

[그림2 : opengenus.org/skip-list ]

그림 1은 일반적으로 알고 있는 링크드 리스트 자료 구조이고, 그림2 는 스킵 리스트를 표현한 것입니다.

링크드 리스트에서 어떠한 값을 찾아가기 위해서는 연결된 링크드 리스트를 따라서 여러 노드를 검색해야 합니다.
하지만 스킵 리스트는 특정 몇개의 노드 검색만 거치면 됩니다.

그림에서는 노드 개수가 몇 개만 표현되어 있지만, 실제로 WiredTiger 내부에서는 스킵 리스트에 저장되는 노드의 개수가 수천개를 넘을 수도 있으며, 그렇기 때문에 실제 검색 성능은 큰 차이를 보이게 됩니다.


스킵 리스트는 B-Tree 에 비해서 검색 성능이 조금 떨어지지만, 구현이 간단하고 메모리 공간도 많이 필요로 하지 않습니다.

WiredTiger 스토리지 엔진에 사용된 스킵 리스트는 새로운 노드를 추가하기 위해서 별도로 잠금을 필요로 하지 않으므로, 검색 또한 별도의 잠금을 필요로 하지 않습니다.

스킵 리스트의 노드 삭제는 잠금을 필요로 하지만, B-Tree 자료 구조에 비해서 잠금이 덜 필요로 하기 때문에 큰 성능 저하의 이슈는 아닙니다.

그래서 여러 쓰레드가 동시에 하나의 스킵 리스트에 노드를 저장하거나 검색하더라도 서로 전혀 잠금 경합을 하지 않습니다.


MongoDB의 WiredTiger 스토리지 엔진도 보통의 RDBMS 와 동일하게 변경되기 전 레코드(언두 로그, Undo Log) 를 별도의 저장 공간에 관리하며, 언두 로그를 관리하는 이유는 트랜잭션의 롤백(Rollback) 될 때 기존 데이터를 복구하기 위함인데, 많은 RDBMS에서는 언두 로그를 잠금 없는 다중 버전의 데이터 읽기(MVCC) 용도로도 같이 사용하고 있습니다.

MongoDB 의 WiredTiger 스토리지 엔진에서는 언두 로그를 스킵 리스트로 관리하며, 독특하게 페이지의 레코드를 직접 변경하지 않고 변경 이후의 데이터를 스킵 리스트에 추가를 하게 됩니다.

WiredTiger 스토리지 엔진에서는 데이터가 변경되어도 디스크에서 읽어 캐시된 데이터 페이지에 변경된 내용을 직접적으로 변경하지 않습니다. 대신 데이터 변경되면 변경된 내용을 스킵 리스트에 기록하게 됩니다.


사용자가 쿼리를 수행하는 시점에 변경 이력이 저장된 스킵 리스트를 검색해서 원하는 시점의 데이터를 가져가게 됩니다.
이렇게 처리하는 이유는 직접 데이터 페이지에 덮어쓰지 않고 별도의 리스트로 관리함에 따라서 쓰기 처리를 빠르게 하기 위함입니다.


보통의 RDBMS에서 데이터가 변경되면 기존의 레코드보다 데이터의 크기가 더 커져서 데이터 페이지내에서 레코드의 위치를 옮겨야 할 수도 있지만, 이런 과정을 데이터 페이지 내에서 처리하게 되면 사용자는 그만큼을 기다려야 합니다.

WiredTiger 스토리지 엔진에서는 변경되는 내용을 스킵 리스트에 추가하기만 하면 됩니다. 또한 스킵 리스트에 변경된 데이터를 추가하는 작업은 매우 빠르게 처리되므로 사용자의 응답 시간도 훨씬 빨라지게 됩니다.


WiredTiger 스토리지 엔진은 이런 관리 방식의 도움으로 여러 쓰레드가 하나의 페이지를 동시에 읽거나 쓸 수 있어서 동시 처리 성능이 매우 향상되게 됩니다.
          

캐시 이빅션(Cache Eviction)

WiredTiger 스토리지 엔진의 공유 캐시에서 또 하나 중요한 것은 이빅션(Eviction) 서버입니다.

공유 캐시내에 새로운 디스크 데이터 페이지를 읽어서 적재할 수 있도록 빈 공간을 항상 적절하게 유지를 해야 하며, 그렇지 않을 경우 쿼리 수행에 필요한 데이터 페이지를 디스크에서 가져오지 못하기 때문에 쿼리 응답 속도가 느려 질 수 있습니다.

WiredTiger 스토리지 엔진은 공유 캐시에 적절한 빈 공간을 유지하기 위해서 이빅션 모듈을 가지고 있으며, 이를 이빅션 서버 라고도 표현합니다. 이빅션 서버는 사용자의 요청을 처리하는 쓰레드 와는 별개로 백그라운드 쓰레드로 실행됩니다.

공유 캐시에 적재된 데이터 페이지 중에서 자주 사용되지 않는 데이터 페이지 위주로 공유 캐시에서 제거하는 작업을 수행합니다.

이 과정에서 캐시 스캔을 많이 수행하게 되며, 캐시가 자주 접근되는 비율과 관계없이 가능하면 B-Tree의 브랜치 노드에 캐시에 남겨 두려고 하는 경향이 있습니다.



WiredTiger 스토리지 엔진의 백그라운드 이빅션 쓰레드가 적절히 공유 캐시의 여유 공간을 확보하지 못하면, WiredTiger 스토리지 엔진에서는 사용자의 쿼리를 처리하는 포그라운드 쓰레드에서 직접 캐시 이빅션을 실행하게 됩니다. 이런 상황이 되면 쿼리 처리를 해야하는 쓰레드들이 캐시 이빅션까지 처리 해야 하기 때문에 처리 성능은 현저히 떨어지게 됩니다.


WiredTiger 스토리지 엔진의 이빅션 모듈을 튜닝할 수 있는 파라미터로는 다음과 같은 것이 있습니다.

• threads_max : 이빅션 쓰레드를 최대 몇 개까지 사용할지를 설정(1~20)
• threads_min : 이빅션 쓰레드를 최소 몇개 부터 사용할지를 설정(1~20)
• eviction_dirty_target : 공유 캐시의 더티 페이지의 비율로 설정한 비율이 넘지 않도록 유지(기본 80)
• eviction_target : 공유 캐시에서 데이터 페이지의 비율(전체 캐시 대비 사용률, 기본 80)
• eviction_trigger :  전체 공유 캐시 크기 대비 데이터 페이지의 사용률이 eviction_trigger 를 넘어서면 사용자 쓰레드가 이빅션을 처리함

위의 옵션 값도 표준 설정은 없으며 configString 옵션에 명시하여 사용을 합니다.
               

체크포인트(Checkpoint)

WiredTiger 스토리지 엔진도 다른 RDBMS 와 유사하게 사용자의 요청을 빠르게 처리하면서 데이터 영속성을 보장하기 위해서 선 로그 기법(트랜잭션 로그, WAL - 저널 로그) 를 사용합니다. 그 다음에 데이터 파일에 기록하는 작업은 사용자의 트랜잭션과 관계없이 뒤로 미뤄서 배치 형태로 내려써지게 됩니다.

이런 유형의 데이터 파일 기록 방식에서는 대부분 체크포인트(Checkpoint) 라는 개념을 가지고 있으며, 체크포인트는 데이터 파일과 트랜잭션 로그가 동기화되는 시점을 의미합니다.

체크포인트는 주기적으로 실행되며, 체크포인트가 실행되어야만 오래된 트랜잭션 로그를 삭제하거나 새로운 트랜잭션 로그로 덮어쓸 수 있습니다.


체크포인트는 DBMS서버가 크래시(Crash) 되거나 서버(OS) 자체자 재시작 되는 등의 장애에 의해서 비정상 종료되었다가 다시 시작할 때 복구할 시점을 결정하는 기준이 되게 됩니다.

체크포인트의 간격이 길다면 복구 시간이 길어지게 되고, 짧다면(빈번하다면) 내려쓰기 작업이 빈번하게 됨에 따라서 쓰기 IO부하 증가와 DBMS 쿼리 처리 성능이 떨어지게 됩니다.



오라클(Oracle) 이나 MySQL 의 InnoDB 스토리지 엔진은 퍼지(Fuzzy) 체크포인트 방식을 사용합니다. 퍼지 체크포인트는 강제적으로 데이터 파일과 트랜잭션 로그를 최근 시점의 트랜잭션 시점으로 동기화 하는 것이 아닌, 조금 오래전 시점에 발생한 트랜잭션을 체크포인트 기준점으로 선택하는 방식을 말합니다.

이런 퍼지 체크포인트 방식은 문제가 발생되었을 경우 복구 시간이 조금 더 길어지지만, 체크포인트 시점에 과다한 디스크 쓰기 부하를 피하거나 줄일 수 있습니다. 


WiredTiger 스토리지 엔진의 체크포인트 방식은 샤프 체크포인트(Sharp Checkpoint) 방식을 채택하여 사용하고 있습니다.

샤프 체크포인트 방식은 평상시에는 디스크 쓰기가 많지 않지만, 체크포인트가 실행되는 시점에 한 번에 더티 페이지를 모아서 내려쓰는 패턴을 보이게 됩니다.


MySQL 의 InnoDB 스토리지 엔진은 체크포인트와 무관하게 초 단위로(또는 10초 단위로) 일정하게 디스크에 내려쓰기를 실행하고, WiredTiger 스토리지 엔진은 평상시에는 쓰기를 거의 하지 않지만 체크포인트가 발생되면 해당 시점에 상당히 많은 쓰기가 순간적으로 발생하게 됩니다.

이렇게 순간적으로 많은 쓰기가 발생하면 동일 시점에 디스크의 데이터 파일에 데이터 페이지를 읽어야 하는 쿼리들도 같이 처리가 지연되고 이로 인해서 시스템 부하가 높아질수도 있습니다.


MongoDB의 WiredTiger 의 체크포인트 과정은 일반적인 형태와는 다르며, 기존의 B-Tree 구조를 덮어쓰지 않습니다. 체크포인트가 이런 과정을 거치므로 WiredTiger 스토리지 엔진의 데이터 파일은 어떤 상태로 DB가 크래시 되더라도 트랜잭션 로그의 복구 과정 없이 마지막 체크포인트의 데이터 상태를 유지할 수 있게 됩니다. 


WiredTiger 의 이런 체크포인트의 단점으로는 데이터 파일의 공간 활용 면에서 조금은 불리한 부분도 있긴 합니다.

B-Tree 의 모든 페이지가 변경되었다고 가정한다면 WiredTiger 스토리지 엔진은 체크포인트 시점에 모든 B-Tree 노드를 디스크 데이터 파일에 모두 기록해서 완전히 새로운 B-Tree 를 만들게 됩니다. 그래서 최악의 경우 체크포인트가 실행되면 기존 데이터 파일의 2배가 될 수도 있습니다.

이는 공간적인 측면에서의 불리한 점이며, WiredTiger 스토리지 엔진의 체크포인트 방식으로 인해서 더 많은 디스크 IO가 발생하는 것은 아닙니다.


WiredTiger 스토리지 엔진의 체크포인트를 제어할 수 있는 옵션은 다음과 같은 옵션을 제공합니다.

  • log_size : WiredTiger 스토리지 엔진이 얼마나 자주 체크포인트를 실행할 것인지를 결정하는 옵션으로 log_size 만큼 트랜잭션 로그 쓰기가 발생하면 체크포인트가 실행하게 됩니다.
    기본 값은 0 으로 WiredTiger 스토리지 엔진이 체크포인트 시점을 결정하도록 합니다.
  • wait : WiredTiger 스토리지 엔진이 지정된 시간 동안 대기하였다가 주기적으로 체크포인트를 실행하도록 설정 합니다. 실행할 주기는 초 단위로 설정, 기본 값은 0 으로 WiredTiger 스토리지 엔진이 체크포인트 시점을 결정하도록 합니다.
  • name : 체크포인트 이름을 지정합니다. WiredTiger 스토리지 엔진을 별도로 응용프로그램에서 임베디드 해서 사용하는 경우 필요로 하는 옵션으로 MongoDB 서버의 스토리지 엔진으로 사용할 경우에는 설정하지 않는 것이 좋습니다.

         

체크포인트와 레플리카 셋

MongoDB 3.2 버전부터는 레플리카 셋의 데이터 일관성과 빠른 페일 오버(프라이머리 선출)를 위해서 세컨더리(투표권을 가진 세컨더리)의 데이터가 디스크에 영구적으로 보관되도록 강제화 하고 있습니다.

그런데 WiredTiger 스토리지 엔진의 저널 로그가 활성화되지 않았을 경우 데이터의 영구적인 보관을 위해서 변경된 데이터를 항상 파일에 동기화해야 합니다. 이를 위해서 3.2 버전부터 레플리카 셋에 투입된 세컨더리 멤버가 저널로그 없을 경우 계속 해서 체크포인트가 발생되게 됩니다.

그리고 인하여 계속된(끊임없이) 체크포인트가 발생되고, 그만큼 디스크 쓰기 IO 가 증가하게 됩니다.


MongoDB 3.2 버전 부터 저널 로그가 활성화되면 레플리카 셋에서 투표권을 가진 세컨더리 멤버는 체크포인트가 빈번하게 실행되는 대신에 저널 로그를 매우 빈번하게 디스크로 동기화하도록 변경되었습니다.

이는 MongoDB 서버의 설정파일에서 storage.journal.commitIntervalMs 옵션에 설정한 시간과 무관하게 매우 빈번하게 저널 로그가 디스크에 쓰기가 발생되게 됩니다.

• 이어지는 다음 글


해당 포스팅은 Real MongoDB  책의 많은 내용 중에서 일부분의 내용만 함축적으로 정리한 것으로 모든 내용 확인 및 이해를 위해서 직접 책을 통해 모든 내용을 확인하시는 것을 권해 드립니다.
                      

Reference

Reference Book
• Real MongoDB


Reference URL
• mongodb.com/what-size-wiredtiger
• mongodb.com/serverStatus/wiredtiger
• mongodb.com/mongodb-setting-storage.wiredTiger
• mongodb.com/4.0/deprecate-mmapv1
• mongodb.com/4.2/removed-mmapv1-storage-engine


연관된 다른 글

 

            

0
글에 대한 당신의 생각을 기다립니다. 댓글 의견 주세요!x