这个命令是干啥的
优化脚本之前,你得先知道它到底慢在哪。time 就是干这个的 - 算一个命令从开始到结束花了多少时间。
有次我写了个数据备份脚本,每天早上定时跑,但越来越慢。最开始几秒就跑完了,后来要跑半小时。我大概知道是数据量大了所以慢,但具体慢在哪个环节不好说。
这时候 time 就派上用场了。给脚本前面加个 time 一跑,时间数据清清楚楚,再配合分段计时,马上找到瓶颈在哪。
time 可以是个shell内建命令(bash自带的),也可以是个独立的系统工具(/usr/bin/time)。两个输出的信息量差别挺大。
基本用法(3分钟上手)
最简单的用法
# 统计 sleep 命令的执行时间
time sleep 2
输出:
real 0m2.003s
user 0m0.000s
sys 0m0.002s
给任何命令前面加 time
# 看看ping一个网站花多久
time ping -c 4 baidu.com
# 统计脚本执行时间
time ./backup.sh
# 统计命令管道的时间
time tar -czf backup.tar.gz /data | ssh server "cat > backup.tar.gz"
进阶骚操作
读懂 real、user、sys 三个时间
这是 time 最核心的概念,搞懂了才算会用。
# 用计算密集的任务来演示
time find / -name "*.log" 2>/dev/null
比如上面 find 命令跑完,输出可能是:
real 0m12.345s
user 0m1.200s
sys 0m3.100s
real(实际时间): 从按下回车到命令结束,墙上的钟走了多久。包括等CPU、等IO、被别的进程挤占的时间。也就是你"感觉"到的时间。
user(用户态时间): 程序自己在用户空间跑代码花的时间。相当于真正干活的CPU时间。
sys(内核态时间): 程序调系统调用(读写文件、网络通信)花的时间。这部分时间花在内核里。
三者关系:
- user + sys 约等于程序消耗的CPU总时间
- real 往往大于 user + sys,因为程序需要等(等磁盘、等网络、等调度)
- 多核机器上 real 可能小于 user + sys,因为多个CPU同时干活
举个例子:你调了个菜,厨师花了5分钟做(user),中间让帮手切菜花了2分钟(sys),但因为前面排了3桌(其他进程),最后等了15分钟才吃上(real)。
多次运行取平均
单次运行的时间可能有波动(磁盘缓存、CPU调度都有影响)。正确做法是跑多次取平均:
# 用循环跑10次,统计每次的时间
for i in {1..10}; do
time ./my_script.sh 2>&1
done
# 或者用专用工具 hyperfine(需要安装)
# hyperfine './my_script.sh'
格式化输出
shell内建的 time 格式比较固定。想自定义输出格式,得用外部的 /usr/bin/time:
# 使用外部time命令
/usr/bin/time -f "用时: %E, CPU: %P, 内存: %M KB" ls
# 自定义格式输出
/usr/bin/time -f "\n真实时间: %E\n用户态: %U\n核心态: %S\nCPU占比: %P\n最大内存: %M KB\n上下文切换: %c+%w" ./script.sh
常用格式变量:
- %E - real时间(小时:分:秒)
- %U - user时间(秒)
- %S - sys时间(秒)
- %P - CPU使用百分比
- %M - 最大常驻内存(KB)
- %K - 平均内存总量(KB)
输出重定向的问题
注意 time 的输出是到 stderr(标准错误),不是 stdout。想保存到文件:
# 正确做法:把 stderr 重定向到文件
time ./script.sh 2> time.log
# 如果想分开保存:标准输出不要,时间保留
time ./script.sh > /dev/null 2> time.log
内建 time vs /usr/bin/time
# 查看当前用的是内建还是外部
type time
# bash内建 version:输出为 real/m sys/user 格式
# 外部 version:支持更多格式选项
区别:
- bash内建:简单快速,但输出格式固定,不支持 -f
- /usr/bin/time:功能丰富,支持格式控制、内存统计
# 强制用外部命令
/usr/bin/time -v ./script.sh
# -v 参数输出非常详细的信息
避坑指南
坑1:time 的统计管道不准
# 这样统计会把整个管道的所有进程一起算
time cmd1 | cmd2 | cmd3
如果想分别知道管道里每个命令的时间,用 \time:
# 需要在子shell里分别统计
time (cmd1 | cmd2 | cmd3)
更精确的办法是各段分别计时。
坑2:缓存影响测试结果
第二次运行同一个命令,结果往往比第一次快,因为有磁盘缓存。
# 第一次:从磁盘读文件
time wc -l huge_file.txt # real: 5.2s
# 第二次:文件已经在缓存里了
time wc -l huge_file.txt # real: 0.8s
跑性能测试时,先清缓存看看"冷启动"速度:
# 清除页面缓存(需要root)
sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
# 再跑测试
time ./script.sh
或者反过来,热数据场景就预热一下:
# 先预热一次
./script.sh > /dev/null
# 真正计时
time ./script.sh > /dev/null
坑3:脚本本身的开销干扰
如果脚本本身只有一两行,time 测出来的时间主要花在 shell 启动上:
# 这样测一个 echo,大部分时间是启动 shell 本身
time bash -c 'echo hello'
# real: 0m0.004s
# 其实 echo 本身不到 0.001s
所以对于特别快的命令,多跑几次看平均更准确。
坑4:user + sys 大于 real
遇到这种情况,说明你的程序在多核CPU上并行跑。不是bug,是特性:
# 并行压缩,多个CPU同时干活
time gzip largefile.tar
# user 2.0s + sys 0.5s = 2.5s
# real 可能只有 1.5s(两个核分担了)
实战场景(重点!结合真实运维场景)
场景1:数据库备份脚本性能诊断
有次发现凌晨备份脚本越来越慢,用 time 来排查:
# 先对整个脚本计时
time ./backup_db.sh
# 输出
# real: 18m32s
# user: 2m15s
# sys: 1m40s
real 远大于 user+sys,说明时间花在等IO上。分段排查:
# 压缩阶段
time gzip /tmp/db_dump.sql
# 传输阶段(最可疑)
time scp /tmp/db_dump.sql.gz backup-server:/backup/
果然,传输阶段耗时15分钟。方案改成 rsync 增量传输,并把压缩和传输改为并行跑,总时间从18分钟降到4分钟。
场景2:代码性能对比
哪个版本快?用 time 说话:
# 方案A:用 find
time find /data -name "*.txt" -exec grep "error" {} \; > /dev/null
# 方案B:用 grep -r
time grep -r "error" /data/*.txt > /dev/null
实际测试下来,grep -r 通常比 find+exec 快不少。用数据说话,而不是靠猜。
场景3:监控脚本执行耗时
脚本跑得慢没关系,但突然变慢就要报警。可以写个检测:
#!/bin/bash
# 监控备份脚本耗时,超过阈值报警
start=$(date +%s)
./backup.sh
end=$(date +%s)
duration=$((end - start))
# 如果超过10分钟(600秒),发报警
if [ $duration -gt 600 ]; then
echo "WARNING: 备份耗时 ${duration}s,超过阈值!" | mail -s "备份超时告警" admin@example.com
fi
场景4:用 time 做简单的基准测试
服务器性能对比:
# 计算能力测试(计算圆周率1万位)
time echo "scale=10000; 4*a(1)" | bc -l
# IO能力测试
time dd if=/dev/zero of=/tmp/test bs=1M count=1024
# 网络延迟测试
time curl -s -o /dev/null https://api.example.com/health
今日作业
写一个简单的脚本,执行以下操作,并用 time 统计时间:
#!/bin/bash
# 我的测试脚本
echo "开始测试..."
# 生成1000个0-999的随机数
for i in {1..1000}; do
echo $RANDOM
done > /tmp/random.txt
# 排序并去重
sort -n /tmp/random.txt | uniq > /tmp/sorted.txt
# 统计行数
wc -l /tmp/sorted.txt
echo "完成"
要求:
1. 保存为 /tmp/test.sh 并加执行权限
2. 用 time 运行它
3. 运行3次,记录下来每次的 real、user、sys
4. 分析为什么 user 和 sys 加起来不等于 real
5. 试着改成 /usr/bin/time -v 看看详细输出
做完后,把3次的结果和你的分析写下来。