MongoDB는 데이터를 어떻게 저장하고 관리할까

저니·2023년 4월 4일
7
post-thumbnail

https://medium.com/@hnasr/mongodb-internal-architecture-9a32f1403d6f 를 읽고 번역 및 추가적으로 정리해본 글입니다.

MongoDB의 구성요소

Documents

MongoDB는 RDBMS(Relational Database)와는 다른 document(이하 도큐먼트) 기반의 Non-Relational Database이다. 도큐먼트는 JSON 포맷을 따르고, Mongo는 내부적으로 이 JSON 도큐먼트를 저장 속도와 효율을 위해 BSON(Binary JSON) 형태로 변환해서 저장한다. 그리고 Mongo는 다시 이 도큐먼트를 꺼내올 때 BSON을 JSON으로 변환해서 가져온다.

MongoDB 문서에서 BSON을 설명을 보면 다음과 같다.

BSON은 “Binary JSON”을 의미한다. BSON은 타입과 길이 정보를 인코딩하기 때문에 JSON에 비해서 훨씬 빠르게 탐색할 수 있다. 그리고 BSON은 MongoDB가 중요한 정보를 잃지 않도록 날짜나 이진 데이터같은 몇 가지의 JSON 타입이 아닌 데이터도 추가한다.

{"hello": "world"} →
\x16\x00\x00\x00           // total document size
\x02                       // 0x02 = type String
hello\x00                  // field name
\x06\x00\x00\x00world\x00  // field value
\x00                       // 0x00 = type EOO ('end of object')

도큐먼트는 사이즈가 클 수 있기 때문에, Mongo는 가끔 사이즈를 줄이기 위해서 도큐먼트를 압축할 수도 있다 (이후에 설명할 WireTiger엔진은 항상 자동으로 압축해서 저장한다. 또는 사용자가 수동으로 document를 압축할 수도 있다 - compact 명령어).

Collections

Collections(이하 컬렉션)는 RDBMS의 테이블과 같은 개념이며, 여러개의 도큐먼트를 가질 수 있다. 다만 MongoDB는 RDBMS처럼 스키마를 정의하지는 않는 데이터베이스이기 때문에, 하나의 컬렉션안에 여러 스키마의 도큐먼트가 존재하는 것도 가능하다. 스키마에 엄격하지 않은 이런 점 때문에 주로 빠르게 개발할 때 많이 쓰인다.

_id Index (Primary Index)

각 도큐먼트는 고유한 _id 필드를 가지며, 이게 기본 키의 역할을 한다. _id필드는 항상 도큐먼트의 첫번째 필드여야하기 때문에 만약 _id필드가 첫번째가 아니라면 DB 서버가 알아서 맨처음으로 옮겨놓는다. 그리고 MongoDB는 _id값이 없는 도큐먼트가 들어오면 ObjectId값으로 _id필드를 자동으로 만들어준다.

기본키인_id 타입은 12바이트인 ObjectId인데, 12바이트로 이렇게 큰 이유는 Mongo는 확장성이 중요한 시스템에 많이 사용하기 때문에 여러 샤드에서도 고유한 값을 가질 수 있어야 해서다. ObjectId에 대한 자세한 설명은 공식 문서를 참조하자.

그리고 MongoDB에서 컬렉션을 생성하면 도큐먼트의 기본키인 _idB+Tree 주 인덱스를 생성한다.

Secondary Index

컬렉션의 다른 필드로 보조 B+Tree 인덱스를 생성할 수도 있다. 보조 인덱스의 크기는 2가지 요인에 의해 결정되는데, 바로 1) 인덱싱 될 필드의 key 사이즈와, 2) 도큐먼트 pointer 사이즈에 의해 결정된다.

Mongo 스토리지 엔진의 변천사

MMAPv1

MongoDB가 처음 릴리즈되었을 때, MMAPv1이라는 스토리지엔진을 사용했다. 이 스토리지 엔진에선 BSON 도큐먼트는 압축되지 않은 상태 그대로 디스크에 저장되었고, _id 기본키 인덱스는 Diskloc이라는 특수한 값을 가리켰는데, 이 Diskloc은 디스크내에서 도큐먼트가 존재하는 (파일 번호, 파일 offset) 쌍을 나타냈다.

그래서 _id로 도큐먼트를 가져오려고 하면, 인덱스 리프 페이지에서 Diskloc 값을 찾아서 이 값으로 파일을 찾고 Offset만큼 디스크에서 파일을 바로 읽었다.

이 방식에는 한계점이 있었는데, Diskloc 방식은 O(1)만에 도큐먼트를 찾을 수는 있었지만, 도큐먼트가 새로 추가되거나 수정되면 인덱스를 유지하기가 힘들었다. 왜냐하면 어떤 도큐먼트를 수정했을 때, 사이즈가 커진다면 offset 도 변경되어야 하는데, 이 변경된 값에 맞춰서 다른 Diskloc offset들도 업데이트되어야 했기 때문이다.

또 추가적인 한계점은 MMAPv1은 쓰기 연산에 글로벌 락을 사용해서, 동시에 쓰기 요청이 들어오면 현저하게 느렸다. 이후 Mongo는 MMAPv1이 글로벌 쓰기 락이 아니라 컬렉션 단위 락(테이블 단위 락)으로 변경했지만, 나중엔 아예 WiredTiger을 기본 엔진으로 변경하면서 더 이상 MMAPv1을 사용하지 않는다.

WireTiger

MongoDB는 2014년 부터 WireTiger를 기본 스토리지엔진으로 선정했다. WireTiger는 도큐먼트 단위 락이나 압축 같은 여러가지 특징을 가지고 있었기 때문에 MMAPv1엔진에서는 하지 못했던, 같은 컬렉션 내부의 다른 도큐먼트에 동시에 쓰기 같은 것들도 할 수 있었다.

WireTiger에서는 BSON 도큐먼트는 압축된 후, 리프 노드가 (recordId, BSON) 쌍인 hidden clustered 인덱스에 저장되었다. 이로 인해 더 많은 BSON 도큐먼트를 더 적은 I/O로 가져올 수 있었고, 전체적인 성능이 향상되었다.

이제 주 인덱스인 _id와 보조 인덱스들은 Diskloc 대신에 새로운 값인 recordId(64bit)를 가리키도록 변경되었다. 하지만 이 방식에도 단점이 있는데, 유저가 도큐먼트의 _id 값으로 조회할 때, 이전과는 다르게 BSON 도큐먼트를 찾기 위해 lookup을 한 번 더 해야 한다는 것을 의미한다(이전엔 인덱스에서 _id로 바로 실제 도큐먼트를 조회했다면, 이제는 _id로 recordId를 찾은 후, hidden 인덱스에서 recordId로 다시 BSON 도큐먼트를 찾아야 하는 것이다)

이건 모든 인덱스에 다 적용되는 문제였기 때문에(주 인덱스, 보조 인덱스 할 것 없이 전부 다 해당 인덱스로 recordId를 찾기 위한 추가 인덱스가 필요하다는 의미) 결국 Discord에서는 인덱스가 RAM에 더 이상 올라가지 않아서 Cassandra로 옮기게 되었다고 한다.

Clustered Collections Architecture

Clustered Collections 은 2022년 6월에 MongoDB에서 새로 도입된 기능이다(MongoDB 5.3부터 적용). Clustered Index 는 조회 시 필요한 값이 리프 페이지에 바로 저장되어 있는 인덱스를 말한다('Index-only Scans'이라고도 한다)

Clustered Index는 주 인덱스인 _id에 한해서만 적용되었는데, 이 덕분에 이제 _id로 도큐먼트를 조회할 때 추가적인 lookup이 필요하지 않고 바로 BSON 도큐먼트를 반환할 수 있게 되었다.
다만 보조 인덱스는 Clustered Index를 적용하진 않고, 대신에 더 이상 쓰지 않는 recordId가 아니라 _id를 가리키게끔 바뀌었다.

여기서 모든 보조 인덱스가 12bit인 recordId대신에 12byte(_id 크기)를 저장하게 되었기 때문에 인덱스가 기존에 비해 훨씬 커질 수 있게 되었단 점을 주의해야 한다.

vs MySQL InnoDB

여기까지 살펴보면 보조 인덱스가 primary key를 가리키는 InnoDB구조와 비슷하지만 MySQL은 테이블들이 무조건 Clustered Index를 가지는 반면에(직접 정의하지 않으면, primary key로 InnoDB가 알아서 생성한다), MongoDB는 컬렉션에 Clustered Index를 적용할지 하지 않을 지 선택할 수 있다는 점에 차이가 있다.

0개의 댓글