개인 프로젝트 중 Mongodb로 트랜잭션을 사용하려면 Replica Set를 구성해야 한다고 해서 작성하게 되었다.
선착순 퀴즈 참여 서비스를 주제로 하다보니 여러 유형의 문제(객관식, 단답형, 주관식...) 를 만들 수 있어야 했고 나중에 유형이 더 추가될 수도 있었다. 그렇기 때문에 스키마 변경이 자유로운 MongoDB를 채택하게 되었다.
하지만 프로젝트가 복잡해지면서 단일 문서에 대한 작업보단 여러 문서에 걸친 작업이 많아져 트랜잭션을 도입하게 되었다. (MongoDB의 경우 기본적으로 단일 문서에 대한 작업은 원자적으로 이루어지지만 여러 문서에서는 보장되지 않는다.)
Mongodb Replica Set는 동일한 데이터 세트를 유지관리하는 mongodb 프로세스 그룹으로 중복성과 고가용성을 제공한다.
DB에 장애가 발생하는 경우 빠르게 복구할 수 있는 장점이 있다.
공식 답변에 따르면 트랜잭션은 논리적 세션의 개념으로 만들어졌기 때문에, 레플리카 셋 환경에서만 가능한 oplog 와 같은 기술이 필요하다고 한다.
여기서 oplog는 레플리카 셋의 데이터 동기화를 위해 내부에서 발생한 로그를 기록한 것을 뜻한다.
레플리카 셋은 세가지 역할로 나눌 수 있다.
여기서 HeartBeat는 Replica Set내의 node 간 정해진 초마다 서로에게 ping를 보낸다. 만약 Heartbeat가 특정 초마다 수신되지 않으면 해당 DB 가 죽었다 판단하고 다른 node끼리 Election을 준비한다.
Replica Set의 구성에는 두가지의 방법이 있다
PSS는 하나의 Primary + 2개의 Secondary 로 구성되어 있으며, Secondary가 2개나 있어 높은 안정성을 보장한다.
PSA는 Primary, Secondary, Arbiter 이 각각 하나씩 있는 구성이다.
Arbiter는 데이터를 저장하지는 않고 Primary 장애시 Secondary 중 누구를 Primary로 대체할지 Election 하는 기능을 가지고 있다.
PSS 보단 안정성이 낮으나 서버 리소스를 덜 잡아 먹는 장점이 있다.
먼저 도커 컨테이너 끼리 통신할 외부 네트워크를 생성한다.
> docker network create mongoCluster
docker-compose.yml
services:
mongodb1:
image: mongo
hostname: mongodb1
container_name: mongodb1
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: <username>
MONGO_INITDB_ROOT_PASSWORD: <password>
volumes:
- ./mongo/mongo1/mongod.conf:/etc/mongod.conf
- ./key/mongodb.key:/etc/mongodb.key
- ./data/mongodb1:/data/db
command: mongod --replSet rs0 --port 27017 --keyFile /etc/mongodb.key --bind_ip_all
ports:
- 27017:27017
networks:
- mongoCluster
mongodb2:
image: mongo
hostname: mongodb2
container_name: mongodb2
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: <username>
MONGO_INITDB_ROOT_PASSWORD: <password>
volumes:
- ./mongo/mongo2/mongod.conf:/etc/mongod.conf
- ./key/mongodb.key:/etc/mongodb.key
- ./data/mongodb2:/data/db
command: mongod --replSet rs0 --port 27018 --keyFile /etc/mongodb.key --bind_ip_all
ports:
- 27018:27018
networks:
- mongoCluster
depends_on:
- mongodb1
mongodb3:
image: mongo
hostname: mongodb3
container_name: mongodb3
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: <username>
MONGO_INITDB_ROOT_PASSWORD: <password>
volumes:
- ./mongo/mongo3/mongod.conf:/etc/mongod.conf
- ./key/mongodb.key:/etc/mongodb.key
- ./data/mongodb3:/data/db
command: mongod --replSet rs0 --port 27019 --keyFile /etc/mongodb.key --bind_ip_all
ports:
- 27019:27019
networks:
- mongoCluster
depends_on:
- mongodb1
networks:
mongoCluster:
external: true
MongoDB의 conf 파일은 etc/mongod.conf
에 저장된다.
mongoDB가 총 3개이고 port 가 각기 다르므로 3개를 만들어야 했다.
# mongod.conf
# Where and how to store data.
storage:
dbPath: /var/lib/mongodb
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1
# how the process runs
processManagement:
timeZoneInfo: /usr/share/zoneinfo
# Keyfile 위치 설정
security:
authorization: enabled
clusterAuthMode: keyFile
keyFile: /etc/mongodb.key
# Replica Set 이름 설정
replication:
replSetName: rs0
mkdir key
cd key
openssl rand -base64 756 > mongodb.key
chmod 400 mongodb.key
chown 999:999 mongodb.key
mac 유저라면 위와 같이 간단히 키 설정을 할 수 있으나
윈도우 유저라면 위와 같은 방식으로는 키 설정이 되지 않을 것이다.(아마도)
필자 노트북은 Windows라 위의 방법을 사용하지 못했다. (맥북 갖고 싶다)
결국 이 블로그 글을 통해 Docker의 volume mount 기능을 활용하여 권한 설정을 할 수 있었다.
3-1. nginx 생성을 위한 docker-compose.yml 생성
cd nginx
vim docker-compose.yml
docker-compose.yml
version: "3"
services:
nginx:
image: nginx
container_name: nginx
ports:
- "3001:80"
volumes:
- ./key:/key
3-2. nginx 실행
docker-compose up -d
3-3. docker desktop나 docker exec -it nginx bash
로 컨테이너 접속 후 키 파일 생성
cd /key
openssl rand -base64 756 > mongodb.key
chmod 400 mongodb.key
chown 999:999 mongodb.key
이러면 volume 마운트로 인해 권한 설정이 된 키 파일이 로컬에도 생성 될 것이다.
(참고로 docker-compose는 해당 파일 기준으로 파일위치를 설정해줘야 한다. 만약 docker-compose.yml이 /filename
에 위치하고 key file이 /filename/key
에 위치한다면 volume에는 키 위치를 ./key/mongodb.key
로 설정해주어야 한다. )
docker-compose up -d
이후 docker desktop의 mongodb1의 exec에 들어가서 replication set 초기화를 해주면 된다.
# mongosh 접속
mongosh -u <username> -p <password>
# 사용자 전환
test> use admin
# 초기화
admin> rs.initiate({_id:"rs0", members:[{_id: 0, host:"mongodb1:27017"},{_id: 1, host: "mongodb2:27018"},{_id: 2, host: "mongodb3:27019"}]})
rs.status() 로 확인
Primary DB shell에 테스트로 DB와 collection 을 생성 한 후 데이터를 insert 한 후, Secondary에서 확인해보자
# Primary DB exec
rs0 [direct: primary] admin> use testdb
switched to db testdb
rs0 [direct: primary] testdb> db.createCollection("testCollection")
{ ok: 1 }
rs0 [direct: primary] testdb> show dbs
admin 140.00 KiB
config 192.00 KiB
local 452.00 KiB
testdb 8.00 KiB
rs0 [direct: primary] testdb> db.testcollection.insert({"name": "test"})
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
acknowledged: true,
insertedIds: { '0': ObjectId('65fa2e223456c76da7db83b0') }
}
rs0 [direct: primary] testdb> db.testcollection.find()
[ { _id: ObjectId('65fa2e223456c76da7db83b0'), name: 'test' } ]
############# Secondary DB exec ###############
rs0 [direct: secondary] test> rs.secondaryOk()
rs0 [direct: secondary] test> use admin
switched to db admin
rs0 [direct: secondary] admin> db.auth("root", "password1!")
{ ok: 1 }
rs0 [direct: secondary] admin> show dbs
admin 140.00 KiB
config 244.00 KiB
local 468.00 KiB
testdb 48.00 KiB
rs0 [direct: secondary] admin> use testdb
switched to db testdb
rs0 [direct: secondary] testdb> db.testcollection.find()
[ { _id: ObjectId('65fa2e223456c76da7db83b0'), name: 'test' } ]
보다시피 Primary에서 insert한 데이터가 Secondary에도 저장되있는 것을 확인할 수 있다.
+) 추가적으로 windows에서 C:\Windows\System32\drivers\etc\hosts
에 아래와 같은 내용을 추가하여 도커의 호스트 이름과 로컬 IP 주소를 매핑 시켜줘야 한다.
127.0.0.1 mongodb1
127.0.0.1 mongodb2
127.0.0.1 mongodb3
(mac의 경우 /etc/hosts
에 추가)