本文属于专栏《构建工业级QPS百万级服务》
继续上篇《QPS百万级的有状态服务实践》03 - 消息队列。目前我们的系统如图1,已经可以完成数据生产和更新。但是目前我们的业务是分布式集群,每台机器收到的的消息时间不一样,那每个机器的数据版本在更新数据期间,就可能不一样。那在服务数据升级时,请求被分到不同机器上,那就会导致请求计算使用的数据版本来不一致,可能会影响用户体验。
图1
那如何保证用户请求返回的一致性呢。首先,没有观察者和被观察者就没有一致性这个说法,因为从客观的角度来看,消息一定不会同一时间到达,所以一定会有一段时间,服务的数据版本不一致。除了我们的系统,任何分布式系统都一样,客观角度,在服务变更的时候,没有可能做到机器之间完全一致。这是由客观物理规律决定的,简单的说,光速是固定的,而到达机器的路径长度是不一致的。当然,量子计算机不在本文的讨论范围内。
客观的不一致是不可避免的,但是从观察者的角度来看,不同请求的一致性,还有优化的空间的。最理想的情况就是,在用户的眼中,服务集群的行为和单机一摸一样。那么我们第一个解法就自然出现了,让同一个用户的请求,始终发往同一台机器,目前大部分负载均衡的服务器都支持该功能,不过这不能完全保证一致性,因为机器可能重启或者被置换。另外这种方法,不适合用户量不大,且用户之间请求十分不均匀的情况下,可能导致数据倾斜。
在同一个用户的请求必须分布到不同机器的时候,那服务返回的数据不一致将不可避免。但是客户端如果也在我们的控制范围内,事情似乎也变得简单,我们在客户端记录上次获取的结果使用的数据版本,如果此次请求返回的版本比上次还小,就重试,直到得到获取到的结果使用的数据版本大于等于上次,这个方法并不是没有代价,我是用了部分请求的延迟,换了一致性。正如这个方案表现的这样,系统设计中没有银弹,只有取舍。但是上面这个办法还不够完美,因为客户端重启甚至重装的时候,上次请求的版本就丢失了,而上一次请求已经用了新版本的数据,虽然这种情况,一般不会对版本顺序一致性有高要求,但是如果有,则客户端记录就变得困难。虽然我们可以积累一小段时间的请求,如果版本都一致我们才使用,但如果用户足够多,部分请求还是可能恰好全在老版本机器上,导致重启后,请求到的结果版本反而是老的。所以客户端记录虽然能解决大部分问题,但是小部分极端情况,我们还得在服务端解决一致性问题。
服务端要保证每个用户得到的请求结果是一致的,那就是需要机器之间通信,保证任意一个机器除了用户请求时,知道上次该用户请求到的数据版本。这意味着,用户上次请求的信息,在某个地方持久化了,并且一个用户的多个请求,在不同机器不能同时执行,必须一个机器执行完了,才能执行下一个,这大大降低了请求处理的吞吐量。强一致性(线性一致性和顺序一致性)的代价就是降低了系统吞吐量。
如果系统可以容忍,短时间的版本回退,也就是对客户端来说,服务端是最终一致性,那就大大增加了系统吞吐量。当然最终一致性也会因为“最终”是指需要10秒,还是1小时,而影响系统的复杂度。
这里没有完美的方案,需要根据实际业务做取舍,对于我们的日期间隔计算系统,我的选择是系统为最终一致性,具体做法是在服务侧让用户请求尽量到一台机器,用户如果期望降低不一致的情况,自行在客户端存储版本来过滤。核心原因是,我主观地认为这个场景,保证强一致性增加的收益,远小于增加的服务器资源。但是这个观点不是永恒的,它会随着客户需求,以及资源成本的变化而变化。
通过本章,我们的架构更清晰了,我们明确了架构在一致性问题上的能力边界。虽然架构可以提供节假日查询了,但是用户如果才能查询过去的请求记录呢,我会之后的章节中分享我的经验。