这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

分布式系统高可用详解

本专栏将主要讲解当前常用的各大系统的高可用方案

1 - 01.常见的高可用技术方案

大家好我是一灰灰,本文将接着前文 1w5字详细介绍分布式系统的那些技术方案 文章基础上,进行实际的案例解析

高可用对于当下的系统而言,可以说是一个硬指标,常年专注于业务开发的我们,对于高可用最直观的感觉可能就是祈祷应用不要出问题,不要报错;即便有问题,也最好不是我们的业务代码逻辑导致的,如果是服务器、DB、中间件(如注册中心、配置中心等)的异常那就抛给对应的sre, dba;然而常在河边走,哪有不湿鞋,为了保障服务的高可用,我们可以从哪些方面进行努力呢?

本文将作为高可用的开篇,通过简述一些常用的系统的高可用方案,给大家介绍一下我们可以从哪些方面努力让我们的系统达到高可用,主要涉及到的系统如下

  • 缓存:Redis
  • 数据库:MySql
  • 消息队列:RabbitMQ
  • 搜索: ElasticSearch

1 redis高可用策略

redis广泛应用于缓存的业务场景,当然也有将其当做持久化存储的nosql数据库使用,这些都不重要,重点是redis在提供服务的时候,是如何支持高可用的呢?

redis官方支持了四种策略:

  • 数据持久化
  • 主从同步
  • 哨兵模式
  • 集群

除以上姿势之外,我们自己在使用时还可以选择根据业务场景使用不同的redis实例(即传说中的不把所有鸡蛋放在一个篮子里)

接下来将针对redis的几种高可用策略进行简述说明

1.1 数据持久化

官方手册: Redis persistence

持久化是在高可用、一致性的场景中经常会看到的一种技术手段;

在高可用的场景中,数据的持久化主要是为了解决在服务出现问题(如宕机)之后,可以快速恢复并对外继续提供服务能力;

redis官方提供了两种持久化策略

  • AOF: 将更新的操作命令记录在对应的日志文件中,在重启的时候采用“回放”策略,将所有的命令重新执行一遍来实现场景恢复
  • RDB: 定时存储redis中的数据快照到数据文件中,在重启的时候,加载rdb文件,恢复所有的数据

简单来讲AOF记录的是操作动作,采用回放执行的机制进行恢复;RDB则相当于数据落盘,重新读取加载的机制进行恢复

注:AOF RDB可以一起工作,没有排他性

1.2 主从方式

虽然redis性能爆炸,但是单机依然存在性能瓶颈;当我们遇到单机的性能瓶颈的时候,一般怎么做?

没错,加机器

redis也支持多机服务,比如常见的一主多从策略:

  • 主机:提供读写能力
  • 从机:只提供读

针对绝大多数读多写少的场景,我们可以起多个redis实例,其中一个设置为主,提供所有的写请求;其他的实例则设置为从,客户端通过负载策略路由到不同的从redis,从而实现流量分摊;

同时也因为有多个实例,所以单台或几台实例下线,对整个服务的可用性影响并不会太大(及时摘除故障机器,其他的实例依然可以正常提供服务;当然前提是流量所示太大把其他的实例也打挂,那就gg了)

redis主从模式

主从模式还有一个变种,叫做从从模式,主要是为了解决主redis的同步压力,改成主 -> 从,然后由一个从同步给其他的从实例,具体架构图如下

redis主从从模式

使用主从、主从从模式实现高可用可算是分布式系统的经典策略,其主要思想在于:

  • 多实例提供服务,实现负载均衡
  • 每个实例冗余一份全量数据

1.3 哨兵模式

官方手册: High availability with Redis Sentinel

哨兵模式主要是为了解决主从模式中,主机宕机的场景,由于主机本身存在单点,所以主节点对成了高可用的关键因素了;那么如果实现主节点宕机之后,自动选择一个新的主节点,这样不就可以提高系统的可用性了么; redis官方提供的机制就是 - 哨兵模式

主要工作原理:

  • 哨兵:监听redis实例,判断是否存活(不太对外提供服务能力)
  • 通过 PING 命令,检查与主从服务器之间的连接情况,若正常相应,则认为存活;否则认为主观下线
  • n/2 + 1半数以上哨兵认为主节点下线,则认为主节点客观下线,尝试选新的主节点
  • 从所有从节点中,选择与之前主库相似度最高的从节点作为新的主库

哨兵模式

哨兵模式,可以理解为探活 + 选主,而这也常见于各大分布式系统的技术方案中

1.4 集群模式

官方手册: Scaling with Redis Cluster

相比于主从模式的全量冗余,redis的集群策略在在于数据分片,每个实例上存储部分的数据;而不是全量数据,从而解决数据量大的场景下,对于redis服务本身以及数据同步的压力

集群模式的特点在于多个实例,构建成一个实例,每个实例上存储部分的数据;redis并没有采用一致性hash来做数据分布,而是使用特有的slots插槽机制,来实现数据的hash映射

集群模式

集群模式,主要特点在于数据分片,每个实例存部分数据,其思路在于拆分

从上面的图中也可以看出,集群一般与主从搭配使用,集群中的每个分片对应的是主从模式的redis服务,从而加强高可用

1.5 小结

这一节主要介绍的是redis的高可用策略,从中也可以看到很多经典的技术方案

  • 持久化:RDB数据落盘加载方式 + AOF记录操作命令用于回放策略
  • 主从,主从从:全量数据冗余、读写请求分离,负载均衡的思想;核心问题在于主节点挂掉之后需要人工参与手动指定主库
  • 哨兵机制:PING/PONG的探活机制,监听主节点,宕机之后自动选主,确保高可用;核心问题在于所有的实例冗余相同的一份数据,数据量大时不友好
  • 集群:数据分片,每个实例提供部分服务能力

看到这里的小伙伴自然会想到,为什么redis会提供这些不同的策略?它们各自的应用场景是什么,优缺点是啥?这些疑问就放在后续的redis高可用详解中介绍

相关博文:

2 MySql高可用策略

MySql数据库的高可用策略就比较多了,同样也非常的经典;仅仅主节点的保活策略就非常多了;在这里将主要的重心放在MySql的高可用架构主备、主从、一主多从,多主多从上,至于主节点故障时转移策略则放在后续详细的文章中进行介绍

2.1 数据持久化

对于每个开发者而言,大多都听说过数据库的ACID特性,其中的D对应的就是这里说到的持久化;区别于redis的持久化,以MySql的InnoDB引擎为例,其持久化涉及到多个日志文件(undo log,redo log,binlog),缓存区(buffer),磁盘(idb文件)

接下来看一下完整的数据更新/插入的流程

mysql数据持久化流程图

接下来描述一下核心思想:

  • 数据更新策略:总是更新缓存的内容(缓存未命中,则从磁盘加载到缓存)
  • 先写undolog日志文件:记录之前的数据,支持mvcc、支持回滚就靠它
  • redolog记录的两阶段提交:(先是prepare,待binlog写完之后,再次更新状态为commit)
  • 最后异步刷新缓存数据到磁盘

虽然上面的描述比较简单,但是这里的知识点非常多,如

  • 为什么先更新缓存,最后异步刷磁盘?
    • 核心在于操作内存的速度 » 操作磁盘
  • undolog作用是什么,怎么支持mvcc,实现事务回滚的?
    • 保障事务原子性的关键所在,数据行非主键变更时,记录修改前的数据到undolog,并指向它,其他sql读这个undolog中的副本数据从而支持mvcc,回滚时则是根据undo log进行逻辑恢复
  • redolog作用是什么,为什么两阶段方案?
    • 主要保障事务的持久性,当数据库异常宕机之后,可以通过重新执行redo log来恢复未及时落盘的数据;两阶段的主要目的是为了解决redolog与binlog的一致性问题,避免出现redolog第一阶段成功,但是binlog失败导致不一致问题
    • redolog属于innodb引擎,固定大小,环形结构覆盖写策略;内部同样是先写缓存,再刷磁盘的策略

更多详情内容,后面到mysql的专题时再详细介绍

2.2 主备架构

保证高可用的一个最简单策略就是“冗余”,也就是我们这里说到的主备架构,对mysql而言,就是我启动两个实例;一个主库对外提供读写服务,一个备库,冗余主库的所有数据内容,并不对外提供服务;

当主库gg之后,然后备库升级,切换为主库

话说这个思想和古代的储君制非常像了,平时都是皇帝总领朝堂,太子就当吉祥物;皇帝驾崩之后,太子就晋升为皇帝(论备胎的重要性)

MySql主备

主备的最大特点就是多备一台实例,在出问题时顶上,当然缺点就很明显了,严重的资源浪费

2.3 主从架构

主从和前面mysql的思路差不多,主从模式一般又叫做读写分离,即写主库,读从库;相比于主备而言,最主要的突破点在于另外一个mysql实例不会干放着,而是尤其来承担读请求

MySql主从

主从的核心思想在于读写分离

2.4 一主多从

在前面主从的基础上多挂几个从库,主要出发点在于当前的互联网场景下,绝大多数的应用都是读多写少,通过挂多个从库,可以有效提供整体服务的性能指标

同样一主多从的模式,也会区分为主从 + 主从从两种,后者则主要是为了减少主库的同步压力,下图为核心4架构模型

MySql主从

2.5 多主多从

一主多从可以解决读多写少的场景,但总会出现写瓶颈的场景;在不考虑分库分表的业务手段之前(这种方式也可以理解为数据分片,类似上面说到的redis集群模式),仅仅从mysql的架构模式出发,自然会想到的策略就是多个主库提供写能力,这就是我们说的多主多从的架构了

MySql主从

多主多从,其中每个主库都可以独立对外提供写请求;从库则对外提供读请求

需要注意的是主库之间的数据同步,即一个写请求落到一个任意一个主库之后,所有的主库都会同步这个写操作

2.6 主库切换策略、主从同步策略

前面介绍的是几种不同的主从架构特点,主要通过主、备/从来新增实例来提高可用性;但是还有两个非常重要的点没有细说,一个是故障之后,如何确定新的主库;另外一个则是主从/主主之间的数据如何同步,如何保证数据的一致性;

接下来我们将简单的介绍下mysql中常见的一些做法(更详细的当然留在后面的专题)

主库切换策略

VIP + KeepAlived

  • vip: 即virtual ip虚拟ip
  • KeepAlived: 保活脚本

其主要思路在于外部通过VIP访问mysql实例(主从/主主),而KeepAlived用于检测主库是否存活,当挂掉之后,VIP偏移到另外一个主库(或者选一个从库作为主库)上,从而实现自动的切主流程

缺点:

  • 级联复制(主->从->从这种复制模式叫做级联复制)或者一主多从在切换之后,其他从实例需要重新配置连接新主

MHA

Master High Avaliable 主库高可用机制,也是当下很多公司采用的策略;其包含一套完整的工具,在检测到主库不可用后,会自动将同步到最接近主库的slave提升为master,然后将其他的slave指向新的master

其优点非常明显,通常可以实现十秒内的主从切换,扩展MySql节点也非常方便;而缺点则在于主要监控主库

MXC

PXC(Percona XtraDB Cluster)是一个完全开源的 MySQL 高可用解决方案。它将 Percona Server、Percona XtraBackup 与 Galera 库集成在一起,以实现多主复制的 MySQL 集群

其核心特点在于写请求会自动同步到其他节点,要求在所有的节点都验证之后才会提交,保证数据的强一致性

因此缺点就在于木桶效应,性能取决于最差的那个节点

MGR/InnoDB Cluste

MySQL 5.7 推出了 MGR(MySQL Group Replication),与 PXC 类似,也实现了多节点数据写入和强一致性的特点。MGR 采用 GCS(Group Communication System)协议同步数据,GCS 可保证消息的原子性

外部连接通过 MySql router与一组mysql实例进行交互,当主库切换时,mysql router会自动切换到新的主节点

Xenon

给予Raft协议的MySql高可用和复制性管理工具,无中心化选主,支持秒级切换

主从同步策略

当存在主从库时,必然会存在同步问题,如何保障主库与从库数据的一致性呢?

主从同步流程

主从同步主要借助Binlog来实现,这个在前面的图中有简单的体现,下面则是相对完整的同步流程

MySql主从数据同步

  • 主库生成binlog日志文件
    • statement:记录具体引起改动的操作语句,比如insert xxxxx,缺点是某些函数会导致数据不一致(如now())
    • row:基于数据行的,原来数据行是xx值改为了yy 值,缺点是数据量大
    • mixed: 上面两个混用
  • 从库的io线程拉主库的binlog日志,写入自己的relaylog(中继日志),然后由sql线程读取relaylog日志进行回放,实现数据同步

主从同步策略

使用主从之后,在实际的业务开发中,最最常遇到的问题就是主从延迟,即主库数据已经写入了,但是读从库却读不到对应的数据,这个就是主从延迟了,它直接导致数据的不一致;当然一般这种影响还好,但是如果因为主从延迟,现在主库挂了,所有的从库都没有最新的记录,这不就导致数据丢失了么,会导致严重的数据一致性问题

所以在主从同步的策略上,有下面几种

case1:异步复制

主库完成写请求之后,理解返回结果,并不关心从库是否同步接收处理,此时就可能出现上面说的,主库挂了之后,所有从库还存在未同步的数据,导致数据丢失

case2:半同步复制

为了避免出现上面的问题,我们要求最少有一个从库同步完之后,才响应用户端请求,这样表明主库宕机之后还有个兜底的

case3:全同步复制

这个更激进一点,要求所有的从库都同步完,才算真正的ok,保证强一致性,缺点则在于性能会受到影响

2.7 小结

这一小节主要介绍的是MySql的高可用策略,从架构方面出发,有主备,主从,一主多从,多主多从,同时也简单的介绍了下如何实现主库的自动切换(MHA,MXC,MGR等)、主从数据同步流程,同步策略;如果想了解更详细的内容,请移步到mysql的高可用专题

下面小结一下保持高可用的主要思路

  • 通过冗余来实现高可用:如主备
  • 读写分离,实现负载均衡:主从、主从从模式
  • 数据持久化策略:操作内存(buffer),异步刷盘,两阶段提交保障一致性

相关博文:

3. RabbitMq高可用方案

消息中间件也是大家或多或少会接触的一类系统,接下来将以RabbitMq来看一下它的高可用是如何实现的

3.1 数据持久化

不同于前面MySql必然会持久化,RabbitMq的数据持久化是可选的,当我们对数据的完整性要求高时,最好开启持久化

首先简单看一下rabbitmq的模型

RabbitMq架构

我们这里说的持久化主要指

  • exchange持久化: 即exchange本身不会因为rabbitmq宕机而被删除,需要手动指定durable=true
  • topic持久化:消费者通过topic从exchange中读取消息,需要指定durable=true,避免出现宕机后队列中的消息丢失
  • msg消息持久化:即生产者投递到echange的消息,需要持久化到磁盘

注意rabbitmq的消息持久化也是先写到buffer,然后再定时刷新到磁盘;

当我们为了保障数据的完整性时,一般会开启消息的确认机制/事务机制,每次投递等到mq回复一个确认ack之后,才表示真正的投递成功,而mq的应答则是在消息的持久化之后进行

3.2 主备模式

同前面的MySql的主备,主节点提供读写,备节点同步主节点的数据,不对外提供服务能力;当主节点挂了之后,启用备节点对外服务,原主节点恢复之后则作为备节点存在

3.3 Shovel远程模式

官方文档: * Shovel Plugin — RabbitMQ

远程模式可以实现双活的一种模式,简称 shovel 模式,所谓的 shovel 就是把消息进行不同数据中心的复制工作,可以跨地域的让两个 MQ 集群互联,远距离通信和复制。

  • Shovel 就是我们可以把消息进行数据中心的复制工作,我们可以跨地域的让两个 MQ 集群互联。

RabbitMq远程模式

如上图,有两个异地的 MQ 集群(可以是更多的集群),当用户在地区 1 这里下单了,系统发消息到 1 区的 MQ 服务器,发现 MQ 服务已超过设定的阈值,负载过高,这条消息就会被转到 地区 2 的 MQ 服务器上,由 2 区的去执行后面的业务逻辑,相当于分摊我们的服务压力。

3.4 镜像模式

如下图,用 KeepAlived 做了 HA-Proxy 的高可用,然后有 3 个节点的 MQ 服务,消息发送到主节点上,主节点通过 mirror 队列把数据同步到其他的 MQ 节点,这样来实现其高可靠

RabbitMq镜像模式

镜像模式的主要特点在于每个mq实例都包含一份完整的数据镜像,内部有一个master选举算法,通过VIP对外提供连接

  • consumer,任意连接一个节点,若连上的不是master,请求会转发给master,为了保证消息的可靠性,consumer回复ack给master后,master删除消息并广播所有的slaver去删除。
  • publisher ,任意连接一个节点,若连上的不是master,则转发给master,由master存储并转发给其他的slaver存储。 如果master挂掉,则从slaver中选择消息队列最长的为master,

3.5 普通集群模式

exchange,buindling再所有的节点上都会保存一份,但是queue只会存储在其中的一个节点上,但是所有的节点都会存储一份queue的meta信息

如果生产者连接的是另外一个节点,将会把消息转发到存储该队列的节点上。如果消费者连接了非存储队列的节点取数据,则从存储消息的节点拉取数据。

其核心特点在于:

  • 数据拆分存储,若纯消息的节点挂了,则只能等待它恢复之后才能正常工作

3.6 多活模式

这个模式我的理解也不够深刻,以下内容来自于网上摘录,待后面到rabbitmq专题之后调研后进一步阐述

rabbitMQ 部署架构采用双中心模式(多中心),那么在两套(或多套)数据中心各部署一套 rabbitMQ 集群,各中心的rabbitMQ 服务除了需要为业务提供正常的消息服务外,中心之间还需要实现部分队列消息共享

RabbitMq镜像模式

federation 插件是一个不需要构建 cluster ,而在 brokers 之间传输消息的高性能插件,federation 插件可以在 brokers 或者 cluster 之间传输消息,连接的双方可以使用不同的 users 和 virtual hosts,双方也可以使用不同版本的 rabbitMQ 和 erlang。

federation 插件使用 AMQP 协议通信,可以接受不连续的传输。federation 不是建立在集群上的,而是建立在单个节点上的,如图上黄色的 rabbit node 3 可以与绿色的 node1、node2、node3 中的任意一个利用 federation 插件进行数据同步。

3.7 小结

rabbitmq的高可用机制的方案也比较好理解

  • 主备模式
  • 镜像模式:全量冗余一份数据,主对外提供服务,可以实现自动切主
  • 普通集群模式:数据拆分到集群的实例中,consumer/publisher连接到实例之后,会从具体持有exchange/topic的实例上拉数据
  • 远程模式:适用于多中心的场景,将消息转发给其他中心的实例

这里采用的高可用思路也无外乎常见的几种:持久化 + 数据冗余 + 拆分

相关博文:

4. ElasticSearch高可用方案

接下来我们再看一下现在非常流行的分布式搜索引擎ElasticSearch是如何保证高可用的

4.1 集群

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎

by 官网描述

对于es而言,通常都是集群方式对外提供服务,每启动一个实例叫做一个节点(Node),每个节点会定义一个节点名(Node Name),集群名(Cluster Name),相同集群名的节点会构建为一个集群;

ES集群

上图包含了es集群的核心要素:

  • 每个节点包含集群名 + 节点名两个属性,相同集群名的节点挂在一个集群内
  • 节点启动之后,开始PING其他节点(连接上后会得到对应节点所在集群的所有信息)
  • 节点发现主要靠Zen Discover来实现,选举也是靠它来实现

选举主要流程如下

ES选举

  • 选举同样也是依赖Zen Discover来实现
  • 每个节点上报自己任务的主节点,然后票数最多的就是主节点;票数相同的情况下,根据ID排序,选第一个

上面就是es集群的构建与主节点的选举过程;es支持任意节点数目的集群(1- N),无法完全依赖投票的机制来选主,而是通过一个规则。

只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。

但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题。

大多数解决方案就是设置一个 Quorum 值,要求可用节点必须大于 Quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个 Quorum 的配置就是 discovery.zen.minimum_master_nodes,当候选主节点的个数超过这个参数值时,开始选举,选主完成之后对外提供服务

ES作为分布式、近实时搜索系统,天然支持集群的服务能力,通过Zen Discover来实现节点通信、集群管理、选主

4.2 脑裂问题

上面提到了脑裂,接下来简单看一下ES是如何解决脑裂问题的

脑裂:由于网络或者集群健康监测问题,导致整个集群出现多个master节点,这种现象就是脑裂

es对节点进行了角色划分

  • 数据节点:负责数据的存储和相关的操作(CURD,聚合)等,因此对机器性能要求较高
  • 候选主节点:拥有选举权和被选举权,主节点在候选主节点中评选出来,负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给哪些的节点、追踪集群中节点的状态等

一个节点,可以即是数据节点,又是候选主节点,但是注意它们两者的定位,主节点对机器性能要求没有数据节点高,当一台机器既是数据节点又是主节点时,可能出现长耗时、耗资源的请求导致主节点服务异常;

通常更推荐的方案是使用性能低一点的作为候选主节点,性能高的作为数据节点

ES角色划分

接下来看下脑裂出现的情况

  • 网络问题,导致分区:即部分节点连接不到主节点,认为它挂了,然后选举出现的主节点
  • 主节点负载、响应延迟:主节点由于负载过高、或者响应超时,导致重新选举新的主节点

解决方案:

  • 适当调大ping timeout响应时间,避免因为网络、主节点性能问题导致的选举
  • 设置最少选举节点数大于候选主节点的半数,这样只要有半数以上的候选节点存活,则可以选举出一个主节点;而当可用节点数小于半数时,不参与选举,集群无法使用,也不会出现状态异常的情况
  • 角色分离:数据节点 + 候选主节点不放在一台机器上;

在有主节点的系统中,一般都需要考虑脑裂问题,常见的策略无非是:

  • 半数节点以上的投票才算有效
  • es额外提供了节点的角色定位,数据节点和候选主节点,其中只有候选主节点才有选举权和被选举权,提供一种角色分离的可选方案,来避免主节点被其他数据服务影响

4.3 数据分片

当数据量过大时,es支持自动拆分,将一个索引的上数据水平拆分到不同的数据块–分片(Shards),为了提供可用性,每个索引在定义时除了分片之外,还会定义副本数量,这里的副本可以理解为数据冗余,其中副本和分片必然不在一个节点上,在主节点异常时,副本可以提供数据查询能力

es默认在创建索引时,分片数为5,每个分片对应一个副本

ES分片

ES通过分片,将索引数据水平拆分,分片数越多,每个分片上的数据量就越少;而副本则是对应的每个分片的冗余,可以理解为主备,副本越多,消耗则越大

两点小说明

  • 对应副本的概念,上面的分片也叫做主分片
  • 当一个数据写入/更新到分片时,只有所有的副本都更新完毕之后,才算完成(可以MySql的全同步)

4.4 数据持久化

最后再说一下es的持久化机制,与前面先说持久化不同,es这里则需要先了解上面的基本流程,索引数据需要保存到主分片上,最终落盘,接下来看一下完整的流程

主分片数据更新流程

ES数据更新流程

简述一下上面的流程

  • 首先请求随机连一个es节点(这个节点叫做协调节点),然后通过路由算法,确定数据对应的主分片
  • 写数据到主分片,然后同步到副本(多个副本时采用并发同步,乐观锁控制)
  • 所有副本同步完成之后,主分片节点告诉协调节点最终结果,然后协调节点告诉调用者响应

当数据写入到主分片上之后,接下来再看一下这个数据时如何刷新到磁盘上的

分段存储

索引文档以段的形式存储磁盘,即一个索引文件会划分为很多个子文件,这里的子文件就是段

每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改;段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件

段的特性,有下面几个有点

  • 分段存储,可以有效避免读写时加锁的问题
  • 不变性,数据只读可以高效缓存,无需考虑更新
  • 一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索

由于段不可变,所以在更新时需要额外处理

  • 新增:当前文档新增一个段
  • 删除:新增一个.del文件,记录被删除的文档信息;被标记删除的文档仍然可以被检索到,只是最终返回时被移除
  • 更新:删除文件中标记旧的文档删除,插入新的段

延迟写

ES并不会实时将内存中的数据写入段,而是采用延迟写的策略(类似前面的写buffer,然后异步定时刷盘)

es先将内存数据,写入文件缓存系统(操作系统内存),

ES文档写入流程

上图来自 * 两万字教程,带你遨游ElasticSearch

注意几个事项

  • 写入文件缓存系统,之后异步落盘,可能导致丢数据,es采用事务日志的方式来处理恢复策略(即mysql的先写日志,崩溃之后做回放恢复)
  • es对外服务时,检索文件缓存系统 + 段中的文档,而内存中的数据不会被检索到(所以所es是近实时搜索引擎,因为最新写入的数据还在内存中,没有提交,立马查就查不到)
  • 为了避免段过多,es会定时做合并,将很多小的段合并成大的段(合并过程中会自动移除被标记删除的文档)

最后小结一下es的持久化

  • 索引分段存储,段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改
  • 延迟写策略:先更新内存数据,异步提交文件缓存系统,最后再由操作系统刷盘
  • 内存中的数据不能被检索;文件缓存 + 段中的数据提供查询聚合,最终的结果会过滤已标记删除的文档

参考博文

4.5 小结

这一小节主要介绍的是ES的高可用机制,包括ES的集群工作原理,选举策略;采用数据分片支持大数据场景的支持,借助副本来提高可用性;

ES原生支持集群

  • 角色划分:候选主节点 + 数据节点
  • 数据节点:负责数据的存储和相关的操作(CURD,聚合)等,因此对机器性能要求较高
  • 候选主节点:拥有选举权和被选举权,主节点在候选主节点中评选出来,负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给哪些的节点、追踪集群中节点的状态等

ES数据持久化策略

  • 索引分段存储,段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改;当出现修改时,标记原来段中文档删除,在新的段写入数据
  • 延迟写策略:先更新内存数据,异步提交文件缓存系统,最后再由操作系统刷盘
  • 内存中的数据不能被检索;文件缓存 + 段中的数据提供查询聚合,最终的结果会过滤已标记删除的文档

5.一灰灰的总结

5.1 综述

本片文章主要是分析当下不同应用场景下的几个主流系统的高可用策略,来看一下如何来保障的系统的高可用

常见的高可用思路

  • 冗余 (如数据副本、主备服务等)
  • 拆分 (数据拆分、服务能力拆分等)
  • 持久化

redis

  • 持久化:RDB数据落盘加载方式 + AOF记录操作命令用于回放策略
  • 主从,主从从:全量数据冗余、读写请求分离,负载均衡的思想;核心问题在于主节点挂掉之后需要人工参与手动指定主库
  • 哨兵机制:PING/PONG的探活机制,监听主节点,宕机之后自动选主,确保高可用;核心问题在于所有的实例冗余相同的一份数据,数据量大时不友好
  • 集群:数据分片,每个实例提供部分服务能力

mysql

  • 通过冗余来实现高可用:如主备
  • 读写分离,实现负载均衡:主从、主从从模式
  • 数据持久化策略:操作内存(buffer),异步刷盘,两阶段提交保障一致性

rabbitmq

  • 主备模式
  • 镜像模式:全量冗余一份数据,主对外提供服务,可以实现自动切主
  • 普通集群模式:数据拆分到集群的实例中,consumer/publisher连接到实例之后,会从具体持有exchange/topic的实例上拉数据
  • 远程模式:适用于多中心的场景,将消息转发给其他中心的实例

ElasticSearch

  • ES集群:数据节点 + 候选主节点
  • ES持久化:
    • 延迟写策略,先更新内存,然后提交操作系统缓存,最后异步刷新到磁盘;
    • 索引分段存储:段生成checkpoint之后,则只读,因此可以全量缓存,不用考虑更新修改;当出现修改时,标记原来段中文档删除,在新的段写入数据

5.2 主题无关

在准备写本文时,原计划针对不同业务场景各挑一个经典的系统来分析下各自的高可用方案,实际写下来发现工作量有点大;就把最后的一个分布式文件系统hdfs给暂缓了(对于大多数业务开发而言,接触的机会也不会太多),这个会放在《分布式系统-案例剖析》中进行介绍

最近会花大量的时间精力,准备做一个高质量的《分布式专栏》,欢迎有兴趣收藏关注 一灰灰的主站

2 - 02.Redis、ES、Hbase的高可用方案

以下内容来自同事的内部分享,经得同意分享给各位小伙伴

我们常说的高可用是怎么实现的呢?单机向集群的演进中遵循哪些原则,注意哪些事项呢?集群如何协同工作?集群之间的一致性如何保障?

纯干货,推荐看到的小伙伴仔细认证的阅读一下,相信会有不少的收获

一.nosql发展历史

1.关系型数据库

上世纪60年代以来至今,传统的关系型数据库一直被互联网应用的作为首选数据存储系统,典型的代表产品包括有oracle、mysql等。

关系型数据库的核心优势在于:第一,具备事务属性,注重数据一致性,内部实现有复杂的锁机制等还包含有其它一系列机制来保障数据一致性,能够基于AID(原子性、隔离性和持久性)的基础能力而带来事务强一致性来保证我们的数据存、取安全;第二,关系型数据模式其支持的二维表格模式比较契合现实中大部分的业务场景且易于理解,因此得以快速应用和发展。

关系型数据库最大的缺陷在于扩展性不足,在面对大量用户的并发访问以及海量存储的存取场景下,往往很难平滑的去做到性能升级,而使得DB经常作为整个系统应用的发展瓶颈。通常情况下,关系型数据库的扩展思路分为以下两种:

(1)纵向扩展。纵向扩展即提升单机硬件基础设施来提升处理能力。这种方式下虽然可以换来一定的性能提升,但是单机终归是存在有性能上限的,且升级过程中往往需要停机处理而无法做到平滑升级。其整体收益成本比比较低。

(2)横向扩展。横向扩展即通过分片,将数据分散至多台物理节点,降低单点压力,来提升处理能力。这种方式通常是对上层应用抽象出一个逻辑数据库,背后则是将数据分散到不同的物理数据库上。整体上来说,这种方式虽然可以在大部分常规场景下带来较大的性能提升,但与此同时又会引入另一个新的问题分布式事务问题,当然还包括有跨库join、非路由键查询等其它一系列问题。

2.NoSql的诞生

在随着互联网业务与场景不断发展的背景下,由于在应对海量存储数据时传统的关系型数据在扩展能力上的不足,以及出现了越来越多的场景在关系型模式下显得并不适用,典型的如OLAP数据分析类型场景。促使Nosql技术开始诞生,Nosql的核心思想在于放弃传统关系型数据库的事务强一致性与关系模式,以此换取更高的可扩展性以及面对高并发海量数据时具备更强的处理能力。

对于Nosql而言,其定位并不是取代关系型数据库,而是作为关系型数据库的一种补充,两者分别有各自适合的领域场景。典型的nosql产品包括:基于kv的redis、列存储的hbase、文档型数据库ES等。

3.NewSql

nosql虽然具备高扩展性的优势但其实在放弃了传统关系型数据库的强事务一致性的代价下换来的。因此,在关系型数据库与nosql均存在明显局限的背景下,NewSql概念开始应运而生。NewSQL可以说是传统的RDBMS与NoSQL技术结合之下的产物。这些系统既拥有NoSQL数据库的高扩展性,又希望能保持传统数据库的事务特性。典型的产品代表如google的spanner和国内的TiDB。

二.常见的Nosql产品简介

常见的nosql产品如下:

产品名称 特点 适用场景
redis 1.k-v结构; 2.内存数据库; 3.高性能,单机2C4G下读可达10WQPS; 1.缓存; 2.分布式锁、延时队列、限流等;
ES 1.文档型数据库; 2.结构化查询。支持多字段查询,以及复杂的过滤和聚合统计功能; 3.近实时查询。默认1s refresh一次将内存中的数据固化生成一个新的segment,此时为该segment创建倒排索引,外部读请求才能访问到这个segment的内容; 1.大数量背景下的检索类场景。例如日志搜索、大宽表解决mysql跨库join问题以及作为辅助索引解决分表下的非路由键查询问题; 2.数据统计、分析; 3.全文检索;
Hbase 1.列存储; 2.采用块存储机制,底层数据结构采用的是LSM合并树,将随机IO写转变为一次性顺序写,相比于B+树在写性能上表现更加优秀。但读性能会更弱; 1.PB级数据存储规模; 2.适合写多读少的场景,例如下沉的冷数据存储; 3.OLAP数据分析类场景;

三. 集群工作原理

3.1 集群模式

对于大规模数据存储系统都会面临一个问题就是如何进行横向扩展,当数据集越来越大时,一主多从的模式无法支持这么大的数据存储与访问量,此时一般情况下就会考虑进行横向扩展,将多个主从模式组合在一起对外提供服务。但是这里有两个首要问题就是如何实现数据的分片逻辑以及分片逻辑放在哪里。于是在这种背景下就会衍生出两种不同的集群模式,一种就是集中式模式,一种则是去中心化的模式。

1.集中式

集中式集群模式下,通常会引入一个中心节点作为集群的管理者,由管理者来进行集群状态管理、故障处理以及元数据维护等,其它节点只需响应数据请求,而无需知道集群中其它节点的情况。典型的解决方案都会借助于zookeeper分布式协调服务来进行集群管理,比如Hbase、kafka等。

Zookeeper:维护集群中的服务状态,并提供服务故障通知;

master:存储和维护集群元数据,以及故障转移等集群事务处理;

2.去中心化

即P2P交互模式,客户端与集群节点直接进行交互,而非之前业界的Proxy方式。典型的集群代表如redis cluster、es集群。

redis Cluster介绍

redis3.0版本开始,官方正式支持集群模式。redis官方集群模式最大的两个特点在于:

(1)去中心化。即P2P交互模式,客户端与集群节点直接进行交互,而非业界之前的Proxy方式。

(2)内部自治。redis 集群模式并未像Hbase、Kafka等引入第三方组件比如ZK,来实现对集群的节点状态管理、故障转移以及元数据管理等,而是基于Gssiop协议实现集群内节点监控、状态同步,并内置选举算法实现故障自动转移,在集群内部高度自治。

如下图是一个三主三从的redis cluster,三个机房部署,其中一主一从构成一个分片,之间通过异步复制同步数据。节点之间基于ping-pong心跳机制相互通信感知对方状态,一旦某个机房掉线,则分片上位于另一个机房的slave会被提升为master继续对外提供服务。

3.2 数据分片

分布式集群在进行横向扩展时,首要问题就是如何实现数据的分片逻辑。

1.分片策略

常见分片策略如下:

(1)hash分片。hash分片也是我们最常用的分片策略。例如ES默认采用的就是这种方式。hash分片的好处在于数据会被打的比较分散,其次不用额外存储映射关系,客户端与服务端以约定好的hash公式进行路由。但是它的问题在于如果一旦需要进行扩缩容,那么整个映射关系都会被打破,此时需要进行一次全量的rehash数据迁移,工作量非常大。所以一般情况下,在设计的时候会尽可能的让这个hash模值大一点,避免频繁的进行扩容。

(2)基于某一key值的范围划分。例如基于时间范围或者id范围分片。这种分片方式的优劣势其实与hash的方式是相反的。它的好处在于,当需要进行扩缩容时,不会像hash一样破坏掉全局的映射关系,只需要对部分分片的映射关系产生影响。但是这种方式的问题在于它会存在一定的热点数据问题,导致整个集群各个节点的负载不均衡。例如Habse采用的就是这种方式,HBase 表根据 RowKey 的开始和结束范围水平拆分为多个 Region,一个region就是分片。每个 Region 都包含了 StartKey 和 EndKey 之间的所有行。每个 Region 都会分配到集群的一个节点上,即 RegionServer,由它们提供读写服务。

(3)一致性hash。一致性hash是通过构建一个环形的hash空间,对于用户的请求,先经过hash映射到这个环上,这就是第一层的映射关系,只要这个hash的模值不变,这层关系就不会变。其次,顺着环的顺时针方向找到的第一个节点,就是负责该请求对应的节点。

一致性hash的优势在于当进行扩缩容时,不会破坏全局的映射关系,而导致整个rehash,发起全局的数据迁移,而只会影响局部数据的映射关系。比如缩容减少一个节点,因为第一层映射依然保持不变,原来的请求该分配到哪个节点还是在哪个节点上,只是改变了第二层从环上到节点之间的一个局部映射关系。从环上来看,只会影响这个节点的上一个节点到这个这个节点的这一段弧区间上,整个环上的其它区间由于第一层关系不变,其映射关系不会受到影响。原来去掉的这个节点之间负责的那一段弧上请求,会全部顺移到它的下一个节点,我们只需要把去掉节点负责的数据迁移到下一个节点即可,其它的所有节点不用做任何变更。

2.基于Hash槽的数据分片

redis cluster中,数据分片借助与hash槽slot来实现,集群预先划分16384个slot,对于每个请求集群的键值对,根据key 按CRC hash算法散列生成的值唯一匹配一个slot。在redis集群中每个master节点分片负责其中一部分槽位的读写请求,而且当且仅当每个slot都有对应节点负责时,集群才会进入可用状态。当动态扩缩容时,需求将16384个slot做一次再分配,相应数据也要进行迁移。

redis hash槽的算法与一致性hash算法的本质思想是一样的,通过不直接建立请求到节点的映射关系,而是建立一种间接的映射关系。避免在发生扩缩容时对于传统hash算法而言因为模值的变化而打乱整个映射关系。如下图所示,将映射关系分为两层,hash槽通过槽位路由表作为中间映射,因为槽位数量是16384不会变,这样当发生扩缩容时,对于请求而言该映射到哪个槽位还是映射到哪个槽位,即Part1映射不变,只用针对Part2部分中需要迁移的slot产生影响,而并非会让全部请求受到影响;

3.3 客户端交互流程

Redis集群交互

redis客户端与集群之间的交互是基于槽位映射表来进行的,该映射表类似于集群的数据分布图,其中维护着槽位与负责该槽位的节点地址信息,客户端根据该映射关系与节点进行直连交互。

redis客户端首次连接集群时,会从集群中拉取一份完整的槽位映射表,缓存在本地。在进行请求访问时,首先会采用CRC16冗余校验法的值对16384取模,映射到具体一个槽位,随之通过查询槽位映射表定位到具体负责该槽位的节点,进而直接与节点进行通信。对于服务端节点来说在收到请求后首先会判断该槽位是否是自己负责的槽位,如果是,则会响应客户端请求。如果不是,例如集群发生扩缩容,此时槽位发生迁移,则会返回Moved/ask指令,引导客户端重定向至正确的节点进行访问。

Moved指令:当迁移已经全部完成,此时该slot已经永久转交给另一个节点时,A节点会返回Moved指令。当Client收到Moved指令后,则会重定向至正确的节点再次进行访问,同时更新本地的槽位映射表,下次直接访问到正确的节点。

ASK指令:ASK指令主要是在迁移过程中,此时该slot的数据可能一部分位于B,而另一部分key可能还在源节点A上。此时对于读请求而言,源节点A在收到请求后,会先在自己的数据库中查找,如果存在则直接返回结果;如果不存在则说明可能已经迁移至B,则会返回ask错误指令,引导client转向目的地节点查询key;

当Client收到Moved/AKS指令后,会去重定向至新的节点访问,同时还会更新本地的槽位映射表,在下次访问时直接定位至正确的节点上;

ES集群交互过程

es作为搜索引擎而言,其支持的查询条件不局限于路由key,还包括其它关键字作为条件进行查询,因此其在查询流程不太一样。

es默认的查询模式为query then fetch模式,此模式下整个查询分为query 和 fetch两个步骤,query步骤负责查询符合条件文档id以及汇总排序截取limit等,fetch阶段则是查询完整数据,查询过程中需要进行两次交互。

(1)client首先会将查询请求发送至任一协调节点;

(2)协调节点在收到请求后,会并发的将请求发送至所有的数据节点;

(3)数据节点在收到请求后根据查询条件在自己负责的分片上查询符合条件的文档集合,不过只取文档 id和排名相关的字段信息,并将数据集返回至协调节点;

(4)协调节点在收到数据节点返回的结果集后,进行汇总排序取limit等,随着得到需要返回的结果集docId集合;

(5)此时query阶段结束,进入fetch阶段,协调节点会根据hash算法对docId进行路由,得到对应结果分别在哪些分片节点后,再次发送请求至数据节点,fetch数据;

(6)数据节点根据docId查询完整结果数据,并将数据再次返回至协调节点;

(7)协调结果进行完数据汇总后,将数据返回至客户端;

除了query then fetch之外,es还有另外一种比较常见的查询模式:query and fetch**。**此模式下向索引的所有分片 ( shard)都发出查询请求, 各分片执行完query 后再执行fetch,即在分片节点中做完查询、排序和截取后将完整的数据一并返回至协调节点。这种搜索方式是最快的。 因为这种查询方法只需要去 shard查询一次。 但是各个 shard 返回的结果的数量之和可能是用户要求的 size 的 n 倍。

Hbase集群交互过程

Hbase集群与redis集群不一样,其基于ZK进行集群状态管理以及元数据维护,集群中数据节点只知道自己负责的数据分片而不知其他节点。因此,在客户端进行集群访问时,通常需要先于ZK进行一次访问,在获取路由表后,再与集群节点直连访问。kafka也是同理。

如下图所示,HBase集群中的读取流程大致如下所示:

(1)client首先会访问一次zk,查询集群中master节点;

(2)在查询到master地址信息后,Client第二次发起请求访问master,查询路由信息表,路由表中记录着每个region节点负责处理哪个范围的rowkey;

(3)client在查询到路由信息后,会将其缓存在本地,随之基于路由信息表,查询rowkey对应的节点地址信息;

(4)直连数据节点服务器,发送查询请求获取数据;

(5)节点服务器在收到请求后,查询对应的完整数据并将结果返回至客户端;

   

3.4 集群管理

1.集群元数据管理

在集中式集群中,通常情况下会直接基于第三方协调服务zk来管理和维护集群元数据,zk在作为分布式协调服务之外,本身也是一个内存数据库。不过通常为减轻zk压力以及降低对zk的依赖,因此一般情况下,集群还会基于zk选举出一个master节点,代理zk进行元数据管理和维护以及非master节点的故障转移等相关事务处理。同时,zk中也会备份一份集群的元数据信息,避免master故障后集群元数据丢失,当选举出来的新master,会从zk中拉取一份集群元数据继续进行维护。

在去中心化的集群中,例如redis集群下每个节点都存储有整个集群的元数据信息,包括自己以及其它节点的存活状态、负责的slot槽位信息等。各节点间基于 Gossip 协议来相互交换信息,Gossip协议又叫病毒协议,是基于流行病传播的方式在节点或者进程之间信息交换的协议,在P2P去中心化的分布式系统中应用比较广泛。

Gossip协议的特点在于:

1.去中心化。Gossip 协议不要求任何中心节点,所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,就可以把消息散播到全集群。

2.最终一致性。数据的传播过程是由一传十十传百逐步流散开来,整个传播过程需要经历多个周期,可能需要一定的时间,不过在一个处于有界网络的集群里,理论上集群各个节点对该份信息的认知最终都将会收敛一致。

Redis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态,主要包括:

a.集群中各节点所负责的slots信息;

b.集群中各节点的存活状态信息;

对于集群中每个节点而言,会按照一定的频率周期,从自己的节点列表中随机挑选部分最长时间没有与它进行过通信的节点,对这些节点发送ping消息,并附加上自己视角下的集群状态信息,节点在收到其他节点发送的ping消息后再回复一个pong,以交换彼此的状态信息,对于差异化数据则版本决定是否更新本地状态数据,最终集群内所有节点达成统一认知。

优点:

(1)容错。Gossip 协议具有天然的分布式系统容错特性,集群中任何节点的状态发生变化,例如上下线都不会影响 Gossip 消息的传播,且当节点重新上线后,依然会接收集群内其他节点的状态数据,并最终与其他节点达成一致。

缺点:

(1)Gossip是最终一致性,当集群状态发生变更时,变更数据需要经过多伦同步,整个集群的节点才会达成一致,相比于ZK而言其感知会出现明显延迟;

(2)Gossip协议下,每个节点按自己的节奏频率周期性的发送消息,而由于同步全量状态信息使得Gossip包体积较大,会存在一定的网络压力。其次由于随机的发送消息,而收到消息的节点也会重复该步骤,不可避免的引起同一节点消息多次接收,增加消息处理压力。

2.集群状态检测

对于集中式集群模式的Hbase、kafka来说,对于集群的状态检测也是基于ZooKeeper 来做的,每台节点机器在启动时,都需要事先在zookeeper中注册一个节点,zk会与该节点维持一个会话关系,基于心跳检测来感知节点的状态变化。

具体来说,客户端会周期性的向服务端发送PING请求来保持心跳,一旦客户端发生故障,超过限定时间后,Zookeeper服务器会判定会话超时,并基于Watch机制实时通知给Master节点,master进行元数据更新以及后续的故障转移,以此来完成对集群中节点的状态检测。

跟大多数分布式系统一样,Redis cluster也是基于heart beat来进行节点状态检测。redis内部节点基于Gossip协议通信交互,具体来说,每个节点会定期会与其它节点发送ping-pong消息进行交互,以此来感知对方是否状态发生变化。对于集群中每个节点而言,每次随机挑选5个最长时间没有与它进行过通信的节点,对这些节点发送ping消息,节点在收到其他节点发送的ping消息后再回复一个pong。每个节点根据自己是否收到pong消息的结果来感知其它节点的存活状态。

节点上线

Redis Cluster 加入新节点时,首先需要在客户端需要执行 CLUSTER MEET 命令,命令中需要指定新增节点的地址信息。

redis集群中任一节点在收到 MEET命令后,会根据据 MEET 命令中的 IP 地址和端口号,向新节点发送一条 MEET 消息。

接着,新节点在收到Meet消息后,会向节点一返回一条PONG消息。

节点一接收到新节点返回的PONG消息后,得知新节点已经成功的接收了自己发送的MEET消息。随着将该新节点加入自己的元数据信息库中,从而完成了新节点接入的握手操作。

Meet成功之后,节点一会在下次周期性信息交互过程中,将新节点加入的消息传递出去。因为节点之间基于Gossip协议进行工作,在随着时间的推移,最终集群的所有节点都感知它的存在。

节点下线

redis集群中节点会周期性心跳同步,当某一节点在发其ping请求后,发现某个节点超过一定未给出回复,那么它会把这个节点的状态标记为pfail预下线的状态。

节点一会在下一轮交互中,会将节点二疑似下线消息同步出去。对于节点三在同步到这条消息后,并不会直接把自己的节点列表中该故障节点的状态也标记为预下线,因为这时候可能只是该节点一个人的主观认为下线,只是先记录下来节点一在XX时间认为节点二疑似下线;同时在节点三的下一轮ping-pong中,会优先选择节点二进行交互;

随着时间的推移,经过多轮同步后,对于节点X也超时未收到节点二的PONG,也认为节点二疑似下线,此时节点X发现集群中大部分超过一半的节点都认为它下线时,节点X会把该节点二标记为fail下线状态,并同时在集群中广播该节点fail。所有收到该消息的节点在发现某节点已经被标记为fail状态时,都会更新自己的节点列表将它标记为下线状态,如果该节点是leader副本的节点,则其对应的slave节点在收到下线消息会开始进行选举,进入故障转移流程。

3.5 高可用性

对于分布式存储系统而言,集群高可用保证在于解决两个前提,第一个是要保证数据的可靠性,即当节点机器出现故障时,数据不能因此出现丢失。第二,在故障发生后集群需具备自动故障转移机制。

1.数据的可靠性保证

通常而言,数据的可靠性都是基于多副本机制来解决的,即构建主从模式,为每个主节点部署多个slave从节点,当主节点故障时由从节点顶替。

对于多副本机制而言,其核心问题在于如何解决多副本之间的一致性。在多副本数据一致性问题上,一般会有两种解决方案。一种是基于ACK应答机制下的主从复制机制;另一种是目前业界更为主流的方案,基于分布式共识算法Paxos或者Raft来解决多副本之间的一致性问题。

主从复制+ACK机制

基于ACK的应答机制十分常见,首先从同步方式上来说又分为推模式和拉模式,拉模式相对而言十分常见,例如mysql的主从复制就是拉模式。两种模式的比较如下:

模式 代表产品 优劣势
mysql、es\kafka等 优势: 从节点可基于自身消费能力处理同步数据; 劣势: 数据同步及时性相对差一点
redis 优势: 数据同步相对更加及时; 劣势: 从节点一旦同步过程中出现重启,则重新启动后需要再次完整的同步一次全量数据。因此,在这种模式下一般还需要配备相关的缓冲区机制。例如redis中会配置同步缓冲区,在commit之前同时会先在缓冲区中备份一份。从节点重启后同步位移还在缓冲区中,则从缓冲区增量同步进行对齐。

从应答机制的角度上来说又分为异步复制、半同步复制与全同步复制。

(1)异步复制。主节点在收到写请求后,会将数据写入内存以及同步至中继日志后,进行commit提交。随后通知slave节点过来复制,slave是否成功复制主节点并不关心。对于异步复制而言,它是存在有数据丢失风险的,当master宕机时,从节点可能还没来得及复制数据。

(2)半同步复制。半同步复制每次都会至少有一个从节点ack应答,相对而言它可以有更强的一个数据一致性保证。但还是会存在不一致的问题的场景,比如脑裂问题,导致数据丢失。当发生网络分区时,master节点和一个从节点被划分到一个区域与其它的从节点分离,这时其它从节点发现与master失联后就会选出一个新的master来提供服务,但是原来的master并不知道自己被失联了,而且每次依然会有一个从节点给它ack应答,因此它也可以正常处理客户端请求,这个时候就会存在两个master同时对外提供服务,接收客户端的写请求,而当网络分区结束后旧的master发现有新的master了,就会向新的master看齐,丢弃掉脑裂期间客户端提交的数据了。

(3)全同步复制。全同步复制则是必须每个从节点都给出ack应答才提交数据,这样可以避免脑裂情况发生,因为当发生脑裂时旧master因为不能得不到所有从节点的ack应答,所以是不会处理客户端的请求写从而旧可以避免脑裂问题。但是它的问题是在于性能较低,因为需要全部副本的响应,如果其中一个节点响应较慢则会拖慢整体的提交时间。

分布式共识算法

对于paxos、raft这类共识算法来说,因为它采用的多数决的机制,在出现网络分区时,只会存在有一个大多数而不会同时出现两个大多数。如果master位于网络分区后的少数派中,那这个master在接收到用户请求后,由于与它连通的只有少数节点达不到超过一半节点的支持,因此它是无法提交数据的。只会由多数节点构成的集群选举出来的新master这一个master对外提供服务;如果master处于多数节点构成的集群中,对于分隔出去的少数派节点构成的集群中因为节点数量不超过一半,所以根本就选取不出来一个新master。因此对于共识算法来说天然不会出现脑裂现象,相比于主从复制+ack的做法来说它能够带来更强的一致性保证。

分布式共识算法核心优势在于:

(1)容错。因为其多数派的原则,在出现网络分区时,只要不要超过半数以上的节点不可用,整个共识系统仍然是满足大多数原则的,仍然可以正常运转,在可用性方面具备非常强的一个容错能力。

(2)在强一致性的同时具备一定的性能优势。相比于全同步复制而言,因为多数决的机制,每次commit并不需要全部的节点同意,因此性能上而言相比于全同步复制更具有优势。

因为共识算法它所带来的强一致性保证和对集群节点的超强的容错能力,所以现在越来越多的分布式存储系统在解决多副本一致性问题上都在使用共识算法,比如new sql的tidb,内部就是基于raft算法以及mysql自身也推出了MGR集群,内部就是使用的mutil-paxos算法取代传统的半同步复制来解决多副本的一致性问题。

2.故障转移

一般来说,对于引入第三方协调服务的存储系统来说,会事先在集群中选举一个Master,此master并非我们所说的主从复制中的leader副本节点。以kafka为例,在Kafka集群中这类节点称之为Controller。当节点发生故障时,会由ZK将故障通知至Controller节点,此时触发controller节点进行故障转移。

按故障节点类型来说分为以下几类:

(1)leader副本节点故障。当故障节点为某分片的leader副本节点时,则直接会由Controller负责为该分区重新选举新的Leader副本;Controller在watch关于某leader副本节点故障后,则会直接从该leader副本节点的从节点列表中找到位移提交最大也就是数据最新的节点作为新的master。

(2)Controller节点故障。当故障节点为Controller自身时,则由借助于ZK从集群中的其他leader节点中选取一个新的controller节点。整个选举过程本质上也是ZK的一次分布式锁的抢占过程。当controller产生时,会从ZK中拉取一份集群元数据备份存储到本地。同时一般来说Controller节点并不是单独的物理节点实例,而是由集群中某leader分片节点担任。当controller节点故障时,同时也是leader副本节点故障,因此当新master产生后,同时还会为旧master节点的slave节点中选举新的leader副本。

对于redis、ES这些在集群内部实现自治的集群系统而言,则通常会在集群内部实现选举算法,来实现故障转移。

当集群中某节点在发现半数以上的节点都认为某节点疑似下线后,会将该节点标记为确定下线并在集群中进行广播。当slave收到节点下线通知后,判断如果是自己的master节点,则触发选举流程,开始进行故障转移。

以redis为例其选举算法流程如下:

(1)slave收到master下线通知,开启一个纪元,将currentEpoch+1,开启选举;

(2)slave计算发起投票的延时时间。对于所有有资格参选的节点来说,并不会一收到选举通知后立马就开始发起选举,而是会先延迟一段时间。其延时时间的计算基于当前slave复制的数据总量,如果总量越高比较数据越接近master,那么它的延时时间会越短,被选中的概率也就越大。

(3)发起投票。slave在延时时间到期后,会向集群广播投票请求;

(4)投票。集群中只有master节点具备投票权利,且在每个纪元中只有一次投票机会,master的投票原则是先到先得。当master收到投票请求后,会先基于自身的元数据审查该节点是否为故障节点的slave节点,如果是且当前还未给其他的slave节点投过票,则会将票投给该节点,因此理论上而言,数据越新的从节点获得票数会越高;

(5)票数统计。每个节点在达到指定时间后会统计自身的票数,因为每个节点只能投一次票,所以得票超过一半以上的只会有一个节点。

(6)广播通知。当该从节点发现自己得票一半以后,就会像整个集群中广播新master节点的消息,让其它节点都知道它已经是最新的主节点,其它的主节点在收到后会更新自己的节点表,从节点则会将它设为新的主节点,此时选举结束。如果有一些从节点发现自己既没有达到半数以上的投票,又在指定时间内没有收到新master的消息,则会开启新的纪元,再次发起选票,但是此次其它的主节点发现如果直接的节点列表中该主节点的状态不是fail状态或者对该纪元已经进行过投票,不会再进行投票。