Shell重定向(二):管道符 |,多命令联动与数据传递

—— 当你的脚本需要“把一个命令的输出,变成另一个命令的思考”

“Unix 的哲学不是写大程序,而是让小程序彼此对话。而管道,就是它们的语言。”
—— 一个被冗余临时文件逼出流水线思维的架构师


上个月,我们接到一个需求:统计电科金仓 KES 中所有慢查询(执行时间 > 1秒)的 SQL 模板,并按调用频次排序

实习生写了个脚本,逻辑是这样的:

  1. ksql 导出所有日志到 /tmp/full.log
  2. grep 筛出慢查询,写入 /tmp/slow.log
  3. awk 提取 SQL 模板,写入 /tmp/templates.txt
  4. sort | uniq -c 统计,写入 /tmp/report.txt
  5. 最后 cat /tmp/report.txt

结果:

  • 生成了4个临时文件
  • 磁盘 I/O 高
  • 脚本跑完忘了清理,/tmp 被占满
  • 中间任一环节失败,残留文件干扰下次运行

我问他:“为什么不让命令直接对话?”

他愣住。

今天,我们就讲清楚 管道(| —— Unix 世界最优雅的数据接力机制,以及如何用它写出无中间文件、高效、可组合的运维流水线。


一、管道的本质:进程间的“内存接力”

当你写:

cmd1 | cmd2 | cmd3

Shell 会:

  1. 启动 cmd1cmd2
  2. cmd1stdout 直接连接到 cmd2stdin
  3. 数据在内核缓冲区流动,不经过磁盘

这带来三大优势:

  • 零临时文件
  • 内存高效(流式处理,不全载入)
  • 天然错误传播(任一命令失败,管道中断)

二、基础用法:从日志中提取关键信息

假设 KES 的日志格式如下(简化):

2024-06-01 10:05:23.123 [INFO] duration: 1250 ms  statement: SELECT * FROM orders WHERE user_id=$1
2024-06-01 10:05:24.456 [WARN] duration: 800 ms   statement: UPDATE users SET ...

目标:找出所有 >1000ms 的 SQL 模板,去参数化,统计频次。

传统方式(反面教材):

grep "duration:" kes.log > /tmp/stage1
awk '$4 > 1000 {print $7}' /tmp/stage1 > /tmp/stage2
sed 's/\$[0-9]/?/g' /tmp/stage2 > /tmp/stage3
sort /tmp/stage3 | uniq -c | sort -nr > report.txt

管道方式(推荐):

grep "duration:" kes.log \
  | awk '$4 > 1000 {print $7}' \
  | sed 's/\$[0-9]/?/g' \
  | sort \
  | uniq -c \
  | sort -nr \
  > slow_query_report.txt
  • 无临时文件
  • 一行表达完整逻辑
  • 可读性反而更高(从左到右即数据流向)

三、实战一:实时监控 KES 连接来源

你想知道当前哪些 IP 在高频连接数据库:

# 从 sys_stat_activity 提取 client_addr,统计 Top 10
ksql -d kingbase-t -A -c "
    SELECT client_addr FROM sys_stat_activity 
    WHERE client_addr IS NOT NULL;
" \
  | grep -v '^$' \
  | sort \
  | uniq -c \
  | sort -nr \
  | head -10

输出示例:

   45 10.10.5.23
   32 10.10.5.41
   18 10.10.6.12

整个过程:

  • ksql 输出原始地址
  • grep -v '^$' 过滤空行
  • sort | uniq -c 统计频次
  • head 截取前10

全部在内存完成,无磁盘开销。


四、实战二:安全地批量终止异常会话

假设发现某个 IP(如 10.10.99.99)发起大量空闲连接,需批量 kill:

# 获取该 IP 的所有 backend_pid
ksql -d kingbase-t -A -c "
    SELECT pid FROM sys_stat_activity 
    WHERE client_addr = '10.10.99.99';
" \
  | grep -v '^$' \
  | xargs -r -I {} ksql -d kingbase-c "SELECT sys_cancel({});"

这里:

  • xargs 将 stdin 的每一行作为参数传给后续命令
  • -r 表示若输入为空,不执行命令(防误杀)
  • 整个流程无中间文件,原子性强

⚠️ 注意:生产环境 kill 会话需谨慎,此处仅为技术示例。


五、管道与重定向结合:构建完整工作流

你可能需要将管道结果同时:

  • 显示在终端(用于调试)
  • 保存到日志
  • 提取关键指标发给监控

tee 实现分流:

# 分析慢日志,同时存档和提取摘要
grep "duration:" /opt/Kingbase/log/kes.log \
  | awk '$4 > 2000' \
  | tee /var/log/slow_queries_$(date +%Y%m%d).log \
  | wc -l \
  > /tmp/slow_count.txt
  • tee 将输入同时写入文件和 stdout
  • wc -l 统计行数(即慢查询次数)
  • 最终数字存入 slow_count.txt 供监控采集

六、陷阱与最佳实践

1. 管道中的退出状态

默认,set -e 下管道只检查最后一个命令的退出码。

# 即使 grep 失败(无匹配),只要 sort 成功,脚本继续
grep "ERROR" log | sort

✅ 解决方案:启用 pipefail

set -euo pipefail

# 现在任一命令失败,整个管道返回非0
grep "FATAL" kes.log | mail admin@example.com

2. 避免在管道中修改外部变量

# 错误!while 在子 shell,count 不会传出
count=0
echo -e "a\nb\nc" | while read line; do
    ((count++))
done
echo $count  # 仍是 0

✅ 改用重定向:

count=0
while read line; do
    ((count++))
done < <(echo -e "a\nb\nc")

3. 大数据量时注意内存

管道虽高效,但若上游输出极大(如 GB 级日志),下游处理慢会导致内核缓冲区满,上游阻塞。

此时可考虑分块处理或使用 pv 监控流速。


七、何时不用管道?

  • 需要多次使用中间结果(可考虑命名管道或临时文件)
  • 命令依赖复杂分支逻辑(管道适合线性流程)
  • 跨主机数据传递(需结合 ssh 或消息队列)

但在 90% 的本地文本处理场景中,管道是最简洁、最 Unix 的选择


结语:管道是自动化的“神经突触”

在管理像 电科金仓 KES 这类产生海量日志与指标的数据库系统时(技术背景参考),你的脚本不应是孤立的命令堆砌,而应是数据流动的管道网络

每个命令专注做好一件事,通过 | 传递信息,共同完成复杂任务。

这才是 Unix 哲学的精髓:简单、组合、高效

今日实践
写一条管道命令,从 ksql -c "SELECT version();" 的输出中,
提取纯版本号(如 V009R001C10B1234),并打印。

做完这个,你就真正“连通”了命令。


注:文中涉及的 KES(Kingbase Enterprise Server)是由电科金仓开发的企业级关系型数据库,其运维常需通过管道实现日志分析与会话管理。技术细节可参考 产品页面

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐