Last Updated on 4월 22, 2024 by Jade(정현호)
안녕하세요
이번 포스팅에서는 MongoDB 스토리지 중에서 WiredTiger 스토리지 엔진에 대한 내용으로 MVCC 동작과 데이터 블록(페이지) 그리고 압축에 대해서 확인해보려고 합니다.
포스팅은 Real MongoDB 책 내용을 정리한 것이 주요 내용이며, 추가적으로 MongoDB Document 의 내용이 포함되어 있습니다.
아래 글에서 이어지는 내용입니다.
Contents
MVCC
MVCC(Multi Version Concurrency Control) 은 하나의 레코드(MongoDB 에서는 도큐먼트)에 대해서 여러 개의 버전을 동시에 관리하면서 필요에 따라 적절한 버전을 사용할 수 있게 해주는 기술입니다.
아래 그림 1 은 디스크의 데이터 페이지가 WiredTiger 스토리지 엔진에서 공유 캐시에 적재된 초기 상태 모습을 이미지화 한 것입니다. 초기 상태에서는 아무런 데이터를 변경하지 않았기 때문에 스킵 리스트로 구현된 변경 이력(Change history) 은 비어 있는 상태 입니다.
[그림 1]
스킵 리스트에 대한 추가적인 정보는 이전 포스팅을 참조하시면 됩니다.
이런 상태에서 새로운 커넥션에서 트랜잭션 3 으로 새로운 트랜잭션을 시작하고 다음과 같이 UPDATE 명령으로 name 필드가 "tom" 인 도큐먼트를 변경하도록 하겠습니다.
db.users.update( {name:"tom"}, {$set: {score:90}} )
업데이트를 수행하면 MongoDB의 WiredTiger 스토리지 엔진은 디스크에서 읽어 온 데이터 페이지(Data Page Image) 의 도큐먼트를 직접 변경하지 않고 아래 그림 2와 같이 변경 이력에 새로운 데이터를 기록을 합니다. 이때 트랜잭션 아이디 3에 의해서 변경되었다고 하는 정보도 아래 이미지와 같이 기록하게 됩니다.
[그림 2]
WiredTiger 스토리지 엔진은 메모리 상에서 도큐먼트의 변경 이력을 저장하기 위해서 스킵 리스트를 사용하며, 최근의 변경은 스킵 리스트의 앞쪽으로 정렬하여 최근의 데이터를 더 빠르게 검색할 수 있도록 구현되어 있습니다.
그림 2와 같이 WiredTiger 스토리지 엔진은 디스크 데이터를 직접 덮어쓰지 않고 변경된 데이터에 대한 정보를 리스트로 관리하면서 각각의 도큐먼트에 대해서 여러 개의 버전이 관리도 되도록 유지를 하게 됩니다.
그런데 이런 식으로 새로운 버전을 계속 리스트에 추가가 된다면 메모리 사용량이 많아지는, 즉 상당히 많은 메모리가 추가로 필요하게 될 것입니다.
WiredTiger 스토리지 엔진은 변경 이력이 계속 늘어나게 되어서 memory_page_max 설정 값보다 큰 메모리를 사용하는 페이지가 있다면 해당 페이지를 디스크에 기록하는 작업(Eviction) 을 수행하게 됩니다. 이때 리컨실리에이션(Reconciliation) 과정을 거치면서 원래의 데이터 페이지 내용과 변경된 내용이 병합되어 디스크에 기록이 됩니다.
데이터가 너무 큰 경우에 2개 이상의 페이지로 나뉘어져서 디스크에 기록될 수도 있습니다.
memory_page_max 는 현재 기본값이 10MB 입니다.
다음으로 데이터가 변경되는 과정에서 다른 사용자가 조회 쿼리를 실행하였을 경우를 살펴본다면 중요한 것은 트랜잭션 아이디가 되게 됩니다.
쿼리를 실행하는 트랜잭션 번호 보다는 낮은 트랜잭션이 변경한 마지막 데이터만 볼 수 있으며, 자신의 트랜잭션 번호보다 높은 변경은 트랜잭션이 시작한 이후의 변경 즉, 미래의 데이터가 되게 되는 것임으로 보이지 않게 됩니다.
부등호 표시로 표현을 한다면 조회가 가능한 시점의 데이터는 아래와 같이 표현할 수 있을 것입니다.
이전번호 TRX <=자신의 TRX_ID < 다음 번호 TRX
이러한 격리 레벨은 MySQL의 기본 격리 레벨인 REPEATABLE-READ 와 동일한 형태로 처리가 되는 방식입니다.
WiredTIger 스토리지 엔진은 READ_UNCOMMITTED 와 READ_COMMITED 그리고 SNAPSHOT 이라는 3가지 격리 수준을 지원합니다.
SNAPSHOT 을 격리 수준 기본값으로 사용하고 있으며 이 격리 수준은 MySQL 의 REPEATABLE-READ 와 동일한 격리수준입니다.
MongoDB 에서 사용 가능한 격리 수준은 사실상 SNAPSHOT이며, 그 외 읽기와 쓰기 관련된 처리에 관해서는 Read Concern , Transactions and Write Concern 이 관련되어 있습니다.
Write & Read Concern에 관련된 내용은 다음 포스팅을 참조하시면 됩니다.
트랜잭션 및 Isolation Level 에 대한 더 자세한 내용은 다음의 포스팅을 참조하시면 됩니다.
데이터 블록(페이지)
WiredTiger 스토리지 엔진은 데이터를 저장하기 위해서 고정된 크기의 블록(페이지)을 사용하지는 않습니다. 하지만 하나의 페이지가 너무 커지는 것을 막기 위해서 하나의 페이지에 대해서는 최대 크기를 제한은 하고 있습니다.
일반적인 다른 DBMS 와 달리 MongoDB의 WiredTiger 스토리지 엔진은 B-Tree 의 인터널 노드(브랜치 노드)와 리프 노드의 페이지 크기를 다르게 설정할 수 있는 이유로 인터널 노드(브랜치 노드)는 리프 노드를 구분하는 인덱스 키만 가지고 있기 때문에 저장되는 데이터가 그다지 많지 않아도 되기 때문입니다.
그래서 MongoDB WiredTiger 스토리지 엔진의 자체적인 기본값은 인터널 페이지의 최대 크기는 4KB 이며, 리프 페이지의 최대 크기는 32KB 입니다.
WiredTiger 스토리지 엔진에서 하나 더 독특한 점은 컬렉션의 데이터 페이지 크기와 인덱스의 페이지 크기를 다르게 설정할 수 있다는 것입니다. 사실 WiredTiger 스토리지 엔진에서 이렇게 용도별로 페이지의 크기를 다양화 할수 있는 것은 WiredTiger 스토리지 엔진이 내부적으로는 고정된 크기 페이지 사이즈를 사용하지 않기 때문입니다.
WiredTiger 스토리지 엔진의 컬렉션과 인덱스에 대해서 페이지 크기를 변경하려면 설정 파일에서 collectionConfig 와 indexConfig 옵션에서 configString 으로 변경하면 됩니다.
인덱스와 컬렉션의 페이지 사이즈를 조절할 경우에는 애플리케이션이 사용하는 데이터 패턴 확인이 필요 합니다. 만약 대량의 INSERT 와 분석이나 배치 작업을 위한 대량의 조회가 주요 접근 패턴이라면 가능하면 페이지의 크기를 크게 가져가는 것이 좋을 것이며, 소규모의 도큐먼트를 아주 빈번하게 그리고 랜덤하게 데이터를 읽고 변경한다면 페이지의 크기를 작게 설정하는 것이 좋을 수 있습니다.
압축
MongoDB의 WiredTiger 스토리지 엔진의 또 다른 장점 중 하나는 압축 기능입니다.
WiredTiger 스토리지 엔진이 다른 RDBMS 와 비교하였을 때 장점은 크게 3가지가 있습니다.
• 가변 사이즈의 페이지 사용
• 데이터 입출력 레이어에서의 압축 지원
• 다양한 압축 알고리즘 지원
WiredTiger 스토리지 엔진은 가변 크기의 페이지를 사용함으로 압축으로 인해서 줄어든 만큼의 공간을 절약할 수 있게 됩니다.
많이 사용되는 MySQL InnoDB 스토리지 엔진은 고정 크기의 페이지를 사용하며, 데이터 페이지를 압축할 때에도 고정된 크기의 페이지만 사용하게 됩니다. 그래서 InnoDB 스토리지 엔진에서는 16KB 의 페이지를 압축하는 경우 압축된 페이지의 크기가 4KB 또는 8KB 크기 이하로 되어야 합니다. 그렇지 않을 경우 16KB 페이지를 2개의 페이지로 나눈 후 다시 압축하는 과정을 거치게 됩니다.
이러한 과정 처리는 오버헤드를 발생시키고 이런 처리 지연을 회피하기 위해 일부러 16KB 페이지를 비워(padding) 두는 전략까지 사용하고 있습니다.
하지만 WiredTiger 스토리지 엔진은 16KB 페이즈이 데이터를 압축하였을 경우, 4KB 또는 8KB 이하의 사이즈가 아니더라도 상관없이 굳이 페이지를 다시 스플릿해서 다시 압축하는 과정을 거칠 필요가 없습니다. 모든 페이지가 가변 사이즈 이기 때문에 압축 결과 페이지를 그대로 디스크에 기록을 하면 되기 때문입니다.
WiredTiger 스토리지 엔진의 압축이 가지는 두번째 장점은 아래 그림 3 과 같이 WiredTiger 스토리지 엔진이 데이터를 읽고 쓰는 시점에서 압축과 해제가 진행이 되게 됩니다.
[그림 3]
조금 더 자세하게 설명하면 WiredTiger 캐시와 파일 시스템 캐시 두가지 캐시를 사용할 수 있으며, 위의 설명 처럼 WiredTiger 의 캐시에는 압축이 해제된 데이터 페이지가 저장되어 메모리와 유사한 성능을 제공하며, 운영 체제의 파일 시스템 캐시는 압축된 형태로 저장하게 됩니다. MongoDB 에 쿼리가 수행되면서 WiredTiger 캐시에서 데이터 페이지를 찾을 수 없을 경우 파일시스템 캐시에서 데이터 페이지를 찾게 되고, 찾은 데이터 페이지는 WiredTiger 캐시로 이동하기 전에 먼저 압축 해제 프로세스를 거치게 됩니다.
MySQL 서버를 포함하여 일부의 RDBMS 에서는 공유 캐시에 압축된 상태와 압축이 해제된 상태 2개의 데이터 페이지(블럭)가 공존하게 사용되는 형태가 있으며 이럴 경우 공유 캐시를 참조하는 경우라도 압축 해제와 압축 작업이 계속 반복되어 수행될 수 있습니다. 또한 이렇게 2가지의 페이지가 같이 공존하다 보니 공유 캐시의 메모리 효율도 떨어지게 됩니다.
MongoDB WiredTiger 스토리지 엔진에서 블록 매니저(Block Manager) 가 디스크의 데이터 페이지를 읽어 오면서 압축을 해제한 상태로만 공유 캐시에 적재합니다.
그리고 공유 캐시에서 디스크로 기록되는 페이지에 대해서만 압축을 실행해서 저장을 합니다.
WiredTiger 스토리지 엔진의 압축이 가지는 세번째 장점으로 다양한 형태의 압축 기능이 제공된다는 점입니다.
WiredTiger 스토리지 엔진은 네 가지 형태의 압축 기능을 제공하고 있습니다.
• 블록 압축(Block Compression)
• 인덱스 프리픽스 압축(Key Pair Compression)
• 사전 압축(Dictionary Compression)
• 허프만 인코딩(Huffman Encoding)
현재 MongoDB에 WiredTiger 스토리지 엔진에서 사용할 수 있는 압축 형태는 블록 압축과 인덱스 프리픽스 압축입니다.
블록 압축은 일반적인 압축 기능으로, 데이터가 저장된 페이지(블록) 단위로 압축을 실행하는 방식을 의미합니다.
블록 압축은 다양한 압축 알고리즘을 지원하며, 버전 4.2 기준으로 MongoDB 에 내장된 WiredTiger 에서 사용할 수 있는 알고리즘은 zlib, snappy, zstd(Zstandard) 를 사용할 수 있습니다.
zstd는 2016년쯤 페이스북에서 개발한 차세대 압축 알고리즘으로 무손실 데이터 압축 형태로 높은 압축률에 비해 압축속도와 해제속도가 준수한 편이며 MongoDB 4.2 버전 부터 사용가능 합니다.
WiredTiger 스토리지 엔진 자체에서는 2-3년 전에 LZ4 알고리즘도 추가가 되었지만, MongoDB에 내장된 WiredTiger 에서는 사용할 수 없습니다. LZ4 와 zstd 둘중 하나가 채용된 것으로 보이며, 결론적으로 zstd 가 선택된 것으로 확인 됩니다.(관련된 Jira Ticket)
압축의 효율이 중요할 경우에는 주로 zlib 알고리즘을 선택하게 할수도 있겠지만, zlib 사용에 있어서 문제점은 압축 효율은 좋지만 너무 성능이 떨어진다는 점입니다. 그런데 최근에 추가된 zstd 알고리즘이 zlib 압축 알고리즘의 대안이 되고 있습니다.
WiredTiger 스토리지 엔진에서는 압축 알고리즘뿐만 아니라 컬렉션과 인덱스 그리고 저널 로그에 대해서 각각 압축을 적용할 것 인지 결정할 수 있습니다.
....... journalCompressor: snappy collectionConfig: blockCompressor: snappy indexConfig: prefixCompression : true
일반적으로 MongoDB의 컬렉션에 저장되는 도큐먼트는 키와 값이 쌍으로 저장되기 때문에 하나의 페이지에서 반복되는 키 값이 상당히 많이 포함되어 있습니다. 그래서 MongoDB 에서는 압축이 필수적인 요소 중 하나입니다.
MongoDB 인덱스는 키와 값의 쌍으로 저장 되어있지 않고, 일반적인 RDBMS와 동일한 형태의 인덱스 키 엔트리로 인덱스가 구성되어 있습니다. 그 말은 인덱스에서는 큰 효과가 없을 수도 있다는 것을 의미하며 그래서 컬렉션의 데이터 파일에는 압축이 적용되지만, 인덱스 파일에는 압축이 적용되지 않습니다.
인덱스 파일에 압축이 적용되지 않는 대신에 인덱스 프리픽스 압축 기능을 제공하고 있습니다. 해당 기능은 이름(명칭) 그대로 인덱스 키에서 왼쪽 부분의 중복 영역을 생략하는 압축 방식을 의미합니다.
가령 키로 advice 와 advise 가 있을 경우 인덱스 프리픽스가 적용된다면 아래와 같이 중복된 영역을 생략할 수 있게 됩니다.
------ advice advise ---- to compression ------ advice *se ----
공통된 부분이 너무 짧을 때는 압축 효율이 없기 때문에 프리픽스 압축을 생략되는 경우도 있습니다.
프리픽스 압축은 인덱스에만 적용할 수 있으며 프리픽스 압축은 블록 압축과 달리 공유 캐시에서도 압축 상태를 유지합니다.
즉 디스크의 데이터 파일에서 페이지가 2KB 이라면 WiredTiger 의 공유 캐시에서도 2KB 메모리가 사용됩니다.
하지만 데이터를 읽을 때는 항상 완전한 키 값을 얻기 위해서 조립 과정을 거쳐야 합니다.
안 좋은 케이스 일 경우 페이지의 제일 첫번째에 있는 인덱스 키부터 재조립 과정을 거쳐야 할 수도 있습니다.
그래서 압축률이 좋을 수록 특정 인덱스 키를 읽는 속도가 떨어지는 형상이 발생할 수도 있습니다(재조합을 해야 하는 과정이 많아 질수 있으므로)
사전 압축(Dictionary Compression) 과 허프만 인코딩(Huffman Encoding)은 디스크와 메모리에서 압축된 형태로 사용이 됩니다. 그래서 디스크의 데이터 파일 용량에서도 메모리에서도 공간을 절약하는 효과를 얻을 수 있는 압축 방식입니다
하지만 MongoDB 에서는 아직 지원되지 않고 있습니다.
해당 포스팅은 Real MongoDB 책의 많은 내용 중에서 일부분의 내용만 함축적으로 정리한 것으로 모든 내용 확인 및 이해를 위해서 직접 책을 통해 모든 내용을 확인하시는 것을 권해 드립니다.
Reference
Reference URL
• mongodb.com/read-isolation-consistency-recency
• mongodb.com/pluggable-storage-engines-part-1
• mongodb.com/mongodb-network-compression
• jira.mongodb.org/SERVER-32595
Reference Book
• Real MongoDB
연관된 다른 글
Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io