目录

1 简介

2 以可用性和规模为目标

3 A TOUR OF Megastore

4 复制

5 经验

6 相关工作

7 结论


Megastore:为交互式服务提供可扩展的高可用性存储

摘要:Megastore 是为满足当今交互式在线服务的要求而开发的存储系统。Megastore 以一种新颖的方式将 NoSQL 数据存储的可扩展性与传统关系数据库管理系统的便利性相结合,并提供了强大的一致性保证和高可用性。我们在数据的细粒度分区(fine-grained partitions of data)中提供完全可序列化的ACID语义。这种分区允许我们以合理的延迟在广域网上同步复制每个写操作,并支持数据中心之间的无缝故障转移。本文描述了 Megastore 的语义和复制算法。它还描述了我们支持使用 Megastore 构建的各种谷歌生产服务的经验。

关键词:大型数据库,分布式事务,BigTable,Paxos

1 简介

随着桌面应用程序向云迁移,交互式在线服务正迫使存储社区满足新的需求。电子邮件、协作文档和社交网络等服务一直呈指数级增长,并在测试现有基础架构的局限性。由于许多相互冲突的需求,满足这些服务的存储需求具有挑战性。

首先,互联网带来了大量的潜在用户,因此应用程序必须具有高度的可扩展性。服务可以使用 MySQL 作为其数据存储库快速构建,但要将服务扩展到数百万用户,需要对其存储基础架构进行彻底的重新设计。第二,服务必须争夺用户。这需要快速开发功能和快速上市。第三,服务必须有响应性;因此,存储系统必须具有低延迟。第四,服务应该为用户提供一致的数据视图——更新的结果应该是立即可见且持久的。看到云托管的电子表格编辑消失,无论多么短暂,都是糟糕的用户体验。最后,用户已经开始期望互联网服务全天候运行,因此服务必须高度可用。该服务必须能够应对多种故障,从单个磁盘、机器或路由器的故障一直到影响整个数据中心的大规模停机。

这些要求是冲突的。关系数据库为轻松构建应用程序提供了一套丰富的功能,但它们很难扩展到数亿用户。NoSQL 的数据存储库,如谷歌的 Bigtable 、Apache Hadoop 的 HbBase 或 Facebook  Cassandra 具有高度可扩展性,但它们有限的 API 和松散的一致性模型使应用程序开发变得复杂。在提供低延迟的同时跨远程数据中心复制数据是一项挑战,因为保证复制数据的一致视图也是一项挑战,尤其是在故障期间。

Megastore 是为满足当今交互式在线服务的存储要求而开发的存储系统。它的新颖之处在于它将 NoSQL 数据存储的可扩展性与传统关系数据库管理系统的便利性结合在一起。它使用同步复制来实现高可用性和一致的数据视图。简而言之,它在远程副本上提供了完全可序列化的ACID语义,具有足够低的延迟来支持交互式应用程序。

我们通过在关系数据库管理系统和 NoSQL 的设计采取折中的办法实现这一点:我们对数据存储区进行分区,并分别复制每个分区,在分区内提供完整的ACID语义,但在它们之间只有有限的一致性保证。我们提供了传统的数据库功能,例如辅助索引,但是只有那些能够在用户可容忍的延迟限制内扩展的功能,并且只有我们的分区方案能够支持的语义。我们认为,大多数互联网服务的数据可以进行适当的分区(例如,按用户),以使这种方法可行,并且一组小而不简单的功能可以大大减轻开发云应用程序的负担。

与传统观点相反,我们能够使用 Paxos 构建一个高可用性的系统,该系统在跨地理分布的数据中心同步复制写操作的同时,为交互式应用程序提供合理的延迟。虽然许多系统只将 Paxos 用于锁定、选主、复制元数据和配置,但我们认为,Megastore 是部署的最大的系统,它在每次写入时都使用 Paxos 跨数据中心复制主用户数据。

Megastore 已经在谷歌内部广泛部署了几年。它每天处理 30 多亿次写入和 200 亿次读取事务,并在许多全球数据中心存储近 1PB 的主要的数据。本文的主要贡献是:

(1) 数据模型和存储系统的设计,允许交互式应用程序的快速开发,从一开始就内置高可用性和可扩展性;

(2) Paxos复制和一致性算法的实现,针对地理上分布的数据中心的低延迟操作进行了优化,为系统提供了高可用性;

(3) 一份关于我们在谷歌大规模部署 Megastore 的经验的报告。

论文组织如下。第 2 节描述了 Megastore 如何使用分区提供可用性和可扩展性,并证明了我们的设计对于许多交互式互联网应用程序的充分性。 3 节概述了 Megastore 的数据模型和功能。第 4 节详细解释了复制算法,并给出了一些关于它们在实践中如何执行的度量。第 5 节总结了我们开发该系统的经验。我们在第 6 节回顾相关工作。第七节总结。

2 以可用性和规模为目标

与我们对全球、可靠且任意大规模的存储平台的需求形成对比的是,我们的硬件构建模块受到地理位置的限制,容易出现故障,并且容量有限。我们必须将这些组件绑定到一个统一的整体中,以提供更大的吞吐量和可靠性。为此,我们采取了双管齐下的方法:

(1) 为了可用性,我们实现了一个同步、容错的日志复制器,该复制器针对长距离链路进行了优化;

(2) 为了扩大规模,我们将数据划分到一个巨大的小型数据库空间中,每个数据库都有自己的复制日志,存储在每个副本的 NoSQL 数据存储区中。

2.1 复制

在单个数据中心内跨主机复制数据通过克服特定的主机的故障提高了可用性,但回报不断减少。我们仍然必须面对将它们与外部世界连接起来的网络,以及为它们供电、散热和提供住所的基础设施。经济建设的站点面临一定程度的设施范围内的停机风险,并且容易受到区域性灾难的影响。为了让云存储满足可用性需求,服务提供商必须在广阔的地理区域内复制数据。

2.1.1 战略

我们评估了广域复制(wide-area replication)的常见策略:

异步主/从(Asynchronous Master/Slave)

主节点将预写日志条目(write-ahead log entries)复制到至少一个从节点。日志追加在主机上得到确认,同时传输到从机。主服务器可以支持快速 ACID 事务,但在故障转移到从服务器期间有停机或数据丢失的风险。调解主导权需要一个共识协议。

同步主/从(Synchronous Master/Slave)

主机在确认更改之前会等待将其镜像到从机,这样就可以在不丢失数据的情况下进行故障转移。主设备和从设备故障需要外部系统及时检测。

乐观复制(Optimistic Replication)

同一个复制组的任何成员的突变(mutations,指数据的变化)是可以接受的,改变的数据通过组内异步传播。可用性和延迟非常好(Any member of a homogeneous replica group can accept mutations, which are asynchronously propagated through the group)。然而,全局变异排序在提交时是未知的,因此事务是不可能的。

我们避免了可能丢失故障数据的策略,这在大规模系统中很常见。我们还放弃了不允许 ACID事务的策略。尽管最终一致性的系统具有操作上的优势,但在快速的应用程序开发中,放弃读-修改-写习惯用法是非常困难的。

我们也抛弃了重量级 master 的选项。故障转移需要一系列高延迟阶段,通常会导致用户可见的中断,并且仍然存在巨大的复杂性。如果我们可以完全避免 master,为什么要建立一个容错系统来选主(arbitrate mastership)和故障转移工作流?

2.1.2 Enter Paxos

我们决定使用 Paxos,这是一种经过验证的最佳容错一致性算法,不需要杰出的 master。我们在一组对称的对等体上复制预写日志。任何节点都可以启动读取和写入。每个日志都在大多数副本的确认中添加块,少数副本尽可能赶上来——该算法固有的容错性消除了对可区分的"失败"状态的需要。第 4.4.1 节详细介绍了 Paxos 的一个新扩展,它允许在任何最新的副本上进行本地读取。另一个扩展允许单路往返写入(single-roundtrip writes)。

即使 Paxos 具有容错能力,使用单个日志也有局限性。由于副本分布在很广的区域,通信延迟限制了整体吞吐量。此外,当没有当前副本或大多数副本未能确认写入时,进度会受到阻碍。在一个拥有数千或数百万用户的传统数据库中,使用同步复制的日志会有造成广泛影响的中断风险。因此,为了提高可用性和吞吐量,我们使用多个复制的日志,每个日志管理自己的数据集分区。

2.2 分区和局部性

为了扩展我们的复制方案并最大限度地提高底层数据存储的性能,我们为应用程序提供了对其数据分区和位置的细粒度控制。

2.2.1 实体组

为了扩大吞吐量和本地化中断,我们将数据划分为一组实体组,每个实体组在很大范围内独立同步复制。底层数据存储在每个数据中心的可扩展 NoSQL 数据存储中(参见图1)。

实体组中的实体使用单阶段 ACID 事务(通过 Paxos 复制提交记录)进行数据改写。跨实体组的操作可能依赖于昂贵的两阶段提交,但通常利用 Megastore 的高效异步消息传递。发送实体组中的事务在队列中放置一条或多条消息;接收实体组的事务原子地使用这些消息并应用随后发生的数据改写。

请注意,我们在逻辑上遥远的实体组之间使用异步消息传递,而不是物理上遥远的副本。数据中心之间的所有网络流量都来自同步且一致的复制操作。实体组的局部索引服从 ACID 语义;跨实体组的一致性较弱。有关实体组上和实体组之间的各种操作,请参见图2。

2.2.2 选择实体组边界

实体组为快速操作定义了数据的优先分组。过于细粒度的边界会强制执行过多的跨组操作,但是在单个组中放置太多不相关的数据会序列化不相关的写入,从而降低吞吐量。以下示例显示了应用程序在这些约束条件下的工作方式:

Email 每个电子邮件帐户形成一个自然实体组。帐户内的操作是事务性的与一致性的:发送或标记消息的用户保证会观察到更改,尽管可能会故障转移到另一个副本。外部邮件路由器处理帐户之间的通信。

Blogs 一个博客应用程序可以用多个实体组类来建模。每个用户都有一个配置文件,这自然是它自己的实体组。然而,博客是协作性的,没有单一的永久所有者。我们创建了第二类实体组来保存每个博客的帖子和元数据。第三类提供了每个博客的唯一名称。当单个用户操作同时影响博客和个人资料时,应用程序依赖异步消息传递。对于流量较低的操作,比如创建一个新的博客并声明它的唯一名称,两阶段提交更方便,性能也更好。

Maps 地理数据没有任何一致性或方便的自然粒度。映射应用程序可以通过将地球划分为不重叠的面片来创建实体组。对于跨越补丁的突变,应用程序使用两阶段提交来使它们成为原子的。补丁必须足够大,以至于两阶段事务不常见,但又足够小,以至于每个补丁只需要很小的写吞吐量。与前面的示例不同,实体组的数量不会随着使用的增加而增加,因此最初必须创建足够的补丁,以便在以后的规模上获得足够的聚合吞吐量。

几乎所有基于 Megastore 构建的应用程序都找到了自然的方法来绘制实体组边界。

2.2.3 物理布局

我们使用谷歌的 Bigtable 在单个数据中心内实现可扩展的容错存储,允许我们通过跨多行扩展操作来支持任意的读写吞吐量。我们通过让应用程序控制数据的放置来最小化延迟和最大化吞吐量:通过选择 Bigtable 实例和指定实例内的位置。

为了最大限度地减少延迟,应用程序试图将数据保持在用户附近,并将副本保持在彼此附近。他们将每个实体组分配给最常访问它的区域或大陆。在该区域内,他们将三个或五个副本分配给具有独立故障域的数据中心。

为了获得低延迟、缓存效率和吞吐量,实体组的数据保存在连续的 Bigtable 行范围内。我们的模式语言允许应用程序控制分层数据的放置,将一起访问的数据存储在附近的行中,或者反规范化到同一行中。

3 A TOUR OF Megastore

Megastore 将这种架构映射到精心选择的功能集上,以鼓励快速开发可扩展的应用程序。这一部分推动了折衷,并描述了由此产生的面向开发人员的特性。

3.1 API 设计理念

ACID 事务简化了对正确性的推理,但是能够对性能进行推理也同样重要。Megastore 强调成本透明的 API,运行时成本与应用程序开发人员的直觉相匹配。规范化的关系模式依赖于查询时的连接来服务用户操作。出于以下几个原因,这种模式不适合 Megastore 应用:

(1) 与表达性查询语言相比,高容量交互式工作负载从可预测的性能中获益更多。

(2) 在我们的目标应用程序中,读取多于写入,因此将工作从读取时间转移到写入时间是值得的。

(3) 在像 Bigtable 这样的键值存储中,存储和查询分层数据非常简单。

考虑到这一点,我们设计了一个数据模型和模式语言来提供对物理位置的细粒度控制。分层布局和声明性非规范化有助于消除大多数连接的需要。查询指定对特定表和索引的扫描或查找。需要时,连接在应用程序代码中实现。我们提供了合并连接算法在合并阶段的实现,其中用户提供了多个查询,这些查询以相同的顺序返回同一表的主键;然后,我们返回所有提供的查询的键的交集。

我们也有通过并行查询实现外部连接的应用程序。这通常涉及索引查找,然后使用初始查找的结果进行并行索引查找。我们发现,当并行进行二级索引查找,并且第一次查找的结果数量相当少时,这就为 SQL 风格的连接提供了一个有效的替代。

虽然模式更改需要对查询实现代码进行相应的修改,但该系统保证了在构建特性时对其性能影响有清晰的理解。例如,当用户(可能没有数据库背景)发现自己在写类似嵌套循环连接算法的东西时,他们很快意识到最好添加一个索引,并遵循上面的基于索引的连接方法。

3.2 数据模型

Megastore 定义了一个位于关系数据库管理系统的抽象元组和 NoSQL 的具体行列存储之间的数据模型。像在关系数据库管理系统中一样,数据模型在模式中声明,并且是强类型的。每个模式都有一组表,每个表都包含一组实体,这些实体又包含一组属性。属性是命名和类型化的值。类型可以是字符串、各种类型的数字或谷歌的协议缓冲区。它们可以是必需的、可选的或重复的(允许在单个属性中列出值)。表中的所有实体都具有相同的允许属性集。属性序列用于形成实体的主键,主键在表中必须是唯一的。图 3 显示了一个简单照片存储应用程序的示例模式。

Megastore 表可以是实体组根表,也可以是子表。每个子表必须声明一个引用根表的可分辨外键,如图 3 中的实体组键注释所示。因此,每个子实体引用其根表中的特定实体(称为根实体)。实体组由根实体以及子表中引用它的所有实体组成。一个 Megastore 实例可以有几个根表,从而产生不同类别的实体组。在图 3 的示例模式中,每个用户的照片集合是一个单独的实体组。根实体是用户,照片是子实体。请注意,"照片标签"字段是重复的,允许每个照片有多个标签,而不需要子表。

3.2.1 用键预连接

传统的关系建模建议所有的主键都采用代理值,而 Megastore 键则被选择来集群将一起读取的实体。每个实体映射到一个单一的 Bigtable 行;主键值被连接起来形成 Bigtable 行键,每个剩余的属性占用它自己的 Bigtable 列。

请注意图 3 中的照片和用户表如何共享一个公共的 user_id key 的前缀。表内用户指令指示 Megastore 将这两个表放在同一个 Bigtable 中,键排序确保照片实体存储在相应用户的旁边。这种机制可以递归地应用于加速沿任意连接深度的查询。因此,用户可以通过操作 key 顺序来强制分层布局。

模式声明键是升序还是降序排序,或者完全避免排序:SCATTER 属性指示 Megastore 在每个键前添加一个两字节的散列。以这种方式编码单调递增的键可以防止跨 Bigtable 服务器的大型数据集中出现热点。

3.2.2  Indexes

辅助索引可以在任何实体属性列表上声明,也可以在协议缓冲区中的字段上声明。我们区分了两个高层次的索引类别:本地和全局(见图2)。本地索引被视为每个实体组的单独索引。它用于在实体组中查找数据。在图 3 中,PhotosByTime 是一个本地索引的例子。索引条目存储在实体组中,并自动更新,与主要实体数据保持一致。

全局索引跨越实体组。它用于在事先不知道包含实体的实体组的情况下查找实体。图 3 中的 PhotosByTag 索引是全局的,可以发现带有给定标签的照片,而不考虑所有者。全局索引扫描可以读取许多实体组拥有的数据,但不能保证反映所有最近的更新。Megastore 提供了额外的索引功能:

3.2.2.1 存储条款

通过索引访问实体数据通常是一个两步过程:首先读取索引以找到匹配的主键,然后使用这些键来获取实体。我们提供了一种将实体数据的部分直接反规格化为索引条目的方法。通过向索引中添加 STORING 子句,应用程序可以存储主表中的附加属性,以便在读取时更快地进行访问。例如,PhotosByTag 索引存储照片缩略图网址,以便更快地检索,而不需要额外的查找。

3.2.2.2 重复索引

Megastore 提供了索引重复属性和协议缓冲子字段的能力。重复索引是子表的有效替代。照片字节标签是一个重复的索引:标签属性中的每个唯一条目都会代表照片创建一个索引条目。

3.2.2.3 内联索引

内联索引提供了一种将来自源实体的数据反规范化为相关目标实体的方法:来自源实体的索引条目在目标条目中显示为虚拟的重复列。通过使用目标实体的第一个主键作为索引的第一个组成部分,并在与目标实体相同的 Bigtable 中物理定位数据,可以在任何具有引用另一个表的外键的表上创建内联索引。

内联索引对于从子实体中提取信息片段并将数据存储在父实体中以便快速访问非常有用。与维护多对多链接表相比,再加上重复的索引,它们还可以更有效地实现多对多关系。

PhotosByTime 索引可以作为父用户表的内联索引来实现。这将使数据可作为正常索引或用户的虚拟重复属性访问,每个包含的照片都有一个按时间排序的条目。

3.2.3 映射到 BigTable

Bigtable 列名是 Megastore 表名和属性名的串联,允许来自不同 Megastore 表的实体映射到同一个 Bigtable 行,而不会发生冲突。图 4 显示了示例照片应用程序中的数据在 Bigtable 中的外观。

在根实体的 Bigtable 行中,我们存储实体组的事务和复制元数据,包括事务日志。将所有元数据存储在一个 Bigtable 行中允许我们通过一个 Bigtable 事务自动更新它。每个索引条目都表示为一个 Bigtable 行;使用与索引实体的主键连接的索引属性值来构造单元格的行键。例如,PhotosByTime 索引行关键字将是每张照片的元组(用户 id、时间、主键)。对重复字段进行索引会为每个重复元素生成一个索引条目。例如,一张带有三个标签的照片的主键会在 PhotosByTag 索引中出现三次。

3.3 事务和并发控制

每个大型实体组的功能就像一个微型数据库,提供可序列化的 ACID 语义。事务将其突变写入实体组的提前写日志,然后对数据应用这些突变。

Bigtable 提供了在具有不同时间戳的同一行/列对中存储多个值的能力。我们使用这个特性来实现多版本并发控制(MVCC):当一个事务中的突变被应用时,这些值被写入它们的事务的时间戳。读者使用上次完全应用的事务的时间戳来避免看到部分更新。读者和写者不会相互阻塞,并且在事务期间,读取和写入是隔离的。

Megastore 提供当前、快照和不一致的读取当前和快照读取总是在单个实体组的范围内完成。当开始当前读取时,事务系统首先确保应用所有先前提交的写入;然后,应用程序读取最近提交的事务的时间戳。对于快照读取,系统获取最后一个已知的完全应用的事务的时间戳,并从那里读取,即使一些已提交的事务尚未应用。Megastore 还提供不一致读取,忽略日志状态,直接读取最新值。这对于具有更严格的延迟要求并且可以容忍陈旧或部分应用的数据的操作非常有用。

写事务总是从当前读取开始,以确定下一个可用的日志位置。提交操作将变化收集到一个日志条目中,给它分配一个比以前更高的时间戳,并使用 Paxos 将其附加到日志中。该协议使用乐观并发:虽然多个写入者可能试图写入同一日志位置,但只有一个人会赢得写入权利。其余的将注意到胜利的写,中止,并重试他们的操作。咨询锁定可用于减少争用的影响。通过会话关联性对特定前端服务器的写入进行批处理可以完全避免争用。完整的事务生命周期如下:

(1) 读取:获取上次提交事务的时间戳和日志位置。

(2) 应用程序逻辑:从 Bigtable 中读取,并将写入内容收集到日志条目中。

(3) 提交:使用 Paxos 来实现将该条目附加到日志的一致性。

(4) 应用:将突变写入 Bigtable 中的实体和索引。

(5) 清理:删除不再需要的数据。

写操作可以在提交后的任何时间点返回到客户端,尽管它会尽最大努力等待最近的复制副本应用。

3.3.1 队列

队列提供实体组之间的事务消息传递。它们可用于跨组操作、将多个更新批量处理到单个事务或延迟工作。实体组上的事务除了更新其实体外,还可以原子地发送或接收多个消息。每条消息有一个发送和接收实体组;如果它们不同,则交付是异步的(见图2)。

队列提供了一种执行影响许多实体组的操作的方法。例如,考虑一个日历应用程序,其中每个日历都有一个不同的实体组,我们希望向一组日历发送邀请。单个事务可以自动向许多不同的日历发送邀请队列消息。每个接收消息的日历将在其自己的事务中处理邀请,该事务更新被邀请者的状态并删除消息。

在功能齐全的关系数据库管理系统中,消息队列有着悠久的历史。我们的支持以其规模而闻名:声明一个队列会自动在每个实体组上创建一个收件箱,给我们提供数百万个端点。

3.3.2 两阶段提交

Megastore 支持跨实体组的原子更新的两阶段提交。由于这些事务具有高得多的延迟并增加了争用的风险,我们通常不鼓励应用程序使用该特性来支持队列。然而,它们在简化应用程序代码以实现唯一的二级密钥实施方面很有用。

3.4 其他功能

我们已经与 Bigtable 的全文索引建立了紧密的集成,其中更新和搜索参与 Megastore 的事务和多版本并发。在 Megastore 模式中声明的全文索引可以索引表的文本或其他应用程序生成的属性。

同步复制足以抵御最常见的损坏和事故,但在程序员或操作员出错的情况下,备份可能是无价的。Megastore 的集成备份系统支持事务日志的定期完整快照和增量备份。恢复过程可以将实体组的状态恢复到任意时间点,也可以省略选定的日志条目(在意外删除之后)。备份系统符合已删除数据过期的规则和常识原则。

应用程序可以选择加密静态数据,包括事务日志。加密对每个实体组使用不同的密钥。我们避免授予相同的操作员访问加密密钥和加密数据的权限。

4 复制

本节详细介绍了我们的同步复制方案的核心:Paxos 的低延迟实现。我们讨论运营细节,并介绍我们生产服务的一些衡量标准。

4.1 概述

Megastore 的复制系统提供了存储在其底层副本中的数据的单一且一致的视图。读取和写入可以从任何副本启动,并且无论客户端从哪个副本启动,ACID 语义都会得到保留。复制是通过将每个实体组的事务日志同步复制到复制副本的仲裁中来完成的。写操作通常需要一轮数据中心间通信,正常情况下的读操作在本地运行。当前读取有以下保证:

(1) 读取总是最后确认的写入。

(2) 观察到写入后,所有将来的读取都会观察到该写入。(在确认之前,可能会观察到写入)

4.2 Paxos 简介

Paxos 算法是一种在一组副本中就单个值达成共识的方法。它可以容忍延迟或重新排序的消息以及因停止而失败的副本。大多数副本必须是活跃的和可达的,算法才能取得进展,也就是说,它允许 2F + 1 副本最多有 F 个故障。一旦某个值被大多数人选中,以后所有读取或写入该值的尝试都会得到相同的结果。

单独确定单个值的结果的能力对数据库来说用处不大。数据库通常使用 Paxos 来复制事务日志,其中 Paxos 的单独实例用于日志中的每个位置。新值被写入日志中最后选择的位置之后的位置。

最初的 Paxos 算法不适用于高带宽网络链路,因为它需要多轮通信。在达成一致之前,写入至少需要两次副本间往返:一轮准备,保留后续一轮接受的权利。读取至少需要一轮准备,以确定最后选择的值。建立在 Paxos 上的系统减少了使其成为实用算法所需的往返次数。我们将首先回顾基于主机的系统如何使用 Paxos,然后解释我们如何使 Paxos 高效。

4.3 基于 Master 的方法

为了最大限度地减少延迟,许多系统使用一个专用 master,所有读取和写入都指向该master。master 参与所有写入,因此它的状态始终是最新的。它可以在没有任何网络通信的情况下提供当前一致状态的读数。通过在每次接受时进行下一次写操作的准备,写操作被简化为一轮通信。主机可以一起批量写入,以提高吞吐量。

 master 的依赖限制了读和写的灵活性。事务处理必须在主副本附近完成,以避免顺序读取造成的延迟累积。任何潜在的主副本必须有足够的资源用于系统的全部工作负载;从属副本浪费资源,直到他们成为 master。master 故障转移可能需要复杂的状态机,并且在服务恢复之前必须经过一系列计时器。很难避免用户可见的中断。

4.4 Megastore 的方法

在本节中,我们将讨论使 Paxos 对我们的系统实用的优化和创新。

4.4.1 快速读取

我们设置了一个早期要求,即当前读取通常应该在没有副本间 RPC 的任何副本上执行。由于写入通常在所有副本上都成功,因此允许在任何地方进行本地读取是现实的。这些本地读取为我们提供了更好的利用率、所有区域的低延迟、细粒度的读取故障转移和更简单的编程体验。

我们设计了一个名为协调器的服务,每个副本的数据中心都有服务器。协调服务器跟踪一组实体组,其副本已观察到该组实体组的所有 Paxos 写入。对于该集中的实体组,副本具有足够的状态来服务本地读取。保持协调器状态是写算法的责任。如果一个复制副本的 Bigtable 上的写操作失败,则在该复制副本的协调器收回该组的密钥之前,不能将其视为已提交。由于协调器很简单,他们比 Bigtable 更可靠、更快速地做出响应。第 4.7 节描述了罕见故障情况或网络分区的处理。

4.4.2 快速写入

为了实现快速的单往返写入,Megastore 采用了基于 master 的方法所使用的预准备优化。在基于 master 的系统中,每次成功写入都包含一条隐含的准备消息,授予主机为下一个日志位置发出接受消息的权利。如果写入成功,准备工作将得到执行,下一次写入将直接跳到接受阶段。Megastore 不使用专门的 master,而是使用 leader。

我们为每个日志位置运行一个独立的 Paxos 算法实例。每个日志位置的前导是与前一个日志位置的一致的值一起选择的一个可分辨的副本。leader 仲裁哪一个值可以使用 proposal number zero。第一个向 leader 提交值的写者赢得了要求所有副本接受该值作为零号建议书的权利。所有其他写者必须依靠两阶段 Paxos。

由于写入者在将值提交给其他副本之前必须与 leader 沟通,因此我们将写入者延迟降至最低。我们根据大多数应用程序重复提交来自同一地区的写操作的观察,设计了选择下一个写操作负责人的策略。这就产生了一个简单但有效的启发式方法:使用最近的副本。

4.4.3 副本类型

到目前为止,所有副本都是完全副本,这意味着它们包含所有实体和索引数据,并且能够服务于当前读取。我们也支持 witness 副本的概念。witness  Paxos 轮次中投票并存储写前日志,但不应用日志,也不存储实体数据或索引,因此存储成本较低。它们实际上是平局决胜者,在没有足够的完整副本来形成法定人数时使用。因为它们没有协调器,所以当它们未能确认写入时,不会强制执行额外的往返。

只读副本(Read-only replicas) witness 副本相反它们是非投票副本,包含数据的完整快照。对这些副本的读取反映了最近某个时间点的一致的视图。对于可以容忍这种过时的读取,只读副本有助于在广泛的地理区域传播数据,而不会影响写入延迟。

4.5 架构

图 5 显示了具有两个完整副本和一个 witness 副本的实例的 Megastore 的关键组件。

Megastore 是通过客户端库和辅助服务器部署的。应用程序链接到客户端库,该库实现 Paxos 和其他算法:选择一个副本进行读取,追赶一个落后的副本,等等。每个应用服务器都有一个指定的本地副本。客户端库通过直接向本地 Bigtable 提交事务,使该副本上的 Paxos 操作持久。为了最小化广域往返,库将远程 Paxos 操作提交给与本地 Bigtable 通信的无状态中间复制服务器。客户端、网络或 Bigtable 故障可能会导致写操作以不确定的状态被放弃。复制服务器定期扫描未完成的写入,并通过 Paxos 建议无操作值以完成写入。

4.6 数据结构和算法

本节详细介绍了从对单个值的共识到正常运行的复制日志的飞跃所需的数据结构和算法。

4.6.1 复制的日志

每个副本存储组已知的日志条目的突变信息和元数据。为了确保复制副本即使在从以前的中断中恢复时也能参与写入仲裁,我们允许复制副本接受无序建议。我们将日志条目作为独立的单元格存储在 Bigtable 中。

当日志副本包含不完整的日志前缀时,我们称之为"漏洞"图 6 展示了这个场景,其中有一个 Megastore 实体组的一些代表性日志副本。日志位置 0-99 已被完全清除,位置 100 被部分清除,因为每个副本都被告知其他副本将永远不会请求拷贝。日志位置 101 被所有副本接受。日志位置 102 在 A 和 C 中发现了一个空的法定人数。位置 103 值得注意,因为它已经被 A 和 C 接受,给 B 留下了一个 103 的洞。副本 A 和副本 B 上的位置 104 发生了冲突的写入尝试,导致无法达成一致。

4.6.2 Reads

在为当前读取(以及写入之前)做准备时,必须至少更新一个副本:必须将之前提交给日志的所有变化复制到该副本并应用到该副本。我们称这个过程为追赶较为重要的概念

省略了一些期限管理,当前读取的算法(如图 7 所示)如下:

(1) 查询本地:查询本地副本的协调器,以确定实体组在本地是否是最新的。

(2) 查找位置:确定可能实现的最高日志位置,并选择已通过该日志位置应用的副本。

(a)(本地读取)如果步骤 1 表明本地副本是最新的,则从本地副本中读取最高可接受的日志位置和时间戳。

(b)(多数读取)如果本地复制副本不是最新的(或者如果步骤 1 或步骤 2a 超时),从多数复制副本中读取,以找到任何复制副本看到的最大日志位置,并选择要读取的复制副本。我们选择响应速度最快或最新的副本,而不总是本地副本。

(3) 捕捉:选择复制副本后,立即将其捕捉到最大已知日志位置,如下所示:

(a) 对于所选副本不知道一致的值的每个日志位置,从另一个副本读取该值。对于任何没有已知提交值的日志位置,调用 Paxos 来建议一个无操作写入(no-op write)。Paxos 将驱动大多数副本聚合到一个值上,要么是无操作,要么是以前建议的写入。

(b) 顺序应用所有未应用日志位置相一致的值,以将副本的状态提升到分布式一致性状态。如果失败,请在另一个副本上重试。

(4) 验证:如果选择了本地复制副本,并且以前不是最新的,则向协调器发送验证消息,声明(实体组、复制副本)对反映了所有提交的写入。不要等待回复——如果请求失败,下一次读取将重试。

(5) 查询数据:使用选定日志位置的时间戳读取选定的副本。如果选定的复制副本不可用,请选择一个备用复制副本,执行捕获,然后改为从中读取。单个大型查询的结果可以从多个副本透明地组装。

注意,实际上 1 和 2a 是并行执行的。

4.6.3 Writes

完成读算法后,Megastore 观察下一个未使用的日志位置、最后一次写的时间戳和下一个 leader 副本。在提交时,将对状态的所有挂起的更改打包并提出,并使用时间戳和下一个 leader 指定,作为下一个日志位置的共识值。如果这个值赢得了分布式共识,它将应用于所有完全副本的状态;否则,整个事务将中止,必须从读取阶段开始重试。

如上所述,协调器跟踪在其副本中最新的实体组。如果复制副本上的写入不被接受,我们必须从该复制副本的协调器中删除实体组的密钥。这个过程叫做失效(invalidation,较为重要的概念。在写入被视为已提交并准备好应用之前,所有完整复制副本必须已接受该实体组,或者其协调器已失效。写算法(如图 8 所示)如下:

(1) Accept Leader:要求领导接受该值作为零号建议(proposal number zero)。如果成功,请跳到步骤 3。

(2) Prepare:在所有副本上运行 Paxos 准备阶段,该副本的建议编号高于目前在此日志位置看到的任何副本。用发现的最高编号的建议(如果有)替换正在写入的值。

(3) Accept:要求剩余的副本接受该值。如果大多数副本都失败了,请在随机回退后返回步骤 2。

(4) Invalidate:使不接受该值的所有完整副本上的协调器无效。下面第 4.7 节描述了这一步骤的故障处理。

(5) Apply:在尽可能多的副本上应用改变的值。如果选择的值不同于最初建议的值,则返回冲突错误。

步骤 1 实现了第 4.4.2 节中的"快速写入"。写者通过使用单阶段 Paxos 在建议编号为零时发送接受命令来跳过准备消息。在日志位置 n 选择的下一个 leader 副本仲裁用于 n + 1 处的提议零的值。由于多个建议者可能提交建议编号为零的值,因此在此副本中进行序列化可确保只有一个值与特定日志位置的建议编号相对应。

在传统的数据库系统中,提交点(更改是持久的)与可见点(visibility point)(读取可以看到更改,写入器可以收到成功通知)是相同的。在我们的写算法中,当写程序赢得 Paxos 回合时,提交点是在步骤 3 之后,但可见性点是在步骤 4 之后。只有在所有完整副本已经接受或其协调器失效后,写入才能被确认并应用更改。在步骤 4 之前的确认可能会违反我们的一致性保证:在副本上的当前读取,如果无效被跳过,则可能无法观察到已确认的写入。

4.7 Coordinator Availability

协调器进程在每个数据中心运行,并且只保持关于其本地副本的状态。在上面的写算法中,每个完整副本必须接受或使其协调器无效,因此任何单个副本故障(Bigtable 和协调器)都可能导致不可用。实际上,这不是一个常见的问题。协调器是一个简单的过程,没有外部依赖性,也没有持久存储,因此它往往比 Bigtable 服务器稳定得多。然而,网络和主机故障仍然会使协调器不可用。

4.7.1 故障检测

为了解决网络分区问题,协调器使用带外协议来识别其他协调器何时启动、运行正常并且通常可以访问。我们使用谷歌的 Chubby 锁服务:协调器在启动时在远程数据中心获得特定的 Chubby  锁。为了处理请求,协调器必须持有其大部分锁。如果它在崩溃或网络分区中丢失了大部分锁,它会将其状态恢复为保守的默认状态,认为其权限内的所有实体组都已过时。复制副本上的后续读取必须从大多数复制副本中查询日志位置,直到锁被重新获得并且其协调器条目被重新验证。

通过测试协调器是否丢失了锁,写入器与协调器故障隔离开来:在这种情况下,写入器知道协调器在重新获得锁时会认为自己无效。当包含实时协调器的数据中心突然变得不可用时,此算法可能会导致短暂(几十秒)的写入中断——所有写入者都必须等待协调器的 Chubby 锁到期,然后才能完成写入(很像等待主故障切换触发)。与主节点故障转移后不同,当协调器的状态被重建时,读取和写入可以顺利进行。这种短暂而罕见的停机风险是由它所允许的快速本地读取的稳定状态所证明的。

协调器活性协议易受不对称网络分区的影响。如果协调器可以维护其 Chubby 锁的租约,但是已经与提议者失去联系,那么受影响的实体组将经历写中断。在这种情况下,操作员执行手动步骤来禁用部分隔离的协调器。我们只遇到过几次这种情况。

4.7.2 Validation Races

除了可用性问题,读写协调器的协议必须应对各种竞争条件。无效消息总是安全的,但是验证消息必须小心处理。协调器通过始终发送与操作相关联的日志位置来保护早期写入的验证和后期写入的无效之间的竞争。编号较高的无效总是胜过编号较低的有效。(Races between validates for earlier writes and invalidates for later writes are protected in the coordinator by always sending the log position associated with the action. Higher numbered invalidates always trump lower numbered validates)还存在与写入器在位置 n 的无效和在位置 m < n 的验证之间的崩溃相关联的竞争。我们使用协调器的每个化身的唯一时期号来检测崩溃:只有在最近读取协调器的 epoch 保持不变时,validates 才允许修改协调器状态。总之,就对可用性的影响而言,使用协调器允许从任何数据中心进行快速本地读取并不是免费的。但实际上,运行协调器的大多数问题都可以通过以下因素得到缓解:

(1) 协调器是比大型服务器简单得多的过程,具有更少的依赖性,因此自然更加可用。

(2) 协调器简单、同质的工作负载使其成本低廉且可预测。

(3) 协调器的轻网络流量允许使用具有可靠连接的高网络服务质量。

(4) 操作员可以集中禁用维护或不健康期间的协调器。对于某些监控信号,这是自动的。

(5) Chubby 锁的法定数量检测到大多数网络分区和节点不可用。

4.8 写吞吐量

我们的 Paxos 实现在系统行为上有着有趣的权衡。多个数据中心中的应用服务器可以同时启动对同一实体组和日志位置的写入。除了一个之外,所有的都将失败,需要重试它们的事务。对于给定的每个实体组提交速率,同步复制数量的增加提升了延迟增加的冲突的可能性。

将该速率限制为每个实体组每秒几个写入,会产生微不足道的冲突率。对于实体一次被少数用户操纵的应用程序,这种限制通常不是问题。我们的大多数目标客户通过更精细地分割实体组或确保将副本放在同一区域来扩展写吞吐量,从而降低延迟和冲突率。

具有一些服务器"粘性"的应用程序能够很好地将用户操作批处理到更少的 Megastore 事务中。大容量存储队列消息的批量处理是一种常见的批处理技术,可以降低冲突率并提高聚合吞吐量。对于必须经常超过每秒几次写入的组,应用程序可以使用协调服务器分配的细粒度咨询锁。对事务进行背靠背序列化可以避免与重试相关的延迟,并在检测到冲突时恢复到两阶段 Paxos。

4.9 Operational Issues

当一个特定的完整副本变得不可靠或失去连接时,Megastore 的性能可能会下降。我们有许多方法来应对这些失败,包括:路由用户远离有问题的副本、禁用其协调器,或完全禁用它。在实践中,我们依赖于技术的组合,每一种技术都有自己的权衡。

对中断的第一个也是最重要的响应是通过将流量重新路由到其他副本附近的应用服务器来禁用受影响副本上的 Megastore 客户端。这些客户端通常会经历相同的中断,影响其下方的存储堆栈,并且可能无法从外部世界访问。

如果不健康的协调服务器可能继续持有它们的 Chubby 锁,仅仅重新路由流量是不够的。下一个响应是禁用复制副本的协调器,确保问题对写入延迟的影响最小。(第 4.7 节更详细地描述了这个过程。)一旦免除了写入者使复制副本的协调器无效的责任,不健康的复制副本对写入延迟的影响就会受到限制。只有写入算法中最初的"accept leader"步骤依赖于副本,我们在回到两阶段 Paxos 并为下一次写入指定更健康的 leader 之前保持一个严格的截止日期。

一个更严厉且很少使用的操作是完全禁用副本:无论是客户端还是复制服务器都不会尝试与其通信。虽然隔离副本看起来很有吸引力,但主要影响是可用性:少一个副本就有资格帮助写入者形成法定人数。有效的用例是当尝试的操作可能造成伤害时——例如,当底层的 Bigtable 严重过载时。

4.10 生产指标

Megastore 已经在 Google 内部部署了好几年;超过 100 个生产应用程序将它用作存储服务。在本节中,我们将报告对其规模、可用性和性能的一些衡量。

图 9 显示了可用性的分布,以每个应用程序、每个操作为基础进行衡量。尽管机器故障、网络故障、数据中心中断和其他故障源源不断,但我们的大多数客户都看到了极高的可用性(至少五个 9)。我们样本的底端包括一些仍在测试的生产前应用和具有较高故障容限的批处理应用。

根据数据量,平均读取延迟为几十毫秒,表明大多数读取是本地的。根据数据中心之间的距离、写入数据的大小和完整副本的数量,大多数用户的平均写入延迟为 100-400 毫秒。图 10 显示了读取和提交操作的平均延迟分布。

5 经验

对可测试性的强调有助于系统的开发。代码被大量(但廉价)的断言和日志记录所装备,并且具有彻底的单元测试覆盖。但是最有效的 Bug 发现工具是我们的网络模拟器:伪随机测试框架。它能够探索模拟节点或线程之间通信的所有可能顺序和延迟的空间,并在给定相同种子的情况下确定性地再现相同行为。Bug 是通过发现触发断言失败(或不正确结果)的有问题的事件序列而暴露出来的,通常有足够的日志和跟踪信息来诊断问题,然后将其添加到单元测试套件中。虽然不可能对调度状态空间进行穷举搜索,但伪随机模拟探索的内容比通过其他方式实际探索的内容要多。通过每晚运行数千小时的模拟操作,测试发现了许多令人惊讶的问题。

在实际部署中,我们观察到了预期的性能:我们的复制协议优化确实在大多数情况下提供了本地读取,而写入的开销只有一次广域网往返的开销。大多数应用发现延迟是可以容忍的。有些应用程序旨在对用户隐藏写入延迟,有些应用程序必须谨慎选择实体组边界,以最大化其写入吞吐量。这种努力产生了主要的操作优势:Megastore 的延迟尾部比底层的延迟尾部短得多,大多数应用程序可以承受计划内和计划外停机,几乎没有或根本没有手动干预。

大多数应用程序使用 Megastore 模式语言来建模它们的数据。一些应用程序在 Megastore 模式语言中实现了它们自己的实体属性值模型,然后使用它们自己的应用程序逻辑建模它们的数据(最显著的是,谷歌应用引擎)。一些人混合使用两种方法。在静态模式之上构建动态模式,而不是相反,允许大多数应用程序享受静态模式的性能、可用性和完整性好处,同时仍然为那些需要它的人提供动态模式的选项。

术语"高可用性"通常表示屏蔽故障的能力,使系统集合比单个系统更可靠。虽然容错是一个非常理想的目标,但它也有自己的陷阱:它经常隐藏持久的潜在问题。我们组里有句话:"容错就是故障屏蔽"。我们系统的弹性加上对跟踪潜在故障的警惕性不足,往往会导致意想不到的问题:持续未纠正问题之上的微小瞬时错误会导致更大的问题。

另一个问题是流量控制。一个容忍有缺陷的参与者的算法可能会忽视慢参与者。理想情况下,一个由不同机器组成的集合只能和能力最差的成员一样快地取得进展。如果慢被解释为一种错误,并被容忍,大多数最快的机器将按照自己的速度处理请求,只有当被努力追赶的落后者的负载拖慢时,才会达到平衡。我们称这种异常现象为链状团伙扼制,唤起了一群逃跑的罪犯的形象,他们只能尽可能快地拖住掉队者。

Megastore 的预写日志的一个好处是易于集成外部系统。任何幂等运算都可以成为应用日志条目的一个步骤。

要为更复杂的查询获得良好的性能,需要注意 Bigtable 中的物理数据布局。当查询速度较慢时,开发人员需要检查 Bigtable 跟踪,以了解为什么他们的查询执行速度低于预期。 Megastore 不强制实施关于块大小、压缩、表拆分、位置组的特定策略,也不强制实施 Bigtable 提供的其他调优控制。相反,我们公开这些控件,为应用程序开发人员提供优化性能的能力(和负担)。

6 相关工作

最近,人们对 NoSQL 数据存储系统越来越感兴趣,以满足大型网络应用的需求。代表作品包括 Bigtable,Cassandra,雅虎 PNUTS。在这些系统中,可伸缩性是通过牺牲传统关系数据库管理系统的一个或多个属性来实现的,例如事务、模式支持、查询能力。这些系统通常将事务的范围缩小到单个键访问的粒度,因此给构建应用程序设置了一个很大的障碍。有些系统将事务的范围扩展到单个表中的多行,例如 Amazon SimpleDB 使用域的概念作为事务单元。然而,这种努力仍然有限,因为事务不能跨越表格或任意缩放。此外,目前大多数可扩展的数据存储系统缺乏关系数据库管理系统的丰富数据模型,这增加了开发人员的负担。结合了数据库和可扩展数据存储的优点, Megastore 在一个实体组中提供了事务性 ACID 保证,并提供了一个灵活的数据模型,具有用户定义的模式、数据库样式和全文索引以及队列。

跨地理分布的数据中心的数据复制是提高最新存储系统可用性的不可或缺的手段。大多数流行的数据存储系统使用具有较弱一致性模型的异步复制方案。例如,Cassandra、HbPase、CouchDB 和 Dynamo 使用最终一致性模型,PNUTS 使用"时间线"一致性。相比之下,同步复制保证了广域网上强大的事务语义,并提高了当前读取的性能。

传统关系数据库管理系统的同步复制带来了性能挑战,并且难以扩展。一些建议的变通办法允许通过异步复制实现强一致性。一种方法是在复制更新效果之前完成更新,将同步延迟传递给需要读取更新状态的事务。另一种方法是将写操作路由到单个主机,同时在一组副本之间分配只读事务。更新被异步传播到剩余的副本,读取或者被延迟,或者被发送到已经同步的副本。最近一个关于高效同步复制的提议引入了一个排序预处理器,它可以确定性地调度传入的事务,这样它们就可以独立地应用于多个副本,并得到相同的结果。同步负担被转移到预处理器上,预处理器本身必须是可伸缩的。

直到最近,很少有人使用 Paxos 来实现同步复制。SCALARIS 是一个使用 Paxos 提交协议来实现分布式哈希表复制的例子。Keyspace 还使用 Paxos 在通用键值存储上实现复制。然而,这些系统的可扩展性和性能并不为人所知。Megastore 可能是第一个跨数据中心实施基于 Paxos 的复制,同时满足云中可扩展网络应用程序的可扩展性和性能要求的大型存储系统。

传统的数据库系统提供了成熟和复杂的数据管理功能,但在为本文所针对的大规模交互式服务提供服务方面存在困难。MySQL 等开源数据库系统无法扩展到我们要求的水平,而 Oracle  等昂贵的商业数据库系统会显著增加云中大型部署的总拥有成本。此外,它们都没有提供容错同步复制机制,这是在云中构建交互式服务的关键部分。

7 结论

在本文中,我们介绍了 Megastore,这是一个可扩展、高可用性的数据存储,旨在满足交互式互联网服务的存储要求。我们使用 Paxos 进行同步广域复制,为单个操作提供轻量级和快速的故障转移。单个系统映像的便利性和运营商级可用性的操作优势抵消了跨广泛分布的副本进行同步复制的延迟损失。我们使用 Bigtable 作为我们的可扩展数据存储,同时添加更丰富的原语,如 ACID 事务、索引和队列。将数据库划分为实体组子数据库为大多数操作提供了熟悉的事务特性,同时允许存储和吞吐量的可伸缩性。

Megastore 生产中有 100 多个应用程序,面向内部和外部用户,并提供更高级别的基础架构。这些应用程序的数量和多样性证明了兆存的易用性、通用性和强大。我们希望 Megastore 能够展示当今可扩展存储系统在功能集和复制一致性方面的中间立场的可行性。

Megastore:为交互式服务提供可扩展的高可用性存储相关推荐

  1. 2019年的SD-WAN:服务提供商的难题

    一.市场在更新整合 就在您认为SD-WAN的消息在减少时,Oracle收购了Talari.在此之前,VMware收购VeloCloud,思科收购Viptela和Riverbed收购Ocedo.因为市场 ...

  2. Windows Vista 交互式服务编程

    Windows Vista 对快速用户切换,用户账户权限,以及服务程序所运行的会话空间都作了很大的改动,致使一些原本可以工作的程序不再能够正常工作了,我们不得不进行一些改进以跟上 Vista 的步伐. ...

  3. 如何从管理IT服务提供商获得最大收益

    管理服务提供商(managed service provider,MSP)对你的IT服务的部分职能可能大有裨益.除了处理电子邮件主机或客户关系管理等特定领域外,将MSP作为IT组合的一部分,可以解放内 ...

  4. 服务提供商应该如何帮助企业保护数据安全

    本文讲的是 服务提供商应该如何帮助企业保护数据安全,大型.小型.公有.私有.政府.零售.B2B.非盈利组织. 黑客不关心被攻击企业的规模或业务.他们正在利用复杂的多矢量DDoS攻击袭击全球网络,试图获 ...

  5. LSP(分层服务提供程序)

    一.简介 LSP即分层服务提供商,Winsock 作为应用程序的 Windows 的网络套接字工具,可以由称为"分层服务提供商"的机制进行扩展.Winsock LSP 可用于非常广 ...

  6. 微服务架构开发实战:如何实现微服务的自动扩展?

    微服务架构开发实战:如何实现微服务的自动扩展? 程序员高级码农II 2020-09-25 07:25:00 如何实现微服务的自动扩展 前面讲了一些关于自动扩展的理论知识,但如何实现自动扩展,并不是三言 ...

  7. 数字孪生服务提供模式及典型案例

    数字孪生主流服务模式 国内外企业纷纷在数字孪生领域进行布局,数字孪生平台构建成为打造产业生态的关键. 目前主流的数字孪生服务模式可分为领域服务模式和通用服务模式. 一是领域服务模式.领域服务模式与具体 ...

  8. 先传递高八位,后传递低八位_高电子邮件可传递性的7个最佳SMTP服务提供商(2020)

    先传递高八位,后传递低八位 Are you looking for the best SMTP service providers? 您在寻找最佳的SMTP服务提供商吗? An SMTP servic ...

  9. Vercel和Railway都是云端的平台即服务提供商

    Vercel是一个专注于构建响应快速的现代网站和应用程序的服务平台.它被广泛用于构建静态网站.React应用程序等.Vercel提供全球CDN.构建和部署等强大的功能,支持多种前端框架.此外,Verc ...

最新文章

  1. 商汤作价1026亿IPO,商业化汤教授身家220亿
  2. Web services 安全实践: 基于 HTTP Basic Authentication 为 Web services 配置传输层安全机制...
  3. 快速实现dNet三层架构项目图解
  4. 【Android Studio】分类整理res/Layouts中的布局文件(创建子目录)
  5. 二分查找找下标或者值
  6. linux写一个ls命令,linux 下 如何自己写 ls 命令
  7. Android JNI学习(六)——Java与Native实战演习
  8. 值得借鉴的新年海报设计|PSD分层模板,图层素材随心用
  9. 遍历所有点的最短路径matlab_运筹学实验8 最短路的求解
  10. TimeQuest就一定要搞定——时序分析基本公式
  11. Prometheus+Grafana可视化监控SpringBoot项目
  12. r语言pls分析_零基础学习R语言分析GEO
  13. servlet原理+流程图+简单实现案例(javaweb)
  14. python中读取xlsx文件
  15. [瞎搞]Lucas定理证明
  16. CNN with Attention---channal and spatial attention(转)
  17. android触屏对焦_Android相机开发(五): 触摸对焦,触摸测光,二指手势缩放
  18. 也说说互联网产品开发中的设计
  19. 创业的路,每一天都是劫后余生,怎么走?
  20. 大学计算机应用技术基础vb考试,计算机vb考试试题精选

热门文章

  1. 推荐一款cpp解析json工具--rapidjson
  2. Socket之UDP客户端【Python】
  3. 云计算学习路线和经典资料推荐
  4. 如此简单 | ElasticSearch 最全详细使用教程
  5. Go map[int64]int64 写入 redis 占用多少内存
  6. 网络编程套接字(四)
  7. Netflix:我们是如何评估Codec性能的?
  8. 微信看一看实时相关推荐介绍
  9. 腾讯看点投放系统介绍:推荐系统的进化伙伴
  10. Nginx的定时事件的实现(timer)