我有99个问题,一个分布式数据库不是一个

2020-11-01 01:02:12

您的公司正在稳步增长,您的数据也在稳步增长。每天,您都可以从数据驱动的决策中获益,并享受从这个强大的数据库中获得的所有洞察力。尽管前面有很多令人兴奋的事情,但您还是不禁想知道,这种单一的存储设备--这把忠实的瑞士刀--还能持续支持您的业务需求多久。

也许您已经开始注意到某些工作负载的延迟增加,甚至由于此单点故障而面临停机。您还知道不能在单个物理数据库上存储无限数量的数据,但是您不打算停止收集这些数据。

在一个美好的日子里,您决定是时候购买一个新的、闪亮的分布式数据库系统(DDBS)了,然后您提出了以下需求列表:

啊哦…。我可能有个坏消息要告诉你。坏消息是,您永远找不到可以同时成功优化所有这些属性的分布式数据库。另一方面,通过了解分布式系统的限制以及您自己的数据工作负载的特殊性,您一定会找到适合您需要的折衷方案。

让我们借助于2个定理,从研究分布式系统的固有局限性开始。

如果你和我一样,阅读这个定理可能不会给你太多启发。为了更好地理解它对分布式系统设计的影响,让我们首先对凡人的3个属性进行重新表述。

这里我们有一个由4个节点组成的简单的DDBS。用户可以通过某些接口向系统发送查询,该接口将调用委托给任何数据库节点。我们还可以看到,此系统正面临网络分区-此临时故障会阻止分区两端的节点相互通信,因此节点A与其他节点之间的数据同步是不可能的。另一方面,节点A仍然可以从界面访问,因此用户仍然可以从它查询和更新数据。下面是它变得(稍微)有趣的地方:

虽然某些节点之间的数据同步是不可能的,但如果您希望您的系统保持高度一致,则不能允许写入分区的一侧并从另一侧进行查询,因为您最终可能会得到同一数据点的不同版本。这意味着您需要使分区的一侧不可用,而只向分区的另一侧发送读写查询。在这种情况下,为了保持高度一致,您的系统丧失了高可用性。

虽然某些节点之间的数据同步是不可能的,但如果您希望系统高度可用,从而不必丢弃请求,则需要允许对分区两端进行读写。这可能会导致响应不一致,具体取决于分区的哪一侧处理请求。为了实现高可用性,您的系统失去了很强的一致性。

现在,由于各种原因,“三分之二”的说法具有误导性(布鲁尔博士在他12年后的后续论文“CAP:”规则“如何改变”中承认了这一点)。如果您的分布式系统在网络分区期间继续运行,则默认情况下您具有“P”属性(万岁)。在设计您的系统时,您肯定希望它在这样的情况下工作,因此您只能在“C”和“A”之间做出真正的选择。

需要注意的另一件有用的事情是这种一致性与可用性权衡的不对称性。强一致性属性是您可以保证的,从这个意义上说,您可以将系统设计为始终保持强一致性。相反,您的系统永远不能保证高可用性。此属性通常以“9的数量”来衡量正常运行时间,任何系统都不能保证100%的正常运行时间。

最后,CAP定理只处理面向网络分区的分布式系统,而没有说明正常情况下的权衡。考虑到AWS、GCP和Azure等云提供商的流行,以及由于我们不再在祖母的地下室托管东西,网络分区变得不那么频繁,正常制度下的权衡应该是我们系统设计的主要驱动因素。

这就引出了PACELC定理,它是CAP的扩展,也说明了在没有网络分区时的必要权衡。

足够有趣的是,PACELC定理似乎没有引起注意,尽管它比CAP更实用,并且对分布式系统的限制提供了更一般的见解。

我们可以这样总结:在网络分区下,分布式数据库系统需要在可用性和一致性(基于CAP)之间做出选择。否则(在正常情况下),它需要在延迟和一致性之间找到折衷。

在“现代分布式数据库系统设计中的一致性权衡”一文中,作者丹尼尔·J·阿巴迪这样解释:

一旦DDBS复制数据,就会在一致性和延迟之间进行权衡。这是因为实现数据复制只有三种替代方案:系统同时向所有副本发送数据更新,首先向商定的主节点发送数据更新,或者首先向单个(任意)节点发送数据更新。系统可以以各种方式实现其中的每一种情况;但是,每种实现都有一致性/延迟权衡。

John通过向DDBS发送写请求将其用户名从Master Pain更改为Betty(Kung Pow Reference,欢迎使用),由Node A处理。

提醒:如果您希望您的系统具有很强的一致性,这意味着用户检索John用户名的所有后续请求都应该返回Betty,即使Node B、C或D处理这些请求也是如此。

为了实现这一点,一种可能是在节点B、C和D完全同步之前,仅让节点A服务于后续的读请求。这将给物理上远离节点A的用户带来额外的延迟。

另一种可能是等待节点B、C和D与节点A完全同步后,再让任何人查询John的用户名。在这种情况下,还会产生额外的延迟。

前面两个选项的混合也是可能的:法定协议(参见Paxos或RAFT)可以确保在允许读取John的用户名之前已经更新了一定数量的副本。在这种情况下,延迟是协议固有的,同步和异步副本更新之间的权衡仍然有效。

当然,如果相反,您希望您的系统尽可能低延迟,那么您就会非常直观地看到,您不能在允许后续读取请求之前等待所有副本同步更新。您当然可以通过不等待完全同步来提高写入吞吐量,但您也会失去数据一致性的保证。如果节点A负责更新John的用户名,但您的系统不等待其他节点更新后才允许读取此记录,则一些快速请求他的用户名的用户可能会得到Master Pain,而另一些用户可能会得到Betty。

简而言之,这个定理的主要结论是,在正常情况下,您的DDBS既可以是高可用性的,也可以是强一致性的,但是追求更多的一致性必然会导致延迟的增加。这就是很好地了解您的业务要求以及数据工作负载的性质将帮助您选择正确的延迟/一致性权衡的地方。也就是说,让我们更深入地研究这个一致性属性。

延迟/一致性权衡不仅限于分布式数据库系统。事实上,许多事务性数据库(如PostgreSQL和MySQL)也是以这种权衡为核心进行设计的,它们提供了直接影响这种权衡的可配置隔离级别。

这里有一个刁钻的问题来介绍不同的隔离级别。假设您有以下用户表:

然后运行此事务,选择所有用户名,然后等待1分钟,并在返回结果之前重新选择它们:

另一个开始将McSwinster重命名为Bigshot,但在初始查询返回之前没有时间提交,并且。

最后,我要问您一个价值一百万美元的问题:我们能从上面的SELECT请求中期待什么结果呢?答案是…。这取决于您为数据库设置的隔离级别!!1!

基本上,隔离级别决定了事务在多大程度上对外部世界可见。更强的隔离意味着更强的数据完整性,但也意味着更高的延迟。较弱的隔离意味着更高的吞吐量,但您很容易受到异常和不一致的影响。更具体地说,脏读(可以查询未提交的数据)、不可重复读(事务期间查询的行发生变化)和幻影读(当事务读取记录时,可以同时添加或删除影响结果的新行)。

这相当于“没有隔离”。未提交读意味着读取或写入表不会锁定您正在与之交互的记录。并发写请求可能会覆盖您当前尝试更新的行,并且您可以读取已更新但尚未提交的“脏”行。您可以猜测,完全不支持隔离事务意味着并发查询不必等待锁,从而优化了延迟。另一方面,此隔离级别容易出现脏读、不可重复读和幻影读,这使得在应用程序级别需要处理更多内容。关于上面的刁钻的问题,配置了这个隔离级别的系统会返回[Sally,Bigshot,LucieLajoie]。

通过在更新期间锁定行,“已提交读”比“未提交读”提供了更多的数据一致性。这通过仅允许对已提交的记录进行读取来防止脏读取。另一方面,读请求不会锁定所涉及的数据项,因此这样的系统容易出现不可重复读取和幻影读取。在我们的场景中,具有此隔离级别的数据库将返回[Sally,McSwinster,LucieLajoie]。例如,这是PostgreSQL和Oracle数据库的默认隔离级别。

读取和写入请求都会获得锁定。这是对READ COMMITTED进行的更一致的升级,不允许更新并发事务正在读取的记录。此隔离级别仅易于进行幻影读取,因为在您的SELECT查询正在进行时,仍然可以添加记录。在我们的场景中,以这种方式配置的数据库将返回[Betty,McSwinster,LucieLajoie]。例如,这是MySQL的默认隔离级别。

记录在读写查询以及访问结构(如索引)期间被锁定。这可以防止在针对匹配记录的并发读取查询正在进行时添加记录(就像我们在读取尚未返回时添加LucieLajoie一样)。然后,具有该隔离级别的数据库不会出现脏读、不可重复读和幻象读,但会继续授予锁,就像没有明天一样,这会对延迟产生负面影响。在我们的场景中,此隔离级别将生成以下结果集:[Betty,McSwinster]。例如,这是VoltDB的默认隔离级别。

许多用例可以证明使用这4个隔离级别是合理的。同样,您的最佳选择直接取决于您的业务区域、服务级别协议和工作负载。

现在,我们已经更好地理解了一致性、可用性和延迟是如何相互关联的,让我们来看一个简单的案例,在这个案例中,我们希望使用本机复制从单一的PostgreSQL数据库转变为分布式数据库。尽管它是我能选择的最简单的DDBS示例之一,但它是一个非常常见的示例,它很好地说明了无法同时优化所有属性,这适用于所有分布式系统。

假设您有一个PostgreSQL数据库,它的故障将导致整个应用程序不可用。这种想法肯定会让您睡不着觉,但幸运的是,您知道PostgreSQL支持使用本地流复制将写事务同步到其他数据库。简而言之,所有写请求都会转到一个名为主数据库的数据库,并且通过流式传输预写日志记录,将这些事务重放到其他只读副本。

另一方面-我希望这一点开始变得越来越明显-你不牺牲其他东西就得不到这些好处!根据您选择将数据从主服务器复制到复制副本的方式,您可能会增加写入延迟,也可能会丧失很强的一致性:

同步复制:仅当写入事务已复制到所有副本时,才将其视为完成。这肯定会增加您的写入延迟,因为您的系统变得与最慢的节点一样慢。请注意,PostgreSQL支持两种类型的同步复制,每种类型都有其延迟/一致性权衡。

异步复制:只要主数据库认为事务已经完成,您就可以写入事务,而不会对副本上该数据项的状态提供任何保证。这将使您的系统最终保持一致,因为您不能保证所有副本都将返回最新版本的数据。

如果您正在寻找低写入延迟或更大的存储容量,并且仍然希望使用PostgreSQL,那么这个问题没有本机解决方案,但是您可能希望检查可用的PostgreSQL扩展,如CUTUS。再一次,你将不得不选择你的毒药,因为其他的权衡已经近在眼前。

话虽如此,你问我最好的配置是什么?嗯,这完全取决于您的特定用例,所以您来告诉我!好的一面是,您现在知道了这样的分布式系统会带来什么,当针对一个属性进行优化会对其他一些属性产生负面影响时,您不会感到意外。

生命是短暂的,我还有很多其他的话题想在这里讨论。我首先想阐明分布式系统核心的一致性、可用性和延迟权衡,因为我觉得这对许多软件工程师来说并不是常识。另一方面,数据库营销者似乎完全搁置了这些权衡。无论是Aurora、RedShift、DynamoDB、Spanner还是FaunaDB,它们的文档很少说明其设计中隐含的必要权衡。例如,要确定在Amazon RDS中使用多AZ部署(跨可用区同步复制数据)会对您的写入吞吐量产生负面影响,您确实需要在他们的文档中积极查找一句话。

一旦很好地理解了这一点,并考虑到您想要进一步了解DDBS的主题,您接下来应该寻找什么?