LY DB tech blog
Published on

mongos는 어떻게 Target Shard를 빠르게 찾을 수 있을까?

Authors
  • avatar
    Name
    박붕어
    Twitter

이번 글에서는 MongoDB에서 제공하는 Sharded Cluster의 구조에 대해 알아보고 mongos가 어떻게 여러 개의 샤드 중에서 원하는 데이터를 찾아갈 수 있는지 작성했습니다. 

Sharded Cluster 구성 

MongoDB에서는 Standalone, Replica Set, Sharded Cluster라는 세 가지의 배포 형태를 제공합니다.
그 중에서 Sharded Cluster는 데이터의 분산 저장을 통해 대규모의 데이터를 저장할 수 있는 배포 형태입니다. Sharded Cluster가 제공하는 대표적인 장점은 다음과 같습니다. 

  • 데이터 분산 : 데이터를 여러 서버에 분산시켜 저장함으로써 단일 서버의 저장 용량 한계를 극복할 수 있습니다.
  • 쓰기 부하 분산 : 쓰기에 대한 요청과 데이터 처리 부하를 여러 서버에 분산시켜 쓰기에 대한 처리량을 수평적으로 확장할 수 있습니다. 

MongoDB의 Sharded Cluster

 

이런 장점을 제공하기 위해서 Sharded Cluster는 mongos, Config server, Shard 이렇게 3개의 컴포넌트로 구성되는데요. 각각의 역할은 다음과 같습니다. 

1. mongos : 클라이언트로부터 받은 쿼리를 적절한 샤드로 전달하고, 결과를 클라이언트에 반환합니다. 클라이언트는 mongos를 통해서 단일 Replica Set을 사용하는 것처럼 MongoDB를 사용할 수 있습니다. 
2. Config server : Sharded Cluster의 메타데이터를 저장하고 관리합니다. 샤드의 위치, 데이터의 위치 등을 저장하고 있습니다. mongos는 config server로부터 데이터의 위치를 받아 데이터가 위치하는 적절한 샤드에 요청을 보냅니다. 
3. Shard : 실제 데이터가 저장되는 Replica Set입니다. 각각의 샤드가 데이터의 일부를 저장하며 샤드 간에 데이터는 중복되지 않습니다. 

Sharded Cluster를 사용하게 되면 mongos가 클라이언트로부터 쿼리를 받고, mongos가 샤드에서 데이터를 찾아 클라이언트에게 데이터를 반환해 줍니다.
클라이언트는 데이터가 어느 샤드에 위치하는지 알 필요 없이 mongos를 통해 Replica Set을 사용하는 것처럼 MongoDB를 사용할 수 있습니다. 

Config Server 와 Chunk 

보통 Sharded Cluster에서 샤드는 최소 2개, 많으면 수십 개까지 구성이 됩니다. 그런 수십 개의 샤드들에서 mongos는 원하는 데이터를 어떻게 찾을 수 있을까요? 클라이언트로부터 요청이 올 때마다 모든 샤드에 요청을 보내 데이터를 찾는 것일까요?
mongos는 원하는 데이터를 빠르게 찾기 위해 Config Server로부터 데이터의 위치에 대한 정보를 가져옵니다. Config server에는 Sharded Cluster에서 데이터를 나누고 관리하는 단위인 Chunk에 대한 정보가 저장되어 있습니다. 

db.chunks.findOne();
{
  _id: ObjectId("659fa8416fe760f8e7d520f3"),
  lastmod: Timestamp({ t: 2, i: 0 }),
  lastmodEpoch: ObjectId("659fa84176b9c11fdf2bfd18"),
  ns: 'config.system.sessions',
  min: { _id: MinKey() },
  max: { _id: { id: new UUID("00400000-0000-0000-0000-000000000000") } },
  shard: 'replset_3',
  history: [
    {
      validAfter: Timestamp({ t: 1704962114, i: 1210 }),
      shard: 'replset_3'
    },
    {
      validAfter: Timestamp({ t: 1704962113, i: 4 }),
      shard: 'replset_1'
    }
  ]
}

 

chunks 컬렉션의 필드들의 의미는 다음과 같습니다. 

Key Description
_id Chunk의 아이디
lastmod Chunk 정보를 마지막으로 변경한 시점
lastmodEpoch Chunk 정보의 마지막 변경 일시를 MongoDB ObjectId 포맷으로 변경한 값
ns Chunk가 포함된 컬렉션의 네임스페이스
min Chunk가 가지는 값의 범위에서 시작값
max Chunk가 가지는 값의 범위에서 종료값
shard Chunk가 현재 저장된 샤드의 이름 ( Replica Set의 이름 )
history Chunk의 최근 마이그레이션 이력

이 Chunk 정보를 통해서 mongos는 데이터가 있는 샤드를 찾아 적절한 샤드에만 쿼리를 보내 효율적으로 데이터를 가져올 수 있습니다. 
물론 클라이언트에서 요구하는 데이터가 샤드의 위치를 특정할 수 없는 경우 ( ex. 샤드키를 포함하지 않는 경우 ) 모든 샤드에 쿼리를 보내 데이터를 가져오게 됩니다. 

mongos의 Routing table cache 

mongos는 Config server로부터 Chunk에 대한 위치를 가져오는데, 클라이언트로부터 요청을 받을 때마다 mongos가 Config server에 요청을 보내 데이터를 가져오는 방식은 너무 비효율적입니다. 
왜냐하면 Config server에 저장된 Chunk에 대한 정보의 갱신은 클라이언트의 요청에 비해서는 매우 드물게 발생하기 때문입니다. 

따라서 mongos는 클라이언트의 요청마다 Config server로부터 데이터를 가져오기 보다는, Chunk에 대한 정보를 캐싱하여 사용합니다.
이렇게 mongos에서 캐싱하고 있는 데이터를 Routing table cache라고 합니다.  
mongos는 Routing table cache를 통해 어플리케이션이 요청한 데이터의 위치를 빠르게 파악하고 필요한 샤드에만 쿼리를 보낼 수 있게 됩니다. 

Chunk가 변경되는 경우 

그럼 Chunk는 언제 변경될까요? 
샤드 간의 데이터가 불균형하게 분포하는 경우에는, 사용자의 요청이 하나의 샤드에만 몰리는 경우가 발생할 수 있습니다. 이렇게 되면 데이터를 여러 샤드에 분산하여 저장하는 이점이 없어지게 됩니다. 
MongoDB의 Sharded Cluster는 샤드 간의 데이터 분포를 고르게 유지하기 위해 Chunk Split, Chunk Migration이라는 작업을 수행합니다. 

Chunk Split


Chunk의 범위 내에 포함되는 데이터가 많아져 Chunk의 크기가 커지는 경우, MongoDB는 하나의 Chunk를 두 개의 Chunk로 분리합니다. 이를 Chunk Split이라고 합니다.
6.0 미만의 버전까지 Default chunk size는 64MB이며 6.0 버전부터 Default chunk size가 128MB로 변경되었습니다.
데이터가 인입되는 경우 Chunk의 범위가 Chunk size보다 큰지 확인하고, Chunk size보다 큰 경우 두 개의 Chunk로 나눠지게 됩니다. 이 경우 Chunk의 물리적인 위치는 변경되지 않고, 샤드 내에 저장되는 Chunk의 개수만 증가하게 됩니다. 

Chunk Migration

 

특정 샤드에 Chunk가 너무 많아져 샤드 간의 Chunk 개수가 불균형해지는 경우, MongoDB는 Chunk가 많은 샤드에서 Chunk가 적은 샤드로 Chunk를 이동시킵니다. 이를 Chunk Migration이라고 합니다. Chunk Migration은 Config server의 Primary 멤버에 위치한 Balancer라는 프로세스가 담당하고 있습니다. Chunk Migration이 수행되면, 데이터가 저장되고 있는 실제 물리적인 위치가 변경되게 됩니다. 

 

Chunk Migration이나 Chunk Split이 발생하는 경우뿐만 아니라 샤드의 추가나 삭제, 컬렉션의 생성 또는 삭제 시 Config server의 메타데이터가 변경되게 되는데요. 이 경우 mongos가 가지고 있는 Routing table cache와 Config server 간의 데이터 불일치가 생기게 됩니다. MongoDB는 어떻게 이런 데이터의 불일치를 해결할까요?

Routing table 갱신 

Chunk가 변경되는 경우 변경된 Chunk에 대해 샤드나 Config server는 mongos에게 따로 Routing table을 갱신하라는 요청을 보내지 않습니다. mongos 프로세스는 필요한 때에만 Routing table cache를 업데이트하는 lazy refresh 방식을 사용합니다. 

 

mongos가  샤드에게 데이터를 요청할 때, mongos는 원하는 데이터뿐만 아니라 mongos에 캐싱된 Chunk의 버전에 대한 정보를 같이 전달합니다. 해당 Chunk의 버전이 샤드에서 갖고 있는 버전보다 오래된 버전인 경우, 샤드는 mongos에게 Stale Config Exception을 반환합니다. Stale Config Exception을 받은 mongos는 자신의 Routing table cache가 오래된 것임을 깨닫고, Config server에 요청하여 새로운 메타데이터를 가져옵니다. 이러한 방식으로 mongos는 자동으로 Routing table cache를 갱신합니다. 

 

mongos의 Routing table 갱신은 MongoDB 내부적으로 자동으로 일어나지만, 수동으로 사용자가 갱신해 줄 수도 있습니다. 
mongos 프로세스에서 flushRouterConfig 명령을 수행하면 mongos는 Routing table cache를 갱신합니다. 

db.adminCommand(
   {
     flushRouterConfig: "<db.collection>"
   }
)

또는 mongos 프로세스가 기동 시 Config server로부터 메타데이터를 가져오기 때문에 mongos 프로세스를 재기동하여 routing table cache를 갱신할 수도 있습니다. 

 

마무리 

mongos는 routing table cache를 기반으로 쿼리를 routing 하여 원하는 데이터를 가진 샤드에 요청을 보낼 수 있습니다. 그리고 routing table cache는 필요한 때에만 갱신하는 lazy refresh 방식을 사용하여 갱신됩니다. 만약 수동으로 routing table cache를 업데이트하고 싶다면, flushRouterconfig 명령을 사용하거나 mongos를 재기동하여 업데이트할 수 있습니다. 

 

긴 글 읽어주셔서 감사합니다.