在Pyrus的上一次内部集会上,我们谈到了现代分布式存储,Pyrus的首席执行官兼创始人Maxim Nalsky分享了他对FoundationDB的第一印象。 在本文中,我们讨论了在选择一种用于扩展结构化数据存储的技术时所面临的技术细微差别。
当用户一段时间无法使用该服务时,它会令人不愉快,但仍不会致命。 但是丢失客户数据绝对是不可接受的。 因此,我们会严格评估任何通过两到三个参数来存储数据的技术。
其中一些指示了服务的当前负载。
当前负载。 我们选择技术时要考虑这些指标的增长。客户端服务器架构
经典的客户端-服务器模型是分布式系统的最简单示例。 服务器是同步点;它允许多个客户端以协调的方式一起做某事。
客户端-服务器交互的非常简化的方案。客户端-服务器体系结构中不可靠的是什么? 显然,服务器可能会崩溃。 当服务器崩溃时,所有客户端都无法工作。 为了避免这种情况,人们提出了主从关系(在
政治上现在是
正确的,称为领导者跟随者 )。 最重要的是有两台服务器,所有客户端都与主服务器通信,第二台服务器上的所有数据都被简单地复制。
具有将数据复制到关注者的客户端-服务器体系结构。显然,这是一个更可靠的系统:如果主服务器崩溃,则所有数据的副本都在从机上,并且可以快速将其复制。
了解复制的工作原理非常重要。 如果它是同步的,那么事务必须同时存储在领导者和跟随者上,这可能会很慢。 如果复制是异步的,则故障转移后您可能会丢失一些数据。
如果领导者在每个人都睡觉的夜晚掉下来,会发生什么? 有关于追随者的数据,但没有人告诉他他现在是领导者,客户也没有与他建立联系。 好吧,让我们赋予跟随者一种逻辑,即当与领导者失去联系时,他开始以自己为主要对象。 然后,我们可以轻松地分裂大脑-领导者和跟随者之间的连接断开时发生冲突,并且双方都认为他们是主要的。 这确实发生在许多系统上,
例如RabbitMQ ,这是当今最流行的排队技术。
要解决这些问题,请组织自动故障转移-添加第三个服务器(见证人,见证人)。 它确保我们只有一位领导者。 而且,如果领导者下车,则跟随者会自动以最小的停机时间开启,该停机时间可以减少到几秒钟。 当然,采用这种方案的客户必须事先知道领导者和跟随者的地址,并实现他们之间自动重新连接的逻辑。
证人保证只有一名领导人。 如果领导者掉下,那么跟随者会自动打开。这样的系统现在可以与我们一起使用。 有一个主数据库,一个备用数据库,有一个见证人,是的-有时我们是早上来的,看到切换发生在晚上。
但是这种方案也有缺点。 假设您正在安装Service Pack或在领导服务器上更新OS。 在此之前,您手动切换了从动装置上的负载,然后...掉落了! 灾难,您的服务不可用。 如何保护自己免受此伤害? 添加第三个备份服务器-另一个关注者。 三是一种幻数。 如果希望系统可靠地运行,那么两台服务器是不够的,则需要三台。 一个用于维护,第二个跌倒,第三个遗留。
如果前两个服务器不可用,则第三台服务器将提供可靠的操作。总而言之,冗余度应等于2。 一个冗余是不够的。 因此,在磁盘阵列中,人们开始使用RAID6方案而不是RAID5,一次幸免了两个磁盘的损坏。
交易次数
事务的四个基本要求是众所周知的:原子性,一致性,隔离性和持久性(原子性,一致性,隔离性,耐久性-ACID)。
当我们谈论分布式数据库时,我们的意思是必须缩放数据。 读取范围非常好-数千个事务可以并行读取数据而没有任何问题。 但是,当其他事务在读取的同时写入数据时,可能会产生各种不良影响。 很容易出现一个事务将读取相同记录的不同值的情况。 这里有一些例子。
脏读。 在第一个事务中,我们两次发送相同的请求:接收所有ID = 1的用户。如果第二个事务更改了该行然后回滚,则数据库一方面看不到任何更改,但另一方面第一笔交易将读取Joe的不同年龄值。
不可重复读取。 另一种情况是,写入事务成功完成,并且读取事务在执行同一请求期间接收到不同的数据。

在第一种情况下,客户端读取数据库中通常不存在的数据。 在第二种情况下,客户端两次都从数据库读取数据,但是它们是不同的,尽管读取是在同一事务中进行的。
幻像读取是当我们重新读取同一事务中的范围并获得不同的行集时。 在中间的某个地方,另一个事务进入并插入或删除了记录。

为了避免这些不良影响,现代DBMS实施了锁定机制(一个事务将访问它当前正在使用的数据限制到其他事务)或多版本版本控制,
MVCC (一个事务从不更改以前记录的数据并始终创建一个新版本)。
ANSI / ISO SQL标准为影响其相互阻塞程度的事务定义了4个隔离级别。 隔离级别越高,不良影响越小。 这样做的代价是减慢了应用程序的速度(因为事务更多地等待解锁所需的数据)并增加了出现死锁的可能性。

对于应用程序程序员而言,最可取的是可序列化级别-不会产生不良影响,并且确保数据完整性的整个复杂性都将转移到DBMS。
让我们考虑一下可序列化级别的天真的实现-对于每个事务,我们只是阻止其他所有人。 理论上,每个写入事务都可以在50 µs(在现代SSD磁盘上执行一次写入操作的时间)内执行。 我们想将数据保存到三台计算机上,还记得吗? 如果它们位于同一数据中心,则记录将花费1-3毫秒。 而且,如果出于可靠性考虑,它们位于不同的城市,则记录可能很容易花费10到12毫秒(从莫斯科到圣彼得堡,反之亦然,网络数据包的传输时间)。 也就是说,通过序列记录的天真的实现可序列化级别,我们每秒可以执行不超过100个事务。 虽然单独的SSD可以让您每秒执行约20,000个写入操作!
结论:写事务必须并行执行,并且要扩展它们,您需要一个良好的冲突解决机制。
分片
当数据停止在一台服务器上时该怎么办? 有两种标准的缩放机制:
- 当我们仅向该服务器添加内存和磁盘时直立 。 这有其局限性-就每个处理器的内核数,处理器数和内存量而言。
- 水平,当我们使用许多机器并在它们之间分配数据时。 这种机器的集合称为集群。 要将数据放入群集,需要将它们分片-也就是说,对于每条记录,确定数据将位于哪台服务器上。
分片密钥是用于在服务器之间分配数据的参数,例如,客户端或组织标识符。
想象一下,您需要记录有关集群中地球上所有居民的数据。 作为分键,可以使用例如该人的出生年份。 届时,将有116台服务器就足够了(每年都需要添加一台新服务器)。 或者,您可以将人员居住的国家/地区作为关键,那么您将需要大约250台服务器。 不过,第一种方法还是可取的,因为该人的出生日期不会改变,并且您将永远不需要在服务器之间传输有关他的数据。

在Pyrus中,您可以将组织作为分片密钥。 但是它们的规模却大不相同:既有庞大的Sovcombank(拥有超过1.5万用户),又有数千家小型公司。 当您为组织分配特定的服务器时,您不会事先知道它将如何增长。 如果组织规模较大并积极使用该服务,则迟早其数据将不再放置在一台服务器上,您将不得不重新分片。 如果数据为TB,这并不容易。 想象一下:一个加载的系统,事务每秒进行一次,在这种情况下,您需要将数据从一个地方移到另一个地方。 您无法停止系统,这样的容量可以抽几个小时,而业务客户将无法承受如此长的停机时间。
作为分片键,最好选择很少更改的数据。 但是,应用任务远非总是那么容易做到。
集群共识
当群集中有很多计算机,而其中一些与其他计算机失去联系时,那么如何确定谁存储了最新版本的数据呢? 仅分配见证服务器是不够的,因为它还会失去与整个集群的联系。 此外,在大脑裂开的情况下,多台机器可以记录同一数据的不同版本-您需要以某种方式确定哪个是最相关的。 为了解决这个问题,人们提出了共识算法。 他们允许通过投票将几台相同的机器针对任何问题得出单个结果。 1989年,第一个这样的算法
Paxos出版了,2014年,斯坦福大学的人们想出了一个更简单的
Raft来实现。 严格来说,为了使(2N +1)个服务器群集达成共识,同时出现不超过N个故障就足够了。 要承受2个故障,群集必须至少具有5台服务器。
关系DBMS缩放
开发人员习惯于使用支持关系代数的大多数数据库。 数据存储在表中,有时您需要使用JOIN操作来连接来自不同表的数据。 考虑一个数据库示例和一个简单查询。

假定A.id是具有聚集索引的主键。 然后优化器将制定一个计划,该计划最有可能首先从表A中选择必要的记录,然后从适当的索引(A,B)中获得与表B中的记录的适当链接。此查询的执行时间从表中的记录数起对数增长。
现在,假设数据分布在集群中的四台服务器上,您需要执行相同的查询:

如果DBMS不想查看整个集群的所有记录,则它可能会尝试查找A.id等于128、129或130的记录,并从表B中找到适合它们的记录。但是如果A.id不是分片键,则DBMS会提前无法知道表A的数据在哪台服务器上。无论如何,我都必须联系所有服务器以查找是否有适合我们条件的A.id记录。 然后,每个服务器都可以在其内部进行JOIN操作,但这还不够。 您会看到,我们需要样本中节点2上的记录,但是没有A.id = 128的记录? 如果节点1和2将独立执行JOIN,则查询结果将不完整-我们将不会收到部分数据。
因此,为了满足此请求,每个服务器都必须转向其他所有人。 运行时间随着服务器数量的增加而平方增长。 (如果您可以使用相同的密钥对所有表进行分片,那么很幸运,那么您就不必遍历所有服务器。但是,实际上这是不现实的-总是存在一些查询不是基于分片密钥进行的查询。)
因此,JOIN操作从根本上扩展性很差,这是关系方法的一个基本问题。
NoSQL方法
扩展经典DBMS的困难导致人们提出了没有JOIN操作的NoSQL数据库。 没有加入-没问题。 但是没有ACID属性,但是他们没有在营销材料中提到这一点。 快速
找到的工匠将测试各种分布式系统的强度
并将结果公开发布 。 事实证明,在某些情况下,
Redis群集丢失了45%的存储数据, RabbitMQ群集- 丢失了 35%的消息 ,
MongoDB- 丢失了 9%的记录 ,
Cassandra 丢失 了5% 。 我们正在谈论
集群通知客户成功保存后的损失。 通常,您期望所选技术具有更高的可靠性。
Google开发了
Spanner数据库,该数据库在全球范围内运作。 Spanner保证ACID属性,可序列化性以及更多。 它们在数据中心中具有原子钟,可提供准确的时间,这使您可以建立全局事务顺序,而不必在各大洲之间转发网络数据包。 Spanner的想法是,对于程序员来说,处理大量事务引起的性能问题要比围绕缺乏事务的拐杖更好。 但是,Spanner是封闭式技术,如果由于某些原因您不想依赖某个供应商,则它不适合您。
Google的本地人开发了Spanner的开源类似物,并将其命名为CockroachDB(英文为“ cockroach”的“ cockroach”,应象征数据库的生存能力)。 在Habré上
已经写过有关产品无法投入生产的信息,因为集群正在丢失数据。 我们决定查看较新的2.0版,并得出了类似的结论。 我们没有丢失数据,但是一些最简单的查询的执行时间不合理。
结果,今天有关系数据库只能在垂直方向很好地扩展,这是昂贵的。 还有NoSQL解决方案,没有事务且没有ACID保证(如果您需要ACID,请写拐杖)。
如何制作数据不能在一台服务器上存储的任务关键型应用程序? 新解决方案出现在市场上,其中一个解决方案-FoundationDB-我们将在下一篇文章中向您详细介绍。