专栏名称: 美团技术团队
10000+工程师,如何支撑中国领先的生活服务电子商务平台?数亿消费者、数百万商户、2000多个行业、几千亿交易额背后是哪些技术在支撑?这里是美团、大众点评、美团外卖、美团配送、美团优选等技术团队的对外窗口。
目录
相关文章推荐
字节跳动技术团队  ·  远程访问代理+内网穿透:火山引擎边缘网关助力 ... ·  15 小时前  
字节跳动技术团队  ·  稀土掘金 x Trae ... ·  15 小时前  
51好读  ›  专栏  ›  美团技术团队

Replication(上):常见的复制模型&分布式系统的挑战

美团技术团队  · 公众号  · 架构  · 2022-08-25 19:58

正文

请到「今天看啥」查看全文


主节点失效则会稍稍复杂一些,需要经历三个步骤来完成节点的切换。
  1. 确认主节点失效,由于失效的原因有多种多样,大多数系统会采用超时来判定节点失效。一般都是采用节点间互发心跳的方式,如果发现某个节点在较长时间内无响应,则会认定为节点失效。具体到Kafka中,它 是通过和Zookeeper( 下文简称ZK )间的会话来保持心跳的 ,在启动时Kafka会在ZK上注册临时节点,此后会和ZK间维持会话,假设Kafka节点出现故障( 这里指被动的掉线,不包含主动执行停服的操作 ),当会话心跳超时时,ZK上的临时节点会掉线,这时会有专门的组件( Controller )监听到这一信息,并认定节点失效。

  2. 选举新的主节点。这里可以通过通过选举的方式( 民主协商投票,通常使用共识算法 ),或由某个特定的组件指定某个节点作为新的节点( Kafka的Controller )。在选举或指定时,需要尽可能地让新主与原主的差距最小,这样会最小化数据丢失的风险( 让所有节点都认可新的主节点是典型的共识问题 )--这里所谓共识,就是让一个小组的节点就某一个议题达成一致,下一篇文章会重点进行介绍。

  3. 重新配置系统是新的主节点生效,这一阶段基本可以理解为对集群的元数据进行修改,让所有外界知道新主节点的存在( Kafka中Controller通过元数据广播实现 ),后续及时旧的节点启动,也需要确保它不能再认为自己是主节点,从而承担写请求。
问题
虽然上述三个步骤较为清晰,但在实际发生时,还会存在一些问题:
  1. 假设采用异步复制,在失效前,新的主节点与原主节点的数据存在Gap,选举完成后,原主节点很快重新上线加入到集群,这时新的主节点可能会收到冲突的写请求,此时还未完全执行上述步骤的第三步,也就是原主节点没有意识到自己的角色发生变化,还会尝试向新主节点同步数据。这时,一般的做法是,将原主节点上未完成复制的写请求丢掉,但这又可能会发生数据丢失或不一致,假设我们每条数据采用MySQL的自增ID作为主键,并且使用Redis作为缓存,假设发生了MySQL的主从切换,从节点的计数器落后于主节点,那样可能出现应用获取到旧的自增ID,这样就会与Redis上对应ID取到的数据不一致,出现数据泄露或丢失。

  2. 假设上面的问题,原主节点因为一些故障永远不知道自己角色已经变更,则可能发生“脑裂”,两个节点同时操作数据,又没有相应解决冲突( 没有设计这一模块 ),就有可能对数据造成破坏。

  3. 此外,对于超时时间的设定也是个十分复杂的问题,过长会导致服务不可用,设置过短则会导致节点频繁切换,假设本身系统处于高负载状态,频繁角色切换会让负载进一步加重( 团队内部对Kafka僵尸节点的处理逻辑 )。

异步复制面临的主要问题——复制滞后

如前文所述,如果我们使用纯的同步复制,任何一台机器发生故障都会导致服务不可写入,并且在数较多的情况下,吞吐和可用性都会受到比较大的影响。很多系统都会采用半步复制或异步复制来在可用性和一致性之间做权衡。
在异步复制中,由于写请求写到主副本就返回成功,在数据复制到其他副本的过程中,如果客户端进行读取,在不同副本读取到的数据可能会不一致,《DDIA》将这个种现象称为复制滞后( Replication Lag ),存在这种问题的复制行为所形成的数据一致性统称为最终一致性。未来还会重点介绍一下一致性和共识,但在本文不做过多的介绍,感兴趣的同学可以提前阅读《Problems with Replication Lag》这一章节。

2.2 多主节点复制

前文介绍的主从复制模型中存在一个比较严重的弊端,就是所有写请求都需要经过主节点,因为只存在一个主节点,就很容易出现性能问题。虽然有从节点作为冗余应对容错,但对于写入请求实际上这种复制方式是不具备扩展性的。
此外,如果客户端来源于多个地域,不同客户端所感知到的服务相应时间差距会非常大。因此,有些系统顺着传统主从复制进行延伸,采用多个主节点同时承担写请求,主节点接到写入请求之后将数据同步到从节点,不同的是,这个主节点可能还是其他节点的从节点。复制模式如下图所示,可以看到两个主节点在接到写请求后,将数据同步到同一个数据中心的从节点。此外,该主节点还将不断同步在另一数据中心节点上的数据,由于每个主节点同时处理其他主节点的数据和客户端写入的数据,因此需要模型中增加一个冲突处理模块,最后写到主节点的数据需要解决冲突。
图3 多主节点复制

使用场景

a. 多数据中心部署
一般采用多主节点复制,都是为了做多数据中心容灾或让客户端就近访问( 用一个高大上的名词叫做异地多活 ),在同一个地域使用多主节点意义不大,在多个地域或者数据中心部署相比主从复制模型有如下的优势:
  • 性能提升 :性能提升主要表现在两个核心指标上,首先从吞吐方面,传统的主从模型所有写请求都会经过主节点,主节点如果无法采用数据分区的方式进行负载均衡,可能存在性能瓶颈,采用多主节点复制模式下,同一份数据就可以进行负载均衡,可以有效地提升吞吐。另外,由于多个主节点分布在多个地域,处于不同地域的客户端可以就近将请求发送到对应数据中心的主节点,可以最大程度地保证不同地域的客户端能够以相似的延迟读写数据,提升用户的使用体验。

  • 容忍数据中心失效 :对于主从模式,假设主节点所在的数据中心发生网络故障,需要发生一次节点切换才可将流量全部切换到另一个数据中心,而采用多主节点模式,则可无缝切换到新的数据中心,提升整体服务的可用性。
b. 离线客户端操作
除了解决多个地域容错和就近访问的问题,还有一些有趣的场景,其中一个场景则是在网络离线的情况下还能继续工作,例如我们笔记本电脑上的笔记或备忘录,我们不能因为网络离线就禁止使用该程序,我们依然可以在本地愉快的编辑内容( 图中标记为Offline状态 ),当我们连上网之后,这些内容又会同步到远程的节点上,这里面我们把本地的App也当做其中的一个副本,那么就可以承担用户在本地的变更请求。联网之后,再同步到远程的主节点上。
图4 Notion界面
c. 协同编辑
这里我们对离线客户端操作进行扩展,假设我们所有人同时编辑一个文档,每个人通过Web客户端编辑的文档都可以看做一个主节点。这里我们拿美团内部的学城( 内部的Wiki系统 )举例,当我们正在编辑一份文档的时候,基本上都会发现右上角会出现“xxx也在协同编辑文档”的字样,当我们保存的时候,系统就会自动将数据保存到本地并复制到其他主节点上,各自处理各自端上的冲突。
另外,当文档出现了更新时,学城会通知我们有更新,需要我们手动点击更新,来更新我们本地主节点的数据。书中说明,虽然不能将协同编辑完全等同于数据库复制,但却是有很多相似之处,也需要处理冲突问题。

冲突解决

通过上面的分析,我们了解到多主复制模型最大挑战就是解决冲突,下面我们简单看下《DDIA》中给出的通用解法,在介绍之前,我们先来看一个典型的冲突。

a. 冲突实例

图5 冲突实例
在图中,由于多主节点采用异步复制,用户将数据写入到自己的网页就返回成功了,但当尝试把数据复制到另一个主节点时就会出问题,这里我们如果假设主节点更新时采用类似CAS的更新方式时更新时,都会由于预期值不符合从而拒绝更新。针对这样的冲突,书中给出了几种常见的解决思路。

b. 解决思路

1. 避免冲突
所谓解决问题最根本的方式则是尽可能不让它发生,如果能够在应用层保证对特定数据的请求只发生在一个节点上,这样就没有所谓的“写冲突”了。继续拿上面的协同编辑文档举例,如果我们把每个人的都在填有自己姓名表格的一行里面进行编辑,这样就可以最大程度地保证每个人的修改范围不会有重叠,冲突也就迎刃而解了
2. 收敛于一致状态
然而,对更新标题这种情况而言,冲突是没法避免的,但还是需要有方法解决。对于单主节点模式而言,如果同一个字段有多次写入,那么最后写入的一定是最新的。 ZK、KafkaController、KafkaReplica都有类似Epoch的方式去屏蔽过期的写操作 ,由于所有的写请求都经过同一个节点,顺序是绝对的,但对于多主节点而言,由于没有绝对顺序的保证,就只能试图用一些方式来决策相对顺序,使冲突最终收敛,这里提到了几种方法:
给每个写请求分配Uniq-ID,例如一个时间戳,一个随机数,一个UUID或Hash值,最终取最高的ID作为最新的写入。如果基于时间戳,则称作最后写入者获胜( LWW ),这种方式看上去非常直接且简单,并且非常流行。但很遗憾,文章一开始也提到了,分布式系统没有办法在机器间共享一套统一的系统时间,所以这个方案很有可能因为这个问题导致数据丢失( 时钟漂移 )。
每个副本分配一个唯一的ID,ID高的更新优先级高于地域低的,这显然也会丢失数据。
当然,我们可以用某种方式做拼接,或利用预先定义的格式保留冲突相关信息,然后由用户自行解决。
3. 用户自行处理
其实,把这个操作直接交给用户,让用户自己在读取或写入前进行冲突解决,这种例子也是屡见不鲜,Github采用就是这种方式。
这里只是简单举了一些冲突的例子,其实冲突的定义是一个很微妙的概念。《DDIA》第七章介绍了更多关于冲突的概念,感兴趣同学可以先自行阅读,在下一篇文章中也会提到这个问题。

c. 处理细节介绍

此外,在书中将要结束《复制》这一章时,也详细介绍了如何进行冲突的处理,这里也简单进行介绍。
这里我们可以思考一个问题,为什么会发生冲突?通过阅读具体的处理手段后,我们可以尝试这样理解,正是因为我们对事件发生的先后顺序不确定,但这些事件的处理主体都有重叠( 比如都有设置某个数据的值 )。通过我们对冲突的理解,加上我们的常识推测,会有这样几种方式可以帮我们来判断事件的先后顺序。
1. 直接指定事件顺序
对于事件发生的先后顺序,我们一个最直观的想法就是,两个请求谁新要谁的,那这里定义“最新”是个问题,一个很简单的方式是使用时间戳,这种算法叫做最后写入者获胜LWW。
但分布式系统中没有统一的系统时钟,不同机器上的时间戳无法保证精确同步,那就可能存在数据丢失的风险,并且由于数据是覆盖写,可能不会保留中间值,那么最终可能也不是一致的状态,或出现数据丢失。如果是一些缓存系统,覆盖写看上去也是可以的,这种简单粗暴的算法是非常好的收敛冲突的方式,但如果我们对数据一致性要求较高,则这种方式就会引入风险,除非数据写入一次后就不会发生改变。
2. 从事件本身推断因果关系和并发
上面直接简单粗暴的制定很明显过于武断,那么有没有可能时间里面就存在一些因果关系呢,如果有我们很显然可以通过因果关系知道到底需要怎样的顺序,如果不行再通过指定的方式呢?
例如:
图6 违背因果关系示例






请到「今天看啥」查看全文