为什么服务器运维绕不开 jq
现代服务器上的数据,越来越多是 JSON 格式的:Kubernetes 和 Docker 的元数据、云厂商 CLI 的输出、API 的响应、各种应用的配置文件、结构化日志……它们本质上是"给人看的设计",但当你需要在 SSH 里快速提取某一个字段、统计某一类事件、或者把配置改一改再写回去时,用眼睛看、用手改就非常低效了。
jq 就是解决这个问题的工具。它被称为"命令行的 JSON 处理器",用一条命令就能完成"从一段 JSON 里挑出我关心的部分、按我的需要重组"这件事。它功能很深,但日常运维真正高频用到的其实是一小撮用法。本文不讲全部语法,只讲服务器运维场景下最实用的那部分,配上真实的例子,让你看完就能用起来。
先理解 jq 的工作模型:过滤器
jq 的基本形态是:jq '过滤器' 输入文件。它把输入的 JSON 当作一条"流",你的过滤器描述"从这条流里产出什么"。核心概念只有两个:
.:代表当前的整个值。单独写jq .就是把输入原样格式化输出一遍——这本身就是最常用的用法(把一行挤在一起的 JSON 变漂亮)。- 路径:用
.key取对象字段,用.key.subkey取嵌套字段,用.array[0]取数组元素,用.array[]遍历数组。
举个例子,假设有一段 JSON:
{"name":"web-01","cpu":4,"mem_mb":8192,"tags":["prod","web"]}那么:
jq '.name' # 输出 "web-01"(注意带引号) jq '.cpu' # 输出 4 jq '.tags[0]' # 输出 "prod" jq '.tags[]' # 逐个输出 "prod" 和 "web"
看清楚一个新手常被绊住的点:字符串输出默认带引号。想拿到不带引号的裸字符串,加 -r(raw output):
jq -r '.name' # 输出 web-01(无引号)
凡是结果要丢给别的命令(比如 ssh、curl 拼 URL、写进 shell 变量)的场景,几乎都要加 -r,否则引号会被当成内容的一部分。
从 API 与 CLI 输出里提取字段
这是 jq 在运维里最高频的用途。云厂商 CLI 或 REST API 返回的东西往往又大又杂,你只要其中一两个值:
# 从一段 API 响应里取 IP
curl -s http://169.254.169.254/latest/meta-data/ # 有些是纯文本,不是 JSON
# 假设某个 API 返回 {"instance":{"public_ip":"1.2.3.4","state":"running"}}
curl -s https://api.example.com/v1/instance | jq -r '.instance.public_ip'
# 取数组里所有元素的某个字段
echo '[{"id":1,"name":"a"},{"id":2,"name":"b"}]' | jq -r '.[].name'.[] 表示"遍历整个数组",后面接 .name 就是对每个元素取 name。结果一行一个,非常适合再接给 while read、xargs 做批量操作。
把字段拼成你想要的形式
光取值还不够,很多时候你要把 JSON 里的几个字段拼成一行文本、一条命令、或者一个 CSV 行。jq 支持字符串插值:
echo '{"ip":"1.2.3.4","port":8080}' | jq -r '"\(.ip):\(.port)"'
# 输出 1.2.3.4:8080"\(...)" 是 jq 的插值语法,把字段值嵌进字符串。-r 保证外层的引号不出现。这个模式在生成配置文件、拼 ssh 参数、批量生成脚本时极其好用。比如把一组服务器信息生成 ssh config 片段:
echo '[{"host":"web-01","ip":"10.0.0.11"},{"host":"db-01","ip":"10.0.0.21"}]' \
| jq -r '.[] | "Host \(.host)\n HostName \(.ip)\n User ops"'输出直接就是可用的 SSH 配置段落。这种"JSON 数据 → 文本模板"的转换,是 jq 被低估的能力。
筛选:只留我想要的那些
真实数据里往往有一大堆条目,你只关心满足某些条件的:
# 选出所有 state 为 running 的 jq '.[] | select(.state == "running")' # 选出内存大于 4096 的 jq '.[] | select(.mem_mb > 4096)' # 组合条件 jq '.[] | select(.state == "running" and .cpu >= 4)'
select(条件) 会保留满足条件的元素,过滤掉其余。配合管道(|),你可以串起来:
jq -r '.[] | select(.tags | contains(["prod"])) | "\(.name) \(.cpu)c"'
这里 .tags | contains(["prod"]) 表示"该元素的 tags 数组里是否包含 prod"。contains、startswith、endswith、test("正则") 这些是常用的匹配函数。
改配置:jq 也能改 JSON
运维里经常要"读一个 JSON 配置、改一个值、写回去"。手改容易漏括号、破坏格式,用 jq 可以做得又准又安全:
# 把 replicas 从 3 改成 5,其余原样
jq '.spec.replicas = 5' config.json > config.json.tmp && mv config.json.tmp config.json
# 给对象新增一个字段
jq '. + {"debug": true}' config.json
# 删除某个字段
jq 'del(.password)' config.json
# 向数组追加元素
jq '.hosts += ["new-host"]' config.json这里最值得记住的是那个"先写临时文件、再 mv 覆盖"的模式。千万不要 jq '...' config.json > config.json——shell 会在 jq 读取之前就把 config.json 截断成空文件,结果是你既没读到内容、又把原文件清空了。这是重定向比管道更容易犯的错。用 .tmp 中转 + mv(原子替换)两步走,才安全。
统计与聚合:jq 当小型分析器
jq 内置了数组长度、求和、分组等能力,不用写脚本也能做统计:
# 数组长度
jq 'length' data.json
# 对某个数值字段求和
jq '[.[].mem_mb] | add' data.json
# 取最大/最小
jq '[.[].cpu] | max' data.json
# 按某字段分组计数(group_by 前必须先 sort_by)
jq '[.[] | .state] | group_by(.) | map({state: .[0], count: length})' data.json
# 输出 [{"state":"running","count":3},{"state":"stopped","count":1}]这个分组计数在分析结构化日志时特别实用——比如从一批 JSON 日志里统计"每个状态码出现了多少次"、"每个来源 IP 请求了多少次"。以前要写 awk 脚本,现在一条 jq 就够。
注意最后一条里的 map({...}):map 是对数组每个元素做变换,这里把每个分组变成一个带 state 和 count 的对象,比原始的嵌套数组好读得多。jq 的真正威力就在这里:筛选 + 变换 + 重组,一条命令完成。
处理 JSON 日志的两个实战套路
现在越来越多的应用(Nginx 用自定义 log_format、各种 Go/Python 服务)把日志打成一行一个 JSON。用 jq 处理这类日志有两个套路值得记住:
# 套路一:整行 JSON 逐行处理(每行是一个独立 JSON 对象) cat app.log | jq -r 'select(.level == "error") | "\(.time) \(.msg)"' # 套路二:用 sort/uniq 配合 jq 做 Top N cat app.log | jq -r '.client_ip' | sort | uniq -c | sort -rn | head -10
套路一是"过滤出所有 error 级别、拼成可读行";套路二是"统计访问量最高的 10 个 IP"。两者都体现了 jq 的定位——它不是日志分析系统,而是日志处理流水线里那个称手的环节。它输出的是纯文本流,能和 grep、sort、uniq、awk、xargs 无缝组合,这正是 Unix 哲学的体现。
如果 JSON 日志是"多行美化"格式(每个对象跨多行),整行管道就不灵了,这时可以先把连续的对象合并成数组(或用 jq -s 的 slurp 模式读入整个流):
jq -s '.[] | select(.level == "error")' pretty.log
-s 会把输入的多个 JSON 值读成一个数组,然后就能正常遍历。
几个必须知道的坑
- 不加
-r会带引号:结果喂给 shell 会出错,务必养成"要裸值就-r"的习惯。 .[]作用在对象上:如果当前值是对象,.[]遍历的是"值"而不是"键值对"。要同时拿键和值,用to_entries,它把对象转成[{"key":..,"value":..}]。- 字段不存在不报错,只是返回
null:所以拼字符串时可能拼出 "null",用//提供默认值,比如.name // "unknown"。 - 不支持注释:jq 读的是严格 JSON,标准 JSON 不允许注释。带注释的配置文件(如某些
jsonc)要先去掉注释。 - 大文件内存:
-s(slurp)会把整个输入读进内存,处理 GB 级日志时要谨慎,优先用逐行模式。 - 版本差异:
jq 1.5和1.7在某些函数(如正则)上行为略有差异,脚本里用到新特性前先jq --version确认。
yq:当配置是 YAML 时
很多运维配置是 YAML 而不是 JSON(Docker Compose、Kubernetes、Ansible、CI 配置……)。这时用 jq 会报错,需要用姊妹工具 yq。它的语法和 jq 高度相似,学习成本几乎为零:
# 安装(Debian/Ubuntu,来自 snap 或直接下二进制) # Ubuntu 24.04 起也可以 apt install yq yq '.services.web.image' docker-compose.yml yq -r '.services | keys[]' docker-compose.yml yq '.services.web.ports += ["8081:80"]' docker-compose.yml
注意市面有两个都叫 yq 的工具(一个 Go 写的 mikefarah/yq,一个 Python 写的 kislyuk/yq),语法和安装方式不同。建议统一用 Go 版(单文件二进制、无依赖,更适合服务器)。判断方法:yq --version 输出里带不带 Python 字样。
把它纳入你的日常工具链
jq 的价值不在于它有多全,而在于它是一个随手可用的"数据手术刀":服务器上几乎都预装了或一行命令就能装,你不需要为一个临时需求写整套 Python 脚本。日常可以固定养成几个习惯:
- 看到一堆挤在一起的 JSON,先
| jq .格式化再看——比在编辑器里手动格式化快得多。 - 要从 API/CLI 拿值,先
| jq .看清结构,再写精确的路径表达式。 - 结果要交给其他命令,一律
jq -r。 - 改配置文件,永远用"重定向到临时文件 + mv"两步走,绝不原地覆盖。
- 统计日志,先想"能不能用 jq + sort/uniq/awk 一行搞定",能就不写脚本。
把这几点练成肌肉记忆,你会发现很多以前要写小脚本才敢做的事,现在直接在 SSH 里就能秒杀。工具用对了,运维效率的提升往往比调一个参数来得更实在。