目录
JSON格式:mongoexport/mongoimport
BSON格式:mongoexport/mongoimport
为什么要引入复制集?
保证数据在生产部署时的冗余和可靠性,通过在不同的机器上保存副本来保证数据的不会因为单点损坏而丢失。能够随时应对数据丢失、机器损坏带来的风险。换一句话来说,还能提高读取能力,用户的读取服务器和写入服务器在不同的地方,而且,由不同的服务器为不同的用户提供服务,提高整个系统的负载。
在MongoDB中就是复制集(replica set): 一组复制集就是一组mongod实例掌管同一个数据集,实例可以在不同的机器上面。实例中包含一个主导,接受客户端所有的写入操作,其他都是副本实例,从主服务器上获得数据并保持同步。
复制集有哪些成员?
在上图中,我们了解了复制集中的主节点(Primary)和从节点(Secondary), 进一步的我们需要了解更多复制集中的成员,以便深入部署架构和相关配置。
基本成员
让我们看下基本的成员:
- 主节点(Primary)
包含了所有的写操作的日志。但是副本服务器集群包含有所有的主服务器数据,因此当主服务器挂掉了,就会在副本服务器上重新选取一个成为主服务器。
- 从节点(Seconary)
正常情况下,复制集的Seconary会参与Primary选举(自身也可能会被选为Primary),并从Primary同步最新写入的数据,以保证与Primary存储相同的数据。
Secondary可以提供读服务,增加Secondary节点可以提供复制集的读服务能力,同时提升复制集的可用性。另外,Mongodb支持对复制集的Secondary节点进行灵活的配置,以适应多种场景的需求。
- 仲裁节点(Arbiter)
Arbiter节点只参与投票,不能被选为Primary,并且不从Primary同步数据。
比如你部署了一个2个节点的复制集,1个Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加一个Arbiter节点,即使有节点宕机,仍能选出Primary。
Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入一个Arbiter节点,以提升复制集可用性。
主节点(Primary)的细化
依据具体功能实现的需要,MongoDB还细化将主节点(Primary)进行了细化:
- Priority0
作为一个辅助可以作为一个备用。在一些复制集中,可能无法在合理的时间内添加新成员的时候。备用成员保持数据的当前最新数据能够替换不可用的成员。
Priority0节点的选举优先级为0,不会被选举为Primary
比如你跨机房A、B部署了一个复制集,并且想指定Primary必须在A机房,这时可以将B机房的复制集成员Priority设置为0,这样Primary就一定会是A机房的成员。
(注意:如果这样部署,最好将『大多数』节点部署在A机房,否则网络分区时可能无法选出Primary)
- Hidden
客户端将不会把读请求分发到隐藏节点上,即使我们设定了 复制集读选项 。
这些隐藏节点将不会收到来自应用程序的请求。我们可以将隐藏节点专用于报表节点或是备份节点。 延时节点也应该是一个隐藏节点。
Hidden节点不能被选为主(Priority为0),并且对Driver不可见。因Hidden节点不会接受Driver的请求,可使用Hidden节点做一些数据备份、离线计算的任务,不会影响复制集的服务。
- Delayed
延时节点的数据集是延时的,因此它可以帮助我们在人为误操作或是其他意外情况下恢复数据。
举个例子,当应用升级失败,或是误操作删除了表和数据库时,我们可以通过延时节点进行数据恢复。
Delayed节点必须是Hidden节点,并且其数据落后与Primary一段时间(可配置,比如1个小时)。
因Delayed节点的数据比Primary落后一段时间,当错误或者无效的数据写入Primary时,可通过Delayed节点的数据来恢复到之前的时间点。
复制集常见部署架构?
我们将从基础三个节点和跨数据中心两个角度看常见复制集的部署架构:
基础三节点
- 一主两从方式
- 一个主节点;
- 两个从节点组成,主节点宕机时,这两个从节点都可以被选为主节点。
当主节点宕机后,两个从节点都会进行竞选,其中一个变为主节点,当原主节点恢复后,作为从节点加入当前的复制集群即可。
- 一主一从一仲裁方式
- 一个主节点
- 一个从节点,可以在选举中成为主节点
- 一个仲裁节点,在选举中,只进行投票,不能成为主节点
当主节点宕机时,将会选择从节点成为主,主节点修复后,将其加入到现有的复制集群中即可。
跨数据中心
单个数据中心中的复制集易受数据中心故障的影响,比如断电,洪水,断网等;所以多个数据中心便是这么引入的。
为了在数据中心发生故障时保护您的数据,请在备用数据中心中至少保留一个成员。如果可能,请使用奇数个数据中心,并选择成员分布,以最大程度地保证即使丢失数据中心,其余复制集成员也可以构成大多数或最小数量的副本,以提供数据副本。
三个节点
对于三成员复制集,成员的一些可能的分布包括:
- 两个数据中心:两个是数据中心1的成员,一个是数据中心2的成员。如果复制集的成员之一是仲裁者,则将仲裁者与一个承载数据的成员一起分发到数据中心1。
- 如果数据中心1发生故障,则复制集将变为只读。
- 如果数据中心2发生故障,则复制集将保持可写状态,因为数据中心1中的成员可以举行选举。
- 三个数据中心:一个成员是数据中心1,一个成员是数据中心2,一个成员是数据中心3。
- 如果任何数据中心发生故障,复制集将保持可写状态,因为其余成员可以举行选举。
注意
在两个数据中心之间分布复制集成员可提供优于单个数据中心的好处。在两个数据中心分布中,
- 如果其中一个数据中心发生故障,则与单个数据中心分发不同,该数据仍然可供读取。
- 如果具有少数成员的数据中心发生故障,则复制集仍然可以同时执行写操作和读操作。
- 但是,如果具有大多数成员的数据中心发生故障,则复制集将变为只读。
如果可能,请在至少三个数据中心中分配成员。对于配置服务器复制集(CSRS),最佳做法是在三个(或更多,取决于成员的数量)中心之间分布。如果第三个数据中心的成本高得令人望而却步,则一种分配可能性是,在公司政策允许的情况下,在两个数据中心之间平均分配数据承载成员,并将其余成员存储在云中。
五个节点
对于具有5个成员的复制集,成员的一些可能的分布包括(相关注意事项和三个节点一致,这里仅展示分布方案):
- 两个数据中心:数据中心1的三个成员和数据中心2的两个成员。
- 如果数据中心1发生故障,则复制集将变为只读。
- 如果数据中心2发生故障,则复制集将保持可写状态,因为数据中心1中的成员可以创建多数。
- 三个数据中心:两个成员是数据中心1,两个成员是数据中心2,一个成员是站点数据中心3。
- 如果任何数据中心发生故障,复制集将保持可写状态,因为其余成员可以举行选举。
例如,以下5个成员复制集将其成员分布在三个数据中心中。
数据转移的优先级
复制集的某些成员(例如,具有网络限制或资源有限的成员)不应成为故障转移中的主要成员。将不应成为主要成员的成员配置为具priority0。
在某些情况下,您可能希望将一个数据中心中的成员选为主要成员,然后再选择另一数据中心中的成员。您可以修改priority成员的,以使一个数据中心中priority的成员高于 其他数据中心中的成员。
在以下示例中,数据中心1中的复制集成员具有比数据中心2和3中的成员更高的优先级;数据中心2中的成员比数据中心3中的成员具有更高的优先级:
复制集是如何保证数据高可用的?
那么复制集是如何保证数据的高可靠性的呢?或者说它包含有什么机制?这里我们通过两方面阐述:一个是选举机制,另一个是故障转移期间的回滚。
选举机制
复制集通过选举机制来选择主节点。
- 如何选出Primary主节点的?
假设复制集内能够投票的成员数量为N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态。
举例:3投票节点需要2个节点的赞成票,容忍选举失败次数为1;5投票节点需要3个节点的赞成票,容忍选举失败次数为2;通常投票节点为奇数,这样可以减少选举失败的概率。
- 在什么情况下会触发选举机制?
在以下的情况将触发选举机制:
-
往复制集中新加入节点
-
初始化复制集时
-
对复制集进行维护时,比如
rs.stepDown()
或者rs.reconfig()
操作时 -
从节点失联时,比如超时(默认是10秒)
-
哪些成员具备选举权?哪些没有?
首先不是所有的节点都会参与投票,一个复制集最高可以有50个节点,但是只有7个投票节点。一个非投票节点它的votes是0即vote0; 它的priority是0即priority0。
比如:
同时可投票的节点,必须属于如下状态之一:PRIMARY, SECONDARY, STARTUP2, RECOVERING, ARBITER, ROLLBACK.
- 哪些因素可能会影响选举呢?
比如:
- 复制集的选举协议,例如在v4之前是pv0, v4开始为pv1;
- 心跳
- 成员权重
- 数据中心失联
- 网络分区
- 镜像读取(Mirrored Reads)注:MongoDBv4.4开始提供的功能,用来预热从节点最近读取过的数据。
如果你还期望对上述影响因素有更全面的认识,可以参考官方文档 - Factors and Conditions that Affect Elections
故障转移期间的回滚
当成员在故障转移后重新加入其复制集时,回滚将还原以前的主在数据库上的写操作。 本质上就是保证数据的一致性。
仅当主服务器接受了在主服务器降级之前辅助服务器未成功复制的写操作时,才需要回滚。 当主数据库作为辅助数据库重新加入集合时,它会还原或“回滚”其写入操作,以保持数据库与其他成员的一致性。
更多可以参考官方文档 - Rollbacks During Replica Set Failover
复制集中的OptLog
oplog(操作日志)是一个特殊的有上限的集合(老的日志会被overwrite),它保存所有修改数据库中存储的数据的操作的滚动记录。
什么是OptLog
MongoDB在主节点上应用数据库操作,然后将这些操作记录到optlog中。然后从节点通过异步进程复制和应用(数据同步)这些操作。在local.oplog.rs集合中,所有复制集成员都包含oplog的一个副本用来维护数据库的当前状态。
MongoDB 4.4支持以小时为单位指定最小操作日志保留期,其中MongoDB仅在以下情况下删除操作日志条目:
- oplog已达到配置的最大大小
- oplog条目早于配置的小时数
在设计OptLog时要考虑什么
看下MongoDB在设计OptLog时考虑了什么?这对我们在使用和配置optlog有很好的帮助。
-
查看操作日志的状态?
-
操作日志设置多大?默认设置是多大呢?
-
操作日志保存多久?
-
哪些情况需要设置更大的?
-
对操作慢的管理和设置?
更多可以参考官方文档 - Replica Set Oplog
复制集中的数据同步
复制集中的数据同步是为了维护共享数据集的最新副本,包括复制集的辅助成员同步或复制其他成员的数据。 MongoDB使用两种形式的数据同步:
- 初始同步(Initial Sync) 以使用完整的数据集填充新成员, 即全量同步
- 复制(Replication) 以将正在进行的更改应用于整个数据集, 即增量同步
初始同步(Initial Sync)
从节点当出现如下状况时,需要先进行全量同步
- oplog为空
- local.replset.minvalid集合里_initialSyncFlag字段设置为true
- 内存标记initialSyncRequested设置为true
这3个场景分别对应
- 新节点加入,无任何oplog,此时需先进性initial sync
- initial sync开始时,会主动将_initialSyncFlag字段设置为true,正常结束后再设置为false;如果节点重启时,发现_initialSyncFlag为true,说明上次全量同步中途失败了,此时应该重新进行initial sync
- 当用户发送resync命令时,initialSyncRequested会设置为true,此时会重新开始一次initial sync
intial sync流程
- 全量同步开始,设置minvalid集合的_initialSyncFlag
- 获取同步源上最新oplog时间戳为t1
- 全量同步集合数据 (耗时)
- 获取同步源上最新oplog时间戳为t2
- 重放[t1, t2]范围内的所有oplog
- 获取同步源上最新oplog时间戳为t3
- 重放[t2, t3]范围内所有的oplog
- 建立集合所有索引 (耗时)
- 获取同步源上最新oplog时间戳为t4
- 重放[t3, t4]范围内所有的oplog
- 全量同步结束,清除minvalid集合的_initialSyncFlag
复制(Replication)
initial sync结束后,接下来Secondary就会『不断拉取主上新产生的optlog并重放』,这个过程在Secondary同步慢问题分析也介绍过,这里从另一个角度再分析下。
- producer thread,这个线程不断的从同步源上拉取oplog,并加入到一个BlockQueue的队列里保存着。
- replBatcher thread,这个线程负责逐个从producer thread的队列里取出oplog,并放到自己维护的队列里。
- sync线程将replBatcher thread的队列分发到默认16个replWriter线程,由replWriter thread来最终重放每条oplog。
问题来了,为什么一个简单的『拉取oplog并重放』的动作要搞得这么复杂?
性能考虑,拉取oplog是单线程进行,如果把重放也放到拉取的线程里,同步势必会很慢;所以设计上producer thread只干一件事。
为什么不将拉取的oplog直接分发给replWriter thread,而要多一个replBatcher线程来中转?
oplog重放时,要保持顺序性,而且遇到createCollection、dropCollection等DDL命令时,这些命令与其他的增删改查命令是不能并行执行的,而这些控制就是由replBatcher来完成的。
注意事项
这部分内容源自:阿里巴巴在这块的技术专家张友东
-
initial sync单线程复制数据,效率比较低,生产环境应该尽量避免initial sync出现,需合理配置oplog,按默认『5%的可用磁盘空间』来配置oplog在绝大部分场景下都能满足需求,特殊的case(case1, case2)可根据实际情况设置更大的oplog。
-
新加入节点时,可以通过物理复制的方式来避免initial sync,将Primary上的dbpath拷贝到新的节点,直接启动,这样效率更高。
-
当Secondary上需要的oplog在同步源上已经滚掉时,Secondary的同步将无法正常进行,会进入RECOVERING的状态,需向Secondary主动发送resyc命令重新同步。
-
生产环境,最好通过db.printSlaveReplicationInfo()来监控主备同步滞后的情况,当Secondary落后太多时,要及时调查清楚原因。
-
当Secondary同步滞后是因为主上并发写入太高导致,(db.serverStatus().metrics.repl.buffer.sizeBytes持续接近db.serverStatus().metrics.repl.buffer.maxSizeBytes),可通过调整Secondary上replWriter并发线程数来提升。
复制集读写关注(concern)
读的优先级(Read Preference)
默认情况下,复制集的所有读请求都发到Primary,Driver可通过设置Read Preference来将读请求路由到其他的节点。
primary
: 默认规则,所有读请求发到PrimaryprimaryPreferred
: Primary优先,如果Primary不可达,请求Secondarysecondary
: 所有的读请求都发到secondarysecondaryPreferred
:Secondary优先,当所有Secondary不可达时,请求Primarynearest
:读请求发送到最近的可达节点上(通过ping探测得出最近的节点)
Write Concern
默认情况下,Primary完成写操作即返回,Driver可通过设置Write Concern来设置写成功的规则。
如下的write concern规则设置写必须在大多数节点上成功,超时时间为5s。
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: majority, wtimeout: 5000 } }
)
上面的设置方式是针对单个请求的,也可以修改副本集默认的write concern,这样就不用每个请求单独设置。
cfg = rs.conf()
cfg.settings = {}
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)
为什么要引入分片
高数据量和吞吐量的数据库应用会对单机的性能造成较大压力, 大的查询量会将单机的CPU耗尽, 大的数据量对单机的存储压力较大, 最终会耗尽系统的内存而将压力转移到磁盘IO上。
为了解决这些问题, 有两个基本的方法: 垂直扩展
和水平扩展
。
垂直扩展
:增加更多的CPU和存储资源来扩展容量。水平扩展
:将数据集分布在多个服务器上。MongoDB的分片就是水平扩展的体现。
分片设计思想
分片为应对高吞吐量与大数据量提供了方法。使用分片减少了每个分片需要处理的请求数,因此,通过水平扩展,集群可以提高自己的存储容量和吞吐量。举例来说,当插入一条数据时,应用只需要访问存储这条数据的分片.
分片目的
- 读/写能力提升
- 存储容量扩容
- 高可用性
分片集群的结构
一个MongoDB的分片集群包含如下组件:
shard
: 即分片,真正的数据存储位置,以chunk为单位存数据;每个分片可以部署为一个复制集。mongos
: 查询的路由, 提供客户端和分片集群之间的接口。config servers
: 存储元数据和配置数据。
这里要注意mongos提供的是客户端application与MongoDB分片集群的路由功能,这里分片集群包含了分片的collection和非分片的collection。如下展示了通过路由访问分片的collection和非分片的collection:
分片数据如何存储:Chunk
分片的内部是如何管理数据的呢?
Chunk是什么
在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据。chunk的产生,会有以下两个用途:
Splitting
:当一个chunk的大小超过配置中的chunk size时,MongoDB的后台进程会把这个chunk切分成更小的chunk,从而避免chunk过大的情况Balancing
:在MongoDB中,balancer是一个后台进程,负责chunk的迁移,从而均衡各个shard server的负载,系统初始1个chunk,chunk size默认值64M,生产库上选择适合业务的chunk size是最好的。MongoDB会自动拆分和迁移chunks。
分片集群的数据分布(shard节点)
- 使用chunk来存储数据
- 进群搭建完成之后,默认开启一个chunk,大小是64M,
- 存储需求超过64M,chunk会进行分裂,如果单位时间存储需求很大,设置更大的chunk
- chunk会被自动均衡迁移。
chunksize的选择
适合业务的chunksize是最好的。
chunk的分裂和迁移非常消耗IO资源;chunk分裂的时机:在插入和更新,读数据不会分裂。
chunksize的选择:
小的chunksize
:数据均衡是迁移速度快,数据分布更均匀。数据分裂频繁,路由节点消耗更多资源。大的chunksize
:数据分裂少。数据块移动集中消耗IO资源。通常100-200M
chunk分裂及迁移
随着数据的增长,其中的数据大小超过了配置的chunk size,默认是64M,则这个chunk就会分裂成两个。数据的增长会让chunk分裂得越来越多。
这时候,各个shard 上的chunk数量就会不平衡。这时候,mongos中的一个组件balancer 就会执行自动平衡。把chunk从chunk数量最多的shard节点挪动到数量最少的节点。
chunkSize 对分裂及迁移的影响
- MongoDB 默认的 chunkSize 为64MB,如无特殊需求,建议保持默认值;chunkSize 会直接影响到 chunk 分裂、迁移的行为。
- chunkSize 越小,chunk 分裂及迁移越多,数据分布越均衡;反之,chunkSize 越大,chunk 分裂及迁移会更少,但可能导致数据分布不均。
- chunkSize 太小,容易出现
jumbo chunk
(即shardKey 的某个取值出现频率很高,这些文档只能放到一个 chunk 里,无法再分裂)而无法迁移;chunkSize 越大,则可能出现 chunk 内文档数太多(chunk 内文档数不能超过 250000 )而无法迁移。 - chunk 自动分裂只会在数据写入时触发,所以如果将 chunkSize 改小,系统需要一定的时间来将 chunk 分裂到指定的大小。
- chunk 只会分裂,不会合并,所以即使将 chunkSize 改大,现有的 chunk 数量不会减少,但 chunk 大小会随着写入不断增长,直到达到目标大小。
如何进行分片:分片依据和分片算法
MongoDB 中Collection的数据是根据什么进行分片的呢?这就是我们要介绍的分片键(Shard key);那么又是采用过了什么算法进行分片的呢?这就是紧接着要介绍的范围分片(range sharding)和哈希分片(Hash Sharding)。
分片键(Shard key)
分片键就是在集合中选一个字段或者组合字段,用该键的值作为数据拆分的依据。
分片键必须是一个索引,通过sh.shardCollection加会自动创建索引(前提是此集合不存在的情况下)。一个自增的分片键对写入和数据均匀分布就不是很好,因为自增的片键总会在一个分片上写入,后续达到某个阀值可能会写到别的分片。但是按照片键查询会非常高效。
注意:
- 分片键是不可变。
- 分片键必须有索引。
- 分片键大小限制512bytes。
- 分片键用于路由查询。
- MongoDB不接受已进行collection级分片的collection上插入无分片
- 键的文档(也不支持空值插入)
哈希分片(Hash Sharding)
分片过程中利用哈希索引作为分片,基于哈希片键最大的好处就是保证数据在各个节点分布基本均匀。
对于基于哈希的分片,MongoDB计算一个字段的哈希值,并用这个哈希值来创建数据块。在使用基于哈希分片的系统中,拥有相近分片键的文档很可能不会存储在同一个数据块中,因此数据的分离性更好一些。
注意
这里要注意,哈希分片是只能基于一个字段吗?MongoDB4.4版本中已经可以针对复合索引字段
进行哈希分片。
范围分片(range sharding)
将单个Collection的数据分散存储在多个shard上,用户可以指定根据集合内文档的某个字段即shard key来进行范围分片(range sharding)。
对于基于范围的分片,MongoDB按照片键的范围把数据分成不同部分:
在使用片键做范围划分的系统中,拥有相近分片键的文档很可能存储在同一个数据块中,因此也会存储在同一个分片中。
哈希和范围的结合
如下是基于X索引字段进行范围分片,但是随着X的增长,大于20的数据全部进入了Chunk C, 这导致了数据的不均衡。
这时对X索引字段建哈希索引:
分片数据按区域:Zone
在分片群集中可以基于分片键划分数据的区域(zone), 你可以将每个区域(zone)与集群中的一个或多个分片关联。
应用区域(zone)的一些常见部署模式如下:
- 将指定的数据放在指定的分片上。
- 确保最相关的数据驻留在地理上最靠近应用程序服务器的分片上。
- 根据分片硬件的硬件/性能将数据路由到分片。
下图说明了具有三个分片和两个区域的分片集群。 A区域代表下边界为1且上限为10的范围。B区域代表下边界为10且上限为20的范围。分片Alpha和Beta具有A区域。 分片Beta也具有B区。分片Charlie没有与之关联的区域。 群集处于稳定状态。
MongoDB的备份恢复
mongoexport/mongoimport导入/导出的是JSON格式,而mongodump/mongorestore导入/导出的是BSON格式。
JSON可读性强但体积较大,BSON则是二进制文件,体积小但对人类几乎没有可读性。
在一些mongodb版本之间,BSON格式可能会随版本不同而有所不同,所以不同版本之间用mongodump/mongorestore可能不会成功,具体要看版本之间的兼容性。当无法使用BSON进行跨版本的数据迁移的时候,使用JSON格式即mongoexport/mongoimport是一个可选项。跨版本的mongodump/mongorestore并不推荐,实在要做请先检查文档看两个版本是否兼容(大部分时候是的)。
JSON虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引,账户等其他基础信息。使用时应该注意。
JSON格式:mongoexport/mongoimport
JSON可读性强但体积较大,JSON虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引,账户等其他基础信息。
BSON格式:mongoexport/mongoimport
BSON则是二进制文件,体积小但对人类几乎没有可读性。