京东广告基于 Apache Doris 的冷热数据分层实践
一、背景介绍
京东广告围绕 Apache Doris 建设广告数据存储服务,为广告主提供实时广告效果报表和多维数据分析服务。历经多年发展,积累了海量的广告数据,目前系统总数据容量接近 1PB,数据行数达到 18 万亿行+,日查询请求量 8,000 万次+,日最高 QPS2700+。 随着业务的不断增长与迭代,数据量持续激增,存储资源逐渐成为瓶颈。近两年存储资源经历了多次扩容,存储容量增加了近十倍,而日查询请求量仅增长两倍。同时,计算资源的利用率因频繁扩容而相应降低,导致资源浪费。通过对查询请求的分析,我们发现日常查询中有 99%集中在近一年的数据上,数据使用呈现出明显的冷热现象。基于此,希望借助 Apache Doris 探索一种满足线上服务要求的冷热数据分层解决方案,在数据不断膨胀的情况下,降低数据的存储和使用成本。
二、冷热分层方案介绍
截至当前,我们的数据冷热分层实践已历经两种方案,分别是 Doris 冷数据入湖和 Doris 冷热数据分层。Doris 冷数据入湖方案通过 SDC(Spark-Doris-Connector)将 Doris 中的冷数据转入湖中,入湖后的冷数据可通过 Doris 外表进行查询。Doris 冷热数据分层方案则通过在 Doris 中设置数据的 TTL 时间,由 Doris 根据数据的 TTL 时间自动判断冷热数据,并将冷数据移至相对廉价的存储介质。冷数据入湖方案借鉴了腾讯游戏的相关经验(https://cloud.tencent.com/developer/article/2251030),并在 Apache Doris 1.2 版本中进行了实践;而 Doris 冷热数据分层方案则是最近上线的新一代冷热数据分层方案。以下将结合我们过往的实践经验,简要介绍这两种方案。
冷热分层 V1:数据湖方案
在数据湖方案中,需将 Doris 的数据依据业务时间进行冷热划分。在类似场景中,业务时间即为 Doris 表的分区时间。为实现 Doris 数据入湖,需借助 Spark-Doris-Connector(SDC)将 Doris 中的冷数据迁移至数据湖(如 Iceberg)。查询时,需根据业务时间对查询进行冷热区分,将冷数据查询(冷查询)与热数据查询(热查询)分别路由至不同的查询引擎。冷数据查询通过查询改写,将查询重定向至数据湖对应的外部表;热数据查询则无需改写,直接查询 Doris 中的 OLAP 表。

数据入湖方案的优势在于,冷数据的查询与下载能够与线上 Doris 系统实现解耦,从而确保线上操作的稳定性不受影响。这种解耦设计确保了冷数据的处理和查询不会干扰线上 Doris 系统的正常运行。通过将冷数据与线上系统分离,可以确保线上系统在处理实时数据时保持高效和稳定。这对于需要高可用性和低延迟的在线广告报表业务而言尤为重要。
数据入湖方案的主要劣势包括以下几点:首先,需借助 ETL 工具实现数据从 Doris 到数据湖的迁移。ETL 工具能够自动化数据迁移过程,但这也意味着需要额外的资源和时间来配置及运行这些工具。其次,为了获取完整的数据集,必须对 Doris 中的热数据和数据湖中的冷数据进行 UNION 操作。这意味着在进行数据分析或查询时,需要同时访问两个不同的数据存储系统,这不仅增加了系统的复杂性,还可能影响查询性能。例如,如果一个分析需要同时查询热数据和冷数据,那么查询时间可能会显著增加,因为系统需要从两个不同的地方获取数据。最后,数据入湖后,Schema 变更操作需得到相应数据湖的支持。这意味着如果需要对数据结构进行修改,例如添加或删除字段,必须确保数据湖能够支持这些变更。这可能需要额外的技术支持和维护工作。
冷热分层 V2:Apache Doris 冷热数据分层方案
Apache Doris 1.2 的冷热数据分层方案基于本地磁盘,热数据存储于 SSD,而冷数据则转移至性能较低的 HDD,以此实现数据分层。然而,此方案存在以下缺点:首先,该方案更适合物理机部署,而不适用于容器或 Kubernetes (K8S) 部署。当前,大多数公司已转向基于容器或 K8S 的部署方式,物理机部署已较为罕见。其次,需要预先估算冷数据的存储空间,然而,冷数据量会随时间逐渐增加,难以准确预估其容量。因此,我们并未在 Doris 1.2 中采用 Doris 原生的冷热数据分层方案,而是希望探索一种基于分布式文件系统作为冷数据存储的新方案。

Apache Doris 2.0 的冷热数据分层功能支持将冷数据存储于如 OSS 和 HDFS 等分布式存储系统中。用户可通过配置相应的存储策略来指定数据的冷热分层规则,进而通过为表或分区设定存储策略,实现冷数据自动迁移至外部存储系统。基于分布式存储的 Doris 冷热数据分层方案具有简洁性,避免了数据湖方案的复杂性。然而,该方案的缺点在于冷热数据统一在一个集群中进行管理和使用,高优先级的热查询可能会受到冷查询的影响,因此需要对冷数据查询进行适当的限流。以下将介绍我们在从 Doris 1.2 的数据湖方案升级至 Doris 2.0 基于分布式存储的冷热数据分层过程中遇到的一些问题及其解决方案。
三、问题解决
3.1 Apache Doris2.0 性能优化 &问题修复
为了实现基于分布式存储的冷热数据分层,需将 Doris 集群由 1.2 版本升级至 2.0 版本。尽管我们在前期已与社区共同完成了大量 Doris 开发工作,但在具体实施冷热数据分层过程中,仍遇到了若干问题。以下是几个典型问题。
查询性能下降问题
在性能 Diff 阶段,我们发现,在报表小查询场景(平均 tp99<20ms)中,Doris 2.0 相较于我们之前的 Doris 1.2 版本,性能下降约 50%左右。经过分析,我们发现 Doris 2.0 的 FE 默认启用了新的优化器,而该优化器在 SQL Rewrite 阶段使用了更多的规则进行重写,从而导致了性能下降。通过进一步的压测、分析以及与社区的交流,我们得出结论:除非在 Doris 2.0 中对新优化器进行更深层次的优化,否则很难使性能达到 Doris 1.2 的水平。因此,在我们的应用场景中,我们关闭了 Doris 2.0 的新优化器功能。在使用旧优化器的情况下,我们还是遇到了以下性能问题:
分桶裁剪失效
当查询命中表的 Rollup 后,底层数据扫描量明显增多,查询耗时较 1.2 明显升高。查看执行计划,发现执行计划扫描了对应分区下面的所有分桶数据,分桶裁剪没有生效。修复 PR:https://github.com/apache/doris/pull/38565

前缀索引失效
当从 1.2 升级到 2.0 时,升级前于 1.2 时创建的 Date 类型的字段在查询时如果将它和 DateTime 类型(如类似 Date>="2024-10-01 00:00:00")进行比较,FE 会对 Date 类型进行自动类型提升(类似 CAST(Date as datetime) >= Datetime("2024-10-01 00:00:00"))。 提升后的谓词在 BE 处理的时候和底层数据存储的实际类型(Date("2024"))不能进行比较,导致对应前缀索引失效,引起查询性能大幅下降。这种情况我们通过在 FE 端进行类型对齐进行了修复https://github.com/apache/doris/pull/39446。 修复后索引生效,性能得到大幅提升。

FE CPU 使用率高问题
在对 Doris2.0 进行压力测试时,观察到 FE 节点的 CPU 使用率相较于 Doris1.2 显著上升,在相同的 QPS 请求下,Doris2.0 的 CPU 使用率几乎翻倍。资源消耗明显增加。在测试过程中,我们对 FE 节点进行了火焰图分析,识别出性能消耗较高的函数;同时,我们与社区成员进行了充分的沟通,最终确定了多个资源消耗点,并实施了相应的优化措施。

时间比较效率优化
广告报表场景下时间比较操作是几乎每个查询在分区裁剪时都会用到,而 Doris2.0 对时间的比较需要先转化为字符串再进行比较,这种比较没有直接使用数据结构自身的成员变量进行比较效率高,这里我们通过 PR:https://github.com/apache/doris/pull/31970对分区裁剪时的时间比较操作进行了优化,优化后 CPU 使用率整体降低 25%左右。
物化视图字段列重写优化
在表有 Rollup 而没有物化视图时,Doris FE 对查询的执行计划还是会使用只需作用于物化视图的改写规则进行优化改写,这些无效的改写不仅造成 CPU 利用率提升还会影响查询延时。 PR: https://github.com/apache/doris/pull/40000对这种情况进行优化,在无物化视图情况下避免无意义的执行计划改写。
此外,我们还通过使用 for 循环代替流操作、关键路径减少日志输出等进行了 CPU 使用率优化,最总 Doris 2.0 FE CPU 消耗最终达到 1.2 版本等同水平。
BE 内存使用率高
在对 Doris 2.0 版本的集群使用过程中,发现 BE 内存使用率会极缓慢持续升高,长期使用的情况下,Doris BE 阶段存在 OOM 风险。排查该现象和 SegmentCache 的配置有关:
Doris2.0 使用了 SegmentCache,用于对底层数据文件对象缓存。但 2.0 对于 SegmentCache 的内存使用计算存在问题且默认阈值设置过大,导致一直触发不到 SegmentCache 使用阈值;随着 segment 文件数量的增加,SegmentCache 使用量会越来越大。结合日常内存使用量的评估及压测验证,我们重新调整了合理的 SegmentCache 使用阈值;在保证 Cache 命中率基本不变的情况下降 BE 常驻内存使用率从 60%以上降低到 25%一下,有效避免了 BE 节点 OOM 的风险。
经过一系列优化后,2.0 版本查询性能参数(TP99 耗时、FECPU 消耗、BE CPU 消耗)有了较大优化,基本和 1.2 版本对齐。

3.2 冷数据 Schema Change(SC)优化
Schema Change(SC) 是 Apache Doris 等实时数仓日常使用当中的高频操作,其中,Add Key Column 的操作是广告数据报表中使用较多的场景,实践中发现冷数据添加 Key 列的 SC 操作存在如下问题:
1.Schema Change 退化:冷数据的 Add Key Column 操作会退化成 Direct Schema Change(DSC);DSC 操作比较重,需要对全量数据进行重新读取和写入。在实际使用过程中对于含大量冷数据的表进行 Add Key Column 操作需要重新对远端海量数据进行读写,增大系统 IO 负载的同时,SC 任务耗时也很长。实践中一张冷数据量 20T 左右的表,整个 SC 耗时在 7 天以上,对于需要紧急上线的业务体感极差。
2.数据冗余:冷数据 Schema Change(SC)时,Tablet 的每个副本都会独立进行 SC 操作,导致原来冷数据单副本存储在 SC 后变成多副本,冷数据存储资源浪费严重。
为了优化和修复上述冷数据 Schema Change 遇到的问题,我们对冷数据的 Schema Change 进行了如下优化:
实现冷数据 Linked Schema Change
针对冷数据 Add Key Column 类型的 SC 退化成 Direct Schema Change 导致 SC 任务执行缓慢的问题,我们对冷数据 Add Key Column 类型 SC 的流程进行深度优化,利用远端存储系统(ChubaoFS)的 CopyObject 接口,实现在远端直接进行数据复制,避免数据文件从远端拉取到 Apache Doris,经数据重写后再存储到远端存储系统的巨大 IO 开销。该优化减少了两次网络传输和一次数据转换的开销,这样能够极大加速 Add Key Column 场景下冷数据 Schema Change 的执行速度。经测算优化后 SC 执行速度提升 40 倍, 相关 PR 已合并社区 2.0 分支:https://github.com/apache/doris/pull/40963。

实现冷数据单副本 SC
Apache Doris 2.0 中冷数据在进行 Schema Change 时,同一份数据的多个副本之间相互独立进行(SC)。如此, Schema Change 完成后造成冷数据将在远端存储存在多份,造成存储资源浪费的同时也降低 SC 任务执行效率。为了解决这个问题,参考 Raft 协议对冷数据多副本 SC 场景进行了优化。即 SC 只在选举出来的 Leader 副本上执行,非 Leader 副本只生成元数据。

我们已成功解决了 SC 后数据副本冗余的问题。然而,仍存在一个潜在风险:FE 会定期检查 BE 上的 tablet SC 操作是否正常。我们允许不超过半数的 tablet 副本 SC 失败,即使 BE 上的 Leader 副本 SC 失败,整个 SC 任务仍可能成功。因此,若 Leader 副本在复制数据时失败,可能会导致数据丢失。为避免这种情况,我们在 FE 对 Schema Change 任务的健康度判断时,特别考虑了冷数据的“Linked Schema Change”。只有当 tablet 的 Leader 副本成功时,SC 才会被视为成功。这样可以确保数据的完整性和一致性。
实现冷数据的 Light Schema Change
对于存量表中可以直接使用 Light Schema Change 的表,我们希望更进一步支持一种冷数据 Light Schema Change;如果走 Light Schema Change,则只需要修改 FE 元数据信息,不需要进行 BE 端任务创建及数据文件处理;处理时间会达到毫秒级。

当前 Light Schema Change 只支持 Value 列字段的添加,不支持 Key 列字段添加。但对于不涉及分区、分桶、前缀索引的普通 Key 列;可以按照 Light Schema Change 的逻辑进行处理。这里对这一功能进行了升级。主要改动在 FE 阶段 Light Schema Change 判断阶段,支持对 Key 列添加的逻辑。以满足较普通的添加 Key 列操作。
3.3 其他问题解决
随着整体数据量持续增长,在引入冷热数据分层方案之前,为缓解线上存储资源紧缺的现状,我们将 Doris 历史数据通过 backup 的方式结转到外部存储,维持 Doris 集群安全的存储水位。完成 Doris2.0 版本升级后,再将结转的历史数据重新恢复至 Doris 集群。为了便捷高效地操作历史数据,我们实现了一套统一的结转和恢复工具,工具解决了如下三个问题:
1.历史数据总量大,底表数量多,如何准确高效地结转这些数据?
2.历史数据持续结转,线上表 schema 持续变更,如何将这部分 schema 不一致的数据重新恢复?
3.如何实现统一的冷热数据分层和热数据自动冷却?
为解决第一个数据结转的问题,我们实现了一个历史数据自动结转工具 data migrator。支持将线上集群所有 DB 任意时间段内的数据异步并行结转至外部离线存储。
Doris2.0 完成升级后启动建设冷热数据分层,首先需要将结转至外部存储的历史数据恢复至线上环境。此时遇到的最大问题是线上表结构已发生多次变更,导致多次结转的历史数据备份 snapshot 文件所对应的 schema 结构与线上表不一致。为了解决 schema 不一致的问题,我们设计开发了一套自动化数据恢复工具 narwal_cli,如下图中 Data Restore Process 过程所示,narwal_cli 工具支持自动对齐历史结转数据和 Doris 集群中数据的的 schema,并定向恢复至线上环境。

在实施恢复过程中,还遇到 Flink2Doris 实时写入任务失败的情况,具体信息如下:LOAD_RUN_FAIL; msg:errCode = 2, detailMessage = Table xxxxx is in restore process. Can not load into it
经排查,问题原因是 Doris 表在 restore 过程中伴随实时数据写入,写入会对表当前的 meta info 进行 check,但状态检测粒度较粗,仅检测 tableState 而未进一步检测 partitionState,造成状态误判,进而影响了写入任务。问题定位后迅速完成修复和发版上线,详细信息可参考 pr:https://github.com/apache/doris/pull/39595。以上问题解决后,在线上环境快速准确地恢复了所有历史数据,且工具兼顾易用性,做到随时启停、断点续传。
在我们的应用场景中,我们对历史恢复的数据和线上的数据分别设置了不同的冷热分层策略:我们将历史数据的 storage_policy 设置为 cooldown_ttl=10s,实现历史数据立刻冷却至 ChubaoFS。对全量热数据则统一设置了 cooldown_ttl=2years,实现线上热数据随着两年时间窗口推进自动冷却。整个历史数据的恢复和冷却全过程,做到对线上业务透明,准确高效地实现全量历史数据恢复和冷却。同时,在冷却数据过程中发现冷热数据策略设置异常问题,进行了修复,参考 pr:https://github.com/apache/doris/pull/35270。实现统一的冷热分层和自动冷却后,后续存储数据量继续保持增长,也无需再扩容线上存储资源,仅需扩容较低成本的外部离线存储即可,实现计算资源利用率提升的同时,存储经济成本大幅降低。
除了以上优化,我们还在为 Apache Doris 在读写性能提升、问题修复、功能完善等方面积极贡献,已为社区 2.0 版本提交并合并 30+ PR。
四、小结
通过对数据进行冷热分层,我们的存储成本降低了约 87%。对比 Doris 1.2 的冷数据入湖方案与 Doris 2.0 的冷数据分层方案,后者在并发查询能力上提升了超过 10 倍,查询延迟显著减少。此外,冷热数据分层方案简化了存储和查询的维护工作,降低了整体复杂性和成本。冷热分层架构的成功实施,离不开 Apache Doris 社区和中台 OLAP 团队的鼎力支持,特此向所有 Apache Doris 社区和中台 OLAP 团队的成员表示衷心的感谢。展望未来,我们期待继续与 Apache Doris 社区和中台 OLAP 团队在京东广告场景中开展紧密合作,共同探索存算分离架构在该场景中的实际应用。
作者 京东零售广告产研部-投放平台部-投放报表组
评论