jq 处理 JSON 日志的常用招式:嵌套取值、流式统计与大整数精度

举报
yd_242458115 发表于 2026/11/04 13:07:31 2026/11/04
【摘要】 2.3GB 的 JSON 日志,我用 jq -s 一把梭,机器直接 OOM。改成流式后同样统计 47 秒跑完、内存 30MB。文章给出逐行投影、reduce inputs 累加、fromjson 跳过脏行与精度坑的避法。

排查一个慢接口,我手上是 2.3GB 的 JSON 日志,一行一个对象。第一次我用 jq -s 想一把算完,机器直接 OOM——-s 会把整个文件读进内存拼成一个数组。改成流式写法后,同样的统计 47 秒跑完,内存稳定在 30MB 出头。

一、先按 JSONL 读对

jq 默认逐行读取,天然适合一行一个 JSON 的日志,压根不需要 -s:

jq -r '.level' app.jsonl | sort | uniq -c | sort -rn | head
jq -r 'select(.level=="ERROR") | .path' app.jsonl | sort | uniq -c | sort -rn | head -n 5

-r 去掉输出里的引号,不加的话 uniq -c 会把带引号和不带引号当成两种值。select 放在管道最前面,jq 解析完立刻过滤,不为丢弃的行做额外构造。

二、-s 什么时候真的会炸

jq -s 把全部输入拼成一个数组,内存通常是文件大小的 3 到 10 倍(解析后的结构比文本更占地方)。2.3GB 的文件,-s 至少要 7GB 以上,小规格云主机上必然被 OOM killer 干掉。先量三行再决定:

ls -lh app.jsonl
free -m | head -n 2
wc -l app.jsonl

文件大于可用内存的一半,就别考虑 -s。必须全局聚合时,用 reduce 配 inputs 流式累加,内存只跟结果集大小有关:

jq -n 'reduce inputs as $x ({}; .[$x.level] = ((.[$x.level] // 0) + 1))' app.jsonl

-n 表示不拿第一个输入当初始值,inputs 逐个吐出剩下的对象。这段只维护一个计数对象,跑 2.3GB 时 RSS 峰值 30MB 左右。

三、嵌套取值与默认值

日志字段经常缺,.user.id 取不到就是 null,拼进字符串就变成字面的 null:

jq -r '[.ts, (.user.id // "-"), (.req.path // "-"), (.dur|tostring)] | @tsv' app.jsonl | head -n 3

三个要点:// 只在左边为 null 或 false 时取右边,0 和空字符串不会被替换;@tsv 会转义制表符,比自己拼 + "\t" + 稳;.dur|tostring 防止数字混进字符串列,否则后面 sort -n 直接失效。中间层不是对象时会报 Cannot index number with "b",加 ? 就变 null:.a?.b? // "-"。

四、别急着 group_by

group_by 内部要先排序,本质仍要拿到全部数据。逐行读的场景下更划算的是先投影,再交给系统 sort:

jq -r 'select(.level=="ERROR") | [.svc, .code] | @tsv' app.jsonl \
  | sort | uniq -c | sort -rn | head -n 10

同一份 2.3GB 日志,jq -s 'group_by(.svc)' 跑了 11 分钟还被 OOM 杀过两次;投影加 sort 版 47 秒出结果。差在 sort 会用外部归并,吃内存少得多。

按时间窗聚合,先把时间转成秒再整除:

jq -r '(.ts|fromdateiso8601/60|floor) as $m | "\($m) \(.code)"' app.jsonl \
  | sort | uniq -c | sort -k2,2n | head

五、混杂日志:跳过不是 JSON 的行

生产日志里常混着启动横幅、堆栈和空行,直接 jq 会报 parse error 并中断整条管道:

jq -Rr 'fromjson? | .level' app.jsonl | sort | uniq -c | sort -rn

-R 按原始文本逐行读,fromjson? 解析失败返回空,管道自然跳过,所以后面接 .level 是安全的。想知道有多少行解析不了:jq -R 'fromjson? // empty' app.jsonl | wc -l,再和总行数一比就有数了。

六、大整数会被悄悄改掉

jq 1.6 及更早用双精度浮点存数字,超过 2 的 53 次方的 ID 会丢精度:19 位的订单号读出来末尾几位直接变了,而且不报任何错,写回下游就是脏数据。

echo '{"id":1234567890123456789}' | jq -r '.id|tostring'
jq --version

jq 1.7 起保留数字的原始字面量,同样的输入能原样输出。升级不了就让上游把 ID 序列化成字符串,或者用 grep -o 单独抠出来,别让它经过 jq。

七、几条常备招式

-c 输出紧凑单行,喂给别的工具必备;-e 用退出码表达判断,jq -e '.ok' 在 false 或 null 时返回 1,可以直接写进 if;--arg 和 --argjson 传外部值,别把变量拼进查询字符串。最后一条示例把慢请求挑出来:

jq -e --arg svc api 'select(.svc==$svc and .dur>1000) | .ts' app.jsonl \
  > /tmp/slow.txt && echo "有慢请求"

一句话结论:动手前先问一句"这份数据能不能一次读完"。能,-s 随便用;不能,就逐行投影加 sort | uniq -c,或者 reduce inputs 流式累加。我踩过的两个坑都藏在这里:-s 的 OOM 和 2 的 53 次方的精度丢失,它们都不报错,只给你一个错的答案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。