본문 바로가기

카테고리 없음

몽고DB 샤딩

반응형

 

샤딩이란 여러 장비에 걸쳐 데이터를 분할하는 과정을 의미하며, 때로는 파티셔닝이란 용어로도 불린다.

장점
- 데이터를 여러 장비에 나누어 넣음으로써, 더 크거나 강력한 장비 없이도, 많은 데이터를 저장하고 부하를 처리할 수 있다.
- 더 자주 접근하는 데이터를 성능이 더 좋은 HW를 가진 서버에 배치하거나 지역에 따라 데이터셋을 분할해 주로 접근하는서버에서 데이터를 찾을 수 있다. 

 

몽고DB는 애플리케이션에서 구조를 추상화하고 시스템 관리를 간단하게 하는 자동 샤딩을 지원한다.
(수동 샤딩은 대부분의 DBMS에서 지원 가능하지만, 데이터 분산이나 부하 패턴이 변화할 때는 유지하기 어렵다.)



 

클러스터 구성 요소 이해하기


샤딩의 한 가지 목적은 2개, 10개, 1000개의 샤드 클러스터가 하나의 장비처럼 보이게 하는 것이다. 이를 위해, 샤드 앞단에는 mongos라는 라우팅 프로세스가 존재한다. mongos는 어떤 샤드가 어떤 데이터를 포함하고 있는지를 알려주는 '컨텐츠 목차'가 있다. 애플리케이션은 라우터(mongos)에 연결해 요청을 날리면, 라우터는 찾고자하는 데이터가 어떤 샤드에 있는지 알기 때문에 해당 요청을 적절한 샤드(들)로 전달할 수 있다. 

 


 

 

단일 장리 클러스터에서의 샤딩

# --nodb 및 --norc 옵션을 사용해 mongo 셸을 시작한다.
$ mongo --nodb --norc

#클러스터를 만들려면 ShardingTest 클래스를 사용한다. 
#해당 클래스는 내부용으로 설계된 클래스이므로 외부적으로 문서화되지 않았다.
#그러나 몽고DB 서버와 함께 제공되므로 샤드 클러스터를 실험하는 가장 간단한 수단임
$ st = ShardingTest({
    name:"one-min-shards",         # name은 샤드 클러스터에 대한 레이블
    chunkSize:1,                   # 
    shards:2,                      # 클러스터가 두 개의 샤드로 구성되도록 지정
    rs:{                           # 각 샤드를 oplogSize가 10MiB인 3-노드 복제 셋으로 정의
       nodes:3,
       oplogSize:10
    },
    other:{
       enableBalancer:true
    }
});

# ShardingTest는 컴퓨터에 /data/db 디렉터리가 있다고 가정한다. 
# ShardingTest 실행이 실패하면 디렉터리를 만든 후 명령을 다시 실행해보자

 

명령을 실행하면 여러 작업이 수행된다.
1. 두 개의 샤드가 있는 새 클러스터를 생성하며, 각 샤드는 복제 셋이다.
2. mongos를 시작해 전체 샤드의 요청을 관리하며, 따라서 클라이언트는 독립형 mongod와 통신하듯 클러스터와 상호작용할 수 있다.
3. 마지막으로 구성 서버(config server)에 대한 추가 복제 셋을 시작하는데, 이 서버는 쿼리를 올바른 샤드로 전달하는 데 필요한 라우팅 테이블 정보를 유지한다.

 

ShardingTest가 클러스터 설정을 마치면 연결 가능한 실행 중인 프로세스는 10개다.
노드 3개로 구성된 복제 셋 2개, 노드 3개로 구성된 구성 서버 복제셋 1개, mongos 1개가 있다.

mongos는 포트 20009에서 실행해야 한다. 다음으로는 mongos에 연결한다. 다른 터미널 창을 열어 다른 mongo 셸을 시작하자.

# mongo --nodb

#아래 셸을 통해 mongos에 연결한다.
> db= (new Mongo("localhost:20009")).getDB("accounts")

이를 통해 셸은 클라이언트이고, mongos에 연결된다.
이제 mongos에 요청을 전달할 수 있으며, mongos는 요청을 샤드로 라우팅한다.

 

프라이머리 샤드란 '홈 베이스' 샤드이며, 각 데이터베이스마다 무작위로 선택된다. 프라이머리 샤드는 복제 셋의 프라이머리와 다르다. 프라이머리 샤드는 샤드를 구성하는 전체 복제셋을 가리킨다. (복제 셋의 프라이머리는 쓰기를 수행할 수 있는 복제 셋의 단일 서버다.)

모든 데이터는 프라이머리 샤드에 있다. 몽고DB는 아직 데이터를 분산할 방법을 모르기 때문에 아직 데이터를 자동으로 분산할 수 없다. 컬렉션마다 데이터를 어떻게 분산할지 알려야 한다.

특정 컬렉션을 샤딩하려면 먼저 컬렉션의 데이터베이스에서 샤딩을 활성화 한다. 

> sh.enableSharding("accounts")

이제 accounts 데이터베이스에서 샤딩이 활성화 돼 DB 내에서 컬렉션을 샤딩할 수 있다.

컬렉션을 샤딩할 때 샤드 키를 선택하는데, 이는 몽고DB가 데이터를 분할하는 데 사용하는 필드다. 

예를 들어, "학번"을 샤드 키로 선택하면 몽고DB는 데이터를 범위로 나눈다. "00000~49999", "50000~99999"로 나눈다.
즉, 인덱스와 비슷하다. 샤드 키를  선택하는 것은 컬렉션 내 데이터 순서를 선택하는 것으로 생각할 수 있다.

컬렉션이 커질수록 샤드 키가 컬렉션에서 가장 중요한 인덱스가 된다. 샤드 키를 만들려면 필드에 인덱스를 생성해야 한다.

# 샤드 키로 사용하려는 필드에 인덱스 생성
문법 > db.users.createIndex({"필드명": 1})
예시 > db.users.createIndex({"studentno": 1})

# 샤드 키로 선정
문법 > db.shardCollection("DB명.컬렉션명", {"필드명": 1})
예시 > db.shardCollection("accounts.users", {"studentno": 1})

 

이 후, sh.status() 를 실행해보면 많은 정보를 확인할 수 있다. 그 중, chunks 라는 필드를 확인할 수 있다.
cf. sh.status() 란 샤드, 데이터베이스, 컬렉션에 대한 요약을 제공한다.


 

청크


청크란 데이터의 서브셋이다. 자동 샤딩을 사용하면, 컬렉션은 n개의 청크로 분할된다. 
청크는 샤드 키 범위에 따라 나열 된다. ({"studentno": minValue } -->> {"studentno": maxValue }) 

예를 들어, 청크 이전의 데이터 값이 0, 1, 2, 3, 4, 6, 9, 10 었고, 이 데이터가 3개의 청크로 분할된다면 청크의 범위는 아래와 같을 수 있다.
1. ({"studentno": minValue } -->> {"studentno": 2 }) 
2. ({"studentno": 2 } -->> {"studentno": 6 }) 
2. ({"studentno": 6 } -->> {"studentno": maxValue }) 

따라서, 서버가 3대 일때 아래와 같이 저장되어있다고 보면 된다.
1. 하나의 서버엔 0, 1, 2 
2. 두번째 서버엔 3, 4, 6
3. 세번째 서버엔 9, 10 


청크 리스트 시작과 끝에 있는 $minKey는 '음의 무한대' $maxKey는 '양의 무한대'로 생각할 수 있다. 
샤드 키 값은 항상 $minKey와 $maxKey 사이에 있다. 이 값은 BSON 유형이고 애플리케이션에서 사용해서는 안되는 몽고DB 내부용 값이다.

 

find 쿼리를 explain() 옵션을 주어 실행하면, Plan의 stage 단계에서 "single_shard" 가 뜰 수 있다. 이는 하나의 샤드에서만 접근하여 해당 데이터를 가져왔다는 의미가 된다.

이처럼, 여러 샤드보다는 하나의 샤드의 접근만으로도 원하는 데이터를 가져올 수 있도록 샤드 키를 선정하면 쿼리 실행에도 큰 도움이 된다~ 

일반적으로 쿼리에서 샤드 키를 사용하지 않으면 mongos는 모든 샤드에 쿼리를 보내야 한다.
샤드 키를 포함하며 단일 샤드나 샤드 서브셋으로 보낼 수 있는 쿼리를 타겟 쿼리라고 한다. 모든 샤드로 보내야 하는 쿼리는 분산-수집 쿼리 라고 한다.

 


 

언제 샤딩해야 하나


일반적으로 샤딩은 다음과 같은 경우에 사용된다.
- 사용 가능한 메모리를 늘릴 때
- 사용 가능한 디스크 공간을 늘릴 때
- 서버의 부하를 줄일 때
- 한 개의 mongod가 다룰 수 있는 처리량보다 더 많이 데이터를 읽거나 쓸 때

샤딩을 너무 빠르게 하면, 구성 및 운영이 복잡해지고 그렇다고 또 너무 늦게 구성하면 데이터 분산에 부하가 걸릴 수 있다.
따라서 샤딩이 필요한 시점을 결정하는 데 모니터링이 중요하다. 

 


 

 

서버 시작


클러스터를 생성하려면 먼저 필요한 프로세스를 모두 시작해야 한다. mongos와 샤드, 구성서버 말이다.
구성 서버는 클러스터 구성을 저장하는 일반 mongod 서버다.

 

구성 서버란 클러스터의 두뇌부다. 어떤 서버가 무슨 데이터를 갖고 있는지에 대한 모든 메타 데이터를 보유한다. 따라서 구성 서버를 가장 먼저 설정해야 한다. 또한 구성 서버에 있는 데이터는 매우 중요하므로 저널링이 활성화된 채 실행 중이며 데이터가 영구적인 드라이브에 저장돼 있어야 한다.

운영에선, 구성서버의 복제 셋은 3개 이상의 멤버로 구성해야 한다. 각 구성서버는 지리적으로 분산된 별도의 물리 장비에 있어야 한다.

mongos가 구성서버로부터 구성을 가져오므로, 구성서버는 mongo 프로세스에 앞서 시작해야 한다. 
다음 명령을 통해 구성 서버를 시작하자

$ mongod --configsvr --replSet configRS --bind_ip localhost, 198.51.100.51
$ mongod --configsvr --replSet configRS --bind_ip localhost, 198.51.100.52
$ mongod --configsvr --replSet configRS --bind_ip localhost, 198.51.100.53

# --configsvr: mongod를 구성서버로 사용하겠다는 뜻. 이 옵션으로 실행되는 서버에서
  클라이언트는 config와 admin 이외의 데이터베이스에 데이터를 쓸 수 없다.

# admin 데이터베이스는 인증 및 권한 부여와 관련된 컬렉션과, 내부용 기타 system. * 컬렉션을 포함한다.
# config 데이터베이스는 샤딩된 클러스터 메타데이터를 보유하는 컬렉션을 포함한다.
# 몽고DB는 청크 migration이나 청크 분할 후처럼 메타데이터가 변경될 때 config 데이터베이스에 데이터를 쓴다.


# 그 다음 구성서버를 복제 셋으로 시작한다. mongo 셸을 복제 셋 멤버 중 하나에 연결한다.
$ mongo --host <호스트명> --port <포트>

#그리고 rs.initiate() 보조자를 사용하여 복제셋을 구성한다.
> rs.initiate(
  {
    _id: "configRS",                        #복제셋 이름
    configsvr: true,
    members:[
       { _id: 0, host: "각 서버IP:27019"},
       { _id: 1, host: "각 서버IP:27019"},
       { _id: 2, host: "각 서버IP:27019"}    
    ]
  }
)

 

몽고DB는 구성서버의 읽기 쓰기의 readConcern, writeConcern 수준의 "majority"를 사용한다. 이는 샤딩된 클러스터 메타데이터가 롤백될 수 없을 때까지 구성 서버 복제 셋에 커밋되지 않는다. 또한, 구성서버 오류가 발생해도 살아남을 메타데이터만 읽을 수 있다. 이는 모든 mongos 라우터가 일관되게 샤드를 접근하는 데 필요하다.

구성서버는 데이터의 목차만 저장하므로, 필요한 스토리지 리소스가 최소화된다. 
만약 모든 구성서버가 유실되면 어느 데이터가 어디에 위치하는지 알아내려면 샤드들의 데이터를 파헤쳐야만 한다.
가능하긴 하지만, 느리고 불편하다. 구성서버를 자주 백업하고 클러스터 유지보수를 수행하기 전에는 항상 구성서버를 백업하자.

 

 

 

 

구성서버를 모두 실행했다면, 애플리케이션이 접속할 mongos 프로세스를 시작하자. mongos가 구성서버들의 위치를 알아야 하므로 항상 --configdb 옵션으로 mongos 프로세스를 시작해야 한다.

$ mongos --configdb 
          configRS/[config서버1의 IP]:27019, [config서버2의 IP]:27019, [config서버3의 IP]:27019
         --bind_ip localhost, 192,51,100,100 --logpath /var/log/mongos.log

 mongos는 기본적으로 포트 27017로 실행한다. 데이터 디렉터리는 필요 없다.
(mongos는 자체적으로 데이터를 보유하지 않고, 시작할 때 구성서버로부터 클러스터 구성을 가져온다)

적은 수의 mongos 프로세스를 시작해야 하며, 가능한 모든 샤드에 가까이 배치해야 한다. 그러면 여러 샤드에 접근하거나 분산/수집 작업을 수행하는 쿼리의 성능이 향상된다.

또한 HA를 위해 mongos 프로세스가 최소 두 개 필요하다. mongos 프로세스가 많아질 수록, 구성서버에서 리소스 경합을 유발하기 때문에 적은 수의 라우터를 사용하기를 권장한다.

 

 

 

드디어 샤드를 추가할 준비가 됐다. 이제 복제 셋으로부터 샤딩을 추가하면 된다.
애플리케이션에 이미 복제 셋이 있다면, 해당 셋이 첫 번째 샤드가 된다. 복제 셋을 샤드로 전환하려면 멤버의 구성을 약간 수정한 후 mongos에게 샤드를 구성할 복제 셋을 찾는 방법을 알려야 한다.

예를 들어 svr1, svr2, svr3에 rs0 이라는 복제셋이 있으면 먼저 mongo 셸을 사용해 멤버 중 하나에 연결한다.

------- 기존 복제셋이 존재할 경우, 복제셋을 죽이고 샤드 옵션을 달아 다시 실행해야 한다 -------


# 1. 먼저 mongo 셸을 사용해 멤버 중 하나에 연결한다.
$ mongo svr1

# 2. 그 후, rs.status()를 사용해 어떤 멤버가 복제 셋의 프라이머리이고 세컨더리인지 확인한다.
> rs.status()
 
# 몽고DB 3.4 부터 샤드용 mongod 인스턴스는 반드시 --shardsvr 옵션으로 구성해야 한다. 
# 구성 파일 설정 sharding.clusterRole 혹은 명령행 옵션 --shardsvr을 통해 구성한다.


# 3. 세컨더리 서버를 --shardsvr 옵션을 달아 먼저 재시작
$ mongod --replSet "rs0" --shardsvr --port 27017 --bind_ip localhost, <세컨더리 서버의 IP주소>

# 4. 프라이머리 서버 강등
$ mogno <primary서버>      # 먼저 mongo셸을 프라이머리에 연결
$ rs.stepDown()            # Primary 강등

# 5. 프라이머리 서버를 --shardsvr 옵션 달아 재시작
$ mongod --replSet "rs0" --shardsvr --port 27017 --bind_ip localhost, <이전 프라이머리의 IP주소>


# 6. 이제 복제 셋을 샤드로서 추가할 준비가 돼싿. mongo 셸을 mongos의 admin 데이터베이스에 연결하자.
$ sh.addShard( "rs0/svr1:27017, svr2:27017, svr3:27017" )

 

sh.status()를 실행하면 몽고DB가 곧바로 샤드를 나열한다. 복제 셋 이름 rs0은 이제 샤드의 식별자가 된다. 샤드를 제거/옮기려면 rs0을 사용해 설명한다.

 

복제 셋을 샤드로 추가했으면 애플리케이션이 복제 셋 대신에 mongos에 접속하게 할 수 있다. 샤드를 추가할 때 mongos는 복제 셋 내 모든 데이터베이스를 해당 샤드가 '소유'한다는 것을 등록하므로, 모든 쿼리를 새로운 샤드로 전달한다.

 

** 샤드를 추가했으면 모든 클라이언트가 복제 셋에 접속하지 않고 요청을 mongos로 보내도록 반드시 설정해야 한다.
클라이언트가 계속 복제 셋에 직접 요청을 보내면 샤딩은 제대로 작동하지 않는다. 샤드를 추가하자마자 모든 클라이언트가 mongos에 접속하도록 전환하고, 샤드에 직접 접속할수 없도록 방화벽 규칙을 설정하자 **

 

몽고 DB 3.6 이전에서는 독립 실행형 mongod 샤드를 생성할 수 있었지만, 3.6 이후 버전에서는 불가능하며 모든 샤드가 복제셋 이어야 한다.

 


 

데이터 샤딩

몽고DB는 데이터를 어떻게 분산할지 알려주기 전에는 자동으로 데이터를 분산하지 않는다. 분산하려는 데이터베이스와 컬렉션을 명시적으로 알려줘야 한다.

# 1. 데이터베이스의 샤딩을 활성화
> sh.enableSharding("school")


# 2. 컬렉션을 샤딩 (단, 샤드에 인덱스가 설정되어 있어야 한다.)
> sh.shardCollection ("school.student", {"studentno": 1})


# 3. 이제 student 컬렉션은 "studentno" 키로 샤딩된다.

 

shardCollection 명령은 컬렉션을 청크로 나눈다. 그 후, 샤드에 청크를 분산시킨다. 이 프로세스는 즉시 끝나지 않으며, 컬렉션이 크면 최초 분산을 끝내는 데 몇 시간이 걸릴 수도 있다. 데이터를 로드하기 전에, 샤드에서 청크가 생성될 곳을 사전 분할 함으로써 시간을 줄일 수 있다.  이 시점 이후에 로드된 데이터는 추가 밸런싱 없이 현재 샤드에 직접 삽입된다.

반응형