MongoDB 分片:分步实用指南

MongoDB 分片技术详解

在分布式计算环境中,分片是一种将庞大的数据集分散存储在多个 MongoDB 实例上的有效方法。它允许我们将大型数据分割成更易于管理的小块。

什么是分片?

MongoDB 的分片机制提供了一种可扩展的解决方案,使我们能够跨多台服务器存储海量数据,而不是局限于单台服务器。随着数据量的快速增长,单机存储变得越来越不可行。对存储在单个服务器上的庞大数据进行查询,可能会导致资源高度占用,并难以保证理想的读写吞吐量。

处理不断增长的数据量,主要有两种扩展方式:

  • 垂直扩展:通过升级服务器的硬件配置,例如更强大的处理器、更大容量的内存或更多的磁盘空间,来提升单个服务器的性能。然而,在现实应用中,现有技术和硬件配置的限制可能会影响垂直扩展的效益。
  • 水平扩展:通过添加更多服务器,并将负载分散到多个服务器上。每个服务器处理整个数据集的一个子集,这种方式比部署高端硬件更高效、成本效益更高。然而,它也需要对涉及多台服务器的复杂基础设施进行额外的维护。

MongoDB 的分片技术正是基于水平扩展的理念而设计的。

分片的核心组件

要在 MongoDB 中实现分片,需要以下关键组件:

  • 分片 (Shard): 这是一个独立的 MongoDB 实例,负责处理原始数据的一个子集。为了确保数据的可靠性,分片通常部署在副本集中。
  • Mongos:这是一个充当客户端应用程序和分片集群之间接口的 MongoDB 实例。它的主要作用是作为分片的查询路由器,负责将客户端的请求路由到正确的分片。
  • 配置服务器 (Config Server):这是一个存储集群元数据信息和配置细节的 MongoDB 实例。MongoDB 要求配置服务器必须部署为副本集,以保障元数据的可靠性和一致性。

分片架构概览

MongoDB 分片集群由多个副本集构成。每个副本集至少包含 3 个或更多的 MongoDB 实例。一个分片集群可能由多个 MongoDB 分片实例组成,并且每个分片实例都工作在一个分片副本集中。应用程序通过 Mongos 与分片集群交互,而 Mongos 负责与各个分片通信。因此,应用程序在分片架构中不会直接与分片节点交互。查询路由器根据分片键将数据子集分布在不同的分片节点之间。

分片实现的详细步骤

以下是在 MongoDB 中实现分片的详细步骤:

步骤 1

  • 在副本集中启动配置服务器,并启用它们之间的复制功能。
    mongod –configsvr –port 27019 –replSet rs0 –dbpath C:datadata1 –bind_ip localhost
    mongod –configsvr –port 27018 –replSet rs0 –dbpath C:datadata2 –bind_ip localhost
    mongod –configsvr –port 27017 –replSet rs0 –dbpath C:datadata3 –bind_ip localhost
 

步骤 2

  • 在其中一台配置服务器上初始化副本集。
 rs.initiate({_id:"rs0", configsvr:true, members:[{_id:0, host:"IP:27017"}, {_id:1, host:"IP:27018"}, {_id:2, host:"IP:27019"}]})
 
{
        "ok" : 1,
        "$gleStats" : {
                "lastOpTime" : Timestamp(1593569257, 1),
                "electionId" : ObjectId("000000000000000000000000")
        },
        "lastCommittedOpTime" : Timestamp(0, 0),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593569257, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1593569257, 1)
}

步骤 3

  • 在副本集中启动分片服务器,并启用它们之间的复制功能。
   mongod –shardsvr –port 27020 –replSet rs1 –dbpath C:datadata4 –bind_ip localhost
   mongod –shardsvr –port 27021 –replSet rs1 –dbpath C:datadata5 –bind_ip localhost
   mongod –shardsvr –port 27022 –replSet rs1 –dbpath C:datadata6 –bind_ip localhost
 

MongoDB 将初始化第一个分片服务器为主服务器。要移动主分片服务器,可以使用 movePrimary 方法。

步骤 4

  • 在其中一台分片服务器上初始化副本集。
 rs.initiate({_id:"rs0", members:[{_id:0, host:"IP:27020"}, {_id:1, host:"IP:27021"}, {_id:2, host:"IP:27022"}]})
 
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593569748, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1593569748, 1)
}

步骤 5

  • 启动 Mongos 以连接分片集群。
    mongos –port 40000 –configdb rs0/localhost:27019,localhost:27018, localhost:27017
  

步骤 6

  • 连接到 Mongos 路由服务器。
  mongo --port 40000
  
  • 现在,添加分片服务器。
  sh.addShard("rs1/localhost:27020,localhost:27021,localhost:27022")
  
{
        "shardAdded" : "rs1",
        "ok" : 1,
        "operationTime" : Timestamp(1593570212, 2),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570212, 2),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
 

步骤 7

  • 在 MongoDB Shell 上启用对数据库和集合的分片。
  • 在数据库上启用分片。
   sh.enableSharding("geekFlareDB")
  
{
        "ok" : 1,
        "operationTime" : Timestamp(1591630612, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1591630612, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

步骤 8

  • 要对集合进行分片,需要使用分片键(将在下文中详细描述)。

语法:sh.shardCollection(“dbName.collectionName”, { “key” : 1 } )

  sh.shardCollection("geekFlareDB.geekFlareCollection", { "key" : 1 } )
  
{
        "collectionsharded" : "geekFlareDB.geekFlareCollection",
        "collectionUUID" : UUID("0d024925-e46c-472a-bf1a-13a8967e97c1"),
        "ok" : 1,
        "operationTime" : Timestamp(1593570389, 3),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570389, 3),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

请注意,如果该集合不存在,请按照如下方式创建:

  db.createCollection("geekFlareCollection")
 
{
        "ok" : 1,
        "operationTime" : Timestamp(1593570344, 4),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593570344, 5),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

步骤 9

将数据插入到集合中。此时 MongoDB 日志会开始增长,显示平衡器正在运行并尝试在分片之间平衡数据。

步骤 10

最后一步是检查分片的状态。可以在 Mongos 路由节点上通过运行以下命令来检查状态。

分片状态

通过在 Mongos 路由节点上运行以下命令来检查分片状态。

 sh.status()
 
mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("5ede66c22c3262378c706d21")
  }
  shards:
        {  "_id" : "rs1",  "host" : "rs1/localhost:27020,localhost:27021,localhost:27022",  "state" : 1 }
  active mongoses:
        "4.2.7" : 1
  autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  no
        Failed balancer rounds in last 5 attempts:  5
        Last reported error:  Could not find host matching read preference { mode: "primary" } for set rs1
        Time of Reported error:  Tue Jun 09 2020 15:25:03 GMT+0530 (India Standard Time)
        Migration Results for the last 24 hours:
                No recent migrations
  databases:
        {  "_id" : "config",  "primary" : "config",  "partitioned" : true }
                config.system.sessions
                        shard key: { "_id" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1024
                        too many chunks to print, use verbose if you want to force print
        {  "_id" : "geekFlareDB",  "primary" : "rs1",  "partitioned" : true,  "version" : {  "uuid" : UUID("a770da01-1900-401e-9f34-35ce595a5d54"),  "lastMod" : 1 } }
                geekFlareDB.geekFlareCol
                        shard key: { "key" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1
                        { "key" : { "$minKey" : 1 } } -->> { "key" : { "$maxKey" : 1 } } on : rs1 Timestamp(1, 0)
                geekFlareDB.geekFlareCollection
                        shard key: { "product" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs1     1
                        { "product" : { "$minKey" : 1 } } -->> { "product" : { "$maxKey" : 1 } } on : rs1 Timestamp(1, 0)
        {  "_id" : "test",  "primary" : "rs1",  "partitioned" : false,  "version" : {  "uuid" : UUID("fbc00f03-b5b5-4d13-9d09-259d7fdb7289"),  "lastMod" : 1 } }

mongos>

数据分布机制

Mongos 路由器根据分片键将负载分布在不同的分片上,力求数据的均匀分布,这时,平衡器就会开始工作。

数据在分片之间分配的关键组件是:

  • 平衡器负责在分片节点之间平衡数据子集。当 Mongos 服务器开始在分片之间分配负载时,平衡器将启动。一旦启动,它会尝试更均匀地分布数据。要检查平衡器的状态,请运行 sh.status()sh.getBalancerState()sh.isBalancerRunning()
mongos> sh.isBalancerRunning()
true
mongos>

或者

mongos> sh.getBalancerState()
true
mongos>

在插入数据后,我们可以观察到 Mongos 守护进程的一些活动,表明它正在为特定的分片移动数据块。这个过程表明平衡器正在尝试在分片之间平衡数据。运行平衡器可能会导致性能问题,因此建议在特定的 平衡器窗口 内运行。

mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("5efbeff98a8bbb2d27231674")
  }
  shards:
        {  "_id" : "rs1",  "host" : "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022",  "state" : 1 }
        {  "_id" : "rs2",  "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",  "state" : 1 }
  active mongoses:
        "4.2.7" : 1
  autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  yes
        Failed balancer rounds in last 5 attempts:  5
        Last reported error:  Could not find host matching read preference { mode: "primary" } for set rs2
        Time of Reported error:  Wed Jul 01 2020 14:39:59 GMT+0530 (India Standard Time)
        Migration Results for the last 24 hours:
                1024 : Success
  databases:
        {  "_id" : "config",  "primary" : "config",  "partitioned" : true }
                config.system.sessions
                        shard key: { "_id" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs2     1024
                        too many chunks to print, use verbose if you want to force print
        {  "_id" : "geekFlareDB",  "primary" : "rs2",  "partitioned" : true,  "version" : {  "uuid" : UUID("a8b8dc5c-85b0-4481-bda1-00e53f6f35cd"),  "lastMod" : 1 } }
                geekFlareDB.geekFlareCollection
                        shard key: { "key" : 1 }
                        unique: false
                        balancing: true
                        chunks:
                                rs2     1
                        { "key" : { "$minKey" : 1 } } -->> { "key" : { "$maxKey" : 1 } } on : rs2 Timestamp(1, 0)
        {  "_id" : "test",  "primary" : "rs2",  "partitioned" : false,  "version" : {  "uuid" : UUID("a28d7504-1596-460e-9e09-0bdc6450028f"),  "lastMod" : 1 } }

mongos>
  • 分片键 (Shard Key) 决定了如何将分片集合中的文档分配到各个分片。分片键可以是索引字段或索引复合字段,它必须存在于要插入集合的所有文档中。数据将按范围被分成块,每个块与一个分片键关联。路由器根据范围查询,确定哪个分片将存储该数据块。

选择分片键时,需要考虑以下五个关键属性:

  • 基数
  • 写分布
  • 读分布
  • 读定位
  • 读位置

理想的分片键应使 MongoDB 能够在所有分片上平均分配负载。选择一个合适的分片键至关重要。

删除分片节点

在从集群中移除分片之前,务必确保已将该分片上的数据安全迁移到剩余的分片。MongoDB 负责在移除指定分片节点前,安全地将数据迁移到其他分片节点。

运行以下命令以删除所需的分片:

步骤 1

首先,需要确定要删除的分片的主机名。 以下命令将列出集群中存在的所有分片以及分片的状态。

     db.adminCommand( { listShards: 1 } )
    
mongos> db.adminCommand( { listShards: 1 } )
{
        "shards" : [
                {
                        "_id" : "rs1",
                        "host" : "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022",
                        "state" : 1
                },
                {
                        "_id" : "rs2",
                        "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",
                        "state" : 1
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1593572866, 15),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593572866, 15),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

步骤 2

发出以下命令,从集群中移除所需的分片。一旦发出,平衡器将负责从正在耗尽的分片节点中删除数据块,并平衡剩余块在其余分片节点之间的分布。

    db.adminCommand( { removeShard: “shardedReplicaNodes” } )
    
mongos> db.adminCommand( { removeShard: "rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022" } )
{
        "msg" : "draining started successfully",
        "state" : "started",
        "shard" : "rs1",
        "note" : "you need to drop or movePrimary these databases",
        "dbsToMove" : [ ],
        "ok" : 1,
        "operationTime" : Timestamp(1593572385, 2),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593572385, 2),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

步骤 3

要检查耗尽分片的状态,请再次发出相同的命令。

  db.adminCommand( { removeShard: “rs1/127.0.0.1:27020,127.0.0.1:27021,127.0.0.1:27022” } )
  

我们需要等待数据排空完成。 msgstate 字段将显示数据排空是否已完成,如下所示:

   "msg" : "draining ongoing",
   "state" : "ongoing",
   

我们还可以使用 sh.status() 命令检查状态。一旦删除分片节点,该节点将不再显示在输出中。但是,如果数据排空正在进行,则该分片节点的排空状态将为 true。

步骤 4

继续使用上述命令检查排空状态,直到完全删除所需的分片。完成数据排空后,命令输出将反映出已完成的消息和状态。

 "msg" : "removeshard completed successfully",
 "state" : "completed",
 "shard" : "rs1",
 "ok" : 1,
 

步骤 5

最后,我们需要检查集群中剩余的分片。 要检查状态,请输入 sh.status()db.adminCommand( { listShards: 1 } )

  mongos> db.adminCommand( { listShards: 1 } )
  
{
        "shards" : [
                {
                        "_id" : "rs2",
                        "host" : "rs2/127.0.0.1:27023,127.0.0.1:27024,127.0.0.1:27025",
                        "state" : 1
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1593575215, 3),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1593575215, 3),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
  

这里可以看到,已删除的分片不再显示在分片列表中。

分片相对于复制的优势

  • 在复制中,主节点处理所有写入操作,而辅助节点则维护备份副本或提供只读操作。 但是,当分片与副本集一起使用时,负载会分布在多个服务器之间。
  • 单个副本集限制为 12 个节点,而分片集群对分片数量没有限制。
  • 复制需要高端硬件或垂直扩展来处理大型数据集,与在分片中添加额外的服务器相比,这成本更高。
  • 在复制中,可以通过添加更多的从/从节点来提高读取性能,而在分片中,可以通过添加更多的分片节点来提高读取和写入性能。

分片技术的局限性

  • 分片集群不支持跨分片的唯一索引,除非唯一索引以完整的分片键为前缀。
  • 对一个或多个文档的分片集合的所有更新操作,必须在查询中包含分片键或 _id 字段。
  • 只有当集合的大小不超过指定阈值时,才能对集合进行分片。 此阈值可以根据所有分片键的平均大小和配置的块大小来估计。
  • 分片还存在对最大集合大小或数据块拆分数量的操作限制。
  • 选择不合适的分片键可能会导致性能瓶颈。

总结

MongoDB 提供了内置的分片功能,可用于处理大型数据库,同时确保性能不受影响。 希望以上内容能够帮助您理解 MongoDB 分片机制的设置。 接下来,您可能需要进一步熟悉一些常用的 MongoDB 命令。