Skywalking 使用 ClickHouse 存储实践,性能提高 N 倍

简述

ClickHouse 近两年在开源社区愈发火热,不知从何开始大家争相用它来替换 ElasticSearch,大概因为 ElasticSearch 的开销太高,在不作为搜索引擎的场景下,一定程度的暴力搜索是可以容忍的。

我们在使用 Skywalking 后发现,它对后端存储要求太高了,使用 (32C + 64G + 2T) x8 的配置,在云平台上每月两三万的开销,性能依然非常捉急,查询经常超时。前前后后优化了小半年后,最终下定决心替换成 ClickHouse。

在使用为 ClickHouse 后,机器数量减少了 50%;
查询链路列表从 5.53/s 提高到了 166/s,响应时间从 3.52s 降低到了 166ms;
查询链路详情从 5.31/s 提高到了 348/s,响应时间从 3.63s 降低到了 348ms;
链路数据存储时间从 5 天提高到了 10 天,数据量达到数百亿;

值得一提的是,在与 ES 的对比中经常会提到磁盘空间降低,其实 ClickHouse 的压缩率没有那么夸张,起码在我的实际体验两者相差不大。如果 ES 空间占用很高,那很可能是因为没在索引中开启 codec: best_compression

ClickHouse 也并不是没有缺点,本篇文章分享下如何用 ClickHouse 作为 Skywalking 的后端存储。本文不会赘述 ClickHouse 的基本原理,需要读者对 ClickHouse 有一定了解的情况下阅读。

(由于工作量巨大,对 Skywalking 存储的改造仅限于存储链路数据,即 Segment,其余部分就抓大放小,仍使用 ElasticSearch 存储,没有进行改造)

表设计

ClickHouse 基本只能建立一种索引,即 Order By 的索引。而链路表查询条件众多,几乎每个字段都是查询条件,且包含多种排序,设计起来比较辣手。

查询条件:时间、服务名称、服务实例、链路类型、链路 ID、链路名称、响应时间、是否异常
排序条件:时间、响应时间

想要在一张表上设计出符合所有查询模式,基本是不可能的(或完全不可能),在参考了 jaeger-clickhouse 等众多设计后,更加坚定了这个结论。

尝试了数次后,最终的建表语句如下:

CREATE TABLE skywalking.segment
(
    `segment_id` String,
    `trace_id` String,
    `service_id` LowCardinality(String),
    `service_instance_id` LowCardinality(String),
    `endpoint_name` String,
    `endpoint_component_id` LowCardinality(String),
    `start_time` DateTime64(3),
    `end_time` DateTime64(3),
    `latency` Int32,
    `is_error` Enum8('success' = 0, 'error' = 1),
    `data_binary` String,
    INDEX idx_endpoint_name endpoint_name TYPE tokenbf_v1(2048, 2, 0) GRANULARITY 1,
    PROJECTION p_trace_id
    (
        SELECT 
            trace_id,
            groupArrayDistinct(service_id),
            min(start_time) AS min_start_time,
            max(start_time) AS max_start_time
        GROUP BY trace_id
    )
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(start_time)
ORDER BY (start_time, service_id, endpoint_name, is_error)
TTL toDateTime(start_time) + toIntervalDay(10)

首先,在 partition 上还是使用了天作为条件。在 Order By 时,使用 时间 + 服务 + 链路名称 + 异常。为什么不是 服务 + 时间,因为在很多查询中,时间作为条件的情况比服务作为条件的频率更高,如果在服务放在前边,大部分时候 ClickHouse 需要在一个文件的不同部分去遍历,IO 会变得分散。

针对使用链路名称的查询,采用 ordre by + skip index 的方式优化。链路名称通常是接口名,而接口名在不同服务间重复的概率较小。这样在物理上,相同/相似的链路名称是排列在一起的,再使用 skip index 进一步筛选 granularity,剩余的数据很大概率是排列在一起的。这样就尽可能避免了扫描范围内的所有数据。

针对最重要的 traceId 查询就比较麻烦,因为查询可以不带任何条件(包括时间),使用 traceId 实际上是跨时间的。而 traceId 已经没办法再塞到索引的任何位置了,在尝试过各种二级索引后,效果依然非常不理想,可以说基本没什么效果。

最后我使用了当时还是 beta 特性的 Projection,其可以简单理解为表中表(实际在物理上也是这样存储的),即针对 partition 中的数据使用另一种结构存储。Projection 在 ClickHouse 中通常用来做 materialized view(物化视图)使用,相比后者优点是可以自动选择,以及生命周期受控于 partition。

而在这里我使用 projection 存储了 traceId 对应的 最大起始时间、最小起始时间、去重的服务名称列表,再拿得到的结果回源查表。最终的效果还可以接受,这也是为什么前边压测的结果中,查询链路详情的响应时间基本是查询列表的两倍,因为查了两遍 :)

而这套设计方式也有不完美的地方,有两点:

  1. 响应时间排序没有优化。查询列表时,如果筛选条件不足会非常慢。
  2. traceId 对应大量数据或时间跨度非常大时,会非常慢。有时候因为程序问题,或时间较长的延迟任务,会出现这种情况。

刨除这两点不完美,整体使用下来还是很顺畅的,打开链路页面从转个十秒钟,到秒开。在后台使用 ClickHouse 分析服务的链路构成,响应时间,甚至定时扫描设定告警等,都是额外提供的能力。数据的可见延迟也有很大改善,在 ES 中为了提高写入性能,一般 60s 左右才能查询到,在 ClickHouse 中链路从上报到落地只要 5 秒。

ClickHouse 优化与踩坑

相比于 ElasticSearch 成熟的集群管理能力,ClickHouse 还是比较难伺候的。

写入
客户端使用了官方的 JDBC,用 CSV 组装数据导入,尽量节省内存。但是这个 JDBC 实现还是有一定的限制,比如没办法压缩数据包,导致内存占用居高不下。这一点后来在其他项目上使用时有优化,后边开文另谈。

大规模写入时,发现 partition 数量居高不下,导致查询最近的数据反而很慢。于是引入了 CHProxy 作为代理,采用写入本地表,读取分布式表的方式,partition 数量大大降低。

分布式命令
分布式命令(on cluster)是个一言难尽的东西,有时候觉得很方便,有时候又会出很多问题。如分布式命令会在 zookeeper 里堆积,一旦某个命令在一个节点上未执行/执行失败,会卡死后续的所有命令。如 create database xxx on cluster yyy 几乎必定失败。

集群中加入了新的节点,该节点会执行历史的 on cluster 命令,此时很容易失败导致节点启动不成功,需要手动到 zk 中清理历史命令。

Merge Partition 问题
ClickHouse 对 partition 管理有一套策略,可参考 ClickHouse内核分析-MergeTree的Merge和Mutation机制 ,参数比较难调,基本上没什么介入的空间。有时候看它已经堆积很多了,但它就是不紧不慢。针对较大 part 也不会再进一步 merge,于是还得在凌晨跑个定时任务,将头一天的 part 合并为一个(optimize table #table_name partition #partition_name final),没办法通过配置解决。
还出现过比较诡异的现象,某个节点出现了 too many part 写入拒绝,上去一看 merge 任务在跑,但仔细看没有一个任务在真正执行,每个都是进度跑到一半就归零从头开始。结果导致 part 数只增不减。

查阅一番无果,日志中也没有任何提示。最终猜测是因为内存不足,merge 跑到一半申请不到内存,导致任务失败只得从头开始。尤其是在 merge 任务较多的情况下,会相互挤占内存,接连失败。在该节点查询执行一条 group by 命令,果然执行失败提示内存不足(在其他节点可以正常执行)。最后减少了配置中的 background_size,增大了一点内存占用,重启后问题解决。

结语

ClickHouse 是一个很有启发的软件,但也并不是万能的,归根结底更适合分析而不是点查。场景不对硬拗的话,很容易变成你伺候它而不是它伺候你。

目前我们在实践使用 ClickHouse 作为日志存储平台,代替 ElasticSearch(又来)和云平台日志服务,将实现模式无关、存算分离、租户隔离、快速分析等功能,届时会再分享一些经验。

你可能感兴趣的:(Skywalking 使用 ClickHouse 存储实践,性能提高 N 倍)