DoubleCloud 即将停止服务。在有限时间内,使用 ClickHouse 并享受免费迁移服务。立即联系我们 ->->

博客 / 用户案例

滴滴将日志存储系统从 Elasticsearch 迁移到 ClickHouse

author avatar
钟远凯
2024年4月19日

本文翻译自滴滴技术,作者为钟远凯。

滴滴是一个全球化的移动出行平台,为超过 4.5 亿用户提供出租车、网约车、代驾等全面的出行服务。在滴滴,我们使用 ClickHouse 存储日志。在这篇文章中,我们将讨论我们从 Elasticsearch 成功迁移的经验。

自 2020 年以来,ClickHouse 已在滴滴内部广泛应用,服务于网约车、日志检索等核心平台和业务。本文探讨了滴滴将日志检索从 Elasticsearch 迁移到 ClickHouse 的技术探索,使我们的可观测性硬件成本降低了 30% 以上。

背景

之前,滴滴的日志主要存储在 Elasticsearch (ES) 中。为了提供全文搜索功能,Elasticsearch 依赖于分词和倒排索引等功能。不幸的是,这些功能导致写吞吐量出现重大瓶颈。此外,ES 需要存储原始文本和倒排索引,这增加了存储成本并导致高内存需求。随着滴滴数据量的持续增长,ES 的性能已无法满足我们的需求。

为了降低成本和提高效率,我们开始寻找新的存储解决方案。经过研究,我们决定采用 ClickHouse 作为滴滴内部日志的存储支持。我们了解到,京东、携程、Bilibili 等业内公司也成功使用 ClickHouse 构建了日志存储系统,这使我们对进行迁移充满信心。

挑战

我们面临的主要挑战如下:

  1. **海量数据**:我们每天产生 PB 级别的日志数据,需要存储系统稳定地支持 PB 级别数据的实时写入和存储。
  2. **多样化的查询场景**:各种查询场景,包括范围查询、模糊查询和排序,都需要在给定时间段内扫描大量数据,并且查询结果需要在几秒钟内返回。
  3. **高 QPS (每秒查询数)**:对于 PB 级别的日志数据,跟踪查询需要满足高 QPS 的要求。

为什么选择 ClickHouse

ClickHouse 通过以下特性满足了这些需求:

  • **支持海量数据**:ClickHouse 的分布式架构支持动态扩展,可以处理海量的存储需求。
  • **写入性能**:ClickHouse 的 MergeTree 表具有很高的写入速度,可以提供最大吞吐量,并最大程度减少瓶颈。
  • **查询性能**:ClickHouse 支持分区和排序索引,从而实现高效的检索。它可以在单机上每秒扫描数百万行数据。
  • **存储成本**:ClickHouse 使用列式存储,可以提供高数据压缩率。此外,它可以利用 HDFS进行冷热数据分离,进一步降低存储成本。

结果

从 Elasticsearch 成功迁移到 ClickHouse 后,我们的 ClickHouse 日志集群现在已超过 400 个物理节点,峰值写入流量超过 40 GB/s。这支持每天大约 1500 万次查询,峰值 QPS 约为 200。与 Elasticsearch 相比,ClickHouse 的机器成本降低了 30%。

Markdown Image

查询速度比 Elasticsearch 提高了约 4 倍。下图显示了我们的 bamailogbamaitrace 集群的 P99 查询延迟,大多数查询在 1 秒内完成。

Markdown Image

架构升级

didi_architecture.png

在旧的存储架构下,日志数据需要写入 Elasticsearch 和 HDFS,ES 提供实时查询,Spark 在后者上分析数据。这种设计要求用户维护两个独立的写入管道,导致资源消耗加倍,并增加了操作复杂性。

在新升级的存储架构中,ClickHouse 取代了 ES 的作用,并拥有独立的日志和跟踪集群。日志集群专门用于存储详细的日志数据,而跟踪集群则专注于存储跟踪数据。这两个集群在物理上相互隔离,有效地防止了日志上的高消耗查询(例如 LIKE 查询)干扰跟踪上的高 QPS 查询。

日志数据由 Flink 直接写入日志集群,通过物化视图从日志中提取跟踪信息。这些物化视图的结果写入跟踪集群,使用分布式表通过异步写入实现。此过程不仅分离了日志和跟踪数据,还允许日志集群中的后台线程定期将冷数据同步到 HDFS。

新架构仅涉及单个写入管道,所有与 HDFS 中日志数据的冷存储相关的操作以及日志和跟踪的分离都由 ClickHouse 处理。这为用户屏蔽了底层细节,简化了操作流程。

考虑到成本和日志数据的特点,日志和跟踪集群都部署在单副本模式下。最大的日志集群拥有 300 多个节点,而跟踪集群拥有 40 多个节点。

存储设计

存储设计是性能提升最关键的部分,没有它,ClickHouse 强大的检索性能就无法得到充分利用。借鉴时间序列数据库的思路,我们将日志时间四舍五入到最近的小时,并通过在排序键中指定此时间来按时间顺序排列存储中的数据。这样,当使用其他排序键执行查询时,就可以快速定位所需的数据。

下面,我们根据日志查询的特点和 ClickHouse 执行逻辑,展示了我们为日志表、跟踪表和跟踪索引表开发的存储设计方案。

日志表

日志表(位于日志集群中)旨在为详细日志提供存储和查询服务,并在 Flink 从 Pulsar 中消费数据后直接写入。每个日志服务对应一个日志表,因此整个日志集群可能包含数千个日志表。最大的表每天可能会生成 PB 级别的数据。鉴于日志集群面临的挑战,例如表数量众多、每个表的数据量大以及需要冷热数据分离,以下为日志表的设想:

CREATE TABLE ck_bamai_stream.cn_bmauto_local (
   `logTime` Int64 DEFAULT 0,
   `logTimeHour` DateTime MATERIALIZED toStartOfHour(toDateTime(logTime / 1000)),
   `odinLeaf` String DEFAULT '',
   `uri` LowCardinality(String) DEFAULT '',
   `traceid` String DEFAULT '',
   `cspanid` String DEFAULT '',
   `dltag` String DEFAULT '',
   `spanid` String DEFAULT '',
   `message` String DEFAULT '',
   `otherColumn` Map(String, String),
   `_sys_insert_time` DateTime MATERIALIZED now()
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(logTimeHour)
ORDER BY (logTimeHour, odinLeaf, uri, traceid)
TTL _sys_insert_time + toIntervalDay(7), _sys_insert_time + toIntervalDay(3) TO VOLUME 'hdfs'
SETTINGS index_granularity = 8192, min_bytes_for_wide_part = 31457280
  • 分区键:虽然大多数 SQL 查询仅检索一个小时的数据,但按小时分区会导致太多分区并在 HDFS 中产生大量小文件。因此,分区按天而不是按小时进行。

  • 排序键:为了快速定位特定小时的数据,我们创建了一个名为logTimeHour的新字段,该字段将日志时间四舍五入到最接近的小时。然后将其用作主要排序键。由于大多数查询都指定了odinLeafuritraceid,因此这些列分别用作第二、第三和第四排序键,其排序依据是基数从小到大。这意味着查询特定traceid的数据只需要读取少量索引颗粒。通过这种设计,所有等值查询都可以在毫秒内完成。

  • 映射列:引入了Map类型来实现动态方案,允许将不用于过滤的列放入Map中。这有效地减少了分区文件数量,并防止了HDFS上出现大量小文件。

Trace表

Trace表位于Trace集群中,旨在促进高QPS查询。此表的数据是从Log表中使用物化视图提取的。这些数据通过分布式表写入包含Log表的Logs集群。

Trace表面临的挑战在于如何实现快速查询速度并支持高QPS。以下是Trace表的的设计考虑因素

CREATE TABLE ck_bamai_stream.trace_view
(
   `traceid` String,
   `spanid` String,
   `clientHost` String,
   `logTimeHour` DateTime,
   `cspanid` AggregateFunction(groupUniqArray, String),
   `appName` SimpleAggregateFunction(any, String),
   `logTimeMin` SimpleAggregateFunction(min, Int64),
   `logTimeMax` SimpleAggregateFunction(max, Int64),
   `dltag` AggregateFunction(groupUniqArray, String),
   `uri` AggregateFunction(groupUniqArray, String),
   `errno` AggregateFunction(groupUniqArray, String),
   `odinLeaf` SimpleAggregateFunction(any, String),
   `extractLevel` SimpleAggregateFunction(any, String)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMMDD(logTimeHour)
ORDER BY (logTimeHour, traceid, spanid, clientHost)
TTL logTimeHour + toIntervalDay(7)
SETTINGS index_granularity = 1024
  • AggregatingMergeTree:Trace表使用AggregatingMergeTree引擎,该引擎基于traceid聚合数据。这种聚合大大减少了跟踪数据的量,实现了5:1的压缩率,并显著提高了检索速度。
  • 分区和排序键:与Log表的设计类似。
  • index_granularity:此参数控制稀疏索引的粒度,默认值为8192。减小此参数有助于最大程度地减少对颗粒内不匹配数据的扫描,从而加快“traceid”检索速度。

Trace索引表

Trace索引表的主要目的是通过允许快速查找traceid来加快对order_iddriver_iddriver_phone等字段的查询速度。然后,可以使用此traceid查询上述Trace表。为此,我们为需要提高查询速度的字段创建了一个聚合物化视图。数据通过这些物化视图触发器从Log表提取到Trace索引表。这些表可用于快速识别特定列(以下示例中为order_id)的logTimeHourtraceid,然后查询Trace表。

创建专注于提供按order_id快速查找的Trace索引表的语句

CREATE TABLE orderid_traceid_index_view
(
   `order_id` String,
   `traceid` String,
   `logTimeHour` DateTime
)
ENGINE = AggregatingMergeTree
PARTITION BY logTimeHour
ORDER BY (order_id, traceid)
TTL logTimeHour + toIntervalDay(7)
SETTINGS index_granularity = 1024

接下来,我们将讨论迁移到此架构期间遇到的稳定性问题及其解决方案。

挑战

在ClickHouse中支持非常大的日志使用案例时,用户必须考虑由此产生的海量写入流量和极其庞大的集群规模。经过仔细的设计过程,我们可以在关键节假日期间稳定地处理峰值流量。以下部分主要介绍了一些遇到的挑战以及如何解决这些挑战。

大型集群中小数据的分片问题

在Log集群中,90%的Log表的流量低于10MB/s。如果所有表的数据都写入数百个节点,则会导致小表出现严重的分片问题。这不仅会影响查询性能,还会对整体集群性能产生负面影响,并在将冷数据存储到HDFS时产生大量小文件问题。

为了解决这些挑战,我们根据每个表的流量大小动态分配写入节点。分配给每个表的写入节点数量从2到集群中的最大节点数不等,均匀分布在整个集群中。Flink通过接口获取每个表的写入节点列表,并将数据写入相应的ClickHouse节点,有效地解决了大型集群中的数据分散问题。

写入节流和性能改进

在滴滴的峰值流量期和节假日,流量通常会大幅增加。为了避免集群因这些时期的过度流量而过载,我们在Flink上实现了写入节流功能。此功能动态调整写入集群的每个表的流量大小。当流量超过集群限制时,我们可以快速减少非关键表的写入流量,以缓解集群压力,并确保关键表的写入和查询不受影响。

同时,为了提高脉冲写入的性能,我们基于ClickHouse的原生TCP协议为Flink开发了一个原生连接器。与HTTP协议相比,原生连接器具有更低的网络开销。此外,我们还定制了数据类型的序列化机制,使其比以前的Parquet格式更高效。启用原生连接器后,写入延迟率从20%下降到5%,整体性能提高了1.2倍。

HDFS冷热分离的性能问题

在使用ClickHouse的HDFS用于存储冷数据的功能时,我们最初遇到了一些性能问题。这导致我们对HDFS冷热分离功能做出了重大贡献

  • 服务重启缓慢和系统CPU使用率高:服务重启缓慢和系统CPU使用率高归因于libhdfs3库的并发读取性能较差,在服务重启期间从HDFS加载Part的元数据时出现此问题。这导致在到达文件末尾时出现过多的系统调用和异常。为了缓解此问题,对并发读取机制和元数据缓存进行了改进,从而使服务重启时间从1小时显著减少到1分钟。
  • 历史分区数据写入性能差:将数据写入历史分区时,数据直接写入HDFS而不是本地持久化,导致写入性能较差。此外,直接写入HDFS的数据需要被拉回进行本地合并,进一步降低了合并性能。为了改进这一点,我们实施了优化措施以增强写入性能并简化合并过程。
  • 通过UUID映射Part路径和HDFS路径:使用UUID将本地Part路径映射到HDFS路径导致所有表数据都存储在HDFS中的同一路径下。这导致HDFS中100万个目录条目限制被达到。为了解决这个问题,设计了一种新的映射策略,将数据分布到HDFS中的多个目录中,从而防止达到目录条目限制。
  • 节点故障时丢失文件路径映射:文件路径映射关系存储在本地。如果节点发生故障,此信息可能会丢失,从而导致HDFS上的数据丢失以及无法删除数据。为了降低此风险,实施了机制以确保文件路径映射的持久性和弹性,即使在节点故障的情况下也是如此。

总的来说,这些对HDFS冷热分离功能的修改和增强成功解决了遇到的问题,并提高了系统的性能和可靠性。

resource_didi.png

此外,还实施了一个新流程以防止历史数据直接写入HDFS。相反,数据必须首先写入本地,合并,然后上传到HDFS。此外,HDFS中的存储路径结构也进行了改进。以前,所有数据都存储在一个目录下,但现在它根据集群、分片、数据库和表进行分区,并在表级别存储本地路径映射的备份副本,以便在节点故障时进行恢复。这确保了历史数据以更井然有序且弹性的方式进行处理和存储,从而提高了整体系统可靠性和数据管理效率。

总结

将日志从Elasticsearch迁移到ClickHouse不仅显着降低了存储成本,而且为我们提供了更快的查询体验。通过我们上面描述的更改,系统的稳定性和性能都得到了显着提高。但是,在处理模糊查询时,仍然会发生大量的集群资源消耗。未来,我们将继续探索诸如二级索引、ZSTD压缩以及存储和计算分离等技术,以进一步提高日志检索性能。

立即开始使用ClickHouse Cloud并获得300美元的积分。在30天试用期结束时,继续使用按需付费计划,或联系我们以了解有关我们基于容量的折扣的更多信息。访问我们的定价页面以了解更多详情。

分享此文章

订阅我们的新闻

随时了解功能发布、产品路线图、支持和云产品信息!
正在加载表单…
关注我们
Twitter imageSlack imageGitHub image
Telegram imageMeetup imageRss image