InfluxDB集群设计 - 既不是严格的CP也不是AP
作者:Paul Dix / 公司
2015年6月3日
导航到
几周前,我暗示了InfluxDB集群设计的一些重大变化。这些变化是由于我们在过去3个月内对即将在0.9.0版本中发布的集群设计进行的测试而出现的。简而言之,我们原本采取的方法不起作用。它既不可靠也不可扩展,并为我们不需要提供的保证做出了牺牲。
六个星期前,我们退后一步,重新评估了我们对集群的整体方法。我们考虑了我们需要提供的保证,更重要的是,对我们来说不重要的保证。最后,我们回顾了有关分布式系统、共识和冲突解决的文献。我们意识到,我们可以通过放宽对于时间序列数据用例不必要的约束,设计出一个更简单的系统,以实现我们的可扩展性和最大吞吐量目标。
最终结果是即将在InfluxDB的下一次构建中出现的聚类实现。它旨在提高吞吐量,结合了CP系统和AP系统以优化主要写入路径。整体而言,它被设计成高度可用和最终一致性。请继续阅读关于设计的详细说明。非常欢迎反馈。说分布式系统很难可能远远低估了挑战。我们知道这需要迭代,但这个设计是我们建立的基础。
假设和要求
在深入具体设计之前,我们需要列出针对我们针对的时间序列用例的一些关键要求。关于数据库如何使用也有一些假设,这将使我们能够大大简化设计。
假设
- 时间序列数据几乎总是新的数据(或历史回填),基于客户端提供的信息,且不可变。在我们的情况下,每个数据点都是根据测量名称、标签和时间戳进行索引的。
- 如果相同的数据点被多次发送,则它就是客户端刚刚发送两次的完全相同的数据。
- 删除操作很少发生。当它们发生时,几乎总是针对大量旧的、写入冷的数据范围。
- 现有数据的更新很少发生,并且争议性更新从未发生。
- 绝大多数写入都是针对非常最近时间戳的数据。
- 可扩展性至关重要。许多时间序列用例处理的数据集大小在千兆或太字节范围内。
- 能够写入和查询数据比拥有强一致性的视图更重要。
- 许多时间序列是短暂的。通常有只持续几个小时然后消失的时间序列,例如新启动并报告了一段时间的主机然后被关闭。
从假设中可以看出,我们主要谈论的是具有大范围删除的插入操作。争议性更新并未涉及,这使得冲突解决成为更容易处理的问题。关于这一点,我们稍后再谈,现在让我们来讨论具体的要求。
要求
- 水平可扩展性 - 设计应该能够最初支持几百个节点,但能够扩展到几千个节点而不需要完全重构。读取和写入应该与节点数量成线性增长。
- 可用性 - 对于读取和写入路径,倾向于AP设计。时间序列不断移动,我们很少需要最新数据的完整一致视图。
- 应该能够支持数十亿个时间序列,其中单个时间序列由测量名称加标签集组合表示。这源于短暂的假设。
- 弹性 - 节点应该能够离开集群并加入新节点。对于0.9.0版本,这不需要是自动的,但设计应该在未来版本中实现这一点,而不需要重构。
要求相当直接。大部分是您期望的水平可扩展分布式系统,该系统旨在实现最终一致性。对于InfluxDB整体还有一些其他要求,但这些是驱动聚类设计的要求。
如果您觉得这些需求和假设有问题,请告诉我们。但它们清楚地表明,InfluxDB 并未设计为通用数据库。如果您需要这样的数据库,请到其他地方寻找。InfluxDB 专为时间序列、指标和数据分析数据设计,这些数据具有高吞吐量的插入工作负载。
集群设计
新的集群设计包含两个系统:一个用于存储集群元数据的 CP 系统,以及一个用于处理读写操作的 AP 系统。本节将涵盖每个系统的设计,并在最后部分展示这两个系统在配置好的集群中是如何协同工作的。
集群元数据 - CP
集群元数据系统是一个 Raft 集群,用于存储关于集群的元数据。具体来说,它存储以下内容:
- 集群中的服务器 - 唯一标识符、主机名、是否运行集群元数据服务
- 数据库、保留策略和连续查询
- 用户和权限
- 分片组 - 开始和结束时间、分片
- 分片 - 唯一分片标识符、具有分片副本的服务器标识符
对任何这些数据的更新都必须通过集群元数据服务执行。该服务是一个简单的 HTTP API,由 Hashicorp Raft 实现提供支持,使用 BoltDB 作为底层存储引擎。
集群中的每个服务器都保留了一份集群元数据的内存副本。每个服务器将定期刷新整个元数据集以获取任何更改。对于传入的请求,如果发生缓存未命中(例如,它尚未了解的数据库),它将从 CP 服务请求相关信息。
写入 - AP
对于处理读写操作,我们利用了时间序列数据几乎总是新的不可变数据这一事实。这意味着我们可以绕过如向量时钟或推送到客户端的冲突解决方案。我们优先考虑接受写和读操作,而不是强一致性。
有一个注意事项是,分片组和分片是在 CP 系统中创建的。在正常运行期间,这些将在数据写入之前创建。然而,这意味着如果某个节点长时间从 CP 系统中隔离出来,它将无法写入数据,因为它将没有分片组定义。
例如,让我们看看写入操作发生时会发生什么。假设我们有一个由 4 个服务器组成的集群
并且数据由服务器 2-3 拥有。
为了确定哪个服务器拥有数据,我们查看点的时间戳以确定它属于哪个分片组。如果分片组不存在,我们将调用 CP 集群元数据服务以获取或创建该时间范围的分片组。
元数据服务返回的分片组应该包含分片,并且这些分片应该分配给集群中的服务器。因此,集群元数据服务负责在集群中布局数据的位置。当可能时,分片组会提前创建,以避免在新的时间范围出现时,大量写入操作冲击集群元数据服务。
一旦我们有了分片组,我们将测量名称和标签集进行哈希,然后对分片数取模,以确定数据点应该写入组中的哪个分片。请注意,这不是一个一致性哈希算法。我们不使用一致性哈希的原因是因为一旦分片组停止写入,我们不需要担心在重新调整大小的集群中平衡这些分片。而提高读取可扩展性就像将停止写入的分片复制到集群中的其他服务器一样简单。
数据的关键是测量名称、标签集和纳秒时间戳。例如,假设分片存在于服务器2、3和4上。
接下来会发生什么取决于写入操作请求的一致性级别。不同的级别是
- 任何 - 如果拥有数据的任何服务器都接受写入,或者接收服务器(在我们的例子中是1)将数据作为持久的提示交手写入(关于这一点稍后介绍),则成功
- 一个 - 如果拥有数据的任何服务器(2、3、4)响应成功,则成功
- 法定人数 - 当n/2 + 1个服务器接受写入时,成功,其中n是拥有分片副本的服务器数量
- 所有 - 当所有服务器(2、3、4)都接受写入时,成功
写入数据请求是并行进行的,当达到请求级别时,将成功返回给客户端。
写入失败
当写入失败或部分成功时会发生什么?例如,假设您请求法定人数,但只能写入到主机2,而主机3和4超时。
此时,我们利用我们对时间序列数据的一个关键假设:写入总是为新数据,该数据以客户端提供的信息为键。
我们向客户端返回部分写入失败。同时,写入可能在后台通过提示交手进行复制。然后客户端可以选择失败,或者再次发出请求,这将仅覆盖现有值。但重要的是要注意,失败的局部写入最终可能会被接受并完全复制。
提示交手
提示交手对于从服务器重启或GC暂停或大查询导致系统过载等短期中断中快速恢复非常有用。
在我们之前的例子中,当写入到达服务器1时,它将尝试将那个点写入到服务器2、3和4。如果写入一致性是法定人数或更低,写入可以使用提示交手对任何故障服务器进行操作。
例如,如果我们有一个法定人数级别的写入,拥有分片的服务器(2、3或4)中的2个将必须接受写入。然后成功返回给客户端。然后假设由于某种原因服务器4超时了。服务器1然后将数据点作为提示交手写入。
提示交手是服务器错过任何写入的持久写入队列。因此,服务器1现在会在磁盘上存储服务器4的写入。当服务器4恢复时,服务器1会将其提示交手队列中的写入推送到服务器4。
我们应该有提示交手队列的最大大小设置。来自提示交手队列的写入也应节流,以避免在最近因中断恢复的服务器上过载。
如果提示交手写入达到其TTL或服务器1为其服务器4的队列填满,服务器4将不得不通过反熵修复过程进行恢复。
反熵修复
反熵修复确保我们最终在我们的所有数据上获得一致性。集群中的服务器会偶尔交换信息,以确保它们拥有相同的数据。反熵是针对服务器已关闭较长时间且提示交手无法缓冲所有必要写入的情况。
对于每个分片,将使用Merkle树与拥有该分片的其它服务器进行比较。在这里,我们将再次利用时间序列的假设:所有写入都是对新数据的写入,并且我们希望获取所有这些数据。
因此,如果两个服务器发现某个分片的数据存在不匹配,它们将一起遍历Merkle树以找到不匹配的数据点。它们将交换它们的不同数据,并将分片成为来自服务器的该分片数据的并集。
在时间序列数据中,另一个关键的假设将帮助我们减轻比较的负担。具体来说,即所有写入的数据都是近期时间框架的,除非加载历史数据。
因此,我们将不经常检查旧分片的一致性。可能每小时或每天检查一次,这将是可配置的。完整的修复是用户可以请求的操作(类似于Cassandra的节点修复工具)。
对于写入活跃的分片,我们只对超过一些小的可配置时间的数据运行比较。例如,我们只对超过5分钟的分片中的数据进行哈希比较。
冲突解决
只有在用户对现有数据点进行更新时,冲突解决才会发挥作用。我们处理冲突解决的方案很简单:较大的值获胜。如果存在数据在一个或多个服务器上缺失的情况,则最终集应该是来自所有服务器的数据并集。
这使我们的冲突解决开销极低。不需要任何向量时钟。
因此,更新和删除操作应设置ALL一致性级别以确保它们成功,并且不会丢失。如果没有ALL响应,则在反熵修复启动时,删除可能会被撤销或更新被覆盖。
这意味着耐久删除和更新不是高可用的。我们的假设是这两个操作都很少发生。
这个方案的一个限制是,由保留策略强制执行的数据保留实际上是最终一致的。每个节点将对其分片执行本地保留策略。
删除大量历史数据段
如果使用ALL一致性级别,通过正常写入路径运行的删除操作不是高可用的。然而,删除操作可以删除旧历史数据的完整分片,这可能会更加容错。
这是因为删除分片将通过集群元数据服务进行。记住,分片是按时间顺序排列的数据的连续块。删除大量旧历史数据最高效的方法是删除整个分片或分片组。
这些操作是在CP集群元数据服务上执行的。请求可以来自集群中的任何服务器,并将相应地路由。
可调查询一致性
聚类功能的初始版本将检查每个分片都有一个副本的单个服务器,从而使实际的一致性级别为ONE
。后续版本将启用可调级别的一致性。
结论
经过所有这些,你可能会问,这个设计在CAP谱上处于什么位置?答案是,它既不是纯CP系统,也不是纯AP系统。其中有一部分是CP,但该系统的数据在集群中最终是一致的。还有一部分是AP,但如果节点和CP系统之间存在足够长的分区,那么这些部分将无法访问。
关于聚类设计,我们还可以讨论很多内容。特别是如何处理每个故障情况。关于对集群元数据的删除和更新,我们还可以讨论更深入细节。在接下来的几个月里,我们将致力于记录这些内容,并详细阐述每一个案例。在生产环境中,这些情况比CP或AP更为重要,我们将努力提供尽可能多的信息和数据。
此设计基于InfluxDB中三次聚类迭代。我们根据我们认为可以接受的权衡,并专注于我们用例的一些最重要的部分:具有低写入开销的水平扩展。欢迎反馈、想法甚至挑战!