SkywalkingはClickHouseを使用して実際の戦闘を保存します

簡単に言えば

ClickHouseは、過去2年間でオー​​プンソースコミュニティでますます人気が高まっています。おそらく、ElasticSearchのコストが高すぎるために、ElasticSearchの代わりにClickHouseをどこから使用し始めたのかわかりません。検索エンジンでは、ある程度の暴力的な検索は許容できます。

Skywalkingを使用した後、バックエンドストレージの要件が高すぎることがわかりました。(32C + 64G + 2T)x8構成を使用すると、クラウドプラットフォームの月間オーバーヘッドは数万になります。パフォーマンスは依然として非常に緊急であり、クエリがタイムアウトすることがよくあります。半年間最適化した後、ついにClickHouseに置き換えることにしました。

ClickHouseを使用すると、マシンの数が50%減少します。

クエリリンクリストが5.53/秒から166/秒に増加し、応答時間が3.52秒から166ミリ秒に短縮されました。

クエリリンクの詳細が5.31/sから348/sに増加し、応答時間が3.63sから348msに短縮されました。

リンクデータの保存期間が5日から10日に延長され、データ量は数百億に達しました。

ESとの比較では、ディスク容量の削減について言及されることがよくあります。実際、少なくとも私の実際の経験では、ClickHouseの圧縮率はそれほど誇張されていません。ESスペースの使用率が高い場合は、インデックスで有効になっていない可能性がありますcodec: best_compression

ClickHouseには欠点がないわけではありません。この記事では、ClickHouseをSkywalkingのバックエンドストレージとして使用する方法について説明します。この記事では、ClickHouseの基本原則について詳しくは説明しません。読者は、ClickHouseをある程度理解した上でそれを読む必要があります。

(膨大なワークロードのため、Skywalkingストレージの変換はリンクデータ、つまりセグメントの保存に制限され、残りは変換なしでElasticSearchに保存されます)

テーブルデザイン

基本的に、ClickHouseは1種類のインデックス、つまりOrderByのインデックスしか作成できません。リンクテーブルには多くのクエリ条件があり、ほとんどすべてのフィールドがクエリ条件であり、さまざまな並べ替えが含まれているため、設計がより難しくなります。

查询条件:时间、服务名称、服务实例、链路类型、链路 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(又来)和云平台日志服务,将实现模式无关、存算分离、租户隔离、快速分析等功能,届时会再分享一些经验。

おすすめ

転載: juejin.im/post/7079748123148419079