yaml

version: "3.8"

volumes:
  mongo-db-local: { }

services:
  mongo-1:
    image: mongo
    container_name: my-mongo-1
    ports:
      - "30000:30000"
    volumes:
      - mongo-db-local:/data1/db
    command: mongod --replSet anna-replSet --port 30000 --bind_ip_all

  mongo-2:
    image: mongo
    container_name: my-mongo-2
    ports:
      - "30001:30001"
    volumes:
      - mongo-db-local:/data2/db
    command: mongod --replSet anna-replSet --port 30001 --bind_ip_all

  mongo-3:
    image: mongo
    container_name: my-mongo-2
    ports:
      - "30002:30002"
    volumes:
      - mongo-db-local:/data3/db
    command: mongod --replSet anna-replSet --port 30002 --bind_ip_all

  mongo-setup:
    image: mongo
    depends_on:
      - mongo-1
      - mongo-2
      - mongo-3
    restart: "no"
    entrypoint: ["bash", "-c", "sleep 5 && mongo --host my-mongo-1:30000 --eval 'rs.initiate({\"_id\":\"anna-replSet\",\"members\":[{\"_id\":0,\"host\":\"my-mongo-1:30000\"},{\"_id\":1,\"host\":\"my-mongo-2:30001\"},{\"_id\":2,\"host\":\"my-mongo-3:30002\"}]})'"]

 

커맨드

docker-compose -f local-mongo.yaml -p local-mongo up -d

 

레플리카셋 구성까지 잘 되지만 완전하지 않다. MongoDB Compass로 레플리카셋 접속을 하기 위해서는 로컬의 /etc/hosts 파일에 아래 내용을 추가해주어야 한다.

my-mongo-1 127.0.0.1
my-mongo-2 127.0.0.1
my-mongo-3 127.0.0.1

 

 

이 작업을 하지 않으면 my-mongo-{n} 호스트를 찾을 수 없다는 따위의 오류가 발생한다. 커넥션 url에 "localhost" 를 사용해도 이 오류가 표시된다. 오류 내용으로 추측건대, MongoDB Compass는 먼저 지정된 노드로 접근하여 레플리카셋 정보를 쿼리하고 그 안의 호스트 이름으로 접근을 시도하는 것 같다. 몽고디비가 가진 레플리카셋 정보에는 rs.initiate()에 인자로 넘긴 호스트 정보가 담겨있기에, "my-mongo-1" 를 반환하는데, 이게 MongoDB Compass는 이게 로컬호스트인지  알 수 없다. 때문에 위 작업이 필요한 것이다.

반면 스프링부트를 사용한 몽고디비 연결 시에는 localhost 로 지정해도 연결이 잘 된다. 아마 둘의 연결 방식에 차이가 있는 것 같다.

 

====

아래 내용을 몰랐다가 굉장히 고생했다. 역시 쉽게쉽게 가려고 하면 더 힘들어진다....

 

- Docker Compose는 기본적으로 하나의 default network를 생성하고, 각 서비스(컨테이너)는 이 네트워크에 조인한다. 때문에 별도 설정 없이도 컨테이너끼리 자신의 이름을 통해 서로를 식별하고 접근할 수 있다.

 

- network_mode: "host" 를 사용하면 컨테이너끼리는 "localhost"로 통신 가능하지만, 브라우저 등 컨테이너 외부로부터의 localhost 호출은 불가능하다. 알아본 바에 의하면, mac에서의 도커는 리눅스 버추얼머신을 사용하며, 컨테이너는 이 안에 위치한다. network_mode: "host" 설정은 이 버추얼머신 안에서만 유효하기 때문에, 함께 버추얼머신 안에 위치한 컨테이너끼리는 서로 접근 가능하지만 외부에서는 접근이 불가능하다. 이 network_mode: "host"의 사용 목적에 완전히 반하는 동작으로 보인다. (다른 os는 확인하지 않음)

 

 

'DB > MongoDB' 카테고리의 다른 글

도커로 레플리카셋 구성하기  (0) 2022.07.14
트랜잭션에 관하여  (0) 2022.07.14

1. 도커 네트워크 생성

docker network create {network_name}

 

2. 컨테이너 생성

docker run -p {exposing_port1}:27017 --name mongo1 -d --net {network_name} mongo mongod --replSet mongo-set
docker run -p {exposing_port2}:27017 --name mongo2 -d --net {network_name} mongo mongod --replSet mongo-set
docker run -p {exposing_port3}:27017 --name mongo3 -d --net {network_name} mongo mongod --replSet mongo-set

 

3. 레플리카셋 생성

docker exec -it mongo1 mongo

접속 후 몽고쉘에서

config = {
	"_id": "mongo-set",
    "members": [
		{"_id":0, "host":"mongo1:27017"},
		{"_id":1, "host":"mongo2:27017"},
		{"_id":2, "host":"mongo3:27017"}
	]
}

rs.initiate(config)
// output: { "ok" : 1 }

 

4. 확인

아무 컨테이너에 접속해서 아래 커맨드 입력하면 primary, secondaries 등 레플리카 맴버들의 상태, 셋 이름 및 기타 정보가 표시됨.

db.runCommand("ismaster")

 

 

하지만 이 방법으로 하면 MongoDB Compass로 레플리카셋 접근이 되지 않는다. 추측건대 MongoDB Compass는 첫 노드에 접속하고 다른 레플리카 맴버의 정보를 노드를 통해 받아서 접속하는 것 같다. 때문에 mongo{N} 호스트를 알 수 없다는 오류가 뜬다. /etc/hosts 에 등록하면 호스트는 인식하지만 27017 포트에 접근할 수 없다는 오류가 뜬다. 때문에, MongoDB Compass를 사용하려면 몽고디비 실행 시 기본 포트를 사용하지 않고 직접 포트를 지정하되, 레플리카 맴버간 겹치지 않는 포트를 사용하도록 한다. 그리고 도커 포트 매핑을 이 포트로 사용하여 레플리카 맴버간 포트가 겹치지 않게 설정하면 해결할 수 있다.

 

 

 

'DB > MongoDB' 카테고리의 다른 글

Docker Compose 를 이용한 one-command 레플리카셋 구성  (0) 2022.07.15
트랜잭션에 관하여  (0) 2022.07.14

몽고디비의 트랜잭션은 트랜잭션 안에서 연산이 되는 document의 수에 따라 single-document / multi-document 로 나눌 수 있다. 

multi-document 트랜잭션은 여러 documents와 컬렉션에 대한 nomalizing 작업이 필요한데 반하여, single-document 트랜잭션은 embedded documents와 array를 사용하여 데이터간의 관계를 캡처하는 일이 가능하기 때문에 기본적으로 atomic하다. 당연히 multi-document 트랜잭션이 훨씬 어려운 일이고, 비용이 비싼 작업이다.

 

몽고디비 버전 4.0 에서는 레플리카셋에 대한 multi-document 트랜잭션을 지원한다.

버전 4.2 부터는 여기에 더하여 distributed transactions(분산 트랜잭션)이 등장했는데, 샤딩된 클러스터들에 대한 multi-document 트랜잭션을 지원한다.

 

multi-document 트랜잭션은 atomic 하며, 다음과 같은 성질을 지닌다.

  • 트랜잭션 안에서 커밋되지 않은 상태의 변경은 트랜잭션 밖에서 볼 수 없다. 변경된 데이터는 오직 커밋 후에만 보여진다.
  • 여러 샤드에 대한 트랜잭션일 때, 트랜잭션 안에서 변경 중인 데이터라고 해서 트랜잭션 밖에서 무조건 볼 수 없는 것은 아니다. "local" 모드의 read 연산을 통해 아직 모든 샤드에 대한 트랜잭션이 완료되지는 않았어도 특정 샤드에는 커밋된 트랜잭션 데이터는 볼 수 있다. 예를 들어 샤드 A, B 에 각각 데이터 1, 2 를 입력하는 트랜잭션이 있다면, 샤드 A 트랜잭션이 커밋되면 샤드 B 트랜잭션이 완료되지 않았어도 샤드 A 에서의 local read 연산은 데이터 1을 볼 수 있다.
  • 트랜잭션이 취소되면 모든 변경은 롤백된다.

 

WiredTiger 스토리지 엔진의 부담

몽고비디의 write는 먼저 그 내용을 WiredTiger의 공유 캐시에 적용하고 나중에 디스크에 write하는 방식이다. 트랜잭션이 작동하는 중에는 트랜잭션이 시작한 시점의 스냅샷에 모든 write를 보관하고 있어야 한다. 트랜잭션 안에서 데이터는 일관성을 보장해야 하기 때문에, 트랜잭션 중간에 발생하는 write들은 바로 적용되지 못하고 캐시에 쌓이게 된다. 이 write들은 트랜잭션이 커밋 혹은 롤백될 때까지 flush되지 못하고 남아 있다가, 트랜잭션이 종료되면 락이 풀리고 WiredTiger는 스냅샷을 폐기할 수 있게 된다. 이런 작동 방식으로 인해 장시간 유지되는 트랜잭션 혹은 한 트랜잭션 안에서 너무 많은 양의 데이터 변경이 발생하면 WiredTiger에게 부담이 된다.

 

트랜잭션 타임아웃

위의 이유로, 몽고디비는 multi-document 트랜잭션에 대해 기본적으로 60초의 타임아웃이 적용된다. 60초를 초과하는 트랜잭션은 실패로 처리한다. 당연히 이 시간은 조정 가능하다. 하지만 되도록 이 시간을 조정하기보다는 트랜잭션을 가능한대로 잘게 분리하여 실행하는 편이 바람직하다.

 

한 트랜잭션 안에서의 연산 수

몽고디비에 한 트랜잭션 안에서 연산의 수를 제한하지는 않는다. 하지만 best practice 제안으로는 한 트랜잭션 안에서 변경되는 documents는 1000개를 넘지 않는 것이 좋다. 만약 1000개의 documents를 초과하는 트랜잭션이 필요하다면, 역시 마찬가지로 가능한대로 이를 나누어 처리하는 편이 바람직하다.

 

분산 트랜잭션

여러 샤드에 대한 트랜잭션은 다른 여러 노드와의 네트워크 통신을 사용하는 코디네이션을 필요하기 때문에 훨씬 큰 비용이 필요하다. 여러 샤드에 대한 데이터 일관성을 보장하는 read concern은 Snapshot 뿐이다. 만약 여러 샤드에 대한 읽기 일관성이 중요하지 않은 상황이라면, 트레이드오프로 read concern 으로 local 을 취하는 것도 방법이다.

 

예외 핸들링

당연하게도 몽고디비 트랜잭션 실패 시 모든 변경을 롤백된다. 실패 요인이 네트워크 에러와 같은 임시적인 성질을 가지고 있다면, 예외를 캐치하여 트랜잭션을 재시도하는 편이 바람직하다. 네트워크 에러 외에 예상할 수 있는 실패 요인에는 MVCC write 충돌, primary 레플리카 선정 등이 있다. 몽고디비 클라이언트에는 retryable write를 자동으로 지원하는 연산들이 있다. 이에 대해서는 아래 페이지를 참조한다:

https://www.mongodb.com/docs/manual/core/retryable-writes/

multi-document 트랜잭션의 경우 commit or abort 연산이 실패하는 경우 retryWrite=false 설정이라도 연산을 한 번 재시도한다.

 

쓰기 지연

multi-document 트랜잭션을 사용함으로써 얻어지는 성능상 이점에는 commit 지연 감소가 있다. w:majority write concern을 사용하면 10번의 데이터 변경 연산은 각각 10번의 리플리케이션을 대기해야 한다. 1번 부터 10번 까지의 update가 모두 레플리카들에 전파될 때까지 기다려야 한다는 뜻이다.

반면 10번의 변경이 한 트랜잭션 안에서 작동하면 리플리케이션은 오직 한 번, commit 시점에만 이루어진다. 이는 지연을 상당량 줄일 수 있다.

 

적절한 write concern

몽고디비는 write 연산에 대한 영구성 보장 수준을 조정할 수 있는 write concern이라는 컨셉을 제공한다. write concern은 single/multi-document 트랜잭션 모두에 적용 가능하다.

  • Write Acknowledged: default concern이다. 몽고디비는 모든 write 연산 시도에 대해 각각 성공 여부 확인하며, 클라이언트는 네트워크, 키 중복, 스키마 벨리데이션 및 다른 예외들을 캐치할 수 있다.
  • Journal Acknowledged: 몽고디비는 write 연산의 성공 여부를 primary의 journal 에 flush된 이후에만 확인한다. 이 수준의 설정으로 몽고디비에 문제가 발생한 경우 write 연산이 복구되어 디스크에 저장됨을 보장할 수 있다.
  • Replica Acknowledged: 몽고디비가 write 연산이 적용되는 레플리카 맴버들의 acknowledgment 를 대기하도록 한다. 몇 개의 레플리카 맴버의 ack를 받으면 성공으로 볼 것인지 그 숫자를 설정할 수 있다. 또한 write 연산이 secondaries의 journal에까지 적용됨을 보장한다. 
  • Majority: 몽고디비는 write 연산이 과반수의 레플리카와 electable 맴버에 적용될 때까지 대기한다. 이렇게 함으로써 write 연산은 primary election이 이벤트가 발생해도 롤백되지 않으며, primary를 포함하여 여기에 포함된 모든 레플리카들의 journal에 적용됨을 보장한다.

 

적절한 read concern

몽고비디는 read 연산에 대한 데이터 읽기 보장 수준을 조정할 수 있는 read concern이라는 컨셉을 제공한다. 이는 dbms에서 흔히 등장하는 isolation level이라고 볼 수 있다. write concern과 마찬가지로 single/multi-document 트랜잭션 모두에 적용 가능하다.

  • local: 레플리카셋 즉 리플리케이션을 고려하지 않는 읽기 모드이다. 때문에 이 모드로 읽은 데이터는 롤백될 가능성이 있다. 기본적으로 primary, secondaries에 적용되며, 쿼리 대상 데이터가 여기에만 적용되었다면 데이터를 읽는다. causally consistent session, 트랜잭션 사용 여부에 관계없이 사용할 수 있는 모드이다.
  • available: 레플리카셋 즉 리플리케이션을 고려하지 않는 읽기 모드이다. 때문에 이 모드로 읽은 데이터는 롤백될 가능성이 있다. causally consistent session, 트랜잭션 사용 시 사용할 수 없는 모드이다. 이 모드를 샤딩된 클러스터에 사용할 경우 가장 낮은 읽기 지연을 보인다. 하지만 orphaned document(실패 혹은 불완전한 데이터 처리의 결과로 청크로 남아 있는 데이터)가 함께 읽혀질 사능성이 있다. 이 가능성을 제거하기 위해서는 local 을 사용해야 한다.
  • majority: 과반수의 레플리카 맴버로부터 응답을 받은, 즉 리플리케이션된 데이터를 읽는 모드이다. 이 모드로 읽은 데이터는 안정성이 보장된다. majority-commit 포인트의 in-memory view를 통해 반환될 수 있는 데이터만이 이 모드에서 유효한 데이터로 판단된다. 때문에 다른 모드에 비해 성능상 치러야 할 비용이 존재한다. causally consistent session, 트랜잭션 사용 여부에 관계없이 사용할 수 있는 모드이다. 레플리카셋은 반드시 WiredTiger 스토리지 엔진을 사용해야 한다.
  • linearizable: 읽기 연산이 시작하는 시점 전에 과반수의 레플리카 맴버에게서 write 연산이 성공했다는 응답을 받은 데이터만을 읽는다. 이 모드의 읽기 연산이 write 연산과 동시에 발생할 경우, write 연산이 과반수의 레플리카 맴버에게 전파될 때까지 대기하게 된다. read 연산 이후 과반수의 레플리카 맴버에 장애가 발생하거나 재시작되는 경우, read 연산으로 읽은 데이터의 안정성(durability)은  writeConvernMajorityJournalDefault 설정값에 따라 다르게 판단할 수 있다. 설정값이 true(디폴트)라면 데이터는 안전하다. false라면 몽고디비는 w: "majority" write 연산이 디스크 journal에 적용되기까지 대기하지 않기 때문에, write 연산은 롤백될 가능성이 존재한다. 이 읽기 모드는 primary에만 사용할 수 있으며, causally consistent session, 트랜잭션 사용 시 사용할 수 없다.
  • snapshot: read 연산의 트랜잭션 작동 세션이 causally consistent session이 아닌 경우에, majority write concern에서의 트랜잭션 커밋은 majority-committed 데이터의 스냅샷으로부터 읽는다. read 연산의 트랜잭션 작동 세션이 causally consistent session인 경우에도 마찬가지로 majority write concern에서의 트랜잭션 커밋은 majority-committed 데이터의 스냅샷으로부터 읽는데, 이 데이터는 바로 직전에 시작된 트랜잭션에 causal consistency를 제공한다.

 

Causal Consistency

몽고디비 버전 3.6부터 등장한 개념이다. 연산이 앞선 연산에 논리적인 의존성을 가지는 경우, 이 두 연산은 causal relationship을 가졌다고 표현한다. 예를 들어, 특정 조건을 만족하는 모든 documents를 삭제하는 write 연산이 있고, 이어서 원하는 모든 documents가 잘 삭제되었는지 검증하기 위한 read 연산이 있을 때, 이 두 연산은 causal relationship을 가지며, causal relationship을 함께 가지는 연산들을 causal 연산이라고 한다.

몽고디비는 causally consistent session에서 causal 연산들을 causal relationship의 순서에 맞춰 실행한다. 쉽게 말해 연산이 인과관계에 맞게 실행 순서를 맞춘다는 뜻이다.

몽고디비 3.6은 클라이언트 세션에 causal consistency를 활성화한다. Causal consistency session은 majority read concern, majority write concern 연산들의 순서가 causal relationship을 가졌음을 나타내어 실행 순서가 보장되어야 함을 표시한다. 이 때, 클라이언트 어플리케이션에서는 반드시 한번에 한 쓰레드만이 이 연산들을 시도하도록 보장하여야 한다.

 

Causal Consistency는 필요할 때만 사용한다

Causal consistency는 한 클라이언트 세션의 write-read 연산에서 이 때 어떤 레플리카가 연산을 수행했는지와 상관없이 read 연산은 반드시 write 연산의 결과를 볼 수 있도록 보장한다. 이 보장은 비용을 요구한다. Causal consistency는 단조적 읽기 보장(monotonic read guarantees)이 필요한 곳에만 사용함으로써 causal consistency에 의한 지연을 최소화할 수 있다.

 

레퍼런스

https://www.mongodb.com/blog/post/performance-best-practices-transactions-and-read--write-concerns

https://www.mongodb.com/docs/upcoming/core/transactions/

https://www.mongodb.com/docs/manual/reference/read-concern/

https://www.mongodb.com/docs/manual/core/read-isolation-consistency-recency/?&_ga=2.186870447.620770834.1657759959-1870444183.1654842094#std-label-causal-consistency 

+ Recent posts