如何设计一个系统,我们可以在其中查询最近 n 小时内的热门结果

How to design a system in which we can query top results in last n hours

我在面试中被问到这个问题。细节是假设我们得到数百万个事件。每个事件都有时间戳和其他详细信息。系统设计要求能够使最终用户查询最近 10 分钟或 9 小时或可能是 3 个月内最频繁的记录。

事件如下

event_type: {CRUD + Search}
event_info: xxx
timestamp : ts...

我认为您需要将数据持久保存到磁盘,因为

  • 查询时长超级模糊,数据可能会因为某些不可预见的情况而丢失,如进程终止、机器故障等

  • 由于内存原因,您无法将所有事件保留在内存中 约束(百万事件)

我建议使用 mysql 作为数据存储,并将时间戳作为索引键之一。但是两个事件可能具有相同的时间戳。所以做一个自增id+时间戳的复合索引键

Mysql的优点:

  • 复制超级可靠
  • 支持各种CRUD操作和查询

在每个查询中,您基本上可以根据需要获取时间戳的范围。

  • 先数数。满足查询的事件。

    select count(*) from `events` where timestamp >= x and timestamp <=y.
    
  • 如果满足查询的事件过多,则分批查询。

     select * from 'events' where timestamp >= x and timestamp <=y limit 1000 offset 0; 
     select * from 'events' where timestamp >= x and timestamp <=y limit 1000 offset 1000;
    

    依此类推..直到偏移量 <= 与第一个查询匹配的事件数。

弄清楚这一点的最简单方法是查看其他流处理或 map reduce 库如何执行此操作(我感觉您的面试官已经看到了这些库)。它基本上是实时地图减少(您也可以查找它是如何工作的)。

我将概述事件处理的两种技术。事实上,大多数公司都需要两者兼顾。

新学流处理(实时)

让我们假设他们现在不想要实际事件,而是更可能的聚合情况(我认为这是你问题的意图)

一个示例流处理项目是 pipelinedb(他们在主页底部有它的工作原理)。

  1. 事件开始使用 queue/ring 缓冲区
  2. 工作进程分批读取这些事件并将它们汇总到部分存储桶或 window。
  3. 最后是合并器或缩减器,它接收微批次并实际进行更新。一个例子是事件计数。因为我们使用的是来自上述事件的队列,并且根据队列的不同,我们可能会有多个消费者执行组合操作。

因此,如果您想要分钟计数,您可以每分钟进行汇总,并且只存储该分钟的事件总和。事实证明这相当小 space 明智,因此您可以将其存储在内存中。

如果您想要这些月、日甚至年的计数,您只需将所有分钟计数桶相加即可。

现在这个技术当然有一个大问题。您需要知道您希望先验收集哪些聚合和枢轴。

但是您可以非常快速地查找结果。

老式数据仓库(分区)和 Map Reduce(批处理)

现在假设他们确实想要特定时间段内的实际事件。这很昂贵,因为如果您将所有事件存储在一个地方,查找和检索就会很困难。但是,如果您使用时间是分层的这一事实,您可以将事件存储在元组树中。

您想要实际事件的原因是因为您正在进行即席查询并且愿意等待查询执行。

  1. 您需要某种队列来处理事件流。
  2. 工作人员读取队列并根据时间对事件进行分区。例如,您将有一个特定日期的分区。这类似于分片。许多存储系统都支持这个(例如 postgres 分区)。
  3. 当您想要在一段时间内发生一定数量的事件时,您可以合并分区。
  4. 分区本质上是分层的(分钟 < 小时 < 天等),这意味着您可以对它们进行类似树的操作。

有一些方法可以存储此类事件,称为时间序列数据,这样分区索引是自动且快速的。这些称为 TSDBs,您可以 google 了解更多信息。

一个示例 TSDB 产品是 influxdb。

现在回到时间(或至少人类如何表示它)是有组织的树这一事实,我们可以执行并行化操作。这是因为树是 DAG(有向无环图)。使用 DAG,您可以进行一些分析并基本上递归地对分支进行操作(也称为 fork/join)。

一个示例通用并行存储产品是 citusdb。

当然,这种方法有一个很大的缺点。它是昂贵的!即使您通过增加节点数量来加快速度,您也必须为这些节点(分布式分片)付费。理论上性能应该线性扩展,但实际上这不会发生(我会为你保存细节)。