直到最近,在Odnoklassniki中,SQL Server中大约存储了50 TB的实时数据。 对于如此大的卷,几乎不可能使用SQL DBMS提供快速,可靠甚至故障安全的数据中心访问。 通常,在这种情况下,它们使用NoSQL存储库之一,但并非所有内容都可以转移到NoSQL:某些实体需要ACID事务的保证。
这导致我们使用NewSQL存储,即DBMS,它提供NoSQL系统的容错性,可伸缩性和性能,但同时保留了传统系统熟悉的ACID保证。 在这一新的类别中,几乎没有可用的工业系统,因此我们自己实施了这种系统,并将其投入商业运营。
它如何工作以及发生了什么-请仔细阅读。
如今,Odnoklassniki的每月访问者超过了7000万。 我们是
全球五个最大的社交网络之一,也是用户花费时间最多的二十个站点。 基础结构“ OK”处理非常高的负载:每秒有超过一百万个HTTP请求。 服务器群中的8000多个部分彼此靠近-位于四个莫斯科数据中心中,这使得它们之间的网络延迟小于1毫秒。
自2010年起,我们就开始使用Cassandra(版本0.6)。 今天,有几十个集群在运行。 最快的群集每秒处理超过400万次操作,最大的存储库为260 TB。
但是,所有这些都是用于存储
弱一致性数据的普通NoSQL群集。 但是我们想替换主要的一致存储Microsoft SQL Server,该存储自Odnoklassniki成立以来就一直使用。 该存储由300多台SQL Server Standard Edition计算机组成,其中包含50 TB的数据-业务实体。 此数据被修改为ACID事务的一部分,并且要求
高度一致性 。
为了在SQL Server节点之间分配数据,我们使用了垂直
分区和水平
分区 (分片)。 从历史上看,我们使用一种简单的数据分片方案:每个实体都与一个令牌关联-令牌是该实体ID的函数。 具有相同令牌的实体放置在同一SQL服务器上。 实现了主从类型关系,以便主记录和生成记录的令牌始终重合并且位于同一服务器上。 在社交网络中,几乎所有记录都是代表用户生成的-这意味着一个功能子系统内的所有用户数据都存储在一个服务器上。 也就是说,一台SQL Server的表几乎总是参与业务事务,这使得使用本地ACID事务来确保数据一致性成为可能,而无需进行
缓慢且不
可靠的分布式ACID事务。
多亏了分片并加快了SQL的速度:
- 我们不使用外键约束,因为在分片时,实体ID可以在另一台服务器上。
- 由于DBMS CPU的额外负载,我们不使用存储过程和触发器。
- 由于以上所有原因以及从磁盘进行的许多随机读取,因此我们不使用JOIN。
- 在事务外部,为减少死锁,我们使用“读取未提交”隔离级别。
- 我们仅执行短交易(平均短于100毫秒)。
- 由于大量死锁,我们不使用多行UPDATE和DELETE-我们仅更新一条记录。
- 我们始终只按索引执行查询-对我们来说,计划进行全表扫描的查询意味着数据库过载及其故障。
这些步骤使得从SQL服务器中挤出几乎最大的性能成为可能。 然而,问题变得越来越多。 让我们看看它们。
SQL问题
- 由于我们使用专有的分片,因此管理员可以手动添加新的分片。 一直以来,可伸缩的数据副本不满足请求。
- 随着表中记录数量的增加,插入和修改速度会降低,将索引添加到现有表中时,速度会下降多次,创建和重新创建索引会伴随停机时间。
- 生产中的SQL Server Windows很少,使管理基础架构变得困难
但是主要的问题是
容错能力
经典SQL Server具有较差的容错能力。 假设您只有一台数据库服务器,并且每三年发生一次故障。 目前,该网站无法使用20分钟,这是可以接受的。 如果您有64台服务器,则该站点每三周不工作一次。 如果您有200台服务器,则该站点每周都不工作。 这是一个问题。
如何提高SQL Server的弹性? 维基百科为我们构建了一个
高度可访问的集群 :如果其中任何组件发生故障,都将有一个重复的
集群 。
这需要大量昂贵的设备:多个冗余,光纤,共享存储以及保留的包含不能可靠地工作:大约10%的包含由于主节点后面的引擎的备份节点而失败。
但是,这种高度可访问的群集的主要缺点是在其所在的数据中心发生故障的情况下零可用性。 Odnoklassniki有四个数据中心,我们需要在其中一个发生完全事故的情况下提供工作。
为此,您可以使用SQL Server内置的
多主复制。 由于软件成本,该解决方案的成本要高得多,并且会遭受复制方面的众所周知的问题-同步复制期间发生不可预知的事务延迟,异步期间导致复制使用的延迟(以及因此丢失修改)。 隐含的
手动解决冲突使此选项对我们完全不适用。
所有这些问题都需要一个根本解决方案,我们对它们进行了详细分析。 在这里,我们需要熟悉SQL Server的基本功能-事务。
简单交易
从应用SQL程序员的角度考虑,最简单的事务是:将照片添加到相册。 相册和照片存储在不同的盘子中。 相册有一个公共照相馆。 然后,将这样的事务分为以下步骤:
- 我们通过钥匙锁定相册。
- 在照片表中创建一个条目。
- 如果照片具有公开状态,那么我们会关闭相册中的公开照片柜台,更新记录并进行交易。
或采用伪代码的形式:
TX.start("Albums", id); Album album = albums.lock(id); Photo photo = photos.create(…); if (photo.status == PUBLIC ) { album.incPublicPhotosCount(); } album.update(); TX.commit();
我们看到最常见的业务交易场景是将数据库中的数据读取到应用程序服务器的内存中,进行一些更改,然后将新值保存回数据库中。 通常,在这样的事务中,我们更新几个实体,几个表。
执行交易时,可能会发生来自另一个系统的相同数据的竞争性修改。 例如,反垃圾邮件可能会判定用户可疑,因此该用户的所有照片都不再公开,应该将其发送以进行审核,这意味着将photo.status更改为其他值并拧松相应的计数器。 显然,如果进行此操作时没有保证应用程序的原子性和竞争性修改的隔离(如
ACID中那样) ,那么结果将不是所需的-光电计数器显示错误的值,或者不是所有的照片都将发送以进行审核。
在整个Odnoklassniki存在期间,有很多类似的代码可以在一个事务的框架内操纵各种业务实体。 从迁移到具有
最终一致性的 NoSQL的经验
,我们知道最大的困难(和时间成本)是开发旨在保持数据一致性的代码的需要。 因此,我们考虑了为新存储库提供真正的逻辑ACID事务以用于应用程序逻辑的主要要求。
其他同样重要的要求是:
- 如果数据中心发生故障,则对新存储的读写都应该可用。
- 保持当前的发展速度。 也就是说,在使用新存储库时,代码量应大致相同,无需向存储库添加内容,开发用于解决冲突的算法,维护二级索引等。
- 在读取数据和处理事务时,新存储的速度应足够高,这实际上意味着学术上严格,通用但缓慢的解决方案(例如两阶段提交)不适用 。
- 即时自动缩放。
- 使用普通的廉价服务器,无需购买稀有的铁。
- 由公司开发人员开发存储的可能性。 换句话说,优先考虑他们自己的或基于开源的解决方案,最好使用Java。
决定,决定
通过分析可能的解决方案,我们得出了两种可能的体系结构选择:
首先是采用任何SQL Server并实施必要的容错能力,缩放机制,故障转移群集,冲突解决方案以及分布式,可靠和快速的ACID事务。 我们将此选项评为非常重要且耗时的。
第二种选择是采用现成的NoSQL存储库,该存储库具有已实现的扩展,故障转移群集,冲突解决方案,并自己实现事务和SQL。 乍一看,即使执行SQL的任务,更不用说ACID事务,看起来也像是多年的任务。 但是后来我们意识到,在实践中使用的SQL功能集与ANSI SQL相距甚远,而
Cassandra CQL与ANSI SQL相距甚远。 仔细研究CQL,我们意识到它已经足够接近我们的需求了。
卡桑德拉(Cassandra)和CQL
那么,Cassandra的有趣之处在于,它具有什么功能?
首先,在这里您可以创建支持各种数据类型的表,可以在主键上执行SELECT或UPDATE。
CREATE TABLE photos (id bigint KEY, owner bigint,…); SELECT * FROM photos WHERE id=?; UPDATE photos SET … WHERE id=?;
为了确保一致的副本数据,Cassandra使用
定额方法 。 在最简单的情况下,这意味着当将同一行的三个副本放置在群集的不同节点上时,如果大多数节点(即三分之二)确认此写操作成功,则记录被视为成功。 如果在读取时对大多数节点进行了询问并确认了它们,则认为该系列的数据是一致的。 因此,由于存在三个副本,因此在一个节点发生故障的情况下,可以保证完整和即时的数据一致性。 这种方法使我们能够实现一个更可靠的方案:始终将请求发送到所有三个副本,等待两个副本的最快答复。 然后,将丢弃第三副本的较晚响应。 答案迟到的节点可能会遇到严重问题-刹车,JVM中的垃圾回收,Linux内核中的直接内存回收,硬件故障,与网络断开连接。 但是,这不会影响客户的操作或数据。
当我们转向三个节点并从两个节点中得到答案时,这种方法称为
推测 :在“掉线”之前就发出了额外的评论请求。
Cassandra的另一个优点是Batchlog-一种机制,可以保证您所做更改包的完全应用或完全不应用。 这使我们能够立即解决ACID中的A-原子性。
在Cassandra中最接近事务的是所谓的“
轻量级事务 ”。 但是它们远非“真正的” ACID交易:实际上,这是一个机会,可以使用关于繁重协议Paxos的共识,仅对一条记录的数据进行
CAS 。 因此,这种交易的速度很慢。
我们在卡桑德拉错过的一切
因此,我们必须在Cassandra中实现真正的ACID交易。 使用它,我们可以轻松实现经典DBMS的其他两个便捷功能:一致的快速索引,这将使我们不仅可以对主键和通常的单调自动增量ID生成器执行数据采样。
C *一
因此,新的
C * One DBMS诞生了,它由三种类型的服务器节点组成:
- 存储-(几乎)标准的Cassandra服务器,用于将数据存储在本地驱动器上。 随着负载和数据量的增长,它们的数量可以轻松扩展到数十或数百。
- 事务协调器-启用事务执行。
- 客户端是实现业务操作并启动事务的应用程序服务器。 可能有成千上万的此类客户。

所有类型的服务器都在一个公共群集中,使用内部的Cassandra消息协议相互通信,并使用
八卦交换群集信息。 在Heartbeat的帮助下,服务器了解相互故障,支持单一数据方案-表,其结构和复制; 分区方案,集群拓扑等。
客户群

代替标准驱动程序,使用胖客户端模式。 这样的节点不存储数据,但是可以充当查询执行的协调器,也就是说,客户端本身执行其请求的协调器功能:它轮询副本存储库并解决冲突。 这不仅比需要与远程协调器进行通信的标准驱动程序更可靠,更快,而且还使您可以控制请求的传输。 在客户端上打开的事务之外,请求将发送到存储。 如果客户打开了交易,则交易中的所有请求都将发送到交易协调器。

C *一名交易协调员
协调器是我们从零开始为C * One实现的。 他负责管理事务,锁以及事务的应用顺序。
对于正在处理的每个事务,协调器都会生成一个时间戳:每个后续事务都大于前一个事务。 由于Cassandra中的冲突解决系统基于时间戳(两个冲突记录中的一个,因此认为具有最新时间戳的当前记录是相关的),因此始终会解决冲突,以利于后续事务。 因此,我们实现了
Lamport手表 -一种解决分布式系统中冲突的廉价方法。
锁具
为了确保隔离,我们决定使用最简单的方法-记录主键上的悲观锁。 换句话说,在事务中,必须首先锁定记录,然后才可以读取,修改和保存记录。 只有成功提交后,才能解锁记录,以便竞争事务可以使用它。
在未分配的环境中,实现此锁定很简单。 分布式系统中有两种主要方法:要么在集群上实现分布式锁定,要么分布式事务,以便涉及单个记录的事务始终由同一协调器提供服务。
由于在我们的案例中,数据已经由SQL中的本地事务组分发,因此决定将本地事务组分配给协调器:一个协调器执行所有事务的令牌从0到9,第二个事务令牌的令牌从10到19,依此类推。 结果,每个协调器实例都成为事务组主服务器。
然后,可以将这些锁实现为协调器内存中的普通HashMap。
协调器故障
由于一个协调员专门为一组事务服务,因此快速确定其失败的事实非常重要,这样一来,重复执行事务的尝试将超时。 为了使其快速可靠,我们应用了完全连接的定额听音协议:
每个数据中心至少有两个协调器节点。 每个协调器会定期向其他协调器发送心跳消息,并向他们通知其功能以及上一次集群中的协调器发出的心跳消息。

在协调原则的指导下,其他协调员已经从其他人那里收到了类似的信息,因此,每个协调员都会自己决定哪个集群节点起作用,哪些不起作用:如果节点X从集群中的大多数节点接收了有关从节点Y正常接收消息的信息,则,Y有效。 相反,一旦大多数人报告了来自节点Y的消息丢失,则Y已失败。 奇怪的是,如果仲裁告诉节点X它没有收到来自它的更多消息,那么节点X本身将认为自己已失败。
心跳消息以每秒大约20次的高频率发送,周期为50毫秒。 在Java中,由于垃圾收集器导致的暂停时间相当长,因此难以保证50 ms的应用程序响应。 使用G1垃圾收集器,我们能够实现这样的响应时间,这使我们可以指定GC暂停持续时间的目标。 但是,有时(很少)收集器的暂停时间超过50毫秒,这可能导致错误地检测到故障。 为了避免这种情况,当第一个心跳消息从远程节点消失时,协调器不会报告该远程节点的故障,只有几个连续消失的情况下,因此我们设法在200毫秒内检测到该协调器的节点故障。
但是,仅快速了解哪个节点已停止运行还不够。 您需要对此做些事情。
订座
在主人拒绝的情况下,经典方案假定使用一种
流行的 通用算法来开始选举一个新的方案。 但是,这种算法在时间收敛和选举过程本身的持续时间方面存在众所周知的问题。 我们设法在完全连接的网络中使用协调器的等效电路来避免此类额外的延迟:

假设我们要在组50中执行事务。我们将预先确定替代方案,即在主协调器发生故障的情况下哪些节点将执行组50的事务。 我们的目标是在数据中心发生故障时保持系统正常运行。 我们确定第一个保留区将是另一个数据中心的节点,第二个保留区将是第三个数据中心的节点。 该方案仅被选择一次,并且直到集群拓扑更改(即,新节点进入该方案(这种情况很少发生))时才会更改。 在旧的主机发生故障的情况下,选择新的活动主机的过程始终是这样的:第一个备用主机将成为活动主机,如果它已停止运行,则第二个主机将成为主机。
这种方案比通用算法更可靠,因为激活一个新的主机就足以确定旧主机的故障事实。
但是,客户将如何了解哪个大师正在工作? 在50毫秒内,无法将信息发送给成千上万的客户。 当客户端发送请求以打开事务而又不知道此向导不再起作用,并且该请求将在超时时挂起的情况是可能的。 为了防止这种情况的发生,客户推测性地发送了一个请求,要求立即向组主及其两个储备金开立交易,但是目前只有活动主服务器的人会回答该请求。 客户端将仅与活动主服务器进行事务内的所有后续通信。
备份主数据库在未出生的事务队列中接收对非自己事务的请求,这些请求在其中存储了一段时间。 如果活动主服务器去世,则新的主服务器从其队列中处理打开交易的请求,并响应客户端。 如果客户端已经设法与旧的主服务器打开事务,则第二个响应将被忽略(显然,这样的事务将不会完成,并且将由客户端重复)。
交易如何运作
假设客户向协调员发送了一个请求,要求使用这种主键为该实体打开交易。 协调器锁定该实体,并将其放置在内存中的锁定表中。 如有必要,协调器将从存储中读取该实体,并将接收到的数据以事务状态存储在协调器的内存中。

当客户想要更改事务中的数据时,他向协调器发送更新实体的请求,并将新数据放入内存中的事务状态表中。 这样就完成了录制-在存储库中不执行录制。

当客户在活动事务的框架内请求自己更改的数据时,协调器的行为如下:
- 如果该ID已经在交易中,则从存储器中获取数据;
- 如果内存中没有ID,则将从存储节点中读取丢失的数据,并将其与内存中已存储的数据结合起来,并将结果返回给客户端。
因此,客户端可以读取自己的更改,而其他客户端则看不到这些更改,因为它们仅存储在协调器的内存中,而尚未存储在Cassandra节点中。

客户端发送提交时,协调器会将服务状态中的状态保存在已记录的批处理中,并且已经以已记录的批处理的形式将其发送到Cassandra存储库。 存储库执行原子(完全)应用此程序包所需的一切,并将响应返回给协调器,协调器释放锁并向客户端确认事务成功。

并回滚到协调器,足以释放事务状态占用的内存。
通过以上改进,我们实现了ACID的原则:
- 原子性 。 这保证了不会有任何事务部分地提交给系统,它的所有子操作都将完成,或者不会执行任何一个事务。 由于Cassandra中已记录批次,因此我们遵守此原则。
- 连贯性 。 根据定义,每笔成功的交易仅捕获可接受的结果。 如果在打开事务并执行了部分操作之后,发现结果无效,那么将执行回滚。
- 隔离度 。 执行事务时,并行事务不应影响其结果。 使用协调器上的悲观锁隔离竞争事务。 对于事务外部的读取,将遵循“读取已提交”级别的隔离原则。
- 可持续性 。 无论较低级别的问题是什么(系统断电,硬件故障),成功完成的事务所做的更改都必须在恢复操作后保留。
索引阅读
拿一个简单的表:
CREATE TABLE photos ( id bigint primary key, owner bigint, modified timestamp, …)
她有一个ID(主键),所有者和更改日期。 您需要提出一个非常简单的请求-选择更改日期为“最后一天”的所有者数据。
SELECT * WHERE owner=? AND modified>?
为了使这样的查询快速进行,在传统的SQL DBMS中,您需要按列(所有者,已修改)构建索引。 我们可以很简单地完成此操作,因为现在有了ACID保证!
C的索引*一
有一个带有照片的源表,其中的记录ID是主键。

对于C *索引,One创建一个新表,该表是原始表的副本。 该键与索引表达式匹配,并且还包括源表中记录的主键:

现在,可以将“最后一天的所有者”的请求重写为另一个表中的select:
SELECT * FROM i1_test WHERE owner=? AND modified>?
原始照片表和索引i1中数据的一致性由协调器自动维护。 仅基于数据方案,当接收到更改时,协调器不仅会在主表中而且还会在副本更改中生成并记住更改。 索引表不执行任何其他操作,不读取日志,不使用锁。 也就是说,添加索引几乎不会消耗资源,并且实际上不会影响应用修改的速度。
使用ACID,我们能够“像在SQL中一样”实现索引。 它们具有一致性,可以扩展,可以快速工作,可以组合并内置到CQL查询语言中。
为了支持索引,您不需要更改应用程序代码。一切都很简单,就像在SQL中一样。最重要的是,索引不会影响对原始事务表的修改的执行速度。发生什么事了
我们三年前开发了C *,并将其投入商业运营。我们到底得到了什么?让我们以处理和存储照片的子系统为例对此进行评估,照片是社交网络中最重要的数据类型之一。这与照片本身无关,而与各种元信息有关。现在在Odnoklassniki中,大约有200亿条这样的记录,该系统每秒处理8万个读取请求,每秒与数据修改相关的ACID事务多达8000个。当我们使用复制因子= 1的SQL(但在RAID 10中)时,照片元信息存储在具有Microsoft SQL Server(加上11个备份)的32台计算机的高度可访问群集中。它还分配了10台服务器来存储备份。共有50辆昂贵的汽车。同时,系统在额定负载下工作,没有储备。迁移到新系统后,我们得到了复制因子= 3-每个数据中心都有一个副本。该系统由63个Cassandra存储节点和6个协调器计算机组成,总共有69台服务器。但是这些机器要便宜得多,它们的总成本约为SQL系统成本的30%。在这种情况下,负载保持在30%。随着C * One的引入,延迟也减少了:在SQL中,写操作花费了大约4.5毫秒。在C中* *-约1.6毫秒。事务持续时间平均少于40毫秒,落实在2毫秒内完成,读写持续时间平均2毫秒。第99个百分点-仅3-3.1 ms,超时次数减少了100倍-所有这些都是由于推测的广泛使用。迄今为止,大多数SQL Server节点已经退役;仅使用C * One开发新产品。我们将C * One调整为可在单云环境中工作,这使我们能够加快新集群的部署,简化配置和自动化操作。没有源代码,这将变得更加困难和困难。现在,我们正在努力将其他存储设施转移到云中-但这是一个完全不同的故事。