玩转 ClickHouse 日志系统(高级查询篇)

991 阅读3分钟

何为高级查询

用过 DataDog 或者阿里云日志服务的,相信会对他们的高级查询印象深刻,日志原来还可以这样分析: CleanShot 2022-10-18 at 22.22.44@2x.png

如上图中,我在 nginx-access 日志中按域名聚合,统计平均响应时间并排序输出。有这种功能的日志系统就不再是仅限于存储浏览的工具,还可以从系统产生的海量日志中快速分析出有价值的数据,对定位问题、分析系统会非常有帮助。

我们的实现效果

同样是 nginx-access 日志: CleanShot 2022-10-18 at 22.42.26@2x.png

由于结构化日志都是 KV 形式存储在数据库,所以查询列会稍微复杂一些需要指定类型:

select http_host -> select string['http_host']
select upstream_response_time -> select number['upstream_response_time']

不过我们支持更加灵活的写法,如省略 Select 前缀直接写 order by 也可以接受: CleanShot 2022-10-18 at 22.59.08@2x.png

还可以写多个分隔符,将前边的 SQL 作为子查询: CleanShot 2022-10-18 at 23.03.50@2x.png

更多案例

下面分享几个内部文档中总结的案例

统计不存在的节点

image.png

首先准确筛选日志「工作节点不存在」,字符串截取到 workerId 后的数字,再进行 group by 统计:

工作节点不存在  | select sublen(message, 'workerId=', 10) as workerId, count() group by workerId 

结果如下: image.png

查看错误活动配置影响的用户

由于业务方活动配置失误,导致很多用户无法进入活动页,现在希望统计受影响的结果,如下图中 message 中的 activityId、uid、inviteCode 和时间字段。日志内容如下:

CleanShot 2022-10-18 at 23.08.19@2x.png 通过高级查询则可以直接从日志中拆出对应的字段:

参与的活动不存在 and BIZ_ERROR 
| select dt, JSONExtractString(message, 'requestPayload') AS requestPayload 
| select dt, JSONExtractString(requestPayload, 'eventParam', 'uid') AS uid,
JSONExtractString(requestPayload, 'eventParam', 'activityId') AS activityId,
JSONExtractString(requestPayload, 'eventParam', 'inviteCode') AS inviteCode 

查询结果: CleanShot 2022-10-18 at 23.11.28@2x.png 这时发现有 uid、activityId 重复的情况,希望进一步再去重一下。在 ClickHouse 中有 limit by语法可以很方便地实现,现在高级查询如下:

参与的活动不存在 and BIZ_ERROR 
| select dt, JSONExtractString(message, 'requestPayload') AS requestPayload 
| select dt, JSONExtractString(requestPayload, 'eventParam', 'uid') AS uid,
JSONExtractString(requestPayload, 'eventParam', 'activityId') AS activityId,
JSONExtractString(requestPayload, 'eventParam', 'inviteCode') AS inviteCode 
limit 1 by uid, activityId

查询结果中可见已经实现去重,结果数量也少了很多:

CleanShot 2022-10-18 at 23.13.02@2x.png

分组统计接口响应时间

业务方发现 Dubbo 线程池用尽,此时调用方在跑异步任务,大量调用该服务。从日志中的 Dubbo 接口响应时间看不出明显上升,因为总共有十几万条日志,没办法一一翻阅。 Dubbo 接口日志如下:

CleanShot 2022-10-18 at 23.13.44@2x.png

筛选日志后,在高级查询汇总取出接口响应时间进行统计:

trace_ext.endpoint_name:cn.jojo.mall.gravity.rpc.api.service.BuyCourseActionRpcService.queryActionCount(BuyCourseActionQueryReq)
| select toInt64(subend(message, 'timeUsed ', 'ms'))  as time 
| select max(time), quantile(0.99)(time) as p99, quantile(0.90)(time) as p90, avg(time) 

结果如下,可以看出最大响应时间 905ms,说明确实在某些时刻响应时间发生了波动:

image.png

但这时看不出来趋势,于是将时间加入进去进行分组统计,使用 toStartOfInterval(date, interval 5 second)并 group by 该时间:

trace_ext.endpoint_name:"cn.jojo.mall.gravity.rpc.api.service.BuyCourseActionRpcService.queryActionCount(BuyCourseActionQueryReq)"
| select date, toInt64(subend(message, 'timeUsed ', 'ms'))  as time 
| select max(time), quantile(0.99)(time) as p99, quantile(0.90)(time) as p90, avg(time), toStartOfInterval(date, interval 5 second) as fiveSecond, count() 
group by fiveSecond order by fiveSecond

结果如下,可以明显看出在 06:20 ~ 06:30 响应时间和调用量有明显上升,确定了具体时间点,同时确定了原因是并发量突增导致数据库波动。

image.png