主从结构下分布式系统中的一致性

使用单机系统时常常会遇到单机故障后系统整体不可用,单机性能扩展困难容易成为整体系统瓶颈等问题。使用分布式系统则可以很大程度上解决上面的问题,但与此同时,很多在单机系统中并不存在的问题在分布式场景下成为了让人头痛无比的难点,首当其冲就是分布式系统中的一致性问题。分布式系统大致可以分为主从(leader-follower)结构和无主(p2p或者no-leader)结构两类,我们先聚焦在leader-follower这种结构中,看一下这种结构下的分布式系统如何保障一致性。

一致性问题的产生

首先,产生不一致的情况一定意味着数据存在多个副本,换句话说,如果系统中不存在副本,则不会有一致性问题。但对于分布式系统来说,副本是必不可少的,因为:

  1. 副本大大提高了系统的可用性,使得单个节点不可用不会引起整体系统瘫痪
  2. 副本能够提高系统查询性能,不同的数据请求可以由不同节点来提供服务

因此,副本的出现看来是无法避免的,那么是否可能在有副本的情况下还能保证不同副本上的数据在每时每刻都能够保持一致呢?个人认为严格意义上完美的一致性是无法达成的,因为这本质上是要求N个副本通过不可靠的网络通信组成一个大的单机系统,但这单台机器却很容易故障(至少比单副本扩大了N倍),一旦其中一个节点故障或网络波动就会导致整体完全不可用,这与引入副本的目的是矛盾的。鱼与熊掌不可兼得,根据业务场景不同,一致性与可用性间总是存在取舍

一致性与可用性的平衡

我们的分布式系统的一致性底线是需要保证最终一致性。最终一致性是最弱的一种一致性,它表示在一定时间内系统的数据总会最终达成一致,如果无法满足最终一致性,那么这个系统是不可信的。想要达到最终一致性只需要leader在进行数据操作后,异步通知到每个follower,并进行失败重试即可。我们已经知道,当我们希望得到更强的一致性时,一定会牺牲一部分可用性,这是一种必然的取舍,因此我们期望达成的目标是,结合业务场景,使得一致性在能接受的范围内,不严格追求所有节点数据强一致,以换取尽可能高的可用性。

场景一

用户在完成写入操作后,立即进行读取时,希望能读取到刚刚写入完成的数据。
这个时候,最终一致性已经无法满足需求,因为用户的写操作是在leader上进行的,而读操作可能会在任意一个节点进行,而follwer和leader之间只是异步进行数据同步,无法保证在用户写后读时,每个follower都已经从leader获取到了最新数据。当然,如果在数据写入时不是异步通知到follower,而是同步等待follower全部更新成功,那确实可以解决这个问题,但这样的一致性太强了,使得可用性得不到保障。如果只是需要满足这种场景的一致性需求,其实也有其他办法。比如可以对操作归属的用户进行划分,如果查询的数据最新的操作是属于自己的,则访问leader,否则可以访问follower。满足这种场景的一致性也叫读写一致性

场景二

假设你正在看一场足球比赛,比赛很激烈,你不断地刷新比分页面查看是否有进球,这个场景下,刷新实时比分获取的数据必须比前一次刷新得到的结果版本更新,如果第一次看到的比分已经到1-0,下一次刷新比分又回到了0-0,显然会让人很困惑。
这种情况下,因为用户的读请求是随机由follower或leader中的任意节点来响应的,不同节点相对于leader的数据版本的延迟都可能有差异,所以自然可以想到,如果对用户进行hash,使得每个用户的请求都会由同一个节点负责,那这个问题就解决了。满足这种场景的一致性也叫单调读一致性。但是,如果难以根据用户对请求做划分,或者用户本身分布不均衡,那这种方案就不是很理想了。这个时候,我们可以由leader节点来对每个数据操作生成全局有序递增的日志序列号(你可能会想是否可以用系统时间戳,但集群中每个节点的时钟可能并不能时刻保持同步,所以并不是一个很安全的方案),这样,当用户在请求数据时可以限定本次服务节点的最新日志序列号必须大于或等于上次访问到的节点的最新日志,否则可以等待或者重新请求其他节点,采用这种方案的系统其实已经达成了一个更强的一致性:顺序一致性

场景三

同样以足球比赛为例,假设你和朋友同时在一个房间里通过各自的手机来查看比分,突然你朋友告诉你进球了,比分变成了1-0,但你在手机上刷新后,看到的比分依然是0-0,你一定会感觉很奇怪(注意,虽然过段时间后,你的手机上也能查到1-0的比分,但存在这样一段时间,你查询到的数据版本落后于其他用户查询到的数据版本)。
这种场景下,顺序一致性也无法满足需求了,我们需要更强的一致性保障。如同前一个场景一样,我们需要一个全局有序的日志序列号,每次查询时向leader获取当前已提交的最新日志序号,在请求节点时限定当前节点已生效的数据所采纳的日志序号必须大于或等于leader处获取到的已提交日志序号,这样,我们可以确保,在之后的每次查询只会查询到更新版本的数据,而不会得到一个过期版本的数据。有一个前提是,我们需要确信此时的leader仍然是有效的leader(避免比如正好刚发生网络分区的情况),为此leader节点可能需要发送额外的心跳给所有follower来做确认。当然,这么做以后,leader节点的压力会增大,且能够提供查询服务的节点也受限,我们其实牺牲了一定的可用性来换取了更高的一致性。采用这种方法,我们达成了一个分布式系统中最强的一致性:线性一致性

小结

在主从结构下,由于leader节点作为唯一入口管理数据修改,因此所有的操作都可以在leader节点上进行有序排列,形成全序,follower节点只要保证能按照顺序跟踪执行leader上的操作日志即可,不同的一致性只是在查询时对follower节点的数据版本跟踪进度情况做限制。越强的一致性要求跟踪延迟越小,能够达到要求的follower节点也就越少,可用性自然就会受到影响,但其实根据业务场景的不同,可能并不需要很强的一致性,没有完美的分布式系统,适合自己的就是最好的。

posted @ 2021-04-16 16:23  eedbaa  阅读(275)  评论(0编辑  收藏  举报