存储和分区
当构建一个需要存储功能的重放系统时,通常我们会使用基于 Hadoop 和 HDFS 的架构。而我们这里选择了 Apache Kafka,主要基于以下两个原因:
我们利用实时管道(real-time pipeline)来构建回放管道(replay pipeline),首先要确保应该交付给每个开发者的事件都存储在 Kafka 上。我们把对应的 Kafka 主题称为 delivery_log,每个数据中心都有一个。然后,这些主题在两个数据中心交叉复制,以确保冗余,从而使得任一个数据中心都可以处理重放请求。
在这个 Kafka 主题中,我们使用默认的分区策略(也就是哈希分区)创建了多个分区。开发人员的 webhookId 哈希值对应分区 ID,它是每个记录的键。我们考虑过使用静态分区,但最终决定不使用它,因为如果一个开发人员生成的事件多于其他开发人员,那么一个分区的数据多于其他分区的风险就会增加。相反,我们选择固定数量的分区,使用默认分区策略来分散数据。有了这个,我们减轻了分区不平衡的风险,并且不需要读取 Kafka 主题上所有分区。相反,根据请求进入的 webhookId,重放服务确定要读取的特定分区,并为该分区启动一个新的 Kafka 消费者。主题上的分区数量不会改变,因为这会影响键的散列以及事件的分布方式。
根据每个时间段读取的事件数量,我们选择使用固态磁盘(SSD)。我们选择它而不是传统的机械硬盘(HDD),以获得更快的处理速度,并减少寻道和访问时间带来的开销。最好使用 ssd 以获得更快的读取速度,因为我们访问的数据很少重复使用,因此无法从页面缓存优化中获得好处。
为了减少存储空间,我们使用 snappy 作为压缩算法对这些数据进行压缩。因为我们知道大部分的事情都是在消费端进行的。我们选择 snappy 是因为它的解压速度比其他 kafka 支持的压缩算法 gzip 和 lz4 快。
在我们设计的系统中,一个 API 调用发送重放请求。通过请求的参数里面,我们可以获得 webhookId 和应该重放事件的日期范围。这些请求被持久化到 MySQL 中并进入队列,直到重放服务处理它们为止。请求上的日期范围用于确定要开始读取的分区上的偏移量,消费者上的 offsetForTimes 函数用于获取偏移量。
重放服务实例处理每个重放请求,重放服务实例使用 MySQL 相互协调,以处理数据库中的每个重放请求。每个重放 worker 定期轮询 MySQL 以了解应该处理的作业。每个请求都有一个完整的生命周期:未被选中处理的请求处于打开状态(OPEN STATE);刚刚出队的请求处于启动状态(STARTED STATE);正在处理的请求处于正在进行的状态(ONGOING STATE);已完成的请求将转换为已完成状态(COMPLETED STATE)。重放 worker 只选择尚未启动的请求,也就是处于 OPEN 状态的请求。
在 worker 将请求从队列中取出处理后,worker 会定期地在 MySQL 表写入一条时间戳数据作为心跳(heartbeats),以表示重放作业仍在进行中。当重放 worker 实例在处理请求时宕机的情况下,这样的作业将重新启动。因此,除了处理处于打开状态(OPEN STATE)的请求,重放 worker 还可以选择处理已启动(STARTED)或正在进行(ONGOING)状态的作业,这些作业在几分钟内没有心跳信息。
事件从主题中读取后会进行去重操作,然后发布到客户的 webhook URL 中。数据去重是通过维护正在读取事件的哈希缓存来完成的。如果遇到具有相同哈希值的事件,则不会传递该事件。
总之,我们的解决方案并不是我们熟悉的把 Kafka 当做传统的实时、流处理的组件。然而,我们成功地使用 Kafka 作为存储系统来构建一个 API,在事件回放中优化客户体验和数据访问。利用实时系统的设计使我们的系统易于维护。此外,我们客户数据的恢复速度与我们的预期一致。
本文翻译自:Kafka as a storage system
本博客文章除特别声明,全部都是原创!