MongoDB分片入门

MongoDB学习笔记系列博客


一,概述

MongoDB复制集解决了数据库的备份与自动故障转移,但是围绕数据库的业务中当前还有两个方面的问题变得越来越重要,一是海量数据如何存储,二是如何高效地读写海量数据。尽管复制集也可以实现读写分离,如在primary节点上写,在secondary节点上读,但在这种方式下客户端读出来的数据有可能不是最新的,因为primary节点到secondary节点间的数据同步会带来一定延迟,而且这种方式也不能处理大量数据。MongoDB引入了分片机制,实现了海量数据的分布式存储与高效的读写分离。复制集中的每个成员是一个mongod实例,但在分片部署上,每一个分片可能就是一个复制集。

分片的使用会是数据库系统变得复杂。什么时候使用分片也是需要考虑的问题。

MongoDB使用内存映射文件的方式来读写数据库,对内存的管理由操作系统来负责。随着时间的推移,数据库的索引和数据文件会变得越来越大,对于单节点的机器来说,迟早会突破内存的限制。当磁盘上的数据文件和索引远大于内存的大小时,操作系统会频繁地进行内存交换,导致整个数据库系统的读写性能下降。

因此对于大数据的处理,要时刻监控MongoDB的磁盘I/O性能、可用内存的大小,在数据库内使用率达到一定程度时就要考虑分片了,通过分片使整个数据库分布在各个片上,每个片拥有数据库的一部分数据,从而降低内存使用率,提高读写性能。

为什么使用分片:
1. 复制所有的写入操作到主节点,导致读写性能下降
2. 时间延迟导致查询出现误差
3. 当请求量巨大时会出现内存不足
4. 本地磁盘不足
5. 垂直扩展价格昂贵


二,分片集群架构

MongoDB中使用分片集群结构分布:
这里写图片描述

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

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

Config Server(replica set):
mongod实例,存储了整个 ClusterMetadata,其中包括 chunk信息。

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

分片集群中的一个片shard实际上就是一个复制集,当然一个片也可以是单个mongod实例,只是在分片集群的生产环境中,每个片只是保存整个数据库数据的一部分,如果这部分数据丢失了,那么整个数据库就不完整了。因此我们应该保证每个片数据的稳定性和完整性。我们通过将片配置为复制集的形式,是片shard在默认情况下读写都在复制集的primary节点上,每个片同时具有自动故障转移、冗余备份的功能。

mongos路由进程是一个轻量级且非持久性的进程。它不会保存任何数据库中的数据,它只是将整个分片集群看成一个整体,是分片集群对整个客户端程序来说是透明的。当客户端发起读写操作时,由mongos路由进程将该操作路由到具体的分片上进行;为了实现对读写请求的路由,mongos进行必须知道整个分片集群上所有数据库的分片情况,即元数据。这些信息是从配置服务器上同步过来的,每次进程启动时都会从config server服务器上读元信息,mongos并非持久化保存这些信息。

配置服务器config server在整个分片集群中相当重要。上面说到mongos会从配置服务器同步元信息,因此配置服务器要能实现这些元信息的持久化。配置服务器上的数据如果丢失,那么整个分片集群就无法使用,因此在生产环境中要使用复制集部署配置服务器。

注意:MongoDB 3.4版本后的配置服务器必须部署为复制集,不能采用MongoDB 3.2的mirror模式,不然会报错。

mirrored config server connections are not supported; for config server replica sets be sure to use the replica set connection string


三,分片集群部署

1,配置复制集rs0并启动
先创建好rs0中各节点的数据文件存放路径、日志文件路径以及配置文件,其中配置文件的内容如下:

rs0中primary节点的配置文件为rs0_0.conf。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\rs0_0
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\logs\rs0_0.log
journal = true
port = 40000
replSet = rs0
shardsvr = true

rs0中secondary节点的配置文件为rs0_1.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\rs0_1
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\logs\rs0_1.log
journal = true
port = 40001
replSet = rs0
shardsvr = true

rs0中arbiter节点的配置文件为rs0_2.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\rs0_2
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs0\data\logs\rs0_2.log
journal = true
port = 40002
replSet = rs0
shardsvr = true

启动复制集可以参考本人的另一篇博客:MongoDB复制集入门
注意:如果复制集要做为分片集群的一个片,启动时要带上shardsvr参数。
这里提供具体的命令

//启动primary节点
mongod --shardsvr --config D:\MongoDB\Server\3.4\data\shard\db_rs0\data\configs_rs0\rs0_0.conf

//启动secondary节点
mongod --shardsvr --config D:\MongoDB\Server\3.4\data\shard\db_rs0\data\configs_rs0\rs0_1.conf

//启动arbiter节点
mongod --shardsvr --config D:\MongoDB\Server\3.4\data\shard\db_rs0\data\configs_rs0\rs0_2.conf

//添加节点
rs.initiate()
rs.add("VICTOR-PC:40001")
rs.addArb("VICTOR-PC:40002")

2,配置复制集rs1并启动,步骤与上面相同,这里给出rs1中各节点对应的配置文件。
rs1中primary节点的配置文件为rs1_0.conf。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\rs1_0
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\logs\rs1_0.log
journal = true
port = 40003
replSet = rs1
shardsvr = true

rs1中secondary节点的配置文件为rs1_1.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\rs1_1
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\logs\rs1_1.log
journal = true
port = 40004
replSet = rs1
shardsvr = true

rs1中arbiter节点的配置文件为rs1_2.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\rs1_2
logpath = D:\MongoDB\Server\3.4\data\shard\db_rs1\data\logs\rs1_2.log
journal = true
port = 40005
replSet = rs1
shardsvr = true

具体的启动方式可以参考上面rs0的启动。

3,配置configure服务器。
config server服务器也是一个mongod进程,它与普通的mongod实例没有本质区别,只是它上面的数据库以及集合是特意给分片集群用的,三个独立的配置服务器对应的启动配置文件内容如下。

configure服务器1的配置文件cfgserver_0.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\db_config0
logpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\logs\db_config0.log
journal = true
port = 40006
configsvr = true
replSet = rscfg

configure服务器2的配置文件cfgserver_1.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\db_config1
logpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\logs\db_config1.log
journal = true
port = 40007
configsvr = true
replSet = rscfg

configure服务器2的配置文件cfgserver_2.conf如下所示。

dbpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\db_config2
logpath = D:\MongoDB\Server\3.4\data\shard\db_configs\data\logs\db_config2.log
journal = true
port = 40008
configsvr = true
replSet = rscfg

配置服务器上的mongod实例启动时的配置选项跟普通的mongod实例差不多,只是多了一个config=true的选择,说明这个mongod实例是一个configure的mongod实例。

启动上面三个配置服务器,并部署为复制集(MongoDB 3.4版本配置服务器必须部署成复制集形式)。

//启动三个mongod实例
mongod --config D:\MongoDB\Server\3.4\data\shard\db_configs\data\configs\cfgserver_0.conf
D:\MongoDB\Server\3.4\data\shard\db_configs\data\configs\cfgserver_1.conf
D:\MongoDB\Server\3.4\data\shard\db_configs\data\configs\cfgserver_2.conf
//连接40006端口的mongod实例
mongo --port 40006
rs.initiate()
//注意:配置服务器的节点不能是arbiter
rs.add("VICTOR-PC:40007")
rs.add("VICTOR-PC:40008")

4,配置mongos路由服务器
配置文件cfg_mongos.conf内容如下:

logpath = D:\MongoDB\Server\3.4\data\shard\mongos\logs\mongos.log
port = 40009
configdb = rscfg/VICTOR-PC:40006,VICTOR-PC:40007,VICTOR-PC:40008

启动路由器。

mongos --config D:\MongoDB\Server\3.4\data\shard\mongos\cfg_mongos.conf

实例对应的进程为mongos,路由服务器只是一个轻量级和非持久化操作的进程,因此上面的配置文件里面没有像其他mongod实例那样有一个存放数据文件的路径选项dbpath。

5,添加各分片到集群。
前面已经完成了两个片(复制集)、三个配置服务器、一个路由服务器且它已经知道从哪些配置服务器上同步元数据(configdb = rscfg/VICTOR-PC:40006,VICTOR-PC:40007,VICTOR-PC:40008),下面要做的是将各个片添加到集群中。

//打开一个mongo客户端连接到mongos服务器
mongo --port 40009
//添加两个分片
sh.addShard("rs0/VICTOR-PC:40000,VICTOR-PC:40001")
sh.addShard("rs0/VICTOR-PC:40003,VICTOR-PC:40004")

注意:这里添加分片的命令是sh.addShard(),参数是复制集名已经复制集中不包含arbiter类型的所有节点。

6,最后通过命令sh.status()检查上面的配置是否正确。

mongos> sh.status()
--- Sharding Status ---
  sharding version: {
        "_id" : 1,
        "minCompatibleVersion" : 5,
        "currentVersion" : 6,
        "clusterId" : ObjectId("59552cce22458eb7809b6284")
}
  shards:
        {  "_id" : "rs0",  "host" : "rs0/VICTOR-PC:40000,VICTOR-PC:40001",  "sta
te" : 1 }
        {  "_id" : "rs1",  "host" : "rs1/VICTOR-PC:40003,VICTOR-PC:40004",  "sta
te" : 1 }
  active mongoses:
        "3.4.4" : 1
 autosplit:
        Currently enabled: yes
  balancer:
        Currently enabled:  yes
        Currently running:  no
                Balancer lock taken at Fri Jun 30 2017 00:37:34 GMT+0800 by Conf
igServer:Balancer
        Failed balancer rounds in last 5 attempts:  0
        Migration Results for the last 24 hours:
                No recent migrations
  databases:
        {  "_id" : "test",  "primary" : "rs0",  "partitioned" : false }

mongos>

上面输出的信息中,clusterId字段表示此分片集群的唯一标识;shards为分片集群中包含的所有片,其中_id为此片的名称,host为片中主机的host信息;database为集群中的所有数据库,其中_id为数据库名称,partitioned表示此数据库是否支持分片,primary节点表示当数据库支持分片,此数据库上所有未分片的集合所在的片。

通过路由进程mongos连接集群,执行命令show dbs 我们可以看到集群中存在系统默认创建的一个config数据库,而且这个数据库只存在于三个配置服务器上,config数据库中的集合包含了整个集群的配置信息。执行命令show collections ,我们可以看到有如下集合。

mongos> show collections
changelog //保存被分片的集合的任何元数据的改变,例如chunks的迁移、分割等。
chunks //保存集群中分片集合的所有块的信息,包含块的数据范围与块所在的片。
databases //保存数据库中的所有数据库,包含分片与未分片的。
lockpings //保存跟踪集群中的激活组件。
locks //均衡器balancer执行时产生锁,在此集合中插入一条记录。
migrations 
mongos //保存了集群中所有路由mongos的信息。
shards //保存了集群中的所有片的信息。
tags
version //保存当前所有信息的所有片信息。
mongos>

由上述得知,配置服务器的config数据库的信息对于整个集群来说至关重要的,这也是生产环境中最少需要3个配置服务器做冗余备份的原因。

通过上面的部署可以得知,在实际部署中,一个生产环境最少需要9个mongod实例进程,一个mongos进程实例,理论上说最少需要由10台机器才能组成。但是这些进程中有些不要很多软硬件资源,它们可以与其他进程共存部署在同一个机器上,如复制集中arbiter集成,mongos进程可以部署到应用程序所在的服务器。


四,分片的工作机制

1,使集合分片
部署分片集群的时候我已经创建了一个数据库test,并插入了一条数据。

mongos> use test
mongos> db.test.insert({age:1,name:'a',city:'gz'})
mongos> sh.status()
(省略部分信息)
databases:{  "_id" : "test",  "primary" : "rs0","partitioned" : false }

此时数据库test还没实现分片,且数据库中所有未分片的集合都保存在片rs0中。

MongoDB的分片是基于范围的,也就是说任何一个文档一定位于指定片键的某个范围内。一旦片键选择好了,chunks就会按照片键将一部分docunments从逻辑上组合在一起。

这里对users集合选择city字段作为片键来分片,假如现在city字段有ah,cs,bj,hz,gz,sh,初始时刻随机地向集群中插入包含以上字段值的文档,当chunks的大小未达到默认的阈值时,集群中应该只有一个chunk,随着继续插入文档,超过阈值的chunk会被分割成两个chunks。

下面继续通过命令使集合users分片,使集合分片必须先使其所在的数据库支持分片。

对已有数据的集合进行分片,必须先在所选择的片键上创建一个索引,如果索引初始时没有任何数据,则MongoDB会自动在所选择的片键上创建一个索引。

sh.enableSharding("test") //使数据库支持分片
db.users.ensureIndex({city:1}) //创建基于片键的索引
sh.shardCollection("test.users",{city:1}) //使集合分片
sh.status()
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "minCompatibleVersion" : 5,
    "currentVersion" : 6,
    "clusterId" : ObjectId("59552cce22458eb7809b6284")
}
  shards:
    {  "_id" : "rs0",  "host" : "rs0/VICTOR-PC:40000,VICTOR-PC:40001",  "state" : 1 }
    {  "_id" : "rs1",  "host" : "rs1/VICTOR-PC:40003,VICTOR-PC:40004",  "state" : 1 }
  active mongoses:
    "3.4.4" : 1
  balancer:
    Currently enabled:  yes
    Currently running:  yes
        Balancer lock taken at Fri Jun 30 2017 00:37:34 GMT+0800 by ConfigServer:Balancer
    Failed balancer rounds in last 5 attempts:  0
    Migration Results for the last 24 hours: 
        No recent migrations
  databases:
    {  "_id" : "test",  "primary" : "rs0",  "partitioned" : true }
        test.users //分片的集合
            shard key: { "city" : 1 } //片键
            unique: false
            balancing: true
            chunks: //所有块信息
                rs0 1 //当前只有1个块在rs0上
            { "city" : { "$minKey" : 1 } } -->> { "city" : { "$maxKey" : 1 } } on : rs0 Timestamp(1, 0)  //此块的包含键值范围是-∞~∞,而且在片rs0上,因为此时集合中只有一条记录,还还未进行块的分割、迁移。

其中,$minKey表示负无穷,它比MongoDB中的任何值都要小,$maxKey表示正无穷,它比MongoDB中的任何值都要大。

为了观察到集合被分成多个chunk并分布在多个片上,继续插入一些数据进行分析。可以通过脚本使用for循环导入不同的测试数据,当数据量达到阈值时,就会出现chunk的分割与迁移。

当数据量达到一定程度时,再观察database

databases:
    {  "_id" : "test",  "primary" : "rs0",  "partitioned" : true }
        test.users
            shard key: { "city" : 1 }
            unique: false
            balancing: true
            chunks:
                rs0 2
                rs1 1
            { "city" : { "$minKey" : 1 } } -->> { "city" : "bj" } on : rs1 Timestamp(2, 0) 
            { "city" : "bj" } -->> { "city" : "sh" } on : rs0 Timestamp(2, 1) 
            { "city" : "sh" } -->> { "city" : { "$maxKey" : 1 } } on : rs0 Timestamp(1, 3) 

这说明此时集合中有三个块,其中在rs0上有两个块,在片rs1上有一个块,每个块包含一定区间范围的文档。

开始键值结束键值所在分片
-∞bjrs1
bjshrs0
shrs0

注意,区间是左闭右开区间,即三个区间可以表示为

 -∞ < city < bj
 bj <= city < sh
 sh <= city < ∞

而city为ah的文档就落在片rs1上。

就是这样,MongoDB实现了海量数据的分布式存储,同时由于每个片又是复制集组成的,保证了数据的可靠性。

2,集群平衡器
当一个被分片的集合的所有chunk在集群上分布不均匀时,平衡器就会将chunk从拥有最大数量块的片上迁移到拥有最少数量的片上。例如,如果一个集合有200个块在shard A上,50个块在shard B上,那么平衡器将启动迁移,直到shard A上的块与shard B上的块差不多相等为止。
只有当某个分片的集合中块数量的分布差达到设定的阈值时才会触发平衡器开始工作。

3,集群的写与读
客户端应用程序对集群的读写操作与对单个mongod实例是一样的,mongos路由服务器起到了封装集群的作用,使集群对应用程序来说是透明的,记录的读写路由到哪个chunk都由mongos路由服务器负责。

在分片集群上,除了索引会影响查询性能外,查询语句中是否包含了片键也会对查询性能产生影响。

db.users.getIndexes()
/* 1 */
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "test.users"
    },
    {
        "v" : 2,
        "key" : {
            "city" : 1.0
        },
        "name" : "city_1",
        "ns" : "test.users"
    }
]

上面有两个索引,其中_id_为集合默认创建的id索引,city_1为分片时在选择的片键上所创建的索引。

片键是决定查询落到哪个片哪个chunk上的依据,这个信息保存在集群的配置服务器上。索引时针对每个片上集合来说的,每个片都会为属于自己的那部分集合创建独立的索引数据。因此查询语句中是否包含片键和索引对查询性能影响较大,查询语句中的片键决定了路由到哪个片上,索引进一步决定了在片上的primary节点上高效查询。


五,片键选择策略

第一种情况,升序字段片键。MongoDB会为每个文档创建一个默认的_id字段,这个字段是根据时间戳得到的,因此是一个升序值。假如以这个字段作为片键,所有最新插入的字段根据片键来计算,可能属于同一个区间范围,因此所有的写操作将被路由到同一个chunk上,出现局部热点,当这种情形发生时,MongoDB集群就没有实现我们想要的写负载均衡的目的。

第二种情况:完全随机的片键,即散列片键(Hashed Shard Key)。这种片键虽然可以解决第一种情况不能分发写操作的问题,但由于太过随机,导致写操作将被分散到整个集群上,维护片键索引时,所有的索引文件将被调入内存,由于物理内存大小的限制,最终将导致频繁地页面换入换出,从而降低系统性能,同时太过随机的片键读操作将太过分散,不够局部化,不能实现指定目标范围的查询,每一个读操作可能要查询所有的片,这样影响读的性能。

第三种情况:片键的取值范围有限。就像上面使用city字段作为片键,我们插入的数据会按照city字段进行分析,由于city字段的取值有限,当我们的每一个city区间所对应的文档都分割了一个chunk时,这个时候继续插入大量的文档,将会出现没有可以再来用于分割的片键值,每一个chunk将会不断变大但又不能分割,最终导致集群中的数据严重不平衡。

第四种情况:基于位置的片键。基于位置的片键可以是用户的IP、经纬度、或者是地址。位置片键不必与实际的物理位置字段相关:这里的“位置”比较抽象,数据会根据这个“位置”进行分组。无论如何,所有与该键值比较接近的文档都会保存在同一范围的片中。这样可以方便地将数据与相应的用户,以及相关联的数据保存在一起。例如,假设我们有一个集合的文档是按照IP地址进行分片的。文档会依据IP地址被分成不同的块,并随机分布在集群中。

通过以上三种情况的介绍可见,对于海量数据的读写操作选择一个合适的片键并不容易。一个好的片键应该具有以下特质。

  1. 分发写操作。
  2. 读操作不能太过随机化(尽量局部化)。
  3. 要保证chunk能够一直被分割。

片键决定了集群中一个集合的 documents 在不同 shards 中的分布。片键字段必须被索引,且在集合中的每条记录都不能为空,可以是单个字段或复合字段。


六,小结

分片集群上的锁范围局限在每一个片上,而不是整个集群,每一个片上的操作都是独立的,因此不会影响其他片上的操作。分片集群通过路由器mongos分发读写操作到各个片上,整体上提高了系统的并发性和吞吐量。并不是所有的系统都适合部署成分片集合,只有当数据量打、读写请求很高时才适合用分片集群。一旦部署成分片集群,那么集群中的每一个片都应该部署成复制集的形式,提高系统的可靠性和故障恢复的能力。分片时片键的选择很重要,不好的片键会降低系统的性能。


七,参考阅读

https://docs.mongodb.com/manual/sharding/
《大数据存储 MongoDB实战指南》
《MongoDB权威指南》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值