原文地址 Read Consistency with Database Replicas
数据库副本中的读一致性
在Shopify, 我们一直使用数据图复制进行冗余及回复,但是最近我们开始使用数据库副本作为只读库(读写分离)。可以缓解主库对时间敏感性的读写操作的压力。
这会有一个不可避免的问题:复制延迟。简单来说就是应用读取从库的数据时有可能会读到几秒钟或者几分钟之前的老数据。根据英勇的需要这可能并不一定是一个问题。使用从库的应用必须要接收读取到过期数据的可能性。这只是程度上的问题。当这是原子性操作时,这就会变成一个问题。
有一种情况是,当一个查询的数据是需要多个查询结果合并而成的,这时多个查询会路由到具有不同延迟程度的从库副本中,这时查询出来的数据是不可预测的。
举例来说,一个初始查询可以查到的数据在第二次查询中可能会查不到,因为路由到了一个延迟更大的副本中。很明显这种情况对用户体验非常不友好,如果这种查询数据还会应用到未来的写入操作中,会是个很大的问题。这篇文章讲述了Shopify 的数据连接管理团队是如何解决不同延迟引发的问题的。
紧凑一致性(Tight Consistency (类似 Strick consistency))
一种处理不同延迟问题的方法是执行紧凑一致性,所有副本保证已经与主库同步才可以允许其他操作。这种方案代价很高而且使得使用副本的性能优势无效。即使仍然可以降低主库的负载,但是等待复制会影响性能。
因果一致性(Causal Consistency)
还有一种考虑到的方式就是使用GTID(global transaction identifier)的因果一致性方案(已经开始实现)。主库中的每个业务都会有一个GTID,这个GTID也会被保存到数据副本中。这可以使每个请求根据GTID路由到各个从库副本中,这样我们就可以确定从库相对主库的延迟程度,理论上这并不是紧凑一致性,但是实践时可以达到同样的效果。
这种方式的主要弊端是在各个副本中必须有上报GTID给数据库代理的应用功能,这样数据库代理才可以根据GTID 来路由到指定从库副本中。最终我们决定不采用这种方式,因为我们不需要这个级别的一致性保证及这种复杂度也是没必要的。
我们的一致性解决方案(Monotonic read Consistency)
其他一致性模型必然涉及某种妥协,而我们选择了单调读一致性(monotonic read consistency)方案,连续的读请求即使不需要读到最新的数据,但也会沿着一个固定的时间线。最直接的方式就是一系列相关的读请求都路由到相同的副本数据库,这样连续的请求数据与上一个请求相比都是距离主库相同或更晚的数据。
为了简单的实现及避免没必要的开销,通史也希望避免应用必须了解数据库拓扑并且管理副本。来看一下我们是怎么做的,首先我们先看一下整个流程。
首相应用接入MySQL数据库是通过ProxySQL的代理层使用的一种hostgroups概念:对于应用来说就是一个单一的数据源,内部是一个个变换服务器池。
当一个客户端应用连接并获得身份认证后,代理会路由这一个单独请求到在hostgroup中随机的一个服务器中。(这一部分简化了内部的负载均衡等的一些算法,为了更好的讨论本文的主题)。为了提供读一致性,我们修改了ProxySQL源码中的这个服务选择算法部分。
任何应用必须提供一个 UID(unique identifier),以键值对方式拼接在所有SQL 的最前面
/* consistent_read_id: */ SELECT FROM
首先,需要对这个UID进行HASH算法得到一个integer值。我们通过计算这个integer的模来获得可用服务器中的一个。为了让应用在连续的请求中获得相同的服务器,它们必须传递相同的UID。这个解释简化了ProxySQL在权重方面的配置,以下是包含权重的这个算法的流程。
遇到的一些坑
我已经讲了这个算法,但是要想达到完美还需要解决一些问题。
第一个问题是在代码审查期间出现的,涉及到服务器在连续一致的读请求之间变得不可用的情况。如果不可用的服务器是之前选中的服务器(因此可能会再次选中),则可能出现数据不一致——这是我们的方法的内置限制。然而,即使不可用的服务器不是应该被选择的服务器,直接对可用服务器列表应用选择算法(就像ProxySQL对随机服务器选择所做的那样)也可能导致不一致,但在这种情况下是不必要的。为了解决这个问题,我们首先对主机组中已配置服务器的整个列表进行索引,然后取消所选服务器的资格,并在必要时重新选择。这样,仅当所选服务器关闭时才会影响结果,而不会影响列表中其他服务器的索引。关于这个问题在不同情况下的讨论可以在 ebrary.net 上找到。
第二个问题是在极少数情况会出现的一致性bug中发现的。ProxySQL会在初始服务器选择后在进行一次额外的负载均衡选择。举个例子,我们的目标权重是 1:1 但是实际的链接分布会偏移到 3:1,任何请求都会被强制重新路由到权重不足的服务器,我们重写了服务选择方法并去掉了额外的负载均衡方法。
目前,我们正在评估将灵活使用复制滞后测量作为可调因素的策略,我们可以使用它来修改读取一致性的方法。希望这个特性将继续吸引我们的应用程序开发人员,并为每个人提高数据库性能。
我们的解决方案的有点事简单并且低开销。它的主要缺点是服务停机()所产生的一致性问题难以检测。If your application is tolerant of occasional failures in read consistency,这个解决方案应该非常适合你。如果你需要更严格的一致性方案,GTID 因果一致性方案应该值得去探索。更详细的内容可以在这里查看 Adaptive query routing based on GTID tracking。