ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

grep -r 命令详解:递归搜索日志与线上故障定位实战

grep -r 命令详解:递归搜索日志与线上故障定位实战 凌晨两点被手机吵醒线上报了一个“支付回调超时”的故障。你登录服务器日志目录下堆着几十个按天滚动、按模块分子目录的文件只知道要找timeout却不知道它散落在哪几个文件里。这时候绝大多数人都会一条一条地 grep或者直接在 IDE 里全局搜。但如果你跟我一样经常面对一堆服务器、几个 G 的日志、不知道藏在哪个角落的配置就会发现grep -r才是那个能把你从“翻文件翻到怀疑人生”里捞出来的命令。这篇就把grep -r讲透。我不打算写成 man 手册式的参数大全而是站在“真正在用它解决实际问题”的角度从基础用法、组合参数、踩过的坑到一次线上日志定位的完整复盘把这条命令里里外外说明白。适用人群很明确后端开发、运维、SRE、数据分析以及所有需要在命令行里快速搜内容的人。零基础可以跟着敲有经验的也能在参数组合和坑位章节里找到点新东西。1. 为什么 grep -r 值得单独拎出来写一篇1.1 普通 grep 解决不了“不知道文件在哪”的问题很多人最初接触 grep 的用法是grep error app.log——文件固定、路径明确、一次搜一个文件这当然够用。但现实里更常见的情况是你只知道关键词不知道它在哪个文件。日志按天滚动历史文件叫api-2024-06-01.log、api-2024-06-02.log有时候还有 gz 压缩包代码项目动辄几千个文件某个配置项只出现了两三次服务器上某个服务的配置散落在 /etc 下面的多个子目录里。真到了这种场景还是一个文件一个文件地搜效率低到离谱。你可能也会下意识用通配符比如grep timeout *.log。这样确实能一次搜多个文件但坑也很明显*在 shell 里只匹配当前目录下的文件不递归子目录更不会自动匹配隐藏文件而且文件数量一旦上千命令行参数会直接超出系统上限报一个Argument list too long当场血压升高。普通 grep 加上通配符本质上还是“手动列出文件”跟grep -r的“自动遍历目录”有着本质区别。1.2 grep -r 到底干了什么grep -r做的是把“目录”直接变成搜索对象。你给它一个目录它会递归地把目录里所有文件都过一遍每找到一个匹配就输出一行并且默认带上“哪个文件”这个信息。比如grep -rn timeout /data/logs/app/这条命令会进入/data/logs/app/下每一层子目录逐个文件扫描找到匹配后输出成这种格式/data/logs/app/api/api-2024-06-01.log:187:2024-06-01 12:03:22 ERROR request timeout after 3000ms /data/logs/app/job/job-2024-06-01.log:45:job execution timeout, retry later冒号前面是文件路径中间是行号后面是那一行的完整内容。这个设计非常实用因为你不仅知道了关键词出现在哪个文件还知道它在哪一行接下来的定位工作就有了明确的下手点。这个“文件名:行号:内容”的三段式结构是后面几乎所有进阶用法的基础。1.3 和 find | xargs grep 相比谁更顺手有些人喜欢用 find 配合 grep 做递归搜索find /data/logs/app -type f -name *.log -exec grep timeout {} 效果上确实也能递归但有两个麻烦。一是命令本身写起来长-exec、{}、这些语法对新手不友好参数位置稍微写错就会得到一堆奇怪报错。二是文件名里有空格、换行这类特殊字符时find 传参给 xargs 还得考虑-print0和-0否则结果会错乱。grep -r自己内部处理了目录遍历和文件读取你不用担心文件名里有没有空格命令也短了一大截。我的看法是find 有 find 的价值按时间、大小过滤文件再处理但单纯做“内容递归搜索”这件事grep -r就是更顺手的那个。工具选型的第一原则是“能用最简单的方式解决问题”不要为了显得高级而叠加命令。1.4 适合什么人、什么场景如果你属于下面任意一种情况这篇内容值得往下看经常登录服务器看日志但日志路径深、文件多、需要跨目录检索在大型代码仓库里找一个引用、一个魔法数字、一个环境变量名排查线上问题时手头只有 Shell需要快速从一堆文件里拉出关键证据或者你只是想把 Linux 这套基础命令用得更明白。2. 先把基础用法吃透参数、输出和最容易忽略的细节2.1 新手必看grep -r 的基本语法和默认路径行为grep -r的完整语法跟普通 grep 几乎没有区别grep [选项] 匹配模式 [路径...]区别在于路径可以是一个目录甚至可以是多个目录。但这里有个很多人经历过但没留神的坑如果你只写grep -r timeout而不给路径它不会默认去搜当前目录而是像普通 grep 一样从标准输入读数据——也就是说它会在那里等着你敲键盘看起来像“卡住了”。正确习惯是显式给一个点grep -rn timeout .这个点表示当前目录。多写一个点能避免很多“命令怎么没反应”的尴尬时刻。在脚本里这个问题更容易埋雷。我见过有人写了grep -r pattern然后在 cron 里跑脚本挂在那里什么也不干就因为管道没有输入。所以记忆点就一条grep -r后面要么跟目录要么跟文件列表别让它裸奔。2.2 记住这 8 个高频参数就够了参数不用一次背全先记住高频的这组参数作用使用示例-n显示行号定位必备grep -rn timeout .-i忽略大小写grep -ri error /var/log-l只列出包含匹配项的文件名grep -rl config /etc-c统计每个文件匹配次数grep -rc ERROR logs/-w整词匹配避免 err 匹配到 errorgrep -rnw err src/-x整行匹配常用于精确找配置grep -rx host127.0.0.1 conf/-v反向匹配排除掉不想要的行grep -rv ^# /etc/nginx/-o只输出匹配到的部分配合统计很香grep -roE [0-9] logs/-E使用扩展正则写表达式更省心grep -rnE (ERR|WARN) logs/这几个参数大多数可以自由组合。比如grep -rni经常一起用表示递归、不区分大小写、带行号。排查问题时三条一起上基本能覆盖大部分初筛需求。我自己的习惯是第一遍搜索必带-n因为光知道“有匹配”没有意义知道“在哪一行”才能继续往下查。2.3 输出格式文件名、行号和它们的取舍默认情况下grep -r在匹配到的行前面加上“文件路径:”前缀加上-n后是“文件路径:行号:”。但在某些场景下你不想看到文件名或者恰恰相反你只想看文件名。如果要只输出文件名用-l。注意-l的价值被很多人低估了它不只是“把匹配文件路径打出来看看”而是可以作为下一步操作的输入。比如你想在项目里找到所有包含TODO的文件然后统一改可以先grep -rl TODO src/拿到文件列表后再交给 sed 或 xargs 做批量处理。这类用法后面会展开讲。如果不想让匹配行前面带文件名比如你只关心内容本身准备把输出再喂给别的程序可以用-h去掉前缀。反过来如果你想强调某个文件甚至加了-h也想显示文件名用-H强制加上。这两个参数在实际工作中没有前几个高频但用对之后脚本输出会干净很多。3. 实战组合拳过滤、排除、管道和正则配齐3.1 只搜想要的文件类型--include 和 --exclude 的用法递归搜索最大的副作用是目录里什么文件都搜包括你不想搜的图片、二进制、临时文件。普通文本和日志还好如果项目里有压缩包、可执行文件、min.js 这种压缩过的东西匹配结果会很乱还可能刷出大量Binary file ... matches之类的干扰信息。解决办法是用--include限定只搜索特定类型的文件grep -rn --include*.log timeout /data/logs/app/ grep -rn --include*.py --include*.js config /project/src/注意--include是写文件后缀的匹配模式支持通配符。如果只搜 py 和 js 两种文件就写两个--include它们之间是“或”的关系。同样地用--exclude排除某些文件grep -rn secret /etc/ --exclude*.bak --exclude*.swp这个习惯非常好用尤其在大型项目里配合--include可以把搜索结果从几千行瞬间压缩到几十行。我自己的经验是搜源代码时先想清楚“这个关键词最可能出现在哪类文件”再加对应的--include比全量搜完再肉眼过滤高效太多。3.2 把烦人的目录排除掉--exclude-dir如果说--include解决的是“搜什么”那--exclude-dir解决的就是“不进哪里”。在实际项目中最典型的就是.git目录和node_modules目录。在 Git 仓库里执行grep -rn foo .系统默认会把.git目录里的内容也扫一遍。Git 对象文件全是压缩过的数据搜索起来又慢又乱结果里充满了二进制文件提示。而node_modules动辄几万个文件全量扫一轮浪费的时间足够喝杯咖啡了。正确的姿势是提前把它们排掉grep -rn foo . --exclude-dir.git --exclude-dirnode_modules --exclude-dirdist--exclude-dir同样支持通配符比如--exclude-dir*cache*。我的建议是在新项目、新环境第一次做递归搜索前先想一下目录里哪些子目录是不用进的把它们写进命令里。养成这个习惯之后搜索的准确率和速度都会上一个台阶。3.3 用上下文还原现场-B、-A、-C默认 grep 只输出匹配到的那一行。但在看日志的时候你往往想知道错误发生之前发生了什么、之后又恢复了没有。这时候单独一行信息是不够的。比如日志里有一条request timeout它前面可能是某个接口的入参后面可能是下一次重试的时间。想还原这个现场参数很直白grep -rn -B 2 -A 5 request timeout /data/logs/app/api/-B 2表示把匹配行之前的 2 行也一起输出-A 5表示把匹配行之后的 5 行也输出-C 3则是前后各 3 行相当于-B 3 -A 3。输出里不同文件、不同匹配块之间会用--分隔。第一次用可能觉得这个分隔线多余但当你拿到几百条匹配需要人工定位时就知道这个分隔符有多重要了。不过也要提醒一句上下文行数别设得太大5 到 10 行已经足够还原大多数日志场景设个-C 50反而会让输出变成一坨失去快速扫读的意义。3.4 让 grep 结果变成管道里的数据批量替换、统计、提取一条龙grep -r的威力在管道组合里最能体现。先说批量替换。项目里某个老配置名要换掉但不想一个个文件手工改可以这样grep -rl old_key src/ --include*.py --exclude-dir.git | xargs sed -i s/old_key/new_key/g思路是先用-l拿到包含old_key的文件列表再交给 sed 做全文替换。这里必须用-l而不是默认输出因为替换需要的是“文件名”而不是“匹配行”。这个组合我几乎每周都用比打开 IDE 一个个文件改快得多。再说统计。比如你想知道日志里 ERROR 大致分布在哪些文件、每个文件有多少条grep -rc ERROR /data/logs/app/ --include*.log输出长这样/data/logs/app/api/api-2024-06-01.log:187 /data/logs/app/job/job-2024-06-01.log:89如果想把所有文件的数量加起来可以接着用 awk 汇总不用手工去加grep -rc ERROR /data/logs/app/ --include*.log | awk -F: {sum $2} END {print sum}再比如从日志里提取 IP 并按出现次数排序这也是排查“哪个来源 IP 最多”的标准套路grep -rhoE ([0-9]{1,3}\.){3}[0-9]{1,3} /data/logs/app/api/ | sort | uniq -c | sort -rn | head -10这里-h是关键它去掉文件名前缀让输出变成纯 IP 列表uniq -c才能正确统计。如果涉及压缩日志直接用zgrep就能穿透压缩内容搜索不用先解压zgrep -n ERROR /data/logs/app/api-2024-06-01.log.gz4. 高频踩坑与排查思路4.1 满屏 Permission denied匹配结果被淹没在 /etc 或者别的受限目录里递归搜索最常见的就是刷出一堆Permission denied。这个提示本身不是错误只是 grep 告诉你有部分文件没权限读剩下的正常结果照常输出。问题是刷多了之后真正的匹配结果反而看不清。两个解决思路。最简单粗暴的是把错误输出丢弃grep -rn password /etc/ 2/dev/null如果想更规范一点用-s参数让 grep 自己静默错误信息grep -rns password /etc/这里有个小提醒如果你明明知道某个文件里有关键词但grep -r却搜不到先检查一下是不是没权限读那个文件或目录别急着怀疑命令写错了。排查方法很简单先ls -l看看权限位再试试能不能cat那个文件。4.2 Binary file matches二进制文件刷屏递归搜索代码目录或日志目录时遇到可执行文件、图片、压缩包这类二进制内容grep 默认只输出一行Binary file xxx matches不会把内容打出来。这个设计是为了避免终端被乱码刷爆但有时候会干扰判断。如果你确实想搜索二进制文件里的字符串比如排查可执行文件里有没有某个错误提示可以用-a把二进制文件当成文本来处理grep -rna version /usr/local/bin/反过来如果你不想看到任何二进制文件相关输出用-I让 grep 直接跳过二进制文件grep -rnI TODO /project/src/实际经验是绝大多数代码搜索场景加-I更干净因为谁也不想在结果里看到一堆Binary file ... matches。尤其在大项目里二进制文件混在搜索结果中会严重干扰对代码问题的判断。4.3 符号链接目录-r 搜不到-R 才行这是个隐蔽问题。某些部署环境里日志目录是通过符号链接指向另一个位置的比如/data/logs实际指向/mnt/disk1/logs。如果你用grep -r去搜/data/logs会发现结果里少了很多内容甚至什么都没有。原因在于 grep 的-r选项在递归遍历时不会跟随符号链接进入目录。想跟随符号链接目录要改用-Rgrep -Rn timeout /data/logs/经验是判断不了目标路径是不是符号链接时先用ls -l看一眼如果路径后面带了箭头就用-R或者直接搜真实路径。对大多数日常使用来说只需要记住“要跟随链接进目录用-R”这一条就够了。4.4 搜索内容以“-”开头或者正则没转义假设你想在文件里搜索-tulnp这种字符串直接写grep -rn -tulnp /tmp/会直接报错因为 grep 会把-tulnp当成一组选项来解析。解决办法有两个用--显式声明后面是模式而不是选项grep -rn -- -tulnp /tmp/或者用-e参数指定模式grep -rne -tulnp /tmp/这种问题在搜索 JSON、代码片段、shell 命令记录时尤其常见。另外如果搜索带有.、*、[、]这类正则元字符的字符串记得加-F参数把模式当成纯文本处理grep -rnF 192.168.1. /etc/不加-F的话点会被当成“任意字符”结果会多出很多你以为没匹配但实际上匹配了的行。这个坑我踩过不止一次。4.5 搜大目录卡到怀疑人生先想两个方向目录里文件一多grep -r确实可能很慢。最常见的慢都是在不该搜的目录上耗掉的node_modules、.git、dist、target、缓存目录。前面说的--exclude-dir就是针对这个痛点的第一道防线。如果排除之后还是很慢再检查一下是不是搜到了超大文件或者宿主机 IO 本身慢。这时可以考虑先缩小范围把路径从整个目录精确到某个子目录或者用 find 先找出最近修改的文件再交给 grepfind /data/logs/app -mtime -2 -name *.log -exec grep -l timeout {} 另外如果你的工作环境里经常做全文检索我强烈建议直接换 ripgreprg。它默认忽略 .gitignore 里的文件默认跳过二进制速度比grep -r快一个量级语法还很接近。grep -r是“基础武器”rg是“进阶武器”这俩不冲突但大目录场景直接用rg能少踩很多性能坑。4.6 中文搜索乱码和编码问题匹配中文日志时大多数情况下没有问题因为 grep 本质是字节流匹配UTF-8 编码的中文在模式和文件里编码一致就能匹配上。真正出乱子的场景是文件是 UTF-8但终端环境 LANG 设置成了别的编码导致输出中文显示成乱码。这种时候先检查 localeecho $LANG一般设成en_US.UTF-8或zh_CN.UTF-8就能正常显示。另一个容易懵的场景是用 grep 搜一个含有中文的关键词怎么都搜不到。先别怀疑 grep看看目标文件是不是 GBK 编码。可以用file命令确认文件编码再用iconv转码后搜索或者把关键词用对应编码写进模式。不过在纯 Linux 服务器环境里现在绝大多数日志和代码都统一 UTF-8遇到乱码情况先想一下编码别跟命令行较劲。5. 真实案例复盘从一次线上日志定位到故障根因5.1 场景背景与目标模拟一个我遇到过的场景线上服务接到告警支付回调成功率严重下降。服务日志按模块分布在/data/logs/payment/下有 web、api、callback、worker 四个子目录每天滚动还有大量 .gz 压缩包。目标是在尽可能短的时间里搞清楚三件事报错主要出现在哪个模块、错误类型集中在哪几种、有没有明显的时间规律。很多人的第一反应是去那个最大的日志文件里 grep。但不是每次都能这么好运报错可能只集中在某个小模块或者分散在几个文件里。这时用grep -r一次性扫全部模块目录是最省事的开局方式。5.2 第一条命令把错误码分布拉出来先扫整个 payment 日志目录把包含EXCEPTION或ERROR的行带行号列出来grep -rnE EXCEPTION|ERROR /data/logs/payment/ --include*.log | tail -100这里加了--include只搜 .log 文件避免目录里其他类型文件干扰tail -100是因为第一次扫描结果可能很多不必全屏飘屏先看最近一部分。输出很快就显示出错误集中在callback子目录里的当天日志而且同一个错误码CALLBACK_TIMEOUT反复出现。5.3 进一步定位统计数量与上下文有了大致方向下一步确认这个问题影响范围到底多大。统计一下 callback 目录下每个日志文件里CALLBACK_TIMEOUT的出现次数从多到少排序grep -rc CALLBACK_TIMEOUT /data/logs/payment/callback/ | sort -t: -k2 -rn | head -20结果可以看到最近两个小时的日志文件匹配数量明显偏高。然后结合-B和-A看具体内容grep -rn -B 2 -A 5 CALLBACK_TIMEOUT /data/logs/payment/callback/callback-$(date %Y-%m-%d).log | head -80上下文输出显示超时都发生在回调某个第三方接口时而且入参里的下游 IP 全部指向同一个地址。到这里问题范围从一个模糊的“支付失败”收敛到“某个下游通道响应慢”定位效率比手动翻日志高得多。5.4 顺手用 grep 解决端口和进程确认排查过程中往往会顺带确认服务本身是否健康这时会用到另一个 grep 高频场景查看端口监听情况。比如确认支付服务的某个端口是否在监听ss -tulnp | grep :8080如果环境里只有 netstat也可以netstat -tulnp | grep :8080这个用法的本质是把命令输出通过管道交给 grep 过滤跟-r的递归搜索没有直接关系但它是 grep 家族里实用性排前几名的操作。很多人在排查时都经历过“明明服务起了但端口不通”ss或netstat输出一过滤立刻就能看到进程有没有监听、监听在哪个地址上。5.5 这个案例沉淀下来的套路复盘一下整个过程核心其实就三步先用grep -r扫全量、再用-c和排序锁定重灾区、最后用-A/-B看上下文还原现场。整条链路没有用什么高级工具全是基础命令的组合。我自己遇到类似问题时的原则是第一遍搜索要“宽”宁可多匹配也不能漏第二遍统计要“准”用计数和排序找到最值得看的地方第三遍看细节要“稳”加足上下文还原完整脉络。这个三步走配合端口、进程等辅助确认大多数线上问题都能在几分钟内定位到模块级别。6. 我的使用习惯和最后建议6.1 我总结的“肌肉记忆模板”用grep -r这几年我最大的感受是它的价值不在一两个参数上而在“组合意识”上。单看-r、-n、-i、--include每一个都很简单但把它们按场景组合起来效果是乘法级别的。我会建议你在日常工作中刻意建立几个固定的模板。搜代码用grep -rn --include*.py --exclude-dir.git --exclude-dirnode_modules搜日志用grep -rn --include*.log --exclude-dirold处理管道用grep -rl ... | xargs ...。模板一旦成型遇到问题就能条件反射般写出正确的命令省下的时间远比背参数多。好记性不如烂笔头把常用组合放进笔记或者 shell 别名里比每次现查 man page 靠谱得多。6.2 从 grep -r 到 ripgrep什么时候该换工具最后再分享一个小技巧如果你经常在大型项目里做全文搜索可以把常用组合做成 shell 别名。比如alias srggrep -rn --colorauto --exclude-dir.git --exclude-dirnode_modules这样每次只需要输入srg 关键词加上目标路径就能用上排除目录、带行号、高亮显示的全套配置。而当项目规模大到grep -r已经跑不动的时候不妨试试rg它默认遵守 .gitignore、默认跳过二进制、搜索速度快很多几乎可以无缝替换你习惯里的那些组合参数。工具不在多一条命令用透胜过大而全的工具链。我自己就是靠着这套组合拳在无数个让别人抓狂的“找不到”场景里快速脱身。
返回列表