ClickHouse 是2016年开源的用于实时数据分析的一款高性能列式分布式数据库,支持向量化计算引擎、多核并行计算、高压缩比等功能,在分析型数据库中单表查询速度是最快的。2020年开始在滴滴内部大规模地推广和应用,服务网约车和日志检索等核心平台和业务。本文主要介绍滴滴日志检索场景从 ES 迁移到 CK 的技术探索。
此前,滴滴日志主要存储于 ES 中。然而,ES 的分词、倒排和正排等功能导致其写入吞吐量存在明显瓶颈。此外,ES 需要存储原始文本、倒排索引和正排索引,这增加了存储成本,并对内存有较高要求。随着滴滴数据量的不断增长,ES 的性能已无法满足当前需求。
在追求降低成本和提高效率的背景下,我们开始寻求新的存储解决方案。经过研究,我们决定采用 CK 作为滴滴内部日志的存储支持。据了解,京东、携程、B站等多家公司在业界的实践中也在尝试用 CK 构建日志存储系统。
面临的挑战主要来自下面三个方面:
-
数据量大
:
每天会产生 PB 级别的日志数据,存储系统需要稳定
地
支撑 PB 级数据的实时写入和存储。
-
查询场景多
:在一个时间段内的等值查询、模糊查询及排序场景等,查询需要扫描的数据量较大且查询都需要在秒级返回。
-
QPS 高
:在 PB 级的数据量下,对 Trace 查询同时要满足高 QPS 的要求。
-
大数据量
:CK 的分布式架构支持动态扩缩容,可支撑海量数据存储。
-
写入性能
:CK 的 MergeTree 表的写入速度在200MB/s,具有很高吞吐,写入基本没有瓶颈。
-
查询性能
:CK 支持分区索引和排序索引,具有很高的检索效率,单机每秒可扫描数百万行的数据。
-
存储成本
:
CK 基于列式存储,数据压缩比很高,同时基于HDFS做冷热分离,能够进一步
地
降低存储成本。
旧的存储架构下需要将日志数据双写到 ES 和 HDFS 两个存储上,由ES提供实时的查询,Spark 来分析 HDFS 上的数据。
这种设计要求用户维护两条独立的写入链路,导致资源消耗翻倍,且操作复杂性增加。
在新升级的存储架构中,CK 取代了 ES 的角色,分别设有 Log 集群和 Trace 集群。Log 集群专门存储明细日志数据,而 Trace 集群则专注于存储 trace 数据。这两个集群在物理上相互隔离,有效避免了 log 的高消耗查询(如 like 查询)对 trace 的高 QPS 查询产生干扰。此外,独立的 Trace 集群有助于防止trace数据过度分散。
日志数据通过 Flink 直接写入 Log 集群,并通过 Trace 物化视图从 log 中提取 trace 数据,然后利用分布式表的异步写入功能同步至 Trace 集群。这一过程不仅实现了 log 与 trace 数据的分离,还允许 Log 集群的后台线程定期将冷数据同步到 HDFS 中。
新架构仅涉及单一写入链路,所有关于 log 数据冷存储至 HDFS 以及 log 与 trace 分离的处理均由 CK 完成,从而为用户屏蔽了底层细节,简化了操作流程。
考虑到成本和日志数据特点,Log 集群和 Trace 集群均采用单副本部署模式。其中,最大的 Log 集群有300多个节点,Trace 集群有40多个节点。
存储设计是提升性能最关键的部分,
只有经过优化的存储设计才能充分发挥 CK 强大的检索性能
。
借鉴时序数据库的理念,我们将 logTime 调整为以小时为单
位进行取整,并在存储过程中按照小时顺序排列数据。这样,在进行其他排序键查询时,可以快速定位到所需的数据块。例如,查询一个小时内数据时,最多只需读取两个索引块,这对于处理海量日志检索至关重要。
以下是我们根据日志查询特性和 CK 执行逻辑制定的存储设计方案,包括 Log 表、Trace 表和 Trace 索引表:
Log 表旨在为明细日志提供存储和查询服务,它位于 Log 集群中,并由 Flink 直接从 Pulsar 消费数据后写入。每个日志服务都对应一张 Log 表,因此整个 Log 集群可能包含数千张 Log 表。其中,最大的表每天可能会生成 PB 级别的数据。鉴于 Log 集群面临表数量众多、单表数据量大以及需要进行冷热数据分离等挑战,以下是针对 Log 表的设计思路:
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 都只会查1小时的数据,但这里只能按天分区,小时分区导致 Part 过多及 HDFS 小文件过多的问题。
-
排序键
:
为了快速定位到某一个小时的数据,基于 logTime 向小时取整物化了一个新的字段 logTimeHour,将 logTimeHour 作为第一排序键,这样就能将数据范围锁定在小时级别,由于大部分查询都会指定上 odinLeaf、uri、traceid,依据基数从小到大分别将其
作
为第二、三、四排序键,这样查询某
个 traceid 的数据只需要读取少量的索引块,经过上述的设计所有的等值查询都能达到毫秒级。
-
Map 列:
引入了 Map 类型,实现动态的 Scheme,将不需要用来过滤的列统统放入 Map 中,这样能有效减少 Part 的文件数,避免 HDFS 上出现大量小文件。
Trace 表是用来提供 trace 相关的查询,这类查询对 QPS 要求很高,创建在 Trace 集群。数据来源于从 Log 表中提取的 trace 记录。Trace 表只会有一张,所有的 Log 表都会将 trace 记录提取到这张 Trace 表,实现的方式是 Log 表通过物化视图触发器跨集群将数据写到 Trace 表中。
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
Trace 索引表的主要作用是加快 order_id、driver_id、driver_phone 等字段查询 traceid 的速度。为此,我们给需要加速的字段创建了一个聚合物化视图,以提高查询速度。数据则是通过为 Log 表创建相应的物化视图触发器,将数据提取到 Trace 索引表中。
以下是建立 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
存储设计的核心目标是提升查询性能。接下来,我将介绍从 ES 迁移至 CK 过程中,在这一架构下所面临的稳定性问题及其解决方法。
支撑日志场景对 CK 来说是非常大的挑战,面临庞大的写入流量及超大集群规模,经过一年的建设,我们能够稳定的支撑重点节假日的流量高峰,下面的篇幅主要是介绍了在支撑日志场景过程中,遇到的一些问题。
在 Log 集群中,90%的 Log 表流量低于10MB/s。若将所有表的数据都写入数百个节点,会导致大量小表数据碎片化问题。这不仅影响查询性能,还会对整个集群性能产生负面影响,并为冷数据存储到 HDFS 带来大量小文件问题。