WiredTiger 엔진
WiredTiger 스토리지 엔진은 트랜잭션을 지원하는 임베디드 데이터베이스 엔진으로 사용된다.
WiredTiger 스토리지 엔진은 내장된 캐시를 가지고 있다.
이 캐시는 디스크의 인덱스나 데이터 파일을 메모리에 캐시하여 빠르게 쿼리를 처리하고,
데이터 변경을 모아 한번에 디스크로 기록하는 쓰기 배치 기능을 가지고 있다.
사용자가 쿼리를 실행하면 WiredTiger 스토리지 엔진은 블록매니저(Block Manager)를 통해서 필요한 데이터 블록을 디스크에서 읽어서 공유 캐시에 적재하여 쿼리를 수행한다.
만약 사용자가 데이터를 변경하면 WiredTiger 스토리지 엔진은 트랜잭션을 시작하고 커서를 이용해 원하는 도큐먼트의 내용을 변경한다.
도큐먼트의 변경 내용은 먼저 공유 캐시에 적용되는데, 변경된 데이터가 디스크에 기록되는 과정을 기다리지 않고 저널 로그에 기록한 다음 처리 결과를 리턴한다.
이렇게 공유 캐시가 어느정도 쌓이면 체크포인트를 발생시켜 공유 캐시의 더티 페이지를 모아 디스크에 기록한다.
사용자 요청 쿼리가 실행되면서 블록매니저는 디스크의 새로운 데이터 페이지를 계속 캐시로 읽어드리는데
스토리지 엔진의 공유 캐시는 사용자가 설정한 크기의 메모리 내에서 처리가 수행되어야 한다.
만약 캐시에 더이상 데이터 페이지를 읽어들일 공간이 없으면 사용자 쿼리를 처리할 수 없으므로
Eviction 모듈은 공유 캐시가 적절한 메모리 사용량을 유지하도록 공유캐시에서 자주 사용되지 않은 데이터 페이지를 제거하는 작업을 수행한다.
WiredTiger 는 블록사이즈를 가변적으로 사용하는데 이는 압축 기능을 편리하게 만들어 준다.
다른 RDBMS 와 같은 고정사이즈인경우 만약 16KB 데이터 페이지를 압축하면 결과의 크기는 모두 가변적인데,
이를 다시 고정된 크기의 데이터 블록에 저장해야 하므로 오히려 압축 효율이 떨어질 수 있다.
그래서 가변 크기의 블록을 사용하는 WiredTiger 는 데이터 파일의 압축이 선택이 아니라 기본 옵션처럼 사용된다.
WiredTiger 스토리지 엔진의 블록매니저는 변경된 블록을 기록할 때, 프레그멘테이션을 최소화하면서 기록되는 데이터 블록의 크기에 최적인 위치를 찾아서 저장한다. 그 뿐만 아니라 데이터 블록의 압축과 암호화 등과 같은 기능을 내장한다.
저널 파일
다른 DBMS 와 동일하게 B-Tree 구조의 데이터 파일과 비정상 종료로부터 데이터를 복구하기 위한 WAL 을 가지고 있다.
그렇지만 다른 DBMS 처럼 WAL 이 로테이션되면서 재사용되지 않고 새로운 로그파일이 계속 사용된다.
그리고 체크포인트 시점 이전의 저널 로그는 더이상 사용되지 않으므로 자동으로 삭제한다.
스토리지 엔진은 3~10 개 정도의 로그 파일을 미리 만들어 두고, 기존 저널 로그 파일을 다 사용하면 미리 만들어 둔 로그 파일의 이름을 뒷 숫자로 변경하여 트랜잭션 로그를 기록한다.
$ ll
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000001
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000002
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000003
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000004
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000005
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000006
..
파일이 다 사용되면
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000001
->
-rw-r--r-- 1 root root 3005 2024-05-10 10:00 100M WiredTigerLog.000000007
저널파일 설정 방법
storage:
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 10
configString: "log=(archive=true,enabled=true,file_max=100MB,path=/MongoDB/journal/)"
collectionConfig: snappy
필드 | 값 |
enabled | 저널 로그를 활성화할 것인지 설정 |
archive | 체크포인트 이전의 저널 로그는 자동으로 삭제하는데, 이렇게 삭제된 저널 로그를 아카이빙하여 다른 용도로 사용하고자 하는 경우에는 archive 옵션을 true 로 설정 |
file_max | 저러 파일의 최대 크기를 설정 |
path | 저널 로그의 디렉터리 경로를 설정 |
공유캐시
WiredTiger 엔진에서 사용자의 쿼리는 공유 캐시를 거치지 않고 처리할 수 없으며,
때로는 하나의 쿼리를 처리하기 위해 공유 캐시의 데이터 페이지를 참조해야 할 수도 있다.
그래서 공유캐시 사용량이 매우 중요하고 이를 보여주는 명령어가 있다.
만약 WiredTiger 엔진처리가 원할하지 못한 경우 공유 캐시 사용량 그래프에 변화를 보이는 경우가 많다.
-- Total cache bytes
db.serverStatus().wiredTiger.cache."maximum bytes configured"
-- Useed cache bytes
db.serverStatus().wiredTiger.cache."bytes currently in the cache"
-- Dirty cache bytes
db.serverStatus().wiredTiger.cache."tracked dirty bytes in the cache"
3.2 버전 내장된 WiredTiger 엔진의 공유 캐시 크기는 장착된 메모리의 60%-1GB 이며, 만약 이 값이 1GB 보다 적으면 1GB 로 설정된다.
3.4 버전부터는 메모리의 50%-1GB 이며, 만약 이 값이 256MB 보다 작다면 256MB 로 설정된다.
만약 공유 캐시의 크기를 변경하고자 한다면 다음과 같이 설정하면 된다.
재시작은 필요하지 않지만, 공유 캐시 크기 조정 시 많은 내부 동시 처리 작업을 멈추고 크기를 변경하므로 부하가 작을 때 해야한다.
wiredTiger:
engineConfig:
cacheSizeGB: 20
WiredTiger 엔진은 디스크의 데이터 페이지를 공유 캐시 메모리에 적재하면서 메모리에 적합한 트리 형태로 재구성한다.
공유 캐시에서 적재된 페이지를 찾아가는 과정은 모두 별도의 맵핑 과정 없이 메모리 주소(Pointer)를 이용해서 바로 검색할 수 있기 때문에 맵핑 테이블의 경합이나 오버헤드가 없다.
WiredTiger 스토리지 엔진은 디스크에 저장되는 데이터 페이지의 레코드 인덱스를 별도로 관리하지 않고 저장한다.
그리고 데이터 페이지를 공유캐시 메모리에 적재할 때 레코드 인덱스를 새롭게 생성해서 메모리에 적재한다.
이렇게 메모리로 적재하는 과정에서 WiredTiger 엔진은 여러 가지 변환 과정을 거치기 때문에 데이터 페이지를 디스크에서 공유캐시로 읽어 들이는 과정이 기준 RDBMS 보다는 느리게 처리된다.
그렇지만 한번 공유 캐시 메모리에 적재된 데이터 페이지에서 필요한 레코드를 검색하고 변경하는 작업은 기존의 RDBMS 보다 훨씬 빠르고 효율적으로 작동한다.
잠금경합 알고리즘 Lock-Free
WiredTiger 엔진은 공유 캐시의 잠금경합을 최소화하기 위해서 Lock-Free 알고리즘을 채용한다.
Lock-Free 알고리즘은 잠금을 전혀 사용하지 않는 시스템을 의미하는 것이 아니라 잠금 경합을 최소화하는 알고리즘을 의미하는데,
대표적으로 '하자드 포인터'와 '스킵 리스트' 자료구조를 활용하여 Lock-Free 콘셉트를 구현하고 있다.
하자드 포인터
사용자 쓰레드는 사용자의 쿼리를 처리하기 위해 WiredTiger 캐시를 참조하는 스레드이다.
이빅션 쓰레드는 캐시가 다른 데이터페이지를 읽어들일 수 있도록 빈 공간을 만들어주는 역할을 담당하는 스레드이다.
사용자 쓰레드는 캐시의 데이터 페이지를 참조할 때 먼저 하자드 포인터에 자신이 참조하는 페이지를 등록한다.
사용자 쓰레드가 쿼리를 처리하는 동시에 이빅션 쓰레드는 캐시에서 제거해야 할 데이터 페이지를 골라서 캐시에서 삭제하는 작업을 진행한다.
이 때 이빅션 쓰레드는 제거해도 될만한 페이지를 골라서 먼저 하자드포인트에 등록되 있는지 확인 후 없으면 캐시에서 제거한다.
이런 기술덕분에 다양한 쓰레드들이 캐시의 데이터 페이지들에 대해서 잠금 대기 없이 수행된다.
하자드 포인터의 최대 개수는 1000 개이다.
만약 포인터의 개수가 부족하여 엔진의 처리량이 느려진다면 hazard_max 의 설정값을 변경한다.
스킵 리스트
스킵 리스트는 링크드 리스트를 변환한 리스트 방식이다.
예를들어 8번의 노드 검색을 거쳐야 하는 작업을 스킵리스트를 하면 4번만 거칠 수 있다.
이는 검색 성능이 단순 링크드인경우 O(N) 인 반면 스킵리스트는 O(log(n)) 이다.
스킵 리스트는 B-Tree 에 비해 검색 성능이 조금 떨어지지만 구현이 간단하고 메모리 공간도 많이 필요하지 않는다.
그뿐만 아니라 새로운 노드를 추가하기 위해서 별도의 잠금을 필요로 하지 않고 검색 또한 잠금을 필요로 하지 않는다.
스킵 리스트의 노드 삭제는 잠금을 필요로 하지만 B-Tree 자료 구조보다는 잠금을 덜 필요로 하기에 큰 성능 저하 이슈도 아니다.
여러 쓰레드가 동시에 하나의 스킵 리스트에 노드를 저장하거나 검색해도 서로 잠금 경합을 하지 않는다.
WiredTiger 의 언두관리
WiredTiger 도 변경되기 전 레코드를 별도의 저장 공간(언두 로그, Undo Log)에 관리한다.
이렇게 언두 로그를 관리하는 이유는 트랜잭션이 롤백될 때 기존 데이터를 복구하기 위함인데,
많은 RDBMS 에서는 언드 로그를 잠금 없는 데이터 읽기(MVCC) 용도로도 같이 사용한다.
WiredTiger 는 언두 로그를 스킵 리스트로 관리하는데,
조금 독특하게 데이터페이지의 레코드를 직접 변경하지 않고 변경 이후의 데이터를 스킵 리스트에 추가한다.
WiredTiger 는 데이터가 변경되어도 디스크에서 읽어 들인 데이터 페이지에 변경된 내용을 직접 기록하지 않고
변경된 데이터 내용을 스킵 리스트에 차곡차곡 기록해 둔다.
그리고 사용자 쿼리가 데이터를 읽을 때에는 변경 이력이 저장된 스킵 리스트를 검색해서 원하는 시점의 데이터를 가져간다.
이렇게 직접 데이터페이지에 쓰지 않고 별도의 리스트로 관리하는 이유는 쓰기 처리를 빠르게 하기 위해서이다.
기존 RDBMS 에서 데이터가 변경되면 기존의 레코드보다 데이터의 크기가 더 커져서 데이터 페이지 내에서 레코드의 위치를 옮겨야 할 수 있다. 이런 과정을 처리하는 동안 사용자가 기다려야 한다.
WiredTiger 스토리지 엔진에서는 변경되는 내용을 스킵 리스트에 추가하기만 하면 된다.
또한 추가하는 작업은 매우 빠르게 처리되므로 사용자의 응답 시간도 훨씬 빨라지고 여러 쓰레드가 하나의 페이지를 동시에 읽거나 쓸 수 있어 동시성능 처리가 향상된다.
캐시 이빅션
공유 캐시를 위해서 지정된 크기의 메모리 공간만 사용해야 하는데, 이를 위해서 빈 공간을 적절하게 유지해야 한다.
그렇지 않으면 데이터 페이지를 디스크에서 가져오지 못하기 때문에 쿼리의 응답속도가 떨어진다.
이빅션은 백그라운드 쓰레드로 실행되는데, 캐시에서 자주 사용되지 않는 데이터 페이지 위주로 캐시에서 제거하는 작업을 수행한다.
그러나 최근 SSD 와 같이 매우 빠르게 읽고 쓸수있는 저장장치가 나오면서, 한번에 읽어들일 수 있는 데이터 페이지 수가 많아졌다.
그만큼 빠르게 캐시에서 데이터페이지가 제거되어야 하는데, 가끔 이빅션이 지우는 속도가 캐시로 읽어들이는 속도를 따라가지 못해 캐시의 사용량이 급증하는 경우도 있다.
이 현상은 3.2 버전대에서 자주 발생했는데 3.4 버전 이후로는 많이 개선되었다.
이렇게 백그라운드에서 수행되는 이빅션 쓰레드가 캐시 여유공간을 만들어 내지 못하면
포그라운드 쓰레드에서 직접 캐시 이빅션을 실행한다.
그러나 쿼리를 처리해야 할 쓰레드들이 캐시 이빅션까지 처리해야 하기 때문에 쿼리 성능이 현저하게 떨어진다.
-- Evicted by Application Thread
db.serverStatus().wiredTiger.cache."pages evicted by application threads"
-- Evicted by Worker Thread
db.serverStatus().wiredTiger.cache."eviction worker thread evicting pages"
만약 정상적인 상황이라면 By App 의 수치는 0에 가깝고 By Worker 그래프의 수치만 보여야 한다.
이빅션 모듈의 작동 방식을 튜닝할 수 있는 옵션이다.
값 | 설명 |
threads_max | 데이터 페이지를 제거하는 이빅션 쓰레드를 최대 몇 개까지 사용할지 설정. 1~20개까지 가능 |
threads_min | 데이터 페이지를 제거하는 이빅션 쓰레드를 최소 몇 개부터 사용할지 설정. 1~20개까지 가능 처음에는 min 값으로 시작되고 더 빨리 제거가 필요한 경우 max 까지 증가함 |
eviction_dirty_target | 더티 페이지의 비율이 설정한 비율을 넘지 않도록 유지. 기본값은 80% |
eviction_target | 공유 캐시에서 데이터페이지의 비율이 eviction_dirty_target 에 설정한 비율을 넘지 않도록 유지 ( * 더티페이지가 아님 ) |
eviction_trigger | 전체 공유 캐시 크기 대비 데이터 페이지의 사용률이 eviction_trigger 을 넘어서면 사용자 쓰레드의 이빅션을 시작 이빅션 쓰레드(백그라운드)의 페이지 제거작업은 무관하게 항상 작동 |
storage:
engine:wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 10
configString: "eviction=(threads_max=10,threads_min=1),eviction_dirty_target=80)
체크포인트
커밋된 트랜잭션의 영속성을 보장하기 위해 트랜잭션 로그를 먼저 기록하고, 데이터파일에 기록하는 작업은 사용자의 트랜잭션과 관계없이 뒤로 미뤄서 처리한다.
체크포인트는 데이터 파일과 트랜잭션 로그가 동기화되는 시점을 의미하고
체크포인트는 주기적으로 실행되는데 체크포인트가 실행되어야만 오래된 트랜잭션 로그를 삭제하거나 새로운 트랜잭션 로그로 덮어쓸 수 있게된다.
체크포인트는 DBMS 서버가 크래시되거나 응답 불능으로 인해 비정상 종료 후 재시작될 때 복구를 시작할 시점을 결정해준다.
그래서 체크포인트의 간격이 너무 길면 복구시간이 길어지게 되고, 너무 빈번하게 발생하면 서버가 쿼리를 처리하는 능력이 떨어진다.
RDBMS 는 Fuzzy 체크포인트 방식을 사용한다.
Fuzzy 체크포인트는 조금 오래전 시점에 발생했던 트랜잭션을 체크포인트 기준점으로 선택하는 방식을 말한다.
이런 방식은 문제발생시 복구시간이 길어지지만, 체크포인트 시점에 과다한 디스크 쓰기를 피할 수 있다.
그렇지만 WiredTiger 스토리지 엔진은 '샤프 체크포인트' 방식을 채택하고 있다.
샤크 체크포인트는 평상시에는 디스크 쓰기가 별로 많지 않지만 체크포인트가 실행되는 시점에 한번에 모아서 더티 페이지를 기록하는 패턴을 보인다.
그렇기에 한번씩 디스크가 튀는 패턴을 그리는데 아직 체크포인트 시점에 쓰기 양을 제어할 수 있는 옵션은 제공되고 있지 않다. (MongoDB 7버전에서도?)
체크포인트 과정
WiredTiger 엔진의 체크포인트는 RDBMS 와는 조금 다르게 실행된다.
중간브랜치 노드와 리프노드의 데이터가 변경되었고, 특정 중간브랜치 노드 하위에 새로운 페이지가 추가되었다고 한다.
이 때 체크포인트가 발생하면 리프노드만 먼저 기록한다.
새로 생성된 페이지는 데이터 파일 내에 새로운 페이지 공간을 할당받아서 기록한다.
그런데 기존에 있던 페이지 내용이 변경된 경우에도 기존의 데이터페이지를 덮어쓰지 않고 새로운 페이지 공간을 할당받아 저장한다.
이렇게 리프 페이지의 데이터 파일의 저장이 완료되면 변경된 B-Tree 의 브랜치 노드를 데이터 파일에 기록한다.
변경되지 않은 리프노드 페이지는 새로 생성된 브랜치 노드에서 바라보도록 한다.
이렇게 브랜치 노드가 모두 디스크에 기록되면 마지막으로 새로 만들어진 루트노드를 디스크에 기록한다.
이 작업이 완료되면 최종적으로 하나의 컬렉션에 대해 두 개의 B-Tree 가 남게된다.
모든 데이터의 디스크 기록이 완료되면 엔진은 메타 데이터가 새로운 루트 노드를 가리키도록 변경한다.
메타 정보가 가리키는 루트노드가 변경되면 이때부터 모든 사용자가 새로운 B-Tree 에서 데이터를 조회한다.
이제 쓸모없어진 기존 B-Tree 는 삭제하고 빈 공간으로 반납된다.
다만 이 때 새로생성된 B-Tree 에서 바라보는 리프노드들은 유지된다.
이런 과정으로 기존 B-Tree 를 덮어쓰지 않기 때문에 어떤 상태로 크래시되더라도 트랜잭션 로그의 복구 과정 없이 마지막 체크포인트의 데이터 상태를 유지할 수 있다.
물론 공간 활용에서 보면 조금 불리한 부분도 있다. 모든 페이지가 변경되었다고 가정하면 완전히 새로운 B-Tree 를 만들어야 하기 때문에 기존 데이터 * 2배가 될 수도 있다.
체크포인트 제어 옵션
값 | 설명 |
log_size | 얼마나 자주 체크포인트를 실행할지 결정하는 값으로 설정값만큼 트랜잭션 로그 쓰기가 발생하면 체크포인트가 발생 기본값은 0으로 엔진이 체크포인트 시점을 결정하도록 함 |
wait | 설정된 시간초동안 대기했다가 주기적으로 체크포인트를 실행 기본값은 0으로 엔진이 체크포인트 시점을 결정하도록 함 |
name | 체크포인트 이름을 설정 |
storage:
engine:wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 10
configString: "checkpoint=(log_size=2G,wait=60)"
트랜잭션 로그 사이즈가 2GB 를 넘거나 체크포인 시점으로부터 60초가 넘으면 체크포인트가 실행된다.
복제노드의 체크포인트
MongoDB 3.2 버전부터 레클리카 셋의 데이터 일관성과 빠른 페일오버를 위해 세컨드리의 데이터가 디스크에 영구적으로 보관되도록 강제하고 있다.
그런데 WriedTiger 스토리지 엔진의 저널 로그가 활성화되지 않은 경우에는 데이터의 영구적인 보관을 위해 변경된 데이터를 항상 파일에 동기화해두어야 한다.
그래서 센컨드리 멤버가 저널 로그가 없는 경우에는 계속해서 체크포인트를 실행하도록 작동한다.
그로인해 체크포인트가 끊임없이 발생하고 디스크 쓰기가 계속해서 많이 발생할 수 있다.
만약 writeConcern 이 {w:2, j:true} 이상으로 설정되면 복제의 각 멤버는 저널로그가 없기 때문에 동기화를 달성할 수 없어 오류가 발생하는데 이 에러를 막기 위해 writeConcernMajorityJournalDefault 옵션을 false 로 설정한다.
복제 노드는 매우 빈번한 체크포인트가 수행되므로 storage.journal.commitIntervalMs 옵션에 설정된 시간과 무관하게 매우 빈번한 디스크 쓰기가 발생한다.
MVCC
MVCC(Multi Version Concurrency Control) 는 하나의 도큐먼트에 대해 여러 개의 버전을 동시에 관리하면서 필요에 따라 적절한 버전을 사용할 수 있게 해주는 기술이다.
데이터가 변경되면 변경 된 데이터는 '스킵 리스트' 에 저장된다.
최근의 변경은 스킵 리스트의 앞쪽으로 정렬하여 최근의 데이터를 더 빠르게 검색할 수 있도록 유지한다.
이렇게 새로운 버전을 데이터를 계속 리스트에 추가하기만 하면 상당히 많은 메모리가 추가로 필요하다.
엔진은 변경 이력이 늘어나 memory_page_max 설정값보다 큰 메모리를 사용하는 페이지를 찾아서 자동으로 디스크에 기록하는 작업을 수행한다. 이 때 리컨실리에이션(Reconciliation) 과정을 거치며 원래의 데이터 페이지 내용과 변경된 내용이 병합되어 디스크에 기록된다.
만약 몇 개의 트랜잭션 ID 로 인해 하나의 데이터가 여러번 변경된 경우,
조회해야 하는 트랜잭션 ID 가 변경될 때의 트랜잭션 ID 사이의 값을 조회하게 된다.
반드시 검색을 실행하는 커넥션은 자신의 트랜잭션 번호보다 낮은 트랜잭션이 변경한 마지막 데이터만 볼 수 있다.
이는 REPEATABLE-READ 격리 수준과 동일한 처리 방법이다.
WiredTiger 엔진은 READ_UNCOMMITTED, READ_COMMITTED, SNAPSHOT 3가지 격리 수준을 제공하는데
기본 격리엔진 수준이 SNAPSHOT 이며 REPEATABLE-READ 와 동일한 격리 수준을 가진다.
데이터 블록
WiredTiger 는 데이터를 저장하기 위해 고정된 크기의 블록을 사용하지 않는다.
하지만 하나의 페이지가 너무 커지는 것을 방지하기 위해 최대 크기 제한은 하고 있다.
이런 가변적인 크기덕분에 WiredTiger 의 B-Tree 의 브랜치 노드와 리프 노드의 크기를 다르게 설정할 수 있다.
이렇게 다르게 설정하는 이유는 브랜치노드는 리프 노드를 구분하는 인덱스 키만 가지기 때문에 저장하는 데이터가 많지 않아도 되기 때문이다.
그래서 WiredTiger 의 자체적인 기본값은 브랜치 4KB, 리프 32KB 이다.
또 컬렉션의 데이터 페이지와 인덱스 페이지의 크기를 다르게 설정할 수 있다.
storage:
engine:wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 10
collectionConfig:
configString: "internal_page_max=4K,leaf_page_max=64K"
indexConfig:
configString: "internal_page_max=4K,leaf_page_max=16K"
대량의 insert 와 분석, 배치작업을 위한 대량 조회가 주로 이루어진다면 데이터페이지를 크게 가져가는 것이 좋다.
그렇지만 OLTP 성으로 운영되는 환경에서는 가능하면 페이지 크기를 작게 설정하는 것이 좋다.
대용량의 경우에도 인덱스페이지는 기본값이 적절한데, 이는 데이터가 적재될 때 랜덤 액세스로 인한 정렬비용 그리고 B-Tree 인덱스 모든 페이지들이 메모리에 적재, 읽고 쓰는 작업이 많아지며 캐시 부하등이 발생하기 때문이다.
운영체제 페이징 캐시
WiredTiger 는 내장된 공유 캐시를 가지고 있다.
그런데 MongoDB 에 내장된 WiredTiger 스토리지 엔진은 운영체제 캐시를 경유하는 Cached IO 를 기본옵션으로 사용한다.
이 말은 WiredTiger 스토리지 엔진에 참조하고자 하는 데이터 페이지는 리눅스 커널이 먼저 디스크에서 읽어서 자신의 페이지에 캐싱하고, 리눅스 페이지캐시에 있는 데이터를 다시 자신의 내장 캐시에 복사하는 것이다.
결국 참조하고자 하는 데이터는 리눅스 페이지 캐시와 WiredTiger 스토리지 엔진 두 곳에 있다.
이를 더블 버퍼링(Double Buffering) 이라고 한다.
많은 DBMS 에서 이런 더블 버퍼링 문제를 해결하기 위해 Direct IO 방식을 사용한다.
Direct IO 를 사용하면 더블버퍼링 문제도 없어지며 리눅스서버 캐시 사용량도 줄일 수 있다.
리눅스 커널의 메모리 사용으로 인한 문제점
리눅스는 먼저 물리적으로 장착된 전체 메모리에서 커널에 필요한 메모리 공간을 예약한다.
남는 메모리 공간에서 응용 프로그램이 요구할 때마다 조금씩 할당해주는 방식으로 메모리를 활용한다.
그런데도 남은 미사용 공간이 있다면 이 공간을 디스크의 데이터파일 캐시 용도로 활용한다.
이 때 사용되는 메모리 공간을 리눅스 페이지 캐시라고 하며, 다양한 형태의 디스크 읽고 쓰기 작업의 버퍼링을 담당하게 된다.
그리고 미사용 공간의 크기가 줄어들면 페이지 캐시로 사용되고 있는 공간을 반납받아 미사용 공간으로 확보한다.
그런데 리눅스 서버가 페이지 캐시공간을 디스크로 스왑아웃 시켜 갑자기 느려지는 경우가 발생한다.
WiredTiger 컨셉충돌
리눅스의 페이지 캐시를 사용하지 않으려면 Direct IO 를 사용해야 하는데,
WiredTiger 스토리지 엔진은 Direct IO 를 사용할 수 있도록 지원하고 있다.
하지만 Direct IO 를 사용하려면 데이터 읽고 쓰기 작업이 4KB 크기에 맞춰줘야 한다.
즉 가변크기 페이지를 포기해야 하는데, MongoDB 서버에서 검증되지 않은 디스크 읽고 쓰기 방식이고 변경하는 것을 권장하지 않는다.
Direct IO 읽고 쓰기 모드에서는 데이터페이지 크기가 정확하게 디스크 블록의 크기와 일치하지 않으면 데이터가 손실되거나 오히려 성능 역효과가 발생할 수 있다.
리눅스 페이지 캐시 Write-back 모드 작동의 위험성
리눅스의 페이지 캐시가 Write-back 모드로 작동하는 것도 위험하다.
Cached IO 를 사용하는 WritedTiger 스토리지 엔진의 디스크 데이터 쓰기 과정을 보자.
WiredTiger 스토리지 엔진에서 데이터 쓰기를 실행하면 리눅스 서버는 자신의 페이지 캐시에 기록하고 즉시 WiredTiger 스토리지 엔진으로 성공 여부를 반환한다.
하지만 실제 데이터는 아직 디스크에 완전히 기록되지 않은 상태인데, 이 상태에서 리눅스 서버가 크래시되거나 응답 불능 상태가 되면 디스크에 완전히 기록되지 못한 데이터는 손실되는 것이다.
단 트랜잭션 내용을 항상 저널로그에 먼저 기록하기 때문에 비정상 종료 이후 다시 실행될 때 자동으로 복구한다.
그리고 리눅스 서버는 WiredTiger 스토리지 엔진과는 무관하게 적절한 시점에 데이터를 디스크에 기록한다.
이렇게 WiredTiger 스토리지 엔진의 디스크 쓰기 과정이 리눅스 페이지 캐시까지만 저장되면 완료되므로 쓰기가 매우 빠르게 처리된다.
Direct IO 는 리눅스 페이지 캐시를 거치지 않고 즉시 디스크로 쓰기를 전달한다.
그런데 내장 캐시를 가진 RAID 컨트롤러도 write-back 모드로 작동하면서 자신의 캐시에 복사하는 즉시 성공여부로 반환한다.
그리고 RAID 컨트롤러는 자체적으로 버퍼링된 데이터를 실제 디스크에 기록한다.
만약 이 상태에서 서버에 크래시가 발생하더라도 RAID 에 내장된 배터리가 전원을 공급하면서 데이터가 손실되지 않도록 유지한다.
그리고 서버에 다시 전원이 제공되고 작동이 시작되면 RAID 컨트롤러의 캐시에 남아있는 데이터를 자동으로 디스크에 기록한다.
압축
다양한 압축 알고리즘과 데이터 입출력 레이어에서 압축을 지원한다.
RDBMS 의 고정된 크기의 페이지는 16KB 의 페이지를 압축하더라도 4KB 혹은 8KB 로 떨어져야 한다.
그렇지 않은경우 16KB 페이지르 2개의 16KB 로 나눈 후 다시 압축하는 과정을 거치는데 이러한 과정은 처리성능을 상당히 떨어트려 이런 지연을 회피하기 위해 16KB 페이지를 비워두는 패딩전력을 사용한다.
WiredTiger 는 16KB 페이지의 데이터를 압축했을 때 4KB 나 8KB 이하의 사이즈가 아니더라도 굳이 페이지를 분할하고 다시 압축하는 과정을 거칠필요가 없다. 모든 페이지가 가변사이즈이기 때문에 압축 결과 페이지를 그대로 디스크에 기록하면 된다.
두번째 장점은 스토리지 엔진이 데이터를 읽고 쓰는 시점에서 데이터의 압축과 해제가 처리된다는 것이다.
WiredTiger 캐시에서 디스크에 저장될 때 블록매니저는 압축하여 디스크의 저장공간을 줄이고
디스크에서 읽을때 블록매니저가 압축을 해제하여 캐시에 올린다.
세번째 장점은 4가지 형태의 압축 기능이 제공된다는 것이다.
다만 현재 MongoDB 에 내장된 WiredTiger 스토리지 엔진에서 사용할 수 있는 압축 형태는 블록과 인덱스 프리압축이다.
- 블록 압축
- 인덱스 프리압축
- 사전압축
- 허프만 인코딩
블록 압축은 가장 일반적인 압축 형태인데, 데이터가 저장된 페이지 단위로 압축을 실행한다.
3.4 버전의 서버에 내장된 블록압축 방법은 zlib 와 snappy 압축 알고리즘만 사용한다.
WiredTiger 스토리지 엔진에서는 컬렉션, 인덱스, 저널로그에 대해 각각 압축을 어떻게 적용할 것인지 결정할 수 있다.
wiredTiger:
engineConfig:
collectionConfig:
blockCompressor: snappy
indexConfig:
prefixCompression: true
일반적으로 MongoDB 컬렉션에 저장되는 도큐먼트는 키와 값의 쌍으로 저장되므로 하나의 페이지에서 반복되는 키 값이 상당히 많이 포함된다.
그래서 MongoDB 데이터베이스에서는 압축이 필수적인 요소 중 하나다.
그러나 MongoDB 인덱스는 키와 값의 쌍으로 저장되지 않고 일반적인 RDBMS 와 동일한 형태인 키 엔트리로 인덱스가 구성된다. 즉 MongoDB 에서 인덱스는 snappy 나 zlib 압축이 큰 효과가 없을 수 있다.
대신 인덱스 파일의 압축을 위해 프리픽스 압축 기능이 제공된다. 이름 그대로 인덱스 키에서 왼쪽 부분의 중복 영역을 생략하는 압축 방식을 의미한다.
압축 전 | 압축 후 |
Apple | Apple |
AppleLeaf | *Leaf |
AppleTree | *Tree |
Banana | Banana |
Bay | Bay |
Car | Car |
CarDriver | *Driver |
공통되는 글자는 * 로 생략하나 만약 공통되는 길이가 너무 짧은경우에는 프리픽스 압축이 생략되기도 한다.
프리픽스 압축은 블록 압축과 달리 WiredTiger 공유캐시에서도 압축 상태를 유지한다.
그래서 디스크 뿐만 아니라 메모리에서도 사용량을 절약할 수 있다.
단점으로 데이터를 읽을 때 항상 완전한 키 값을 얻기 위해 조립 과정을 거쳐야 한다.
운이 나쁜 경우 제일 첫 번째 인덱스 키부터 재조립 과정을 거쳐야 할 수도 있어 키를 읽는 속도가 떨어진다.
그리고 프리픽스 압축의 이런 오버헤드는 인덱스를 역순으로 읽을때 더 심해진다.
암호화
MongoDB 에서도 데이터 파일과 인덱스 데이터의 암호화를 위한 기능을 지원하고 있다.
실제 디스크에서 데이터 페이지를 읽어오는 순간 암호화된 내용을 복호화해서 메모리에 적재한다.
메모리 스토리지 엔진
MongoDB 는 엔터프라이즈 버전에서 메모리 기반의 스토리지 엔진을 제공하고 있다.
하지만 최근 percona 에서 만든 percona MongoDB 서버에서도 메모리 스토리지 엔진을 라이센스 제약없이 사용할 수 있다.
메모리 스토리지 엔진을 실행하고자 한다면 MongoDB 서버 시작시 --storageEngine=inMemory 를 사용하면 된다.
Percona 는 WriedTiger 스토리지 엔진이 메모리 기반으로 작동할 수 있게 인터페이스를 개발한 것이기 때문에 WiredTiger 와 거의 동일하게 작동한다.
차이점으로는 아래 2가지가 있다.
- 메모리 스토리지 엔진은 체크포인트가 없음
- 공유캐시의 페이지 이빅션이 없음
Percona 의 가장 큰 문제점은 OpLog 를 디스크로 기록하지 않고 공유캐시 메모리에 저장한다는 것이다.
그래서 OpLog 크기를 너무 크게 설정하면 메모리 부족으로 구동이 실패한다.
또 데이터가 메모리에 저장되므로 복제 구조와 관련된 메타정보까지 메모리에 저장해야 한다.
그래서 복제, 샤드 클러스터가 한번에 종료되면 메모리데이터가 유실되어 메타정보가 없어 복구가 불가능하다.
기타 스토리지 엔진
RocksDB : 페이스북에서 개발해서 배포 중, LSM 기반의 데이터 저장소를 가지는데, 느린 쿼리 성능을 보완하기 위해 로우 키를 기준으로 여러 범위로 나누고 각 범위별로 LSM-Tree 를 구성한다.
PerconaFT : 퍼코나에서 배포, Fractal Tree 인덱스의 구조적인 장점으로 인해 빠른 INSERT 성능이 강점
'Database > MongoDB' 카테고리의 다른 글
[MongoDB] 복제(2) (0) | 2024.05.26 |
---|---|
[MongoDB] 복제 (0) | 2024.05.20 |
[MongoDB] WiredTiger 스토리지 엔진, 데이터파일 구조 (0) | 2024.05.13 |
[MongoDB] 데이터를 읽고 쓰는 방법과 프레그멘테이션 관리 (0) | 2024.05.13 |
[MongoDB] MMAPv1 스토리지 엔진 (0) | 2024.05.13 |