/ PHP

MongoDB: oplog.rs 를 이용해 직접 리플리케이션 만들기

MySQL처럼 Replication 기능이 존재하지 않는 MongoDB에서, 동일한 기능을 구현하고 싶은 이를 위해 mongo-connector를 소개한다.

이 어플리케이션은 아래와 같이 생겨먹은 config 파일을 통해 특정 서버에서 원하는 서버의 원하는 데이터베이스, 혹은 원하는 컬렉션 등으로 데이터를 지속해서 동기화해주는 도구이다. 원리는 local.oplog.rstailable 기능을 통해 지속해서 데이터를 가져와서 넘겨주는 것으로 예상이 된다.

{
  "mainAddress": "<host>:27017",
  "oplogFile": "/usr/src/data/oplog.timestamp",
  
  "authentication": {
    "adminUsername": "username",
    "password": "password"
  },

  "namespaces": {
    "mydb.users": {
      "rename": "newdb.users"
    }
  },

  "docManagers": [
    {
      "docManager": "mongo_doc_manager",
      "targetURL": "mongodb://<user>:<PASSWD>@localhost:30000/test?authSource=admin"
    }
  ]
}

사용법은 복잡하지 않다. 도커를 사용하면 덜 귀찮게 작업을 할 수 있으니 아래 Dockerfile 파일을 참고해서 mongo-connector 이미지를 만들어 실행하자.

Docker 세팅하기

Dockerfile

FROM python:3

WORKDIR /usr/src
USER root

RUN pip install mongo-connector \
&&  mkdir /var/log/mongo-connector

ENTRYPOINT ["mongo-connector"]

config.json

샘플 컨피그는 여기서 주울 수 있다. 컨피그에 대한 문서는 여기서 주울 수 있다. 대부분이 자세하게 설명되어 있다. 보충 설명은 아래에 첨부한다. // 를 통해서 설명하였는데, JSON은 // 가 올바른 문법이 아니니 실제 사용할 때에는 삭제하고 쓰기 바란다.

{
    "__comment__": "Configuration options starting with '__' are disabled",
    "__comment__": "To enable them, remove the preceding '__'",

    // 원본 서버의 주소이다. 샤딩되어 있을 경우 mongos 주소를 입력하고, 샤딩이 되어 있지 않은 경우 모든 레플리카들을 입력한다.
    // 샤딩되어 있을 경우에는 localhost:27017 처럼 호스트와 포트만 입력하도록 하고, 모든 레플리카를 입력할 때는 URI String 형식으로 입력해야한다.
    "mainAddress": "localhost:27017",
    // 작업을 다시 Restart 할 때 어디서부터 따라갈지 기록 하는 파일이다. 여러번 재시작 할 계획이 있다면 필수로 설정해야한다.
    "oplogFile": "/var/log/mongo-connector/oplog.timestamp",
    // 이 값을 false 로 하면 싱크 시작 전에 모든 테이블 데이터를 한번 통째로 덤프해서 가져온다. 이 값을 true 로 하면 그러지 않는다. 미리 dump 를 해둔 상태라면 이 값을 true 로 실행하면 된다.
    "noDump": false,
    "batchSize": -1,
    "verbosity": 0,
    "continueOnError": false,

    // type 을 stream 으로 하면 stdout 으로 내용을 볼 수 있다.
    "logging": {
        "type": "file",
        "filename": "/var/log/mongo-connector/mongo-connector.log",
        "__format": "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
        "__rotationWhen": "D",
        "__rotationInterval": 1,
        "__rotationBackups": 10,

        "__type": "syslog",
        "__host": "localhost:514"
    },

    // mainAddress 를 host:port 만 했을 경우, 여기에서 인증 정보를 입력해줘야한다.
    "authentication": {
        "__adminUsername": "username",
        "__password": "password",
        "__passwordFile": "mongo-connector.pwd"
    },

    "__comment__": "For more information about SSL with MongoDB, please see http://docs.mongodb.org/manual/tutorial/configure-ssl-clients/",
    
    // SSL이 활성화 되어 있을경우 이 옵션을 켜야한다. 만약 별도 인증서 파일이 존재하지 않으면서 SSL이 활성화 되어 있는 경우 sslCertificiatePolicy 를 optional 로 해주면 된다.
    "__ssl": {
        "__sslCertfile": "Path to certificate to identify the local connection against MongoDB",
        "__sslKeyfile": "Path to the private key for sslCertfile. Not necessary if already included in sslCertfile.",
        "__sslCACerts": "Path to concatenated set of certificate authority certificates to validate the other side of the connection",
        "__sslCertificatePolicy": "Policy for validating SSL certificates provided from the other end of the connection. Possible values are 'required' (require and validate certificates), 'optional' (validate but don't require a certificate), and 'ignored' (ignore certificates)."
    },

    "__fields": ["field1", "field2", "field3"],

    // 카피할 대상 테이블들을 지정한다. 지정하지 않으면 모든 디비와 테이블들을 시도 한다.
    "__namespaces": {
        "excluded.collection": false,
        "excluded_wildcard.*": false,
        "*.exclude_collection_from_every_database": false,
        "included.collection1": true,
        "included.collection2": {},
        "included.collection4": {
            "includeFields": ["included_field", "included.nested.field"]
        },
        "included.collection5": {
            "rename": "included.new_collection5_name",
            "includeFields": ["included_field", "included.nested.field"]
        },
        "included.collection6": {
            "excludeFields": ["excluded_field", "excluded.nested.field"]
        },
        "included.collection7": {
            "rename": "included.new_collection7_name",
            "excludeFields": ["excluded_field", "excluded.nested.field"]
        },
        "included_wildcard1.*": true,
        "included_wildcard2.*": true,
        "renamed.collection1": "something.else1",
        "renamed.collection2": {
            "rename": "something.else2"
        },
        "renamed_wildcard.*": {
            "rename": "new_name.*"
        },
        "gridfs.collection": {
            "gridfs": true
        },
        "gridfs_wildcard.*": {
            "gridfs": true
        }
    },

    // 카피 될 목적지를 지정한다. elasticsearch, mongodb, solr 을 적을 수 있다. 자세한건 문서 참고.
    "docManagers": [
        {
            "docManager": "elastic_doc_manager",
            "targetURL": "localhost:9200",
            "__bulkSize": 1000,
            "__uniqueKey": "_id",
            "__autoCommitInterval": null
        }
    ]
}

Run Shell

$ docker build . -t mongo-connector
$ docker run -it --rm -v ${PWD}/config.json:/usr/src/config.json mongo-connector

실행 결과

1
정상적으로 싱크가 되고 있는 상태

정상적으로 싱크가 된다면 위와 같은 화면을 볼 수 있다. 2900만건의 데이터를 가졌고 4개의 샤드로 구성된 MongoDB를 noDump=false 옵션과 함께 싱크되고 있는 모습이다. 3번 샤드만 성공했고, 0, 1, 2번 샤드는 아직 작업중이다. 모든 샤드가 성공이 되면, oplog.timestamp 파일은 대략 아래처럼 등록이 된다.

[["pubg-db-cluster-shard-3", 6509515514002276374], ["pubg-db-cluster-shard-0", 6509495168742195731], ["pubg-db-cluster-shard-1", 6509495173037162512], ["pubg-db-cluster-shard-2", 6509495173037162548]]

참고

여러 번 테스트를 하다 보면 초기 dump 과정이 오래 걸려서 mongodumpmongorestore 를 이용해 직접 덤프/복구를 하고, noDump 옵션을 켜고 프로그램을 이용하면 된다.

687474703a2f2f692e696d6775722e636f6d2f68673668774c6b2e676966

만약 mongodumpmongorestore가 귀찮다면 간단하게 스크립트 화 시켜놓은 mongo-sync를 사용하면 편하다. (위 그림) SSH 터널도 쉽게 할 수 있도록 구현되어 있으니 유용하게 사용할 수 있을 것이다.


(참고) 현재 noDump = true 옵션을 이용시 정상적으로 동작하지 않는다.