背景

历史上,Mixpanel 曾经以秒级粒度跟踪事件。例如,您可以跟踪时间戳为 2006 年 1 月 2 日凌晨 3:04:05 的事件。当在同一秒内跟踪多个事件时,这会导致问题。

sharding查询的底层原理(以毫秒为粒度跟踪事件)(1)

在同一秒内发生的事件没有正确排序

一旦被追踪,就无法知道这些事件发生的真实顺序。在内部,我们按照事件时间的递增顺序对事件进行排序,但如果两个事件发生在同一时间戳,我们将按照事件名称的字典顺序对它们进行排序。

这使得很难进行依赖于正确排序事件的准确分析。例如,我们的 Flows 报告通常如下所示:

sharding查询的底层原理(以毫秒为粒度跟踪事件)(2)

Mixpanel 中的一个常见操作序列是查看仪表板,然后通过单击报告进行更深入的挖掘。然而,在我们的内部数据中,我们经常看到相反的序列 ( Viewed Report→ [Web Dash] Viewed Dashboard),而几乎从未看到预期的序列。Report Loaded之前出现Viewed Report是另一个不一致的例子,因为事件发生在同一秒内。

执行

我们的内部数据库由两种不同格式的文件组成,每种格式都针对不同的用例进行了优化。

  1. 一种基于行的仅附加格式,允许快速将新事件附加到末尾。这用于摄取新事件——写入吞吐量是关键的情况。
  2. 基于行的文件定期创建的基于列的不可变格式,并针对查询进行了优化。

我们基于行的二进制格式过去只支持 4 字节的事件时间戳。这造成了两个问题——

  1. 4 字节整数不能存储任何毫秒粒度的 Unix 时间戳
  2. “ 2038 年问题”——2038 年 1 月 19 日之后的任何 Unix 时间戳(以秒为单位)都不能在不溢出的情况下放入 4 字节整数。

sharding查询的底层原理(以毫秒为粒度跟踪事件)(3)

我们的柱状格式已经支持8字节的时间戳,但是由于上一层只有4字节的时间戳,毫秒部分在写入柱状文件之前被截断了。

我们考虑的一种选择是——我们之前在基于行的格式中保留了一些保留字节以供将来使用。这些总是设置为零——这是设计文件格式时的一种有用做法。理论上,我们可以使用这些来存储时间戳的毫秒偏移量。例如,对于发生在凌晨 3:04:05.678 的事件,我们可以将秒级时间存储在我们已有的 4 字节时间戳中,并将毫秒部分(在本例中为 678)存储在额外的保留字节中通过使用 10 位。我们决定反对,因为 -

  1. 它在读取和写出基于行的格式时增加了额外的代码复杂性。
  2. 它没有解决上述“2038 问题”。

我们采用的方法是将基于行的文件格式升级为使用 8 字节整数而不是 4 字节整数。这解决了上述两个问题,但缺点是 -

  1. 使用一些额外的空间(但这与平均事件大小相比可以忽略不计)。
  2. 不向后兼容,这意味着需要额外的工程工作,并且存在更高的破坏数据完整性的风险。

以上内容涵盖了我们最内层数据库存储层所需的更改。我们还需要更新在此之上的所有数据摄取层。例如,实际向 Mixpanel 发送数据的客户端 SDK 并不总是发送毫秒级的时间戳(事件发生的时间需要在客户端设置,以避免网络延迟和延迟导致的不准确)。此外,我们处理这些请求的入口 API 服务器在所有情况下都不遵守毫秒格式的时间戳。

挑战

一个相关的问题是在高度确信不会损害数据完整性的情况下安全地更改文件格式。我们有现有的工具来安全地测试对我们的柱状格式的更改。

但是,我们需要为基于行的格式提供类似的工具。基于行的文件在我们摄取事件时不断创建和更新,这与在单个时间点定期创建的列式文件不同。这使得使用现有架构来测试生成格式略有不同的文件的两组进程变得更加困难。

事件如何保存到文件的高级概述:

sharding查询的底层原理(以毫秒为粒度跟踪事件)(4)

该Compacter服务从基于行的文件创建分栏文件。这个过程的变化相当频繁,所以我们有一个专门的试飞系统来安全地部署这些变化。对于一小部分生产流量,我们使用新版本或实现进行双写,并将生成的文件与原始文件进行比较。任何意想不到的差异都会被标记出来,我们会在手动调查之前推迟推出新的实施。这里的一个关键点是,无论结果如何,飞行版本都不会影响生产数据。

sharding查询的底层原理(以毫秒为粒度跟踪事件)(5)

但是,这完全独立于Event Writer附加到基于行的文件的服务。由于我们更改的是基于行的格式,因此我们无法利用现有系统。

我们在服务中实施了一个类似的飞行系统,Event Writer该系统将为一小部分生产流量对新版本的基于行的格式进行双重写入。然后它会:

这样,我们就可以逐步推出对行格式的更改。

我们还有最后一项挑战要处理。由于我们的用户希望事件数据在摄取数据后的短时间内可查询,因此我们还对存储在基于行的文件中的任何数据运行用户发送的查询。因此,许多不同的服务需要知道如何读取和处理这种基于行的格式。这些服务以多种语言实现,因此我们有针对这些文件的每种语言的现有阅读器实现。因此,在我们开始以这种格式写出文件之前,我们需要更新所有这些读取器以便能够读取新格式。

虽然这不是主要的技术挑战,但它意味着对现有读者以及他们对事件时间戳所做的任何隐含假设进行审核。实际上,这还要求我们更新整套单元和集成测试以在两种文件格式上运行,同时还全面测试新文件格式提供的任何新功能。

推出

我们的 GCP 集群中有两个区域来提供一个额外的复制层。我们也经常将这些用于安全推出——在一个区域中发布更改,并在遇到问题时回滚。这样,即使更改导致数据完整性丢失,我们也可以保证始终拥有一份完全正确的数据副本。

这可能与两个区域之间的数据一致性不一致,因为这两个区域现在可以同时运行不同版本的代码,从而导致区域漂移。通常,这不是问题,因为我们绝对需要保证的唯一一件事是存储的数据在两个区域中在逻辑上是等效的,即使它不是逐字节相同的。我们的大部分更改只会影响数据的内部表示,而不会影响它们的逻辑等价性。

对于这个特殊的变化,事实并非如此——如果我们一次在一个区域进行简单的部署,我们最终会遇到这样一种情况,即同一事件在一个区域有毫秒时间戳,而在另一个区域没有。这将导致 Mixpanel 报告中的不同结果,具体取决于我们从哪个区域获取数据。

为了解决这个问题,我们将部署分为两个阶段 -

  1. 推出新的基于行的格式(即 8 字节事件时间戳),而不实际存储毫秒级信息。
  2. 推出毫秒级信息

这样,我们可以在安全地推出新更改和确保数据一致性之间实现合理(尽管不完美)的权衡。

确切的步骤是 -

  1. 运行新格式(但没有毫秒级时间戳)
  2. 在一个区域推出新格式
  3. 经过几天的监控,在二区铺开
  4. Flight 启用毫秒级时间戳的更改
  5. 在两个区域启用毫秒级时间戳。

一个关键标准是,如果在任何步骤出现问题,我们应该能够回滚而不会丢失数据完整性或一致性。实际上,我们在推出过程中还有几个阶段——首先只启用内部数据的更改,一旦我们确信我们保持了完整性和一致性,就为客户数据启用它。

结论

我们成功地完成了部署,并且没有影响数据的完整性或一致性。除了在影响生产数据之前发现了一些小错误外,整个过程还算顺利。

事件现在可以正确排序,即使它们快速连续触发也是如此。查看我们一开始看到的同一份报告:

sharding查询的底层原理(以毫秒为粒度跟踪事件)(6)

事件现在按预期顺序显示:[Web Dash] Viewed Dashboard→ Viewed Report→Report Loaded现在是常见的操作顺序。

我们的最后一个假设是此功能将有助于改进我们的流量报告的使用。

sharding查询的底层原理(以毫秒为粒度跟踪事件)(7)

根据数字,这是事实。在发布毫秒级时间戳后,我们的 Flows 报告的使用量增幅已经高于其他报告。当然,由于这不是一个对照实验,这种增加也可能是由与该项目无关的其他版本和因素引起的。

作者:Jayant Jain

出处:https://engineering.mixpanel.com/tracking-events-at-milli-second-granularity-7d1fc7f29e31

,