在分片 MySQL 中存储时序数据以支持 Query Insights
PlanetScale 每天处理超过 100 亿次客户查询。我们需要收集、存储并提供由这些查询生成的遥测数据,以支持 Insights ,我们内置的查询性能分析工具。这篇文章介绍了如何使用 Apache Kafka 和分片的 PlanetScale 数据库构建一个可扩展的遥测数据管道。
Insights 的需求
为了展示 Insights 数据,我们需要从以下数据集提供支持:
- 数据库级别的时序数据:例如整个数据库的每秒查询量。
- 查询模式级别的时序数据:例如某个特定查询模式(如
SELECT email FROM users WHERE id = %
)的 p95(95th 百分位)。 - 特定慢查询的执行数据:例如慢查询日志中的数据。
数据挑战
虽然数据库级别的数据可很好地存储在时序数据库(例如 Prometheus)中,但存储查询模式级别的数据在时序数据库中会遇到几个问题:
- PlanetScale 每日使用的查询模式有 **数千万个**,而且这一数字预计会继续增长。在维度基数非常高时,大多数时序数据库都会受到限制。我们评估过在 Prometheus 中存储这些数据的成本,发现这个方案非常昂贵且不具备吸引力。
- 每个查询模式聚合需要存储额外数据,例如完整的标准化 SQL、使用的表以及查询的最近执行时间。
- 为支持 Insights 搜索,我们需要能够对查询模式进行复杂过滤,例如对标准化后的 SQL 进行子字符串匹配。
混合存储解决方案
基于这些需求,我们构建了一种混合解决方案:
- 使用 Prometheus 存储数据库级别的聚合数据(维度基数较低时适合)。
- 使用基于 MySQL 和 Vitess 的分片 PlanetScale 数据库存储查询模式级统计数据和单个慢查询事件。
Insights 数据管道
数据流的工作方式
数据管道从 VTGate 开始。VTGate 是 Vitess 的一个组件,它负责将查询流量代理到底层的 MySQL 实例。我们对 Vitess 的内部版本进行了以下改进:
- 指纹级总结:VTGate 每 15 秒发送一次每个查询指纹的汇总数据到 Kafka(有关如何确定指纹,请阅读我们的查询性能分析博客文章)。15 秒既能保持消息数量可控,又能提供接近实时的体验。
- 慢查询事件:即时发送慢查询事件到 Kafka 主题。
性能和稳定性设计目标
一个主要目标是确保 Insights 的实现**不会拖慢客户的数据库**或导致不可用性。
- 限制每周期唯一查询模式的数量:每个查询都需要在 VTGate 中跟踪其内存,我们必须确保不会消耗过多内存,尤其是在数据库快速出现大量唯一查询模式的情况下。我们会监控 VTGate,确保即便是最大的客户也不会定期超出阈值。
- 令牌桶速率限制器:使用连续填充的令牌桶来限制记录慢查询日志事件的数量,允许捕获查询突发,同时限制整体吞吐量。
- 异步发布到 Kafka:数据在 VTGate 中发布到有界内存缓冲区,随后异步刷新到 Kafka。这种异步机制最大限度减少了每查询的附加开销,确保即使 Kafka 暂时不可用时,我们仍能服务查询。我们设置了 5MB 缓冲区,数据会在 Kafka 恢复后发送。
Kafka 消费者
数据处理流程
Kafka 应用从 Kafka 读取数据并写入 MySQL:
- 查询模式数据聚合:在数据库中按照时间聚合查询模式数据。我们使用按小时和按分钟的汇总表,通过高粒度支持较小时间段、通过较低粒度支持较长时间段。
- 慢查询事件:逐条写入 MySQL 表。
- 数据去重:我们在 MySQL 表中跟踪 Kafka 消息的偏移量及分区信息,使用唯一性约束避免因 Kafka 消费者批次重试而导致数据重复。
Kafka 分区映射
- Kafka 消息的键设置为数据库标识符和查询指纹的确定性哈希值,这样消息会到达同一个分区。
- 在消费者批次中合并 Kafka 消息以减少数据库写入,通常内存合并能减少约 30%–40% 的写入。更大的批次能提高合并率,但需要更多的消费者内存,且可能增加端到端的延迟。正常操作时,平均批次大小约为 **200 条消息**,负载峰值时则可能高达 **1,000 条消息**。
分片架构
Kafka 消费者向 MySQL 数据库发起约 **5,000 次写入/秒**,我们需要确保数据库能够随着 PlanetScale 的发展继续扩展。为防止数据库成为瓶颈,我们构建了一个 **Insights 分片数据库集群**,使用客户数据库 ID 作为分片键。
- Insights 初始使用了 **4 个分片**,今年早些时候扩展至 8 个分片 以应对更高的写入量。
- 为完成分片升级,我们创建新消费者集群,连续为新分片写入 **8 天的数据**,随后切换应用到新的数据库上。此方法能让我们在进入关键路径前充分测试新的分片集群。
分片优势
- Insights 数据库集群可以运行在 **小型服务器(2 vCPUs 和 2GB 内存)**上。
- 使用更多的小分片能快速完成备份和架构变更,并在必要时为新集群添加更多分片。
百分位 Sketch
为什么需要百分位数据?
我们需要对数据库级别和查询模式级别的时序延迟进行监控以检测问题:
- 数据库级数据存储在 Prometheus 中,可使用其内置分位估算工具。
- 查询模式数据存储在 MySQL 中,需要找到一种方法在 MySQL 内存储和检索分位数据。
使用 DDSketch
我们采用了 **DDSketch**,一种概率性数据结构,用于计算误差有界的分位估算。
- DDSketch 的核心思想是将观测值分组到宽度呈指数增长的桶中,并保存每个桶的出现计数。
- 通过合并桶计数,可以计算合并的分位估算,而不会丢失统计准确性。
DDSketch 的实现
- VTGate 实例记录每个查询模式的延迟 Sketch,每 15 秒发送到 Kafka。
- Kafka 消费者读取 Sketch 并以自定义二进制格式写入 MySQL。
- 我们实现了一个可以读取和写入二进制格式的 MySQL C++ 函数库,用于聚合 Sketch 和计算分位。这让我们无需将底层 Sketch 拉入应用中就能计算分位,同时可以利用 SQL 的表达能力获取所需数据。
MySQL 的时序数据存储
MySQL 通常不是时序数据的首选存储,但在我们场景下有以下优势:
- 查询模式指纹维度基数高,使得 Prometheus 等时序数据库的成本过高。
- 我们的维度集已知且变化不频繁。
- 产品需要按查询模式对数据集进行复杂过滤,许多时序数据库不支持这些功能。
- 数据库 ID 是天然的分片键。
虽然有许多 OLAP 数据库能满足我们的需求,但它们带来了显著的运维开销和学习成本。而 Vitess 和 MySQL 的分片解决方案刚好契合我们的问题,避免了部署其他存储系统的复杂性。借助 Kafka 和 Vitess 分片机制,我们能随着流量变化扩展 Insights 管道的所有组件,并保持与 PlanetScale 增长节奏一致。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接
本文链接:http://www.choupangxia.com/2025/09/13/mysql-query-insights/