将高频事件保存到连接限制受限的数据库中


13

我们有一种情况,我必须处理大量涌入服务器的事件,平均每秒大约1000个事件(峰值可能是2000个)。

问题

我们的系统托管在Heroku上,并使用相对昂贵的Heroku Postgres DB,该数据库最多允许500个DB连接。我们使用连接池从服务器连接到数据库。

事件传入的速度快于数据库连接池无法处理的速度

我们遇到的问题是事件的发生速度快于连接池无法处理的速度。到一个连接完成从服务器到DB的网络往返时,它可以释放回池中,而不是n其他事件。

最终,事件堆积起来,等待保存,并且由于池中没有可用的连接,它们超时并且整个系统变得无法运行。

我们已经通过以较慢的速度从客户端发出有问题的高频事件来解决紧急情况,但是我们仍然想知道在需要处理高频事件时如何处理这种情况。

约束条件

其他客户端可能希望同时读取事件

其他客户端连续请求使用特定密钥读取所有事件,即使它们尚未保存在数据库中也是如此。

客户端可以查询GET api/v1/events?clientId=1并获取客户端1发送的所有事件,即使这些事件尚未保存到DB中也是如此。

是否有有关如何处理此问题的“教室”示例?

可能的解决方案

使事件排队在我们的服务器上

我们可以在服务器上排队事件(队列的最大并发性为400,因此连接池不会用完)。

这是个坏主意,因为:

  • 它将耗尽可用的服务器内存。堆积的排队事件将消耗大量RAM。
  • 我们的服务器每24小时重启一次。这是Heroku施加的硬限制。当事件排队时,服务器可以重新启动,导致我们丢失排队的事件。
  • 它在服务器上引入状态,从而损害了可伸缩性。如果我们有一个多服务器设置,并且客户端要读取所有已排队+保存的事件,则我们将不知道已排队事件在哪台服务器上。

使用单独的消息队列

我假设我们可以使用消息队列(例如RabbitMQ吗?),在其中将消息泵入其中,另一方面,还有另一台服务器仅处理将事件保存在DB上。

我不确定消息队列是否允许查询排队的事件(尚未保存),因此,如果另一个客户端想要读取另一个客户端的消息,我只能从数据库中获取已保存的消息,并从队列中获取待处理的消息。并将它们连接在一起,这样我就可以将它们发送回读取请求客户端。

使用多个数据库,每个数据库使用中央数据库协调器服务器保存一部分消息,以管理它们

不过,我们的另一个解决方案是使用多个数据库,并使用一个中央“ DB协调器/负载平衡器”。接收到事件后,此协调器将选择一个数据库来写入消息。这应该允许我们使用多个Heroku数据库,从而将连接限制提高到500 x数据库数。

在进行读取查询时,此协调器可以SELECT向每个数据库发出查询,合并所有结果,然后将其发送回请求读取的客户端。

这是个坏主意,因为:

  • 这个主意听起来像是...太设计了吗?管理(备份等)也将是一场噩梦。它的构建和维护非常复杂,除非绝对必要,否则听起来像是违反了KISS
  • 它牺牲了一致性。如果我们遵循这个想法,那么跨多个数据库进行事务是不可行的。

3
您的瓶颈在哪里?您提到的是连接池,但这仅影响并行性,而不影响每次插入的速度。如果您有500个连接,例如2000QPS,那么如果每个查询在250ms(这是一个很长的时间)内完成,这应该可以正常工作。为什么在15ms以上?还要注意,通过使用PaaS,您将放弃大量的优化机会,例如扩展数据库硬件或使用只读副本来减少主数据库上的负载。除非部署是您最大的问题,否则Heroku不值得。
阿蒙(Amon)

@amon瓶颈确实是连接池。我已经ANALYZE在查询本身上运行,它们不是问题。我还构建了一个原型来测试连接池假设,并验证了这确实是问题所在。数据库和服务器本身位于不同的计算机上,因此存在延迟。另外,除非绝对必要,否则我们不想放弃Heroku,而不必担心部署对我们来说是一个巨大的好处。
Nik Kyriakides

1
话虽如此,我知道我可以做一些微优化来帮助我解决当前的问题。我想知道是否有可扩展的体系结构解决方案来解决我的问题。
Nik Kyriakides

3
您如何确切地验证连接池是问题所在?@amon在他的计算中是正确的。尝试发布select null500个连接。我敢打赌,您会发现连接池不是那里的问题。
usr

1
如果选择null有问题,那么您可能是对的。尽管将所有时间都花在哪里会很有趣。没有网络那么慢。
usr

Answers:


9

输入流

尚不清楚您的1000个事件/秒是否代表峰值或是否是连续负载:

  • 如果是高峰,则可以使用消息队列作为缓冲区,以将负载分散到更长的时间;
  • 如果负载恒定,则仅消息队列是不够的,因为DB服务器将永远无法追赶。然后,您需要考虑一个分布式数据库。

拟议的解决方案

直观地,在两种情况下,我都会选择基于Kafka的事件流:

  • 所有事件都系统地发布在kafka主题上
  • 消费者将订阅事件并将其存储到数据库。
  • 查询处理器将处理来自客户端的请求并查询数据库。

这在所有级别上都是高度可扩展的:

  • 如果数据库服务器是瓶颈,则只需添加几个使用者。每个人都可以订阅该主题,然后写入不同的数据库服务器。但是,如果分布在数据库服务器之间随机发生,则查询处理器将无法预测要使用的数据库服务器,而必须查询多个数据库服务器。这可能会导致查询方面出现新的瓶颈。
  • 因此,可以通过将事件流组织成几个主题(例如,使用键或属性组来根据可预测的逻辑对DB进行分区)来预期DB分配方案。
  • 如果一个消息服务器不足以处理不断增长的输入事件,则可以添加kafka分区,以在多个物理服务器上分布kafka主题。

向数据库提供尚未写入数据库的事件

您希望您的客户也能够访问仍在管道中但尚未写入数据库的信息。这有点微妙。

选项1:使用缓存来补充数据库查询

我没有进行深入分析,但是我想到的第一个想法是使查询处理器成为kafka主题的使用者,但成为另一个kafka使用者组。然后,请求处理器将独立接收DB编写器将接收的所有消息。然后可以将它们保留在本地缓存中。然后,查询将在DB +高速缓存上运行(消除重复项)。

设计将如下所示:

在此处输入图片说明

此查询层的可伸缩性可以通过添加更多查询处理器(每个处理器在其自己的使用者组中)来实现。

选项2:设计双重API

恕我直言,更好的方法是提供双重API(使用单独的消费者组机制):

  • 查询API,用于访问数据库中的事件和/或进行分析
  • 流API,仅直接转发来自主题的消息

好处是,您可以让客户决定有趣的事情。这样可以避免当客户端仅对新的传入事件感兴趣时将系统地将DB数据与新兑现的数据合并。如果确实需要在新事件和已归档事件之间进行微妙的合并,那么客户将不得不对其进行组织。

变体

我之所以提出kafka是因为它是为具有持久性消息的超大容量而设计的,因此您可以根据需要重新启动服务器。

您可以使用RabbitMQ构建类似的体系结构。但是,如果您需要持久队列,则可能会降低性能。另外,据我所知,几个读取器(例如writer + cache)使用RabbitMQ并行消耗相同消息的唯一方法是克隆队列。因此,更高的可扩展性可能会付出更高的代价。


恒星; 你是什么意思a distributed database (for example using a specialization of the server by group of keys)?还有为什么用Kafka代替RabbitMQ?选择一个而不是另一个特定的原因吗?
Nik Kyriakides

@NicholasKyriakides谢谢!1)我只是想着几个独立的数据库服务器,但是有一个清晰的分区方案(键,地理位置等),可以用来有效地分配命令。2)直观地讲,也许是因为Kafka是为很高的吞吐量而设计的,而持久消息需要重新启动服务器?)。我不确定RabbitMQ是否适用于分布式方案,并且持久队列会降低性能
Christophe

对于1),这与我的Use multiple databases想法非常相似,但是您是在说我不应该只是随机地(或循环地)将消息分发到每个数据库。对?
Nik Kyriakides

是。我的第一个想法是不要进行随机分配,因为这可能会增加查询的处理负荷(即,大多数情况下两个数据库的查询)。您还可以考虑使用分布式数据库引擎(例如,Ignite吗?)。但是要做出任何明智的选择,都需要对数据库的使用模式有充分的了解(数据库中还有其他内容,查询的频率,什么样的查询,除了单个事件之外的事务约束等)。
克里斯托弗(Christophe)

3
只是想说,即使kafka可以提供很高的吞吐量,但它可能超出了大多数人的需求。我发现处理kafka及其API对我们来说是一个大错误。RabbitMQ并非
毫无懈可击

11

我的猜测是,您需要更仔细地探索被拒绝的方法

  • 使事件排队在我们的服务器上

我的建议是开始阅读有关LMAX架构的各种文章。他们设法针对其用例进行大量的批处理工作,并且有可能使您的权衡看起来更像他们的。

另外,您可能想看看是否可以将读取清除掉-理想情况下,您希望能够独立于写入而扩展读取。这可能意味着要研究CQRS(命令查询责任隔离)。

当事件排队时,服务器可以重新启动,导致我们丢失排队的事件。

在分布式系统中,我认为您可以确信消息将丢失。您可以通过谨慎地对待序列障碍来减轻这种影响(例如-确保在事件在系统外部共享之前进行对持久性存储的写操作)。

  • 使用多个数据库,每个数据库使用中央数据库协调器服务器保存一部分消息,以管理它们

也许-我更有可能查看您的业务范围,以查看是否存在自然的数据分片场所。

在某些情况下,丢失数据是可以接受的折衷方案?

好吧,我想可能会有,但是那不是我要去的地方。关键是设计应该内置在遇到消息丢失时所需的鲁棒性。

这通常看起来是带有通知的基于请求的模型。提供程序将消息写入有序的持久性存储中。消费者从商店中提取消息,跟踪自己的最高水位。推送通知被用作减少延迟的设备-但如果通知丢失,则仍会(最终)提取消息,因为消费者按固定的时间表进行推送(不同之处在于,如果收到通知,则提早发生)。

请参阅Udi Dahan(已由Andy引用)和Greg Young 撰写的Polyglot Data,作者:Udi Dahan(已由Andy引用)。


In a distributed system, I think you can be pretty confident that messages are going to get lost。真?在某些情况下,丢失数据是可以接受的折衷方案?我的印象是丢失数据=失败。
Nik Kyriakides

1
@NicholasKyriakides,通常是不可接受的,因此OP建议在发出事件之前可以写入持久性存储。请查看这篇文章以及Udi Dahan的这段视频,他将详细解决该问题。
安迪

6

如果我正确理解当前的流程是:

  1. 接收和事件(我通过HTTP承担吗?)
  2. 从池中请求连接。
  3. 将事件插入数据库
  4. 释放到池的连接。

如果是这样的话,我认为对设计的第一个更改将是停止在每个事件上使您甚至处理的代码都返回到池的连接。而是创建一个插入线程/进程池,该池与数据库连接数为1比1。这些将各自拥有专用的数据库连接。

然后使用某种并发队列,使这些线程从并发队列中提取消息并插入它们。从理论上讲,他们永远不需要将连接返回到池中或请求新的连接,但是在连接变坏的情况下,您可能需要进行处理。终止线程/进程并启动新线程/进程可能是最简单的。

这应该有效消除连接池开销。您当然需要能够在每个连接上每秒至少推送1000个/连接事件。您可能想尝试不同数量的连接,因为在同一表上使用500个连接可能会在数据库上引起争用,但这是完全不同的问题。要考虑的另一件事是使用批处理插入,即每个线程提取大量消息并一次全部推送它们。另外,请避免让多个连接尝试更新相同的行。


5

假设条件

我将假设您描述的负载是恒定的,因为这是要解决的更困难的情况。

我还要假设您有某种方式可以在Web应用程序流程之外运行触发的,长期运行的工作负载。

假设您已正确识别瓶颈-进程与Postgres数据库之间的延迟-这是要解决的主要问题。解决方案需要考虑您与其他希望在收到事件后尽快读取事件的其他客户端的一致性约束。

要解决延迟问题,您需要以一种最小化每个事件要存储的延迟量的方式进行工作。如果您不愿意或无法更改硬件,这是您需要实现的关键。鉴于您使用的是PaaS服务,并且无法控制硬件或网络,因此减少每个事件的延迟的唯一方法是对事件进行某种批量写入。

您将需要在本地存储一系列事件,这些事件将在达到给定大小或经过一段时间后定期刷新并定期写入数据库。进程将需要监视此队列以触发刷新到商店。关于如何管理以您选择的语言定期刷新的并发队列,应该有很多示例- 这是C#中的示例,它来自流行的Serilog日志库的定期批处理接收器。

这个SO答案描述了在Postgres中刷新数据的最快方法-尽管这将需要您的批处理将队列存储在磁盘上,并且当在Heroku中重启后磁盘消失时,可能有一个需要解决的问题。

约束

另一个答案已经提到CQRS,这是解决约束的正确方法。您希望在处理每个事件时合并读取模型- 介体模式可以帮助封装事件并将其分发给正在处理的多个处理程序。因此,一个处理程序可以将事件添加到客户端可以查询的内存中的读取模型中,而另一个处理程序可以负责对该事件进行排队以进行最终的批量写入。

CQRS的主要好处是您可以将概念上的读写模型分离开来-这是一种奇特的说法,即您写入一个模型,然后从另一个完全不同的模型中进行读取。为了从CQRS中获得可伸缩性的好处,您通常希望确保以适合其使用方式的最佳方式分别存储每个模型。在这种情况下,我们可以使用聚合读取模型(例如,Redis缓存或仅在内存中)来确保读取快速且一致,同时仍使用事务数据库将数据写入其中。


3

事件传入的速度快于数据库连接池无法处理的速度

如果每个进程都需要一个数据库连接,则会出现问题。系统的设计应使您拥有一组工作程序,其中每个工作程序仅需要一个数据库连接,并且每个工作程序可以处理多个事件。

消息队列可以与该设计一起使用,您需要消息生成器将事件推送到消息队列,并且工作程序(使用者)处理队列中的消息。

其他客户端可能希望同时读取事件

仅当事件存储在数据库中而没有任何处理(原始事件)时,才可以使用此约束。如果在存储事件之前先处理事件,那么获取事件的唯一方法就是从数据库中获取事件。

如果客户只想查询原始事件,那么我建议使用像Elastic Search这样的搜索引擎。您甚至可以免费获得查询/搜索API。

鉴于在将事件保存到数据库之前对事件进行查询似乎对您很重要,因此像Elastic Search这样的简单解决方案应该可以使用。您基本上只是将所有事件存储在其中,而不会通过将它们复制到数据库中来复制相同的数据。

扩展弹性搜索很容易,但是即使使用基本配置,它的性能也很高。

当您需要处理时,您的流程可以从ES中获取事件,进行处理并将其存储在数据库中。我不知道此处理需要什么性能水平,但是它将与从ES查询事件完全分开。无论如何,您都不会出现连接问题,因为您可以有固定数量的工作程序,每个工作程序只有一个数据库连接。


2

如果数据库具有适当的架构和存储引擎,则每秒1k或2k事件(5KB)对于数据库来说并不算多。如@eddyce所建议,具有一个或多个从属的主控器可以将读取查询与提交写入分开。使用更少的数据库连接将为您提供更好的总体吞吐量。

其他客户端可能希望同时读取事件

对于这些请求,它们还需要从主数据库读取数据,因为到读取的从数据库存在复制滞后。

我已经将(Percona)MySQL与TokuDB引擎结合使用,以进行大量写入。还有一个基于LSMtree的MyRocks引擎,非常适合写入负载。对于这些引擎以及PostgreSQL,都有用于事务隔离以及提交同步行为的设置,这些设置可以显着增加写容量。过去,我们最多接受1s丢失的数据,这些数据被报告为已提交给db客户端。在其他情况下,可以使用电池供电的固态硬盘来避免丢失。

MySQL风格的Amazon RDS Aurora声称具有零倍的复制成本(与从站与主站共享文件系统)具有6倍的高写入吞吐量。Aurora PostgreSQL风格还具有不同的高级复制机制。


TBH在足够硬件上的任何管理良好的数据库都应能够应对此负载。OP的问题似乎不是数据库性能,而是连接延迟。我的猜测是Heroku,因为PaaS提供商正在向他们出售另一个AWS区域中的Postgres实例。
阿蒙(Amon)

1

我将所有的heroku放在一起,也就是说,我放下了一种集中式的方法:多次写入使最大池连接达到峰值是发明数据库集群的主要原因之一,主要是导致您不加载写入内容具有读取请求的数据库可以由集群中的其他数据库执行,此外,我将尝试使用主从拓扑结构-正如其他人已经提到的那样,拥有自己的数据库安装将使调整整个数据库成为可能系统以确保将正确处理查询传播时间。

祝好运

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.