复制集
mongodb在集群环境中,通过复制的形式对数据进行冗余。mongodb复制集有Primary、Secondary、Arbiter三种角色。以三节点为例,一般一个Primary两个Secondary较为常见。一个复制集群最多有50个节点,但是最多有7个投票节点。
- Primary:主节点,副本集接收写操作的唯一成员。mongodb集群在主节点上接收写操作,并写入操作日志oplog。Secondary节点复制oplog并将操作应用于自身数据集。集群最多有一个主节点,如果主节点挂了,集群会重新选举主节点。
- Secondary:从节点,维持数据副本。从节点从主节点的操作日志中同步自己的数据。复制集可以有一个或多个从节点。虽然从节点无法接收写操作,但是可以接收读操作,可能读取的不是最新数据。从节点可以通过配置优先级防止执行选举操作,从而可以长期作为数据备份使用。可以设为隐藏节点,从而禁止读操作。可以配置为延时节点,保留历史的快照,用于数据库回滚。
- Arbiter:不保存数据,也不成为主节点。投票节点仅仅是为了保持选举成员为奇数而存在。尽量不要再主从节点上同时运行头片节点。不要在投票成员为奇数时引入投票节点。投票节点对硬件要求低,所以可以比较随意的部署,只要不和主从节点放在一起。
选举
通过心跳检测确定节点是否存在,当发起Primary投票时,获得大多数成员投票支持的节点,会变为Primary。其他节点为Secondary。
假设复制集内投票成员(后续介绍)数量为N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态。当某个节点不能连接上其他节点,那么它不能升为主节点。大多数的意思是大多数投票而不是大多数节点。
Primary选举除了发生在复制集初始化时,还有如下情况:
- 复制集被reconfig
- Secondary节点检测到Primary宕机时,会触发新Primary的选举
- 当有Primary节点主动stepDown(主动降级为Secondary)时,也会触发新的Primary选举。
Primary的选举受节点间心跳、优先级、oplog时间等多种因素影响。
- 复制集成员间默认每2s会发送一次心跳信息,如果10s未收到某个节点的心跳,则认为该节点已宕机;如果宕机的节点为Primary,Secondary(前提是可被选为Primary)会发起新的Primary选举。
- 每个节点都倾向于投票给优先级最高的节点。优先级为0的节点不会主动发起选举。当Primary发现优先级更高的Secondary,并且该Secondary的数据落后在10s以内,则Primary会主动降级,让优先级更高的Secondary有机会成为Primary节点。
Priority0和Vote0:前者是优先级,表示不会被选为Primary,但是可能投票;后者为投票权,表示不参与投票,但是可能被选为Primary。
- 拥有最新optime(最近一条oplog的时间戳)的节点才能被选为Primary。
- 只有更大多数投票节点间保持网络连通,才有机会被选Primary;如果Primary与大多数的节点断开连接,Primary会主动降级为Secondary。当发生网络分区时,可能在短时间内出现多个Primary,故Driver在写入时,最好设置『大多数成功』的策略,这样即使出现多个Primary,也只有一个Primary能成功写入大多数。
数据同步
数据同步通过oplog日志完成。Primary上的写操作完成后,会向特殊的local.oplog.rs特殊集合写入一条oplog,Secondary不断的从Primary取新的oplog并应用于自身。
因oplog的数据会不断增加,local.oplog.rs被设置成为一个capped集合,当容量达到配置上限时,会将最旧的数据删除掉。另外考虑到oplog在Secondary上可能重复应用,oplog必须具有幂等性,即重复应用也会得到相同的结果。
当 Secondary初次同步数据时,会先进行初始化同步。从Primary(或其他数据更新的Secondary)同步全量数据,然后不断通过tailable cursor(游标)从Primary的local.oplog.rs集合里查询最新的oplog并应用到自身。
初始化同步过程如下:
- 从T1时间开始,从Primary同步所有数据库数据(除了local)。假设T2完成同步。并完成_id索引的创建。
- 从Primary同步T1-T2时间段内的所有操作日志oplog。可能部分操作已经包含在第一步的数据中,但是由于oplog的幂等性,可以重复应用。
- 根据Primary的索引设置,在Secondary上设置索引。
初始同步结束后,会进入增量同步。
1、 Sencondary 初始化同步完成之后,开始增量复制,通过produce线程在Primary oplog.rs集合上建立cursor,并且实时请求获取数据。
2、 Primary 返回oplog 数据给Secondary。
3、 Sencondary 读取到Primary 发送过来的oplog,将其写入到队列中。
4、 Sencondary 的同步线程会通过tryPopAndWaitForMore方法一直消费队列,当每次总数据大于100MB,或者已经取到部分数据但没到100MB,但是目前队列没数据了,这个时候会阻塞等待一秒,如果还没有数据则本次取数据完成,将数据给prefetchOps方法处理,prefetchOps方法主要将数据以database级别切分,便于后面多线程写入到数据库中。如果采用的WiredTiger引擎,那这里是以Docment ID 进行切分。
5、 最终将划分好的数据以多线程的方式批量写入到数据库中(在从库批量写入数据的时候MongoDB会阻塞所有的读)。
6、 然后再将Queue中的Oplog数据写入到Sencondary中的oplog.rs集合中。
读操作
默认情况下,所有的读操作发送到Primary节点。Driver可通过设置Read Preference来将读请求路由到其他的节点。
- primary:默认规则,所有读请求发到Primary。
- primaryPreferred:Primary优先。如果Primary不可达,请求Secondary。
- secondary:所有读请求发到Secondary。
- secondaryPreferred:Secondary优先。当所有Secondary不可达,请求Primary。
- nearest:读请求发送到最近的节点(通过ping探测)。
写操作
默认情况下,Primary完成写操作即返回。Driver可通过设置Write Concern来设置写成功的规则。集群写请求使用{ w : majority},即大多数节点写成功。
异常回滚:当一个Primary冗机,如果有数据未同步到Secondary,当原Primary重新加入时,如果新的Primary上已经发生了写操作,则原Primary需要回滚部分操作,以便集群与新的Primary数据保持一致。这里如果Write Concern设置不是majority,则会出现写入丢失。
分片
分片是为了应对高吞吐量和高并发量。通过水平分片,将数据集分散到不同的服务器节点上,提高整体的吞吐量和并发量。分片与复制集不同,复制集每个复制持有全部的数据(新写入的可能没有),而分片是将所有数据分散到不同的节点上,每个分片持有部分数据。比如,1TB的数据,共有四个分片,则每个分片可能只持有256GB的数据;如果40个分片,那么每个分片可能只持有25GB数据。
分片提供了以下优势:
- 使集群透明。mongodb分片集群使用mongos的专用路由进程,将客户端发来的请求准确路由到对应的分片上去,同时将相应拼装返回客户端。
- 提高集群可用性。将分片和复制集结合使用,在确保水平拓展的同时,也确保了每份数据有相应的备份。
- 易于拓展。mongodb分片机制可以保障集群的伸缩性。
mongodb分片集群节点: - mongos:路由节点,负责转发客户端请求。本身不保存数据,也没有分片元数据,所有配置从config server中获取。
- config server:配置节点,保存了所有读写数据的方式、所有节点的信息和分片集群的配置信息。是真实数据的元数据。
- shard:数据节点,存储数据信息,以chunk为单位。
chunk
chunk表示shard的一部分数据。chunk主要有两个动作:
- Splitting:当一个chunk的大小超过了配置的chunksize,mongodb的后台进程会把这个chunk切分为更小的chunk,避免单个chunk过大的情况。
- Balancing:balancer是一个后台进程,负责chunk的迁移,从而平衡各个shard的负载。默认从chunk数量最多的节点移动到数量最少的节点。
系统初始1个chunk,chunksize默认值64M,生产库上选择适合业务的chunksize是最好的。mongoDB会自动拆分和迁移chunks。
chunk和event的区别:chunk为分片集群中的单元,为逻辑上的存储格式;event为数据实际的存储格式。chunk的分裂实际是不同数据在不同节点的移动过程。
chunk的分裂非常消耗IO资源,当插入和更新时,chunk才会分裂,读数据不会触发chunk分裂。所以chunksize的选择很重要:小的chunksize,数据均衡是迁移速度快,数据分布更均匀,数据分裂频繁,路由节点消耗更多资源,容易出现jumbo chunk(即shardKey 的某个取值出现频率很高,这些文档只能放到一个 chunk 里,无法再分裂)而无法迁移;大的chunksize,数据分裂少,数据块移动集中消耗IO资源,可能导致数据分布不均,可能出现 chunk 内文档数太多(chunk 内文档数不能超过 250000 )而无法迁移。通常100-200M。
分片键shard key
mongodb分片是以集合为单位。在集合中,数据通过片键被分为多个部分。片键就是集合中选择一个键作为数据拆分的依据。
所以片键的选择对分片的好坏至关重要。片键必须是一个索引。一个自增的片键总是会在同一个分片上写入数据,但是按照片键进行查询非常高效;随机片键对数据均匀分布的效果更好,但是查询时会在多个分片进行查询,由mongos对结果进行归并。
片键不可变、片键必须有索引、片键大小限制为512bytes、片键用于路由查询、MongoDB不接受已进行collection级分片的collection上插入无片键的文档(空值也不行)
为了将数据按照片键进行分片,mongodb使用基于范围的分片或者基于哈希的分片。
- 基于范围的分片:Mongodb按照片键的范围把数据分为不同的部分。每个分片包含一定范围的数据。基于范围的分片对范围查询有很好的支持,但是拥有相近片键的文档很可能会存储在同一个分片中,写入效率会受到影响。
- 基于哈希的分片:哈希分片的片键只能使用一个字段。mongodb通过计算片键的哈希值,并用这个哈希值来创建数据块。在使用基于哈希分片的系统中,拥有相近片键的文档很可能不会存储在同一个数据块中,因此数据的分离性更好一些。但是对于范围查询,需要将查询分发到后端所有分片中才能找出满足条件的文档。
分片键的选择:
- 递增的shard key:优点是数据差距小,但是所有数据的写入会放到最后一片上,造成部分写热点。同时,随着最后一片数据量增大,会不断发生分裂并迁移到之前的分片上。
- 随机的shard key:优点是数据分布均匀,insert的写IO均匀分布在多个片上。缺点是大量随机IO。
- 混合型key:大方向递增,小范围随机分布。为了防止出现大量的chunk均衡迁移,可能造成的IO压力。我们需要设置合理分片使用策略(片键的选择、分片算法(range、hash))