본문 바로가기
mongodb

MongoDB 클러스터(리플리카 셋, 샤드)

by misankim 2023. 3. 31.

지난번 AWS DocumentDB 에 대한 게시물을 통해 mongoDB와 같은 도큐먼트 데이터베이스란 무엇인지, AWS DocumentDB 클러스터 생성 과정에 대해 간략히 소개해드렸는데요.

오늘은 mongoDB의 클러스터를 구성하는 두 가지 방법인 리플리카 셋(읽기 전용 복제본) 클러스터와 샤드 클러스터를 docker 컨테이너로 구성하는 테스트를 통해 mongoDB 클러스터 구성 방법과 그 특징에 대해 알아보겠습니다.

리플리카 셋(replica-set)

리플리카 셋이란

리플리카 셋은 mongoDB에서 사용하는 읽기 전용 복제본이 포함된 클러스터(마스터/슬레이브 노드 모두 포함)를 말하며 고가용성을 목적으로 사용됩니다. 아래는 리플리카 셋의 기본 구조입니다. 마스터 서버 장애 발생 시 슬레이브 서버들 간 투표를 진행하여 슬레이브 서버 중 1개가 마스터로 승격하는데 이때 투표의 동률이 나오는 것을 막기 위해 멤버 수는 홀수로 구성합니다. 아래의 구조는 PSS(프라이머리-슬레이브-슬레이브) 구조라고 부릅니다.

이번에는 슬레이브 중 하나가 아비터(중재자) 노드로 구성된 경우입니다. 아비터 노드는 데이터를 저장하지 않으며, 투표만을 위해 클러스터에 포함되어 있는 노드입니다.

리플리카 셋을 docker 컨테이너로 구성해보기

리플리카 셋의 멤버를 각각의 서버로 구성해도 되지만, 테스트를 위해 docker에 컨테이너로 구성해봅니다. 컨테이너가 아닌 실제 서버로 구성 시에도 방법은 동일합니다.

먼저 구성할 컨테이너들입니다. 마스터 1개와 슬레이브 2개로 구성된 총 3개의 멤버를 가진 리플리카 셋을 구성하며, 접속 테스트에 사용할 클라이언트 컨테이너도 1개 포함하였습니다.

여기서 --network wp 는 컨테이너간 도메인 기반 통신을 하기 위해 임의로 생성한 도커 네트워크로, 아직 생성된 도커 네트워크가 없는 경우 "docker network create 네트워크이름" 커맨드로 생성 후 진행합니다. (wp는 임의로 제가 정한 네트워크 이름입니다. 동일 도커 네트워크의 컨테이너끼리는 "컨테이너이름.네트워크이름" 형식으로 도메인 기반 통신이 가능합니다.)

# 리플리카 셋 컨테이너
docker run -d --network wp -v /var/lib/mongo1:/data/db --name mongo1 mongo --replSet repltest
docker run -d --network wp -v /var/lib/mongo2:/data/db --name mongo2 mongo --replSet repltest
docker run -d --network wp -v /var/lib/mongo3:/data/db --name mongo3 mongo --replSet repltest

# 클라이언트 컨테이너
docker run -d --network wp --name mongo_client mongo

위의 컨테이너 생성 커맨드에서 --replSet repltest 옵션의 경우 생성할 리플리카 셋과 동일한 이름으로 설정해줘야합니다. 옵션값과 실제 리플리카 셋 구성 시 ID 값이 다르면 리플리카 셋 구성에 실패합니다. 생성된 컨테이너들의 모습입니다.

이제 클라이언트 컨테이너로 접속하여 mongo1.wp(마스터)로 원격 접속해봅니다.

docker exec -it mongo_client bash
mongo mongo1.wp

아래와 같이 정상적으로 접속되는 것을 확인할 수 있습니다.

mongo1.wp 컨테이너로 원격 접속한 상태 그대로 리플리카 셋 구성을 시작합니다.

# 리플리카 셋 설정(초기 1개 노드)
repl_conf={
_id: "repltest",
members: [{_id: 0, host: "mongo1.wp"}]
}

-> mongo1.wp 부분을 구성한 컨테이너 도메인으로 변경하여 진행합니다.

# 리플리카 셋 초기화
rs.initiate(repl_conf)

정상적으로 리플리카 셋이 구성되었는지 리플리카 셋 상태 확인 명령어"rs.status()"를 통해 확인해봅니다.

# 리플리카 셋 상태 확인
rs.status()

# 리플리카 셋 설정값 확인
rs.conf()

출력의 길이가 꽤 길지만 여기서 members 라는 항목에 주목합니다. 현재는 mongo1.wp 컨테이너만 멤버에 포함되어 있습니다.

이제 나머지 슬레이브 멤버인 mongo2.wp 컨테이너와 mongo3.wp 컨테이너를 리플리카 셋에 추가해봅니다.

# 노드 추가
rs.add("mongo2.wp")
rs.add("mongo3.wp")

-> 각 호스트 네임을 추가하려는 2개 컨테이너의 도메인으로 변경하여 실행

이제 다시 리플리카 셋 상태 확인 명령어"rs.status()"를 통해 확인해봅니다. mongo2.wp, mongo3.wp 컨테이너가 멤버로 포함된 것이 확인됩니다.

 

 

그런데 한가지 이상한 점이 있습니다. 위에서 설명한 리플리카 셋의 기본 구조인 아래 그림과 달리 mongo3.wp 멤버의 syncSourceHost 항목 확인 시 mongo1.wp 가 아니라 mongo2.wp 으로 설정되어 있습니다.

 

그 이유는 마스터 서버의 부하를 줄이기 위한 체인 복제 옵션인 chainingAllowed 옵션이 기본 값으로 활성화되어 있기 때문입니다.

장애 조치 및 복구

리플리카 셋은 설정한 하트비트 간격(기본 값 2초)에 따라 서로의 상태를 확인합니다. 그리고 마스터 서버의 장애 발생 시 투표를 통해 슬레이브 서버 중 마스터로 승격할 멤버를 결정하고 자동으로 장애조치를 수행합니다. 기본적으로 투표에 영향을 주는 요인은 각 멤버의 priority(우선 순위) 값입니다.

(참고) priority(우선 순위) 값 조정 방법
https://docs.mongodb.com/manual/tutorial/adjust-replica-set-member-priority/

이제 현재 마스터 서버인 mongo1.wp 컨테이너를 다운시켜봅니다.

상태 확인을 위해 mongo2.wp 컨테이너로 접속하여 클러스터 상태를 확인해봅니다.

의도한대로 기존 마스터였던 mongo1.wp 컨테이너는 장애가 발생한 것으로 확인되며,

"stateStr" : "PRIMARY" 항목을 통해 mongo2.wp 컨테이너가 마스터 서버로 승격했음을 확인할 수 있습니다.

다시 mongo1.wp 컨테이너를 살리고 리플리카 셋의 상태를 확인하면 아래와 같이 mongo1.wp 컨테이너가 슬레이브로 정상 동작하는 것을 확인할 수 있습니다.

 

샤드(shard)

shard란

샤드는 데이터베이스의 테이블을 분할하여 여러 노드로 분산하여 저장하는 기술을 말합니다. 일반적으로 데이터베이스는 수직 확장(스케일 업)에는 용이하나, 수평 확장(스케일 아웃)은 어려운 특성으로 인해 부하 분산에 어려움이 있지만 샤딩을 적용한 클러스터는 여러 노드로 데이터를 분산 저장하기 때문에 수평 확장 및 부하 분산에 용이한 특성을 가집니다.

mongoDB shard 클러스터의 구조

읽기 전용 복제본 클러스터인 리플리카 셋에 비해 샤드 클러스터의 구조는 조금 더 복잡합니다.

1) shard 리플리카 셋 -> 샤드 키에 의해 데이터가 분산 저장된 클러스터. 각 샤드 노드는 PSS(프라이머리-슬레이브-슬레이브) 혹은 PSA(프라이머리-슬레이브-아비터) 형태의 리플리카 셋으로 구성
2) config servers(리플리카 셋) -> 데이터가 각 샤드 서버로 어떻게 분산되어 있는지 기록되어 있는 메타 데이터 및 사용자 정보를 저장. 하나의 리플리카 셋으로 구성
3) mongos(최소 1개) -> 컨피그 서버의 메타 정보로 어떤 샤드로 쿼리를 요청할지 결정하며, 샤드로부터의 결과를 병합하여 클라이언트로 전달

mongoDB 샤드 클러스터를 docker 컨테이너를 이용하여 구성해보기

리플리카 셋 구성과 동일하게 샤드 클러스터를 docker 컨테이너를 이용하여 구성해봅니다.
docker 컨테이너가 아닌 일반 서버에서 구성 시에도 방법은 동일합니다.

구성할 컨테이너는 총 13개입니다.

1) shard 리플리카 셋(PSS, 3개 노드 * 2개 리플리카 셋, 27018 포트)
repl_shard1(3개 컨테이너)
repl_shard2(3개 컨테이너)

2) config servers(PSS, 3개 노드 * 1개 리플리카 셋, 27019 포트)
repl_conf(3개 컨테이너)

3) mongos(3개 노드, 27017 포트)

4) mongo 클라이언트(1개)

먼저 config servers를 생성하기 위해 컨테이너를 3개 생성합니다. config servers 또한 리플리카 셋이기에 --replSet 옵션을 지정하며, config server로 사용할 것이기 때문에 --configsvr 옵션을 포함합니다.

# config server 컨테이너 3개
docker run -d --network wp -v /var/lib/mongo_conf1:/data/db --name mongo_conf1 mongo --replSet repl_conf --configsvr
docker run -d --network wp -v /var/lib/mongo_conf2:/data/db --name mongo_conf2 mongo --replSet repl_conf --configsvr
docker run -d --network wp -v /var/lib/mongo_conf3:/data/db --name mongo_conf3 mongo --replSet repl_conf --configsvr

# mongo 클라이언트 컨테이너 1개
docker run -d --network wp --name mongo_client mongo

mongo 클라이언트 컨테이너로 접속하여 mongo_conf1.wp 컨테이너로 mongo 명령어를 통해 원격 접속합니다. config server의 경우 mongoDB의 기본포트인 27017 포트가 아닌 27019 포트로 접속해야하는 점에 유의합니다.

docker exec -it mongo_client bash
mongo mongo_conf1.wp:27019

mongo_conf1.wp 컨테이너의 mongo shell로 접속했다면 config server 리플리카 셋을 초기화합니다.

rs.initiate({
_id:"repl_conf",
configsvr:true,
members:[{_id:0,host:"mongo_conf1.wp:27019"},{_id:1,host:"mongo_conf2.wp:27019"},{_id:2,host:"mongo_conf3.wp:27019"}] });

-> 구성한 config server 컨테이너의 도커 도메인으로 수정하여 커맨드를 입력합니다.

config server 리플리카 셋이 정상적으로 구성되었다면 이제 shard 리플리카 셋을 2세트(6개 컨테이너) 구성합니다. 컨테이너 생성 시 리플리카 셋 구성을 위한 --replSet 옵션과 샤드 노드임을 설정하는 --shardsvr 옵션을 포함합니다.

# shard 리플리카 셋1
docker run -d --network wp -v /var/lib/mongo_shard1:/data/db --name mongo_shard1 mongo --replSet repl_shard1 --shardsvr
docker run -d --network wp -v /var/lib/mongo_shard2:/data/db --name mongo_shard2 mongo --replSet repl_shard1 --shardsvr
docker run -d --network wp -v /var/lib/mongo_shard3:/data/db --name mongo_shard3 mongo --replSet repl_shard1 --shardsvr

# shard 리플리카 셋2
docker run -d --network wp -v /var/lib/mongo_shard4:/data/db --name mongo_shard4 mongo --replSet repl_shard2 --shardsvr
docker run -d --network wp -v /var/lib/mongo_shard5:/data/db --name mongo_shard5 mongo --replSet repl_shard2 --shardsvr
docker run -d --network wp -v /var/lib/mongo_shard6:/data/db --name mongo_shard6 mongo --replSet repl_shard2 --shardsvr

이제 shard 리플리카 셋1과 2의 컨테이너로 각각 접속하여 리플리카 셋을 구성합니다.
shard 노드의 경우 mongoDB 기본 포트인 27017 포트가 아닌 27018 포트로 접속해야하는 점에 유의합니다.

# shard 리플리카 셋1 구성
docker exec -it mongo_client bash

mongo mongo_shard1.wp:27018

rs.initiate({ _id:"repl_shard1", members:[ {_id:0,host:"mongo_shard1:27018"}, {_id:1,host:"mongo_shard2:27018"}, {_id:2,host:"mongo_shard3:27018"} ]} );

exit

# shard 리플리카 셋2 구성
mongo mongo_shard4.wp:27018

rs.initiate( {_id:"repl_shard2", members:[ {_id:0,host:"mongo_shard4:27018"}, {_id:1,host:"mongo_shard5:27018"}, {_id:2,host:"mongo_shard6:27018"}]} ) ;

아래와 같이 shard 리플리카 셋1과 2가 정상적으로 구성되었는지 확인합니다.

 

이제 mongos 컨테이너를 생성합니다. mongos의 config server 리플리카 셋에 대한 정보를 --configdb 옵션을 통해 설정하고, 외부에서의 접속을 허용하기 위한 옵션인 --bind_ip_all 을 포함합니다. (mongos는 기본 값으로 localhost에만 바인딩됩니다.) config servers와 shard 리플리카 셋과는 달리 mongos는 리플리카 셋의 형태가 아닌 외부로부터의 중복된 진입 지점의 역할을 하여 각각 동작합니다.

docker run -d --network wp -v /var/lib/mongos1:/data/db --name mongos1 mongo mongos --configdb repl_conf/mongo_conf1.wp,mongo_conf2.wp,mongo_conf3.wp --bind_ip_all
docker run -d --network wp -v /var/lib/mongos2:/data/db --name mongos2 mongo mongos --configdb repl_conf/mongo_conf1.wp,mongo_conf2.wp,mongo_conf3.wp --bind_ip_all
docker run -d --network wp -v /var/lib/mongos3:/data/db --name mongos3 mongo mongos --configdb repl_conf/mongo_conf1.wp,mongo_conf2.wp,mongo_conf3.wp --bind_ip_all

이제 mongos1.wp 컨테이너로 접속하여 위의 단계에서 생성한 shard 리플리카 셋 2 세트를 추가합니다.

docker exec -it mongo_client bash

mongo mongos1.wp

sh.addShard("repl_shard1/mongo_shard1:27018,mongo_shard2:27018,mongo_shard3:27018")
sh.addShard("repl_shard2/mongo_shard4:27018,mongo_shard5:27018,mongo_shard6:27018")

이제 모든 샤드 클러스터 구성이 완료되었습니다. 샤드 상태를 확인하는 명령어 "sh.status()"로 샤드 클러스터의 상태를 확인합니다.

sh.status()

이제 mongos를 통해 입력되는 데이터는 shard1과 shard2로 분산(샤딩)되어 저장됩니다. 데이터의 분산은 RDB의 테이블과 같은 개념인 컬렉션 단위에서 이뤄지며, 데이터 분산 시 지정한 청크(덩어리)의 크기만큼 데이터가 번갈아가며 분산됩니다. 샤드 리플리카 셋 간의 청크를 균일하게 분산하기 위해 balancer 라는 기능이 동작합니다. mongoDB 기본 청크 사이즈는 64MB로 데이터가 분산되어 저장되는 것을 눈으로 확인하기 위해 청크 사이즈를 임시로 2MB로 변경해봅니다.

use config
db.settings.save( { _id:"chunksize", value: 2 } )

이제 테스트 데이터를 10만개 입력하여 두개의 샤드 리플리카 셋으로 데이터가 분산되어 저장되는지 테스트해보겠습니다. 그 전에 blog 라는 테스트 db를 생성 후 post 라는 컬렉션(테이블)을 생성합니다.

# 테스트 컬렉션 생성
use blog
db.createCollection("post")

그리고 blog db의 post 라는 컬렉션에 대한 샤딩을 활성화합니다.

# 샤딩 활성화
use admin
db.runCommand({enablesharding:'blog'});
db.runCommand({shardcollection:'blog.post', key:{id:1}});

샤딩 설정은 완료되었으니 아래 쿼리로 데이터 10만개를 입력합니다.(시간이 5~10분정도 소요됩니다. 완료될 때까지 기다립니다.)

use blog

for(var i=0 ; i<100000 ; i++) {
db.post.save({id:i, head:00, body:'test'+i});
}

테스트 입력이 모두 완료되었다면 도큐먼트의 갯수를 확인해봅니다.

db.post.count()

shard 리플리카 셋1과 shard 리플리카 셋2로 청크 단위의 데이터 분산이 잘 이뤄졌는지 확인해봅니다. 도큐먼트의 용량이 워낙 작아서 도큐먼트의 수는 6만 2천개와 3만 9천개의 차이가 있긴 하지만 청크의 수는 3개와 2개로 잘 분산된 것을 확인할 수 있습니다.

use blog
db.post.getShardDistribution()

이제 개별 샤드로 접속하여 실제 데이터의 양도 확인해봅니다. 위의 getShardDistribution 쿼리로 확인한 수와 일치하는 것을 확인할 수 있습니다.

docker exec -it mongo_client bash

mongo mongo_shard1:27018

show dbs
use blog
db.post.count()

mongo mongo_shard4:27018

show dbs
use blog
db.post.count()

AWS DocumentDB 에서 지원하는 클러스터는?

AWS DocumentDB 에서는 리플리카 셋을 이용한 읽기 전용 복제본 클러스터만 지원하고 있습니다. 데이터를 분산 저장하는 샤드 클러스터는 지원하지 않는 점을 참고해야할 것 같습니다.