0%

分布式系统实例之mongo 集群之概述

[TOC]

综述

Mongodb一共有三种集群搭建的方式:

  • Replica Set(副本集)
  • Sharding(切片)
  • Master-Slaver(主从,目前已不推荐使用了

其中,Sharding集群也是三种集群中最复杂的。副本集比起主从可以实现故障转移, 经常使用。

MongoDB 目前已不推荐使用主从模式,取而代之的是副本集模式。副本集其实一种互为主从的关系,可理解为主主。副本集指将数据复制,多份保存,不同服务器保存同一份数据,在出现故障时自动切换。对应的是数据冗余、备份、镜像、读写分离、高可用性等关键词。

而分片则指为处理大量数据,将数据分开存储,不同服务器保存不同的数据,它们的数据总和即为整个数据集。追求的是高性能。在生产环境中,通常是这两种技术结合使用,分片+副本集。

主从集群

主从复制是MongoDB最常用的复制方式,也是一个简单的数据库同步备份的集群技术,这种方式很灵活.可用于备份,故障恢复,读扩展等. 最基本的设置方式就是建立一个主节点和一个或多个从节点,每个从节点要知道主节点的地址。采用双机备份后主节点挂掉了后从节点可以接替主机继续服务。所以这种模式比单节点的高可用性要好很多。

配置主从复制的注意点

  • 在数据库集群中要明确的知道谁是主服务器,主服务器只有一台.
  • 从服务器要知道自己的数据源也就是对应的主服务是谁.
  • –master用来确定主服务器, –slave 和 –source 来控制从服务器

主从配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.............master-node节点配置.............
[root@master-node ~]# vim /usr/local/mongodb/mongodb.conf
port=27017
bind_ip = 182.48.115.238
dbpath=/usr/local/mongodb/data
logpath=/usr/local/mongodb/log/mongo.log
logappend=true
journal = true
fork = true
master = true //确定自己是主服务器


.............slave-node节点配置.............
[root@slave-node ~]# vim /usr/local/mongodb/mongodb.conf
port=27017
dbpath=/usr/local/mongodb/data
logpath=/usr/local/mongodb/log/mongo.log
logappend=true
journal = true
fork = true
bind_ip = 182.48.115.236 //确定主数据库端口
source = 182.48.115.238:27017 //确定主数据库端口
slave = true //确定自己是从服务器

如上配置后,在主节点写入的数据就会同步到从节点

主从复制原理

在主从结构中,主节点的操作记录成为oplog(operation log)。oplog存储在一个系统数据库local的集合oplog.$main 中,这个集合的每个文档都代表主节点上执行的一个操作。 从服务器会定期从主服务器中获取oplog记录,然后在本机上执行。对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随着操作过多,新的操作会覆盖旧的操作。

在上面slave-node从节点的local数据库中,存在一个集合sources。这个集合就保存了这个服务器的主服务器是谁

1
2
use local
db.sources.find()

副本集集群(Replica Sets)

mongodb 不推荐主从复制,推荐建立副本集(Replica Set)来保证1个服务挂了,可以有其他服务顶上,程序正常运行,几个服务的数据都是一样的,后台自动同步。主从复制其实就是一个单副本的应用,没有很好的扩展性和容错性。然而副本集具有多个副本保证了容错性,就算一个副本挂掉了还有很多个副本存在,并且解决了”主节点挂掉后,整个集群内会自动切换”的问题。副本集比传统的Master-Slave主从复制有改进的地方就是它可以进行故障的自动转移,如果我们停掉复制集中的一个成员,那么剩余成员会再自动选举一个成员,作为主库。

在一些场景,可以使用副本集来扩展读性能,客户端有能力发送读写操作给不同的服务器。也可以在不同的数据中心获取不同的副本来扩展分布式应用的能力。mongodb副本集是一组拥有相同数据的mongodb实例,主mongodb接受所有的写操作,所有的其他实例可以接受主实例的操作以保持数据同步。主实例接受客户的写操作,副本集只能有一个主实例,因为为了维持数据一致性,只有一个实例可写,主实例的日志保存在oplog

异步复制

副本节点同步主节点的操作是异步的,这会导致副本集无法返回最新的数据给客户端程序。其实这是典型CAP问题,一致性(Consistency),可用性(Availability),分区容忍性(Partition tolerance)三者只能取其二,mongoDB的主从复制模式,实际上是取了A和P而放弃了C,仅仅保证最终一致性。其实无论负载如何, 数据不一致的延迟的是一定存在的,不过是时间长短而已。

解決异步复制的一致性问题

mongoDB实际上有处理该问题的API,{w: “majority”},即写的时候阻塞到写到大多数结点写完才算完成。有了这点还是不够的,因为你要读的从结点并不能保证一定在“大多数”之内。为了保证读结点在“大多数”之内{readConcern: “majority”}——多数结点有的才算有。但是这样的话一个请求要压在大多数节点上,违背了读写分离,分散数据库压力的初衷,而且也将写操作趋近于同步,影响性能。 于是单独搞了一个可以保证数据一致性的connection,以便需要数据一致性的时候使用,而其他操作则不使用该操作。

副本集的结构及原理

MongoDB 的副本集不同于以往的主从模式。在集群Master故障的时候,副本集可以自动投票,选举出新的Master,并引导其余的Slave服务器连接新的Master,而这个过程对于应用是透明的。可以说MongoDB的副本集是自带故障转移功能的主从复制。一旦 Master 节点故障,则会在其余节点中选举出一个新的 Master 节点。 并引导剩余节点连接到新的 Master 节点。这个过程对于应用是透明的。

心跳检测

整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点每两秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果是主节点,除了维护映射表外还需要检查自己能否和集群中内大部分节点通讯,如果不能则把自己降级为secondary只读节点。

数据同步

副本集同步分为初始化同步和keep复制。初始化同步指全量从主节点同步数据,如果主节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步,一般是增量同步。初始化同步不只是在第一次才会被处罚,有以下两种情况会触发:
1)secondary第一次加入,这个是肯定的。
2)secondary落后的数据量超过了oplog的大小,这样也会被全量复制。

数据同步-副本集数据过程

当Primary节点完成数据操作后,Secondary会做出一系列的动作保证数据的同步:
1)检查自己local库的oplog.rs集合找出最近的时间戳。
2)检查Primary节点local库oplog.rs集合,找出大于此时间戳的记录。
3)将找到的记录插入到自己的oplog.rs集合中,并执行这些操作。

MongoDB 同步延迟问题

什么是同步延迟?

1
2
3
什么是同步延迟?
首先,要出现同步延迟,必然是在有数据同步的场合,在 MongoDB 中,有两种数据冗余方式,一种是Master-Slave 模式,一种是Replica Sets模式。这两个模式本质上都是
在一个节点上执行写操作, 另外的节点将主节点上的写操作同步到自己这边再进行执行。在MongoDB中,所有写操作都会产生 oplog,oplog 是每修改一条数据都会生成一条,如果你采用一个批量 update 命令更新了 N 多条数据, 那么抱歉,oplog 会有很多条,而不是一条。所以同步延迟就是写操作在主节点上执行完后,从节点还没有把 oplog 拿过来再执行一次。而这个写操作的量越大,主节点与从节点的差别也就越大,同步延迟也就越大了。

同步延迟带来的问题

1
2
3
同步延迟带来的问题
首先,同步操作通常有两个效果,一是读写分离,将读操作放到从节点上来执行,从而减少主节点的 压力。对于大多数场景来说,读多写少是基本特性,所以这一点是很有用的。
另一个作用是数据备份, 同一个写操作除了在主节点执行之外,在从节点上也同样执行,这样我们就有多份同样的数据,一旦 主节点的数据因为各种天灾人祸无法恢复的时候,我们至少还有从节点可以依赖。但是主从延迟问题 可能会对上面两个效果都产生不好的影响。

主节点故障

1
2
3
如果主从延迟过大,主节点上会有很多数据更改没有同步到从节点上。这时候如果主节点故障,就有两种情况:
1)主节点故障并且无法恢复,如果应用上又无法忍受这部分数据的丢失,我们就得想各种办法将这部 数据更改找回来,再写入到从节点中去。可以想象,即使是有可能,那这也绝对是一件非常恶心的活。
2)主节点能够恢复,但是需要花的时间比较长,这种情况如果应用能忍受,我们可以直接让从节点提 供服务,只是对用户来说,有一段时间的数据丢失了,而如果应用不能接受数据的不一致,那么就只能下线整个业务,等主节点恢复后再提供服务了。

数据丢失

1
2
如果你只有一个从节点,当主从延迟过大时,由于主节点只保存最近的一部分 oplog,可能会导致从节点青黄不接,不得不进行 resync 操作,全量从主节点同步数据。
带来的问题是:当从节点全量同步的时候,实际只有主节点保存了完整的数据,这时候如果主节点故障,很可能全部数据都丢掉了。

三种节点

副本集包括三种节点:主节点、从节点、仲裁节点。

1
2
3
4
5
6
1)主节点负责处理客户端请求,读、写数据, 记录在其上所有操作的 oplog;
2)从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。默认情况下,从节点不支持外部读取,但可以设置;
副本集的机制在于主节点出现故障的时候,余下的节点会选举出一个新的主节点,从而保证系统可以正常运行。
3)仲裁节点不复制数据,仅参与投票。由于它没有访问的压力,比较空闲,因此不容易出故障。由于副本集出现故障的时候,存活的节点必须大于副本集节点总数的一半,
否则无法选举主节点,或者主节点会自动降级为从节点,整个副本集变为只读。因此,增加一个不容易出故障的仲裁节点,可以增加有效选票,降低整个副本集不可用的
风险。仲裁节点可多于一个。也就是说只参与投票,不接收复制的数据,也不能成为活跃节点。

Mongodb副本集环境部署记录

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@master-node ~]# cat /usr/local/mongodb/mongodb.conf
port=27017
bind_ip = 182.48.115.236 //这个最好配置成本机的ip地址。否则后面进行副本集初始化的时候可能会失败!
dbpath=/usr/local/mongodb/data
logpath=/usr/local/mongodb/log/mongo.log
pidfilepath=/usr/local/mongodb/mongo.pid
fork=true
logappend=true
shardsvr=true
directoryperdb=true
#auth=true
#keyFile =/usr/local/mongodb/keyfile
replSet =hqmongodb

客户端连接副本集

要正确连接复制集,需要先了解下MongoDB的Connection String URI所有官方的driver都支持以Connection String的方式来连接MongoDB。

  • mongodb:// 前缀,代表这是一个Connection String
  • username:password@ 如果启用了鉴权,需要指定用户密码
  • hostX:portX 复制集成员的ip:port信息,多个成员以逗号分割
  • /database 鉴权时,用户帐号所属的数据库
  • ?options 指定额外的连接选项

通过正确的Connection String来连接MongoDB复制集时,客户端会自动检测复制集的主备关系,当主备关系发生变化时,自动将写切换到新的主上,(这里猜测是轮训去获取当前的主从信息),以保证服务的高可用。

如何实现读写分离?

在options里添加readPreference=secondaryPreferred即可实现,读请求优先到Secondary节点,从而实现读写分离的功能,更多读选项参考Read preferences

如何限制连接数?

在options里添加maxPoolSize=xx即可将客户端连接池限制在xx以内。

如何保证数据写入到大多数节点后才返回?

在options里添加w= majority即可保证写请求成功写入大多数节点才向客户端确认,更多写选项参考Write Concern

Mongodb分片集群(Sharding)

Sharding cluster是一种可以水平扩展的模式,在数据量很大时特给力,实际大规模应用一般会采用这种架构去构建。sharding分片很好的解决了单台服务器磁盘空间、内存、cpu等硬件资源的限制问题,把数据水平拆分出去,降低单节点的访问压力。每个分片都是一个独立的数据库,所有的分片组合起来构成一个逻辑上的完整的数据库。因此,分片机制降低了每个分片的数据操作量及需要存储的数据量,达到多台服务器来应对不断增加的负载和数据的效果。

分片的基本思想就是:

  • 将集合切成小块,这些块分散到若干片里,每个片只负责总数据的一部分。通过一个名为 mongos 的路由进程进行操作,mongos 知道数据和片的对应关系(通过配置服务器)。 大部分使用场景都是解决磁盘空间的问题,对于写入有可能会变差,查询则尽量避免跨分片查询。

  • 分片是指将数据拆分,将其分散存在不同机器上的过程.有时也叫分区.将数据分散在不同的机器上MongoDB支持自动分片,可以摆脱手动分片的管理.集群自动切分数据,做负载均衡

三种角色

要构建一个MongoDB Sharding Cluster(分片集群),需要三种角色

分片服务器(Shard Server)

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

高可用性的分片架构还需要对于每一个分片构建 replica set 副本集保 证分片的可靠性。生产环境通常是 2 个副本 + 1 个仲裁。

配置服务器(Config Server)

这是一个独立的mongod进程,保存集群和分片的元数据,即各分片包含了哪些数据的信息。最先开始建立,启用日志功能。像启动普通的 mongod 一样启动, 并指定 configsvr 选项。

由于mongos 本身没有物理存储分片服务器和数据路由信息,只是缓存在内存里,配置服务器则实际存储这些数据。mongos 第一次启动或者关掉重启就会从 config server 加载配置信息,以后如果配置服务器信息变化会通知到所有的 mongos 更新自己的状态,这样mongos 就能继续准确路由。在生产环境通常有多个 config server 配置服务器,因为它存储了分片路由的元数据,这个可不能丢失!就算挂掉其中一台,只要还有存货,mongodb 集群就不会挂掉。

路由服务器(Route Server)

mongos 实例,前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用起到一个路由的功能,供程序连接。本身不保存数据,在启动时从配置服务器加载集群信息,开启 mongos 进程需要知道配置服务器的地址,指定configdb选项。

mongos 是数据库集群请求的入口,所有的请求都通过 mongos 进行协调,不需要在应用程序添加一个路由选择器,mongos 自己就是一个请求分发中心,它负责把对应的数据请求转发到对应的 shard 服务器上。在生产环境通常有多个 mongos 作为请求的入口,防止其中一个挂掉所有的 mongodb 请求都没有办法操作。

分片集群分析

分片集群

golang 连接分片集群

  • 用户访问 mongos 跟访问单个 mongod 类似
  • 所有 mongos 是对等关系,用户访问分片集群可通过任意一个或多个mongos
  • mongos 本身是无状态的,可任意扩展,集群的服务能力为『Shard服务能力之和』与『mongos服务能力之和』的最小值。
  • 访问分片集群时,最好将应用负载均匀的分散到多个 mongos 上

代码:

如何选择shard key?

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

MongoDB使用片键的范围把数据分布在分片中,每个范围,又称为数据块,定义了一个不重叠的片键范围,MongoDB把数据块与他们存储的文档分布到集群中的不同分片中.

shard key 在分片中的主要特点:

  • 数据索引:作为 shard key 首先作用就是作为数据索引,因为建立 shard key 之前的必要条件就是必须是数据索引
  • 不可更改:shard key 是固定的,一旦确定后(写入数据库),将不可进行更改
  • 随机性:shard key 一定要具有一定的随机性。如果没有选择好 shard key,造成顺序性,则数据会落在某个特定的节点中,造成某节点数据过多,而其他节点却没有数据的情况。

一个好的 shard key 应该具备的特点:

  • key 分布足够离散 (sufficient cardinality)

    足够分散才能带来性能上的增加

  • 写请求均匀分布 (evenly distributed write)

    数据应该均匀分布在所有的数据节点上

  • 尽量避免 scatter-gather 查询 (targeted read)

    避免大范围的扫描查询

现有 Shard Key 类型

  • 范围分片:通常能很好的支持基于 shard key 的范围查询
  • Hash 分片:通常能将写入均衡分布到各个 shard,不过对范围查询支持不好

案例分析

以公司 IOT 项目为例,在对采集数据进行 sharding 选择片键时,我们很容易想到:

  • 用户编号 范围分片 为片键

    这个方案我在最开始的时候也是第一个想到的。但是经过思考后,否决掉了。该方案虽然可以解决在查询某个用户数据时不用扫描所有数据节点,但是用因为户数达不到极大的量级,所以这样会使采集的数据在落盘的时候集中在某一个节点的某个数据块中。

    所以,该方案不合适

  • 用户编号 哈希分片 为片键

    这个方案虽然可以解决范围分片带来的集中问题,但是,仍然不能满足应用的需求,因为应用在使用过程中,需要查询某个用户某一个时间段范围内的所有采集到的数据,如果使用这种方案,那么进行数据查询的时候需要对所有节点进行查询扫描。

    所以,该方案不合适

  • created_at 创建时间 范围分片 为片键

    虽然该方案可以解决连续读取一个时间段内的数据问题,但是新的写入都是连续的时间戳,同样都会请求到同一个 shard,造成写分布不均。

    所以,该方案不合适

  • created_at 创建时间 哈希分片 为片键

    该方案的利弊刚好和上一个方案相反。

    所以,该方案不合适

  • 用户编号 哈希分片 +created_at 范围分片 为组合片键

    这个方案其实不用分析就知道为什么不合适了,理由同第2个方案。因为当选择 用户编号哈希方案为首的组合片键时,用户编号哈希分片已经可以分片出数据了,MongoDB就不用再考虑后者了,所以造成的影响和第2个方案类似。

    所以,该方案不合适

  • 用户编号+created_at 范围分片 为组合片键

    同一个 用户编号 的数据能根据时间戳进一步分散到多个数据块中,同时根据 created_at 查询时间范围的数据,能直接利用(用户编号+created_at)复合索引来完成。

    所以,该方案合适

指令sh.shardCollection(your_db.your_collection, {"user_identify": 1, "created_at": 1})

参考

最重要的参考

sharding key 的选择

异步主从复制导致的一致性问题

客户端连接mongo

官方客户端 golang版本

mongo 最佳实践

官方视频讲分片