使用 ClickHouse 构建通用日志系统
序言
ClickHouse 是一款常用于大数据分析的数据库,因为其压缩存储,高性能,丰富的函数等特性,近期有很多尝试 ClickHouse 做日志系统的案例。本文将分享如何用 ClickHouse 做出好用的通用日志系统。
日志系统简述
在聊为什么 ClickHouse 适合做日志系统之前,我们先谈谈日志系统的特点。
-
大数据量。对开发者来说日志最方便的观测手段,而且很多情况下会直接打印 HTTP、RPC 的请求响应日志,这基本上就是把网络流量复制了一份。
-
非固定检索模式。用户有可能使用日志中的任意关键字任意字段来查询。
-
成本要低。日志系统不宜在 IT 成本中占比过高。
-
即席查询。日志对时效性要求普遍较高。
数据量大,检索模式不固定,既要快,还得便宜。所以日志是一道难解的题,它的需求几乎违反了计算机的基本原则,不过幸好它还留了一扇窗,就是对并发要求不高。大部分查询是人为即兴的,即使做一些定时查询,所检索的范围也一定有限。
现有日志方案
ElasticSearch
ES 一定是最深入人心的日志系统了,它可以满足大数据量、全文检索的需求,但在成本与查询效率上很难平衡。ES 对成本过于敏感,配置低了查询速度会下降得非常厉害,保障查询速度又会大幅提高成本。
Loki
Grafana 推出的日志系统。理念上比较符合日志系统的需求,但现在还只是个玩具而已。
三方日志服务
国内比较杰出的有阿里云日志服务,国外的 Humio、DataDog 等,都是抛弃了 ES 技术体系,从存储上重做。国内还有观测云,只不过其存储还是 ES,没什么技术突破。
值得一提的是阿里云日志服务,它对接了诸如 OpenTracing、OpenTelemetry 等标准,可以接入监控、链路数据。因为链路数据与日志具有很高的相似性,完全可以用同一套技术栈搞定。
三方服务优点是日志摄入方式、查询性能、数据分析、监控告警、冷热分离、数据备份等功能齐备,不需要用户自行开发维护。
缺点是贵,虽然都说比 ES 便宜,但那是在相同性能下,正常人不会堆这么多机器追求高性能。最后是要把日志数据交给别人,怎么想都不太放心。
ClickHouse 适合做日志吗?
从第一性原则来分析,看看 ClickHouse 与日志场景是否契合。
大数据量,ClickHouse 作为大数据产品显然是符合的。
非固定模式检索,其本身就是张表,如果只输入关键字没有列名的话,搜索所有列对 ClickHouse 来说显然是效率低下的。但此问题也有解,后文会提到。
成本低,ClickHouse 的压缩存储可将磁盘需求减少一个数量级,并能提高检索速度。与之相比,ES 还需要大量空间维护索引。
即席查询,即席有两个方面,一个是数据可见时间,ClickHouse 写入的能力较 ES 更强,且写入完成即可见,而ES 需要 refresh_interval 配置最少 30s 来保证写入性能;另一方面是查询速度,通常单台 ClickHouse 每秒钟可扫描数百万行数据。
ClickHouse 日志方案对比
很多公司如京东、唯品会、携程等等都在尝试,也写了很多文章,但是大部分都不是「通用日志系统」,只是针对一种固定类型的日志,如 APP 日志,访问日志。所以这类方案不具备普适性,没有效仿实施的必要,在我看来他们只是传达了一个信息,就是 ClickHouse 可以做日志,并且成本确实有降低。
只有 Uber 的 日志方案 真正值得参考,他们将原本基于 ELK 的日志系统全面替换成了 ClickHouse,并承接了系统内的所有日志。
我们的日志方案也是从 Uber 出发,使用 ClickHouse 作为存储,同时参考了三方日志服务中的优秀功能,从前到后构建了一套通用日志系统。ClickHouse 就像一块璞玉,像 ELK 日志系统中的 Lucene,虽然它底子不错,但想用好还需要大量的工作。
设计
存储设计
存储是最核心的部分,存储的设计也会限制最终可以实现哪些功能,在此借鉴了 Uber 的设计并进一步改进。建表语句如下:
create table if not exists log.unified_log
(
-- 项目名称
`project` LowCardinality(String),
-- DoubleDelta 相比默认可以减少 80% 的空间并加速查询
`dt` DateTime64(3) CODEC(DoubleDelta, LZ4),
-- 日志级别
`level` LowCardinality(String),
-- 键值使用一对 Array,查询效率相比 Map 会有很大提升
`string.keys` Array(String),
`string.values` Array(String),
`number.keys` Array(String),
`number.values` Array(Float64),
`unIndex.keys` Array(String),
-- 非索引字段单独保存,提高压缩率
`unIndex.values` Array(String) CODEC (ZST