做公司+网站建设价格,东莞网站建设信科分公司,如何在服务器上做网站,建设网站重庆复制集高可用
复制集选举
MongoDB 的复制集选举使用 Raft 算法#xff08;https://raft.github.io/#xff09;来实现#xff0c;选举成功的必要条件是大多数投票节点存活。在具体的实现中#xff0c;MongoDB 对 raft 协议添加了一些自己的扩展#xff0c;这包括#x…复制集高可用
复制集选举
MongoDB 的复制集选举使用 Raft 算法https://raft.github.io/来实现选举成功的必要条件是大多数投票节点存活。在具体的实现中MongoDB 对 raft 协议添加了一些自己的扩展这包括
支持 chainingAllowed 链式复制即备节点不只是从主节点上同步数据还可以选择一个离自己最近心跳延时最小的节点来复制数据。增加了预投票阶段即 preVote这主要是用来避免网络分区时产生 Term任期值激增的问题。支持投票优先级如果备节点发现自己的优先级比主节点高则会主动发起投票并尝试成为新的主节点。
一个复制集最多可以有 50 个成员但只有 7 个投票成员。这是因为一旦过多的成员参与数据复制、投票过程将会带来更多可靠性方面的问题。
投票成员数大多数容忍失效数110220321431532642743
当复制集内存活的成员数量不足大多数时整个复制集将无法选举出主节点此时无法提供写服务这些节点都将处于只读状态。此外如果希望避免平票结果的产生最好使用奇数个节点成员比如 3 个或 5 个。当然在 MongoDB 复制集的实现中对于平票问题已经提供了解决方案
为选举定时器增加少量的随机时间偏差这样避免各个节点在同一时刻发起选举提高成功率。使用仲裁者角色该角色不做数据复制也不承担读写业务仅仅用来投票。
自动故障转移
在故障转移场景中我们所关心的问题是
备节点是怎么感知到主节点已经发生故障的如何降低故障转移对业务产生的影响
一个影响检测机制的因素是心跳在复制集组建完成之后各成员节点会开启定时器持续向其他成员发起心跳这里涉及的参数为 heartbeatIntervalMillis即心跳间隔时间默认值是 2s。如果心跳成功则会持续以 2s 的频率继续发送心跳如果心跳失败则会立即重试心跳一直到心跳恢复成功。另一个重要的因素是选举超时检测一次心跳检测失败并不会立即触发重新选举。实际上除了心跳成员节点还会启动一个选举超时检测定时器该定时器默认以 10s 的间隔执行具体可以通过 electionTimeoutMillis 参数指定
如果心跳响应成功则取消上一次的 electionTimeout 调度保证不会发起选举并发起新一轮 electionTimeout 调度。如果心跳响应迟迟不能成功那么 electionTimeout 任务被触发进而导致备节点发起选举并成为新的主节点。
在 MongoDB 的实现中选举超时检测的周期要略大于 electionTimeoutMillis 设定。该周期会加入一个随机偏移量大约在 1011.5s如此的设计是为了错开多个备节点主动选举的时间提升成功率。
因此在 electionTimeout 任务中触发选举必须要满足以下条件1当前节点是备节点。2当前节点具备选举权限。3在检测周期内仍然没有与主节点心跳成功。
业务影响评估
在复制集发生主备节点切换的情况下会出现短暂的无主节点阶段此时无法接受业务写操作。如果是因为主节点故障导致的切换则对于该节点的所有读写操作都会产生超时。如果使用 MongoDB 3.6 及以上版本的驱动则可以通过开启 retryWrite 来降低影响。
# MongoDB Drivers 启用可重试写入
mongodb://localhost/?retryWritestrue
# mongo shell
mongosh --retryWrites如果主节点属于强制掉电那么整个 Failover 过程将会变长很可能需要在 Election 定时器超时后才被其他节点感知并恢复这个时间窗口一般会在 12s 以内。然而实际上对于业务呼损的考量还应该加上客户端或 mongos 对于复制集角色的监视和感知行为真实的情况可能需要长达 30s 以上。对于非常重要的业务建议在业务层面做一些防护策略比如设计重试机制。
思考如何优雅的重启复制集如果想不丢数据重启复制集更优雅的打开方式应该是这样的
逐个重启复制集里所有的 Secondary 节点对 Primary 发送rs.stepDown()命令等待 primary 降级为 Secondary重启降级后的 Primary
复制集数据同步机制
在复制集架构中主节点与备节点之间是通过 oplog 来同步数据的这里的 oplog 是一个特殊的固定集合当主节点上的一个写操作完成后会向 oplog 集合写入一条对应的日志而备节点则通过这个 oplog 不断拉取到新的日志在本地进行回放以达到数据同步的目的。
什么是 oplog
MongoDB oplog 是 Local 库下的一个集合用来保存写操作所产生的增量日志类似于 MySQL 中 的 Binlog。它是一个 Capped Collection固定集合即超出配置的最大值后会自动删除最老的历史数据 MongoDB 针对 oplog 的删除有特殊优化以提升删除效率。主节点产生新的 oplog Entry从节点通过复制 oplog 并应用来保持和主节点的状态一致。
查看 oplog
use local
db.oplog.rs.find().sort({$natural:-1}).pretty()说明 local.system.replset用来记录当前复制集的成员。 local.startup_log用来记录本地数据库的启动日志信息。 local.replset.minvalid用来记录复制集的跟踪信息如初始化同步需要的字段。 ts操作时间当前 timestamp 计数器计数器每秒都被重置 voplog 版本信息 op操作类型 i插⼊操作 u更新操作 d删除操作 c执行命令如 createDatabasedropDatabase n空操作特殊用途 ns操作针对的集合 o操作内容 o2操作查询条件仅 update 操作包含该字段 ts 字段描述了oplog 产生的时间戳可称之为 optime。optime 是备节点实现增量日志同步的关键它保证了 oplog 是节点有序的其由两部分组成
当前的系统时间即 UNIX 时间至现在的秒数32 位。整数计时器不同时间值会将计数器进行重置32 位。
optime 属于 BSON 的 Timestamp 类型这个类型一般在 MongoDB 内部使用。既然 oplog 保证了节点级有序那么备节点便可以通过轮询的方式进行拉取这里会用到可持续追踪的游标tailable cursor技术。
每个备节点都分别维护了自己的一个 offset也就是从主节点拉取的最后一条日志的 optime在执行同步时就通过这个 optime 向主节点的 oplog 集合发起查询。为了避免不停地发起新的查询链接在启动第一次查询后可以将 cursor 挂住通过将 cursor 设置为 tailable。这样只要 oplog 中产生了新的记录备节点就能使用同样的请求通道获得这些数据。tailable cursor 只有在查询的集合为固定集合时才允许开启。
oplog 集合的大小
oplog 集合的大小可以通过参数replication.oplogSizeMB设置对于 64 位系统来说oplog 的默认值为
oplogSizeMB min(磁盘可用空间*5%50GB)对于大多数业务场景来说很难在一开始评估出一个合适的 oplogSize所幸的是 MongoDB 在 4.0 版本之后提供了 replSetResizeOplog 命令可以实现动态修改 oplogSize 而不需要重启服务器。
# 将复制集成员的oplog大小修改为60g
db.adminCommand({replSetResizeOplog: 1, size: 60000})
# 查看oplog大小
use local
db.oplog.rs.stats().maxSize幂等性
每一条 oplog 记录都描述了一次数据的原子性变更对于 oplog 来说必须保证是幂等性的。也就是说对于同一个 oplog无论进行多少次回放操作数据的最终状态都会保持不变。某文档 x 字段当前值为 100用户向 Primary 发送一条{$inc: {x: 1}}记录 oplog 时会转化为一条{$set: {x: 101}的操作才能保证幂等性。
1幂等性的代价
简单元素的操作$inc 转化为 $set 并没有什么影响执行开销上也差不多但当遇到数组元素操作时情况就不一样了。测试
db.coll.insert({_id:1,x:[1,2,3]})在数组尾部 push 2 个元素查看 oplog 发现 $push 操作被转换为了 $set 操作设置数组指定位置的元素为某个值
rs0:PRIMARY db.coll.update({_id: 1}, {$push: {x: { $each: [4, 5] }}})
WriteResult({ nMatched : 1, nUpserted : 0, nModified : 1 })
rs0:PRIMARY db.coll.find()
{ _id : 1, x : [ 1, 2, 3, 4, 5 ] }
rs0:PRIMARY use local
switched to db local
rs0:PRIMARY db.oplog.rs.find({ns:test.coll}).sort({$natural:-1}).pretty()
{op : u,ns : test.coll,ui : UUID(69c871e8-8f99-4734-be5f-c9c5d8565198),o : {$v : 1,$set : {x.3 : 4,x.4 : 5}},o2 : {_id : 1},ts : Timestamp(1646223051, 1),t : NumberLong(4),v : NumberLong(2),wall : ISODate(2022-03-02T12:10:51.882Z)
}$push 转换为带具体位置的 $set 开销上也差不多但接下来再看看往数组的头部添加 2 个元素
rs0:PRIMARY use test
switched to db test
rs0:PRIMARY db.coll.update({_id: 1}, {$push: {x: { $each: [6, 7], $position: 0 }}})
WriteResult({ nMatched : 1, nUpserted : 0, nModified : 1 })
rs0:PRIMARY db.coll.find()
{ _id : 1, x : [ 6, 7, 1, 2, 3, 4, 5 ] }
rs0:PRIMARY use local
switched to db local
rs0:PRIMARY db.oplog.rs.find({ns:test.coll}).sort({$natural:-1}).pretty()
{op : u,ns : test.coll,ui : UUID(69c871e8-8f99-4734-be5f-c9c5d8565198),o : {$v : 1,$set : {x : [6,7,1,2,3,4,5]}},o2 : {_id : 1},ts : Timestamp(1646223232, 1),t : NumberLong(4),v : NumberLong(2),wall : ISODate(2022-03-02T12:13:52.076Z)
}可以发现当向数组的头部添加元素时oplog 里的 $set 操作不再是设置数组某个位置的值因为基本所有的元素位置都调整了而是 $set 数组最终的结果即整个数组的内容都要写入 oplog。当 push 操作指定了 $slice 或者 $sort 参数时oplog 的记录方式也是一样的会将整个数组的内容作为 s e t 的参数。 set 的参数。 set的参数。pull、$addToSet 等更新操作符也是类似更新数组后oplog 里会转换成 $set 数组的最终内容才能保证幂等性。
2oplog 的写入被放大导致同步追不上——大数组更新
当数组非常大时对数组的一个小更新可能就需要把整个数组的内容记录到 oplog 里我遇到一个实际的生产环境案例用户的文档内包含一个很大的数组字段1000 个元素总大小在 64KB 左右这个数组里的元素按时间反序存储新插入的元素会放到数组的最前面 p o s i t i o n : 0 然后保留数组的前 1000 个元素 position: 0然后保留数组的前 1000 个元素 position:0然后保留数组的前1000个元素slice: 1000。上述场景导致Primary 上的每次往数组里插入一个新元素请求大概几百字节oplog 里就要记录整个数组的内容Secondary 同步时会拉取 oplog 并重放Primary 到 Secondary 同步 oplog 的流量是客户端到 Primary 网络流量的上百倍导致主备间网卡流量跑满而且由于 oplog 的量太大旧的内容很快被删除掉最终导致 Secondary 追不上转换为 RECOVERING 状态。
在文档里使用数组时一定得注意上述问题避免数组的更新导致同步开销被无限放大的问题。使用数组时尽量注意
数组的元素个数不要太多总的大小也不要太大尽量避免对数组进行更新操作如果一定要更新尽量只在尾部插入元素复杂的逻辑可以考虑在业务层面上来支持
复制延迟
由于 oplog 集合是有固定大小的因此存放在里面的 oplog 随时可能会被新的记录冲掉。如果备节点的复制不够快就无法跟上主节点的步伐从而产生复制延迟replication lag问题。这是不容忽视的一旦备节点的延迟过大则随时会发生复制断裂的风险这意味着备节点的 optime最新一条同步记录已经被主节点老化掉于是备节点将无法继续进行数据同步。
为了尽量避免复制延迟带来的风险我们可以采取一些措施比如
增加 oplog 的容量大小并保持对复制窗口的监视。通过一些扩展手段降低主节点的写入速度。优化主备节点之间的网络。避免字段使用太大的数组可能导致 oplog 膨胀。
数据回滚
由于复制延迟是不可避免的这意味着主备节点之间的数据无法保持绝对的同步。当复制集中的主节点宕机时备节点会重新选举成为新的主节点。那么当旧的主节点重新加入时必须回滚掉之前的一些“脏日志数据”以保证数据集与新的主节点一致。主备复制集合的差距越大发生大量数据回滚的风险就越高。对于写入的业务数据来说如果已经被复制到了复制集的大多数节点则可以避免被回滚的风险。应用上可以通过设定更高的写入级别writeConcernmajority来保证数据的持久性。这些由旧主节点回滚的数据会被写到单独的 rollback 目录下必要的情况下仍然可以恢复这些数据。当 rollback 发生时MongoDB 将把 rollback 的数据以 BSON 格式存放到 dbpath 路径下 rollback 文件夹中 BSON 文件的命名格式如下database.collection.timestamp.bson。
mongorestore --host 192.168.192:27018 --db test --collection emp -ufirechou -pfirechou
--authenticationDatabaseadmin rollback/emp_rollback.bson同步源选择
MongoDB 是允许通过备节点进行复制的这会发生在以下的情况中
在settings.chainingAllowed开启的情况下备节点自动选择一个最近的节点ping 命令时延最小进行同步。settings.chainingAllowed选项默认是开启的也就是说默认情况下备节点并不一定会选择主节点进行同步这个副作用就是会带来延迟的增加你可以通过下面的操作进行关闭
cfg rs.config()
cfg.settings.chainingAllowed false
rs.reconfigcfg)使用 replSetSyncFrom 命令临时更改当前节点的同步源比如在初始化同步时将同步源指向备节点来降低对主节点的影响。
db.adminCommand( { replSetSyncFrom: hostname:port })