这篇文章翻译自滴滴技术博客,作者是 Yuankai Zhong。
滴滴是一个全球分布的移动交通平台,为超过 4.5 亿用户提供出租车、网约车和代驾等全面的出行服务。在滴滴,我们使用 ClickHouse 存储日志。在这篇文章中,我们将讨论我们从 Elasticsearch 成功迁移的经验。
自 2020 年以来,ClickHouse 已在滴滴内部广泛使用,服务于核心平台和业务,如网约车和日志检索。本文探讨了滴滴日志检索从 Elasticsearch 迁移到 ClickHouse 的技术探索,从而将我们的可观测性硬件成本降低了 30% 以上。
背景
此前,滴滴的日志主要存储在 Elasticsearch (ES) 中。为了提供全文搜索,Elasticsearch 依赖于分词和倒排索引等功能。不幸的是,这些功能导致写入吞吐量出现重大瓶颈。此外,ES 需要存储原始文本和倒排索引,这增加了存储成本,并导致高内存需求。随着滴滴数据量的持续增长,ES 的性能已无法满足我们的需求。
为了降低成本和提高效率,我们开始寻求新的存储解决方案。经过研究,我们决定采用 ClickHouse 作为滴滴内部日志的存储支持。我们了解到,行业内的多家公司,如京东、携程和哔哩哔哩,也成功地使用 ClickHouse 构建了日志存储系统,这让我们对进行迁移充满信心。
挑战
我们面临的主要挑战如下:
- 大数据量:我们每天生成 PB 级日志数据,要求存储系统稳定支持 PB 级数据的实时写入和存储。
- 多样化的查询场景:各种查询场景,包括范围查询、模糊查询和排序,需要在给定的时间段内扫描大量数据,并且需要秒级返回查询结果。
- 高 QPS(每秒查询数):对于 PB 级数据,追踪查询需要满足高 QPS 要求。
为什么选择 ClickHouse
ClickHouse 通过以下特性满足这些要求:
- 支持大数据量:ClickHouse 的分布式架构支持动态扩展,使其能够处理海量数据存储需求。
- 写入性能:ClickHouse 的 MergeTree 表具有高写入速度,以最小的瓶颈提供最大的吞吐量。
- 查询性能:ClickHouse 支持分区和排序索引,从而实现高效检索。它可以在单台机器上每秒扫描数百万行数据。
- 存储成本:ClickHouse 使用列式存储,可提供高数据压缩率。此外,它可以利用 HDFS 进行冷热数据分离,从而进一步降低存储成本。
结果
在从 Elasticsearch 成功迁移到 ClickHouse 之后,我们的 ClickHouse 日志集群现在已超过 400 个物理节点,峰值写入流量超过 40 GB/s。这支持大约每天 1500 万次查询,峰值 QPS 约为 200。与 Elasticsearch 相比,ClickHouse 的机器成本降低了 30%。
查询速度比 Elasticsearch 提高了约 4 倍。下图显示了我们的 bamailog
和 bamaitrace
集群的 P99 查询延迟,大多数查询在 1 秒内完成。
架构升级
在旧的存储架构下,日志数据需要同时写入 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 查询仅检索一个小时的数据,但按小时分区将导致过多的 Part,以及 HDFS 中存在大量小文件。因此,分区按天而不是按小时进行。
-
排序键:为了快速定位特定小时的数据,通过将日志时间四舍五入到最接近的小时,创建了一个名为
logTimeHour
的新字段。然后将其用作主排序键。由于大多数查询都指定了odinLeaf
、uri
和traceid
,因此这些列分别用作第二、第三和第四排序键,排序依据是基数从小到大。这意味着查询特定traceid
的数据只需要读取少量的索引粒度。通过这种设计,所有等值查询都可以在毫秒内完成。 -
Map 列:引入 Map 类型是为了实现动态模式,允许将不用于过滤的列放入 Map 中。这有效地减少了 Part 文件数量,并防止了 HDFS 上出现大量小文件。
追踪表
追踪表位于追踪集群内,旨在促进高 QPS 查询。此表的数据使用物化视图从日志表中提取。这些物化视图通过分布式表写入包含日志表的日志集群。
追踪表的挑战在于实现快速查询速度和支持高 QPS。以下是追踪表的设计考虑因素:
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:追踪表使用 AggregatingMergeTree 引擎,该引擎基于
traceid
聚合数据。这种聚合大大减少了追踪数据量,实现了 5:1 的压缩比,并显着提高了检索速度。 - 分区键和排序键:与日志表的设计类似。
index_granularity
:此参数控制稀疏索引的粒度,默认值为 8192。减小此参数有助于最大限度地减少粒度内非匹配数据的扫描,从而加快“traceid”检索。
追踪索引表
追踪索引表的主要目的是通过快速查找 traceid
来加速 order_id
、driver_id
和 driver_phone
等字段的查询速度。此 traceid
反过来可用于查询上面的追踪表。为了实现这一点,我们为需要提高查询速度的字段创建了一个聚合物化视图。数据通过这些物化视图触发器从日志表提取到追踪索引表。这些表可用于快速识别特定列(以下示例中的 order_id
)的 logTimeHour
和 traceid
,然后再查询追踪表。
创建专注于按 order_id
提供快速查找的追踪索引表的语句
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 中支持非常大的日志记录用例时,用户必须考虑将产生的大量写入流量和极其庞大的集群规模。经过仔细的设计过程,我们可以稳定地处理关键节假日期间的峰值流量。以下部分主要介绍遇到的一些挑战以及如何解决这些挑战。
大型集群中小数据的数据碎片问题
在日志集群中,90% 的日志表的流量低于 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 冷热数据分离功能的这些修改和增强成功地解决了遇到的问题,并提高了系统的性能和可靠性。
此外,我们还实施了一个新流程,以防止历史数据直接写入 HDFS。相反,数据必须先在本地写入、合并,然后再上传到 HDFS。此外,我们还改进了 HDFS 中的存储路径结构。以前,所有数据都存储在单个目录下,但现在它基于集群、分片、数据库和表进行分区,并在表级别存储本地路径映射的备份副本,以便在节点故障时进行恢复。这确保了历史数据以更有组织和弹性的方式进行处理和存储,从而提高了整体系统可靠性和数据管理效率。
总结
从 Elasticsearch 迁移到 ClickHouse 不仅显着降低了存储成本,还为我们提供了更快的查询体验。通过我们上面描述的更改,系统的稳定性和性能都得到了显着提高。但是,在处理模糊查询时,仍然会发生大量的集群资源消耗。未来,我们将继续探索二级索引、ZSTD 压缩以及存储和计算分离等技术,以进一步提高日志检索性能。