1

Mongodb分片集群实践

 2 years ago
source link: https://jelly.jd.com/article/621370d8263c2201bb85b050
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

MongoDB中的分片是什么? 分片是MongoDB中的一个概念,它将跨多个MongoDB实例的大型数据集拆分为小型数据集。

有时,MongoDB中的数据会非常庞大​​,以至于对如此大的数据集进行查询会导致服务器上大量的CPU被占用。为了解决这种情况,MongoDB提出了分片的概念,基本上是将数据集拆分到多个MongoDB实例中。

实际上,可能很大的集合实际上被称为多个集合或碎片。逻辑上,所有分片都作为一个集合工作。

MongoDB中的分片是什么?

分片是MongoDB中的一个概念,它将跨多个MongoDB实例的大型数据集拆分为小型数据集。

有时,MongoDB中的数据会非常庞大​​,以至于对如此大的数据集进行查询会导致服务器上大量的CPU被占用。为了解决这种情况,MongoDB提出了分片的概念,基本上是将数据集拆分到多个MongoDB实例中。

实际上,可能很大的集合实际上被称为多个集合或碎片。逻辑上,所有分片都作为一个集合工作。

分片(sharding)是MongoDB通过水平扩展将数据集分布在不同的服务器上来提高自己的存储容量和吞吐量。和MySQL分区方案相比,MongoDB的最大区别在于它几乎能自动完成所有事情,只要告诉MongoDB要分配数据,它就能自动维护数据在不同服务器之间的均衡。

为什么要使用分片?

  • 复制所有的写入操作到主节点
  • 延迟的敏感数据会在主节点查询
  • 单个副本集限制在12个节点
  • 当请求量巨大时会出现内存不足。
  • 本地磁盘不足
  • 垂直扩展价格昂贵
11d36b920f7b79f2.png

上图中主要有如下所述三个主要组件:

Shard:

用于存储实际的数据块,实际生产环境中一个shard server角色可由几台机器组个一个replica set承担,防止主机单点故障

Config Server:

mongod实例,存储了整个 ClusterMetadata,其中包括 chunk信息。

Query Routers:

前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用可以透明使用。

分片集群搭建

生产环境如果采用的大厂提供的MongoDB的服务只需要付费就可以自动化搭建完成,

各个大厂的云MongoDB

我们本地测试的话了,为了减少搭建的复杂度,我们可以采用Docker的方式来部署,关于Docker的相关知识这里就介绍了,相关安装方式可以查看 这篇文章 ,这里假定我们已经安装好了Docker环境。

我们采用docker-compose的方式来搭建。

1、创建compose配置文件:

version: '2'
services:
  shard1:
    image: mongo:4.0.5
    container_name: mongo_shard1
    # --shardsvr: 这个参数仅仅只是将默认的27017端口改为27018,如果指定--port参数,可用不需要这个参数
    # --directoryperdb:每个数据库使用单独的文件夹
    command: mongod --shardsvr --directoryperdb --replSet shard1
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/shard1:/data/db
    privileged: true
    # mem_limit: 16000000000
    networks:
      - mongo   

  shard2:
    image: mongo:4.0.5
    container_name: mongo_shard2
    command: mongod --shardsvr --directoryperdb --replSet shard2
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/shard2:/data/db
    privileged: true
    # mem_limit: 16000000000
    networks:
      - mongo

  shard3:
    image: mongo:4.0.5
    container_name: mongo_shard3
    command: mongod --shardsvr --directoryperdb --replSet shard3
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/shard3:/data/db
    privileged: true
    # mem_limit: 16000000000
    networks:
      - mongo

  config1:
    image: mongo:4.0.5
    container_name: mongo_config1
    # --configsvr: 这个参数仅仅是将默认端口由27017改为27019, 如果指定--port可不添加该参数
    command: mongod --configsvr --directoryperdb --replSet fates-mongo-config --smallfiles
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/config1:/data/configdb
    networks:
      - mongo

  config2:
    image: mongo:4.0.5
    container_name: mongo_config2
    command: mongod --configsvr --directoryperdb --replSet fates-mongo-config --smallfiles
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/config2:/data/configdb
    networks:
      - mongo

  config3:
    image: mongo:4.0.5
    container_name: mongo_config3
    command: mongod --configsvr --directoryperdb --replSet fates-mongo-config --smallfiles
    volumes:
      - /etc/localtime:/etc/localtime
      - /Users/name/monogodata/config3:/data/configdb
    networks:
      - mongo

  mongos:
    image: mongo:4.0.5
    container_name: mongo_mongos
    # mongo3.6版默认绑定IP为127.0.0.1,此处绑定0.0.0.0是允许其他容器或主机可以访问
    command: mongos --configdb fates-mongo-config/config1:27019,config2:27019,config3:27019 --bind_ip 0.0.0.0 --port 27017
    ports:
     - 27017:27017
    volumes:
      - /etc/localtime:/etc/localtime
    depends_on:
      - config1
      - config2
      - config3
    networks:
      - mongo    
networks:
  mongo:
    external: true

这里我们通过/Users/name/monogodata/config3:/data/configdb 将本地的目录映射为MongoDB的数据存储目录。这里根据自己的实际情况替换为自己的文件目录。

上面这个文件里面,我们创建了3个shard,3个config,1个mongos

下面就是启动服务,

2、新建start.sh脚本

#!/bin/sh

docker-compose -f fates-mongo-compose.yaml up -d

#睡眠30s,等待mongodb所有容器起来之后将它们配置加入分片
sleep 30s

docker-compose -f fates-mongo-compose.yaml exec config1 bash -c "echo 'rs.initiate({_id: \"fates-mongo-config\",configsvr: true, members: [{ _id : 0, host : \"config1:27019\" },{ _id : 1, host : \"config2:27019\" }, { _id : 2, host : \"config3:27019\" }]})' | mongo --port 27019"
docker-compose -f fates-mongo-compose.yaml exec shard1 bash -c "echo 'rs.initiate({_id: \"shard1\",members: [{ _id : 0, host : \"shard1:27018\" }]})' | mongo --port 27018"
docker-compose -f fates-mongo-compose.yaml exec shard2 bash -c "echo 'rs.initiate({_id: \"shard2\",members: [{ _id : 0, host : \"shard2:27018\" }]})' | mongo --port 27018"
docker-compose -f fates-mongo-compose.yaml exec shard3 bash -c "echo 'rs.initiate({_id: \"shard3\",members: [{ _id : 0, host : \"shard3:27018\" }]})' | mongo --port 27018"
docker-compose -f fates-mongo-compose.yaml exec mongos bash -c "echo 'sh.addShard(\"shard1/shard1:27018\")' | mongo"
docker-compose -f fates-mongo-compose.yaml exec mongos bash -c "echo 'sh.addShard(\"shard2/shard2:27018\")' | mongo"
docker-compose -f fates-mongo-compose.yaml exec mongos bash -c "echo 'sh.addShard(\"shard3/shard3:27018\")' | mongo"

3、启动服务

chmod +x start.sh
./start.sh

如果一切顺利的话,服务就可以正常启动起来了,下面就来具体介绍一下使用方式。

分片集群的使用其实和普通集群使用方式一样,不管是通过命令行工具还是GUI工具,链接方式都是一样,看到的collection也是一样的,因为具体的分片工作是MongoDB帮我们来做的,我们不用关心怎么去分片,我们唯一需要关心的是怎么设置分片键的设置。

使用前需要开启分片

// 启动分片
sh.enableSharding("test")//先让当前库支持分片

分片键决定了集合内的文档如何在集群的多个分片间的分布状况。分片键要么是一个索引字段,要么是一个存在于集合内所有文档中的复合索引字段。 MongoDB使用分片键值范围对集合中的数据进行分区。每个范围都定义了一个分片键值的非重叠范围,并且与一个chunk(数据块,下同)相关联。

  • 一旦你对一个集合分片,那么其分片键就不可再改变;也就是说,你不可以对这个集合再重新选择另一个不一样的分片键。

分片键格式

为了将一个集合分片,你必须在sh.shardCollection()方法中指定目标集合和分片键:

sh.shardCollection( namespace, key )

namespace参数由字符串<database>.<collection>组成,该字符串指定目标集合的完整命名空间。 key参数由包含一个字段和该字段的索引遍历方向的文档组成。

  • 如果需要进行分片的目标集合是空集合,可以不创建索引直接进行下一步的分片设置,该操作会自动创建索引。

  • 如果需要进行分片的目标集合是非空集合,则需要先创建索引key。然后使用如下命令设置分片键。

分片键类型

  • 1、哈希分片

哈希分片使用哈希索引来在分片集群中对数据进行划分。哈希索引计算某一个字段的哈希值作为索引值,这个值被用作片键。

11d36b920f7b79f2.png

当使用哈希索引来解析查询时,MongoDB会自动计算哈希值。应用程序不需要计算哈希。作为哈希片键的字段应具有良好的基数或者该字段包含大量不同的值。

// hash分片(Hash based sharding)
sh.shardCollection('test.logs',{logId:'hashed'})

2、范围分片

基于范围的分片会将数据划分为由片键值确定的连续范围

11d36b920f7b79f2.png
// 范围分片(Range based sharding)
sh.shardCollection('test.logs',{timestamp:1})
  • 哈希分片 VS 范围分片

给定一个使用单调递增的值X作为片键的集合,使用范围分片会导致插入数据的分布类似于下面这样

11d36b920f7b79f2.png

由于X的值始终在增加,因此具有maxKey(上限)的数据块将接收大多数传入的写操作。 这将插入操作限制在只能定向到包含此块的单个分片,从而减少或消除了分片集群中分布式写入的优势。

通过在X上使用哈希索引,插入的分布将类似于下面这样:

11d36b920f7b79f2.png

由于现在数据分布更加均匀,因此可以在整个集群中更高效地分布式插入数据。

sh.enableSharding("test")
{
    "ok" : 1,
    "operationTime" : Timestamp(1645496776, 4),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1645496776, 4),
        "signature" : {
            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
            "keyId" : NumberLong(0)
        }
    }
}

设置分片键

这里我们为了对比,创建两个集合,分别使用哈希和范围分片

// 范围分片
sh.shardCollection('test.users_time',{uid:1})
// 输出
{
    "collectionsharded" : "test.users_time",
    "collectionUUID" : UUID("c7194191-1428-42c2-a3cb-0f9274c7d168"),
    "ok" : 1,
    "operationTime" : Timestamp(1645497552, 13),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1645497552, 13),
        "signature" : {
            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
            "keyId" : NumberLong(0)
        }
    }
}

// 哈希分片
sh.shardCollection('test.users_hashed',{uid:'hashed'})
// 输出
{
    "collectionsharded" : "test.users_hashed",
    "collectionUUID" : UUID("352d9362-6c16-41e2-86f2-808c22cd2581"),
    "ok" : 1,
    "operationTime" : Timestamp(1645497561, 45),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1645497561, 45),
        "signature" : {
            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
            "keyId" : NumberLong(0)
        }
    }
}
var arr=[];
for(var i=0;i<300000;i++){
  arr.push({"uid":i,"name":"user"+i});
}

db.users_time.insertMany(arr);

db.users_hashed.insertMany(arr);

查看分片状态

sh.status()
{  "_id" : "test",  "primary" : "shard2",  "partitioned" : true,  "version" : {  "uuid" : UUID("aa98453b-c79c-4172-bd34-e5782cc8e8e1"),  "lastMod" : 1 } }
                test.users_hashed
                        shard key: { "uid" : "hashed" }
                        unique: false
                        balancing: true
                        chunks:
                                shard1    2
                                shard2    2
                                shard3    2
                        { "uid" : { "$minKey" : 1 } } -->> { "uid" : NumberLong("-6148914691236517204") } on : shard1 Timestamp(1, 0)
                        { "uid" : NumberLong("-6148914691236517204") } -->> { "uid" : NumberLong("-3074457345618258602") } on : shard1 Timestamp(1, 1)
                        { "uid" : NumberLong("-3074457345618258602") } -->> { "uid" : NumberLong(0) } on : shard2 Timestamp(1, 2)
                        { "uid" : NumberLong(0) } -->> { "uid" : NumberLong("3074457345618258602") } on : shard2 Timestamp(1, 3)
                        { "uid" : NumberLong("3074457345618258602") } -->> { "uid" : NumberLong("6148914691236517204") } on : shard3 Timestamp(1, 4)
                        { "uid" : NumberLong("6148914691236517204") } -->> { "uid" : { "$maxKey" : 1 } } on : shard3 Timestamp(1, 5)
                test.users_time
                        shard key: { "uid" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                shard1    4
                                shard2    4
                                shard3    3
                        { "uid" : { "$minKey" : 1 } } -->> { "uid" : 1 } on : shard3 Timestamp(2, 0)
                        { "uid" : 1 } -->> { "uid" : 500001 } on : shard1 Timestamp(3, 0)
                        { "uid" : 500001 } -->> { "uid" : 750002 } on : shard3 Timestamp(4, 0)
                        { "uid" : 750002 } -->> { "uid" : 1000003 } on : shard1 Timestamp(5, 0)
                        { "uid" : 1000003 } -->> { "uid" : 1250004 } on : shard3 Timestamp(7, 0)
                        { "uid" : 1250004 } -->> { "uid" : 1500004 } on : shard1 Timestamp(8, 0)
                        { "uid" : 1500004 } -->> { "uid" : 1750005 } on : shard2 Timestamp(8, 1)
                        { "uid" : 1750005 } -->> { "uid" : 2000006 } on : shard2 Timestamp(5, 4)
                        { "uid" : 2000006 } -->> { "uid" : 2250007 } on : shard2 Timestamp(5, 5)
                        { "uid" : 2250007 } -->> { "uid" : 2528999 } on : shard2 Timestamp(5, 6)
                        { "uid" : 2528999 } -->> { "uid" : { "$maxKey" : 1 } } on : shard1 Timestamp(6, 0)

从上面的输出信息可以看到,因为我们创建了30w的数据,对于hash分片,数据分布比较均匀,而对于范围分片,因为1-500001这个区间的数据都是放在shard1里面,所以对其它两个分片来说没有保存上数据。

以上就是关于MongoDB分片集群的一些知识点,如果更详细的信息可以查阅官网来了解。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK