开场:四个绕不开的日常
带过几个项目、看过无数遍代码评审之后,我慢慢发现一个规律:真正卡住人的往往不是算法多难、架构多玄,而是日常工具和流程里的各种小问题。Git 操作失误回滚不回去、Bug 改了一下午没找到根因、测试跑绿了上线就翻车、Linux 排查半天才发现是磁盘满了……这些场景几乎每个开发者都遇到过。这个系列笔记的第一篇,我就把 Git、Debug、测试、Linux 这四个最常打交道的方向放在一起,聊聊我在实际工程里沉淀下来的方法和踩过的坑。内容不追求面面俱到,只挑实践里真正有用的部分展开,适合刚工作一两年、想把手头的开发流程梳理得更规范的同学,也适合带新人时直接拿来当参考。
这篇文章里的每一条,都来自真实项目里的教训和复盘,不是教科书式的罗列。有的地方我会给出具体命令,有的地方会给出判断思路,读完你至少能带走几套可以直接套用的操作模板。
1. Git 使用习惯:从“能用”到“用对”的进阶路径
1.1 提交信息规范:从源头减少沟通成本
很多人不把提交信息当回事,觉得代码写对了就行。直到某天线上出了问题,git log 一看全是“fix bug”“update”“修改”,你根本不知道哪个提交对应哪次变更,整个历史跟没写一样。我在团队里推过一个很简单但效果很明显的规范:提交信息统一采用 type(scope): subject 的格式。
feat(user): 增加用户头像上传接口 fix(payment): 修复订单状态重复更新问题 docs(readme): 补充本地开发环境搭建步骤 refactor(utils): 抽取公共日期格式化函数type 控制类型,scope 标注是哪个模块,subject 用一句话说清楚这次改动做了什么。这么做最直接的好处是:看 git log 跟看变更清单一样,不需要打开代码就能知道历史脉络。配合 CI 里的提交信息校验(比如用 commitlint),格式不合法直接拒绝提交,整个仓库的日志会变得非常干净。
标题里只写“改了什么”还不够。我还会额外要求重要提交在正文里写明“为什么这么改”。为什么比改了什么更难保留——代码以后看得到,但当时的决策背景靠记忆是撑不过三个月的。举一个典型的场景:你发现某段代码性能有问题,优化后快了十倍,但引入了对某个运行时版本的最低要求。如果不写进提交正文,三个月后有人升级环境导致线上故障,排查起来会绕很大的弯。
1.2 分支策略:小团队最实用的方案
一说到分支管理,就有人搬出复杂的多人 Git Flow。但就我的经验而言,三到十人的团队,用主干加功能分支的方式就已经很够用了。核心就两条:main 分支永远保持可发布状态,任何新功能都在独立分支上开发,合并前必须过 CI 和代码评审。
git checkout -b feat/user-avatar # 开发、提交、推送远端 git push -u origin feat/user-avatar # 在仓库界面发起 Merge Request / Pull Request功能分支的生命周期尽量控制在几天以内,最长不要超过一周。分支活得太久,合并时的冲突成本和心理负担都会急剧上升。如果某个需求实在太大,我习惯先用一个主线分支把骨架合并进去,再拆分小分支逐步完善。每个合并进 main 的提交,都应该对应一个能独立工作的状态,而不是半成品。
很多人忽略的一点是分支命名。命名里带上需求编号或者 Jira/工单号,比如 feat/user-avatar-12345,好处是分支和需求能直接对应,排查时一条命令就能找到关联的提交。团队里约定清楚,时间越长价值越大。
1.3 冲突解决的实操思路
冲突是 Git 日常操作里最烦人的一件事,但说到底它的本质并不复杂:两个分支改了同一处代码,Git 不知道该听谁的。面对冲突,很多新手的第一反应是慌,然后看到一堆 <<<<<<< ======= >>>>>>> 标记就不知道从哪下手。
我的做法是分三步走。第一步先理解冲突双方各自主张的语义,别急着改。把冲突片段当成两个版本对比来看,往往会发现两边改的其实是不同层面的东西,只是恰好落在相邻几行。第二步是判断保留哪边、删掉哪边、还是两边都要,这完全取决于业务意图,不是技术问题。很多时候两个改动其实都要保留,只是需要调整一下顺序或者重新组织逻辑。第三步是解决完冲突之后,重新编译或跑一遍相关测试,确保没有破坏任何一边的行为。
还要提一个减少冲突频率的技巧:多执行 git fetch,少用 git pull 直接合并。每次开始工作前,先把远端最新的 main 拉下来,在自己的分支上做 rebase,让变更基于最新的代码。虽然人们总觉得 rebase 听起来高级,但它其实就是把变更重新放到一个新基点上,用来保持分支历史线性,减少后面合并时一次性爆出大量冲突的可能。需要说明的是,rebase 更适合自己还没推送到远端的本地分支,不要随意对共享分支做强制变基操作。
1.4 出事之后的补救手段
Git 最吓人的不是出错,而是出错之后不知道能不能恢复。先说两个最常用的补救命令:git revert 和 git reset。
如果你的错误提交已经推到远端,或者被别人拉下来继续开发了,绝对不要用 git reset 去强行改写历史。正确做法是 git revert,它会产生一个新提交,把之前的改动反向撤销,历史是完整的,所有人后续拉取都安全。
git revert 8f3a2b1如果你的错误提交还没推远端,只是想重做最近几次提交,git reset 更方便。区分两个常用参数:soft 只移动 HEAD 指针,工作区代码还在;hard 会连工作区一起清空,属于破坏性操作,用之前想清楚。
git reset --soft HEAD~2 # 撤销最近两次提交,改动保留在暂存区 git reset --hard HEAD~2 # 撤销最近两次提交,改动直接丢弃还有两个日常经常用到但容易被忽略的技巧。git stash 可以临时存放工作区的改动,切换分支或者拉取代码前做个 stash,事情办完再 pop 回来。git reflog 是最后的救命稻草,就算你 reset --hard 误删了提交,只要提交对象还在本地仓库里,reflog 里就能找到对应的哈希值,再 check 出来就能恢复。我靠这个操作救回过一次同事误删的整整两天的工作量,这也是为什么我强烈建议每个开发者至少知道 git reflog 的存在。
2. Debug 排查:先定义问题,再动手修
2.1 复现是第一步,也是最容易被跳过的
Debug 最常见的失败模式,是问题还没稳定复现就开始猜。猜中了叫运气好,猜不中就在代码里到处打日志、加判断、改业务逻辑,忙活半天发现根本不是那回事。
我的经验是:动手改代码之前,先逼自己回答三个问题。第一,这个 Bug 出现的条件是什么?是必现还是偶发?哪个用户、哪个操作路径、什么输入数据、什么环境版本?第二,预期行为是什么?代码本来应该输出什么、返回什么、界面应该怎么展示?第三,实际行为是什么?和预期差在哪里?差多少?如果这三个问题里有一个答不上来,说明信息收集还不够,现在不是修 Bug 的时机。
复现的重要性在于它能把一个模糊的“坏了”变成一组确定的输入输出关系。很多偶现问题排查困难,是因为你连触发条件都没找全。之前有一次线上偶发超时问题,我让测试同事连续跑了几天用例都没复现,最后发现是只在某台特定配置的机器上才会出现,条件里必须包含特定 CPU 型号和特定并发量。一旦复现条件明确了,定位就是时间问题。
2.2 二分定位法:把搜索空间快速变小
面对一段几千行、逻辑复杂的代码,从头到尾一行行读是效率最低的方式。工程上更实用的方法是二分定位:把问题的搜索空间不断对半切割,快速逼近出问题的具体区域。
举个实际例子,一个数据导入功能,某个字段导入后总是丢失。与其全文查代码,我先在入口处打日志确认原始数据还在;再到中间转换层确认数据经过处理后是否还在;再到数据库写入前确认最终值。三步下来,几步之间的代码就是重点嫌疑区,然后在这个区域里再按同样的方式逐层缩小范围。整个过程和玩“猜数字”一样,每次排除一半的可能性,十几步就能锁定到具体行。
对付“之前好好的,现在坏了”这类回归问题,git bisect 是最强的工具。你只要告诉 Git 哪个提交是好的、哪个提交是坏的,它会自动通过二分法拉取中间版本,每到一个版本你只需要跑一次复现脚本,回答“还好还是坏了”,几步就能精确定位到引入问题的那个提交。
git bisect start git bisect bad # 当前版本是坏的 git bisect good v1.2.3 # 已知某个版本是好的 # Git 会检出一个中间版本,按结果持续标记 good / bad git bisect reset # 排查结束后退出这个方法有两个前提:你有一份能快速判断好坏的测试脚本,以及二分范围内的提交数量是合理的。如果几百个提交里全是半成品,bisect 过程中可能会出现大量编译不过的情况,这时候先把范围缩小到从上个稳定版本之后的功能提交会更现实。
2.3 日志质量决定排查效率
调试时日志不是打得越多越好,而是每一行都要有信息量。只写一句 logger.error("error") 等于没写,根本不知道哪里错、错在哪里、影响什么。我见过的优秀日志,格式大概长这样:
2025-06-18 10:23:45.112 ERROR user-service [requestId=8f3a2b1] 订单状态更新失败, orderId=10086, currentStatus=PENDING, targetStatus=PAID, 错误详情: 库存扣减超时拆开来看,一条合格日志至少要包含五个要素:时间、级别、模块、上下文标识、错误内容。上下文标识里 requestId 是我特别想推荐大家配置的东西,它能在微服务或跨系统调用场景里把一串日志串成一条完整的时间线。在某次线上排查里,用户反馈“操作偶发失败”,单看每个服务的日志都是正常的,但把所有服务的日志按 requestId 拼起来之后,立刻发现是下游服务特定时间段超时导致的雪崩。
还有一个被低估的习惯:日志里不要打敏感信息。手机号、身份证号、密码、Token 这些字段处理不好就是安全事故,排查时宁可多查一步也不要直接打全明文。另外,错误日志尽量带上必要的上下文参数,而不是只打一个异常栈。上游传了什么值、当时什么状态、做了哪些处理,有了这些参数,排查问题的时间能少一半。
2.4 典型问题的排查思路参考
我没有统一的 Bug 分类法,但工程现场里有些问题出现频率高、排查套路相对固定,整理出来供参考。
第一类是并发问题。症状通常是偶发、不可稳定复现、数据对不上。排查方向先看共享资源的访问是否加锁,再看查询和更新的原子性,最后看事务边界。很多时候你会发现问题不是没加锁,而是加锁的粒度不对,或者自认为安全的复合操作在另一个线程视角下并不是原子的。
第二类是缓存问题。症状通常是“改了数据库但界面没变”“某些用户看到的数据不一样”。排查方向先确认缓存键和缓存策略,再确认局部操作有没有清理或刷新缓存,最后看缓存过期时长设置。热点数据缓存过期时间太短,高频请求全打到数据库上,也会表现出类似的“偶尔变慢”现象。
第三类是一个比较容易被忽视的情况:环境差异导致的问题。本地正常、测试环境正常、上线就异常的情况,在环境对比上要下功夫。操作系统版本、依赖库版本、系统时区设置、文件系统行为差异,都能成为问题源头。跟这个问题类似但形态相反的,还有一类时区和格式化问题——本地打印出来是北京时间,服务器上打印出来是 UTC,一切看起来“错了”但代码没有一行是错的,只是时区默认值不同。
这类问题锻炼的其实是排查的思路顺序:先确定问题只会出现在哪个范围,再看上下文环境是否一致,最后才深入业务逻辑。
2.5 把问题讲清楚也是一种能力
Debug 不全是对着代码的工作,很大一部分是对着人。无论是向同事求助还是让测试复现,问题描述的质量直接决定协作效率。我常用的描述模板固定在五个要素上:操作步骤、预期结果、实际结果、出现频率、环境信息。描述问题避免使用“很奇怪”“它就是不工作”这类模糊表达,改为“我点击 A 按钮之后,页面没有跳到 B 页面,而是停留在原页面并提示报错,本地环境必现,测试机上百分百复现,版本是 1.3.2”。这样一个描述,别人不需要额外追问太多,就可以开始排查。
还有一个小经验:求助之前先自己排查十分钟,把已经排除了什么、锁定到了哪里说清楚。这会让同事非常愿意帮你,因为你不是甩一个烂摊子过去,而是请他在关键位置帮你把一把方向。反过来,收到求助的时候也不要上来就看代码,先按上面的模板让对方把信息补全,信息不够让你猜来猜去,才是最消耗团队时间的事情。
3. 测试工程化:从完成任务到建立质量防线
3.1 测试金字塔在现实里的样子
理论上大家都会说测试金字塔,底层是大量的单元测试、中间层是较少的集成测试、顶层是少量的端到端测试。现实里很多团队做出来的是个倒金字塔:大量端到端测试,慢而且容易挂;单元测试没几个,核心逻辑全靠手工确认。
我的看法是,测试策略不是越全越好,而是应该根据项目的风险分布来决定。基础设施类、公共工具类的代码,单元测试覆盖率可以要求高一些,因为它们的改动影响面大、回归成本高。业务界面类的代码,端到端测试只需要覆盖核心流程,细枝末节的交互可以靠人工验证和少量针对关键逻辑的单测来兜底。写测试之前先问一句:这个测试是要防止什么回归?如果答不上来,这个测试很可能只是在凑覆盖率数字。
还有一个结构层面的建议:测试代码和被测代码放在同一个仓库、同一套 CI 流程里跑。测试不是发完版之后的仪式,而是功能完成的一部分。功能代码合入主干的条件就是测试通过。只要这个红线保持住,整个团队的交付质量和速度都会明显提升。
3.2 单元测试怎么写才有价值
单元测试最容易踩的坑是把实现细节测了个结实,等哪天重构一下内部写法,代码行为完全一样,测试却红了一片。正确做法是测试外部行为而不是内部实现。比如你要测一个计算订单总价的函数,关心的是输入商品列表和优惠券后,返回值是不是正确的总价,而不是关心函数内部是不是用了某个循环、某个临时变量。
写有价值的单元测试,有一个很实用的清单。测正常输入下的正确输出;测边界值,比如空数组、零、负数、最大值;测异常输入是不是按预期抛错或返回失败;测特殊分支,比如 if 的 else 路径、switch 的 default 分支。再往深一层,有条件的话测一些随机数据或者属性特征,比如“无论输入是什么,返回值都不为负”“排序函数的返回结果长度永远和输入一致”,这类测试能捕获很多边界场景里的偶发问题。
下面的示例是一个典型的、有价值的单测结构,用 JavaScript 的 Jest 风格做展示:
describe('calculateOrderTotal', () => { test('应正确累加商品金额并应用优惠券抵扣', () => { const items = [{ price: 100, count: 2 }, { price: 50, count: 1 }]; const coupon = { type: 'FIXED', value: 30 }; expect(calculateOrderTotal(items, coupon)).toBe(120); }); test('空商品列表返回 0', () => { expect(calculateOrderTotal([], null)).toBe(0); }); test('优惠券面值大于总价时返回 0 而不是负数', () => { const items = [{ price: 10, count: 1 }]; const coupon = { type: 'FIXED', value: 999 }; expect(calculateOrderTotal(items, coupon)).toBe(0); }); test('非法商品数据 (负价格) 应抛出异常', () => { const items = [{ price: -5, count: 1 }]; expect(() => calculateOrderTotal(items, null)).toThrow(); }); });你能从这个例子里看到,四个测试覆盖了常规、边界、异常、极值四种不同场景,代码的防御能力也在写测试的过程中自然地得到加强。很多时候测试写不下去,不是因为你不会写,而是因为被测代码本身设计得不够干净——耦合太重、职责太多、副作用藏在深处。这其实是个重要的信号:代码该重构了。
3.3 Mock、Stub、Fake 没有你想的那么简单
这几个词经常混着用,但工程里用错的代价很大。一句话区分:Stub 是给被测对象提供预设返回值的替身,Mock 除了提供返回值还会记录调用行为并可以验证交互,Fake 则是实现了一个简化版但行为基本真实的替身。单元测试里最常见的需求是 Stub,比如让一个支付接口在测试环境里固定返回成功,不需要真的去请求第三方服务。
但用 Mock 有个明显的反面效果:测试和实现绑得太紧。比如你 Mock 了一个下游方法,然后断言“这个方法必须被调用两三次”,只要实现稍微调整,行为正确但测试挂了。我一般只在两处使用 Mock:一是被测代码需要和外部服务交互且本地没有测试环境;二是需要验证“某操作是否确实发生”这类行为本身,比如发送日志埋点、触发通知回调。
Fake 在集成测试里的价值更大。比如数据库访问层,用一个轻量级的内存数据库当依赖,跑测试的速度比真正的数据库快得多,同时行为更接近真实环境。一个小提醒:使用 Fake 之前,要和真实依赖保持行为一致性,否则会出现 Fake 全绿、上真实环境就挂的尴尬情况。曾经就有团队因为 Fake 的实现没有模拟真实存储的字段长度限制,结果上线后数据截断,而测试永远测不出来。
3.4 集成测试和 CI 的关系
单元测试跑得再全,也管不了模块之间拼接的缝隙。集成测试的价值就是把模块接缝处的行为验证掉。最简单的做法是每次提交代码、或合并分支之前,在 CI 流水线里跑一遍完整的集成测试套件。
我建议 CI 里的测试分成两层。快速层:每次 push 都跑单元测试和静态检查,要求在几分钟内完成,这样开发者有即时反馈,不会等待太长时间。慢速层:合入主干之前跑集成测试和核心端到端场景,允许十几分钟,因为合入主干的频率本来就低于每次 push。如果你把慢速层也放在每次 push 时跑,最终的结果就是开发者为了赶进度开始跳过测试,防线形同虚设。
CI 里的测试还有个容易被忽略的点:稳定性。套件里如果有“偶发挂掉、重跑就好”的测试,这种不稳定本身就是质量缺陷,一定要查到底。多次重跑才通过的测试会导致团队形成“红了先重试”的坏习惯,而真正的回归问题,就这样被掩盖过去了。我见过最严重的案例是,一条端到端测试因为超时设置太短而频繁“闪挂”,持续了两三个月没人管,后来它真正抓到一个严重回归时,所有人看到红的第一反应是点重试,差点把一个线上事故放过去。
3.5 TDD 不是万能的,但在某些场景下很好用
测试驱动开发这些年被捧过、也被唱衰过。我自己的体会是,它不是银弹,但确实有一类场景天然适合 TDD:核心业务逻辑、算法、状态转换、规则引擎。因为这些逻辑特征明显——输入确定、输出可预期、边界能列举,在写实现前用测试把行为定下来,能让实现的思路清晰很多。
这里有一个比较完整的 TDD 实践流程。先写一个针对核心逻辑的失败测试,运行一次确认它是红的,也就是失败状态;接着写最小实现,只求让当前测试通过,不跳步;然后运行全部相关测试,确认没破坏其他行为;最后重构,在测试保护下调整代码结构,让实现更清晰。整个过程依靠“红灯-绿灯-重构”的节奏,每一步都有明确目标,不知不觉中就得到了一套覆盖完整行为的测试用例。
但也有不适合 TDD 的场景。UI 界面的交互流程、外部系统的对接、探索性的原型验证,这些地方先花大量时间写测试很可能得不偿失。大前端项目的页面测试往往需要很长时间稳定下来,这时与其硬上 TDD,不如把核心逻辑抽成独立的函数/模块,先对这些模块做 TDD,界面层只保留少量冒烟测试。
4. Linux 下的工程效率与系统排查
4.1 高频命令组合拳:不只是单个命令
Linux 下面单看每个命令都简单,真正的效率提升来自把命令组合成能直接解决问题的管道。下面这几组命令是我每天都会用到的,整理出来供直接参考。
# 在代码库里快速定位关键词,忽略二进制和 .git 目录 grep -rn "someFunction" --include="*.js" --include="*.py" . # 查看日志文件最后 500 行,并实时跟踪新增内容 tail -n 500 -f app.log # 按访问 IP 统计接口调用次数,倒序展示 awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 20 # 找出当前目录下占用空间最大的前 10 个文件 find . -type f -printf '%s %p\n' | sort -rn | head -n 10 # 批量替换目录下所有匹配文件里的旧版本号 find . -type f -name "*.yaml" | xargs sed -i 's/1.2.3/1.3.0/g'这些组合背后的思路是一样的:每个命令做一件简单的事,管道把上一段输出交给下一段继续加工。grep 负责筛选、awk 负责取列和统计、sort 和 uniq 负责排序去重、head 负责截取顶层结果,任何一步的结果都能随时调整和替换。我用这套组合拳解决过大量的日志分析、配置检查和文件检索问题,熟练之后比打开编辑器和图形工具快上一个数量级。
4.2 资源瓶颈排查四板斧
线上系统变慢,先看资源。CPU、内存、磁盘、网络,四类资源分别对应不同的排查工具和不同的解决路径。
CPU 飙高时,先用 top 确认高占用进程,再用 top -H -p PID 看具体线程号,配合 jstack 或 gdb 把线程栈抓下来,就能定位到代码层面的热点函数。曾经排查过一次 CPU 100% 的问题,前面花了很久在业务代码里找原因,最后发现是 JDBC 连接池配置的请求超时时长过短,导致无限循环重连,而线程栈一眼就指出了问题所在。
内存问题分两类:内存不足和内存泄漏。free -h 可以快速看系统内存水位,如果剩余充足但应用变慢,可能是 GC 频繁;如果内存持续减少且不释放,多半是泄漏,需要结合堆转储分析。日常我会重点关注持续一段时间的监控趋势,单独的瞬时值参考意义有限。
# 查看整体内存和交换分区使用情况 free -h # 动态查看系统负载、进程占用 top # 查看磁盘剩余空间,-h 是人性化显示 df -h # 查看当前监听端口和连接状态 ss -tlnp磁盘和网络的排查里也有几个常见套路。磁盘空间满导致服务写不进日志或数据库,是运维事故里频率最高的一种,先 df -h 排除磁盘问题再排查业务代码,可以少走很多弯路。网络层面,端口占用用 ss 或 lsof -i:端口号 查看;连接数异常、超时问题可以用 netstat 和 tcpdump 结合分析,先用 netstat 看宏观连接分布,再用 tcpdump 抓具体报文判断是否丢包或延迟。
4.3 Shell 脚本健壮性:能跑不算写好了
很多人写 Shell 脚本,只要能跑就行,但生产环境里的脚本和本地跑的脚本,要求完全不一样。我见过最糟糕的情况是:一个定时任务脚本里没有 set -e,中间某条命令失败后脚本继续往下执行,最后把一堆不完整的数据同步到了线上,等发现时已经晚了。从那以后,我写的每个稍微正式一点的脚本都会加上:
#!/usr/bin/env bash set -Eeuo pipefail一行参数说明几个含义:-e 遇到错误立即退出;-u 使用未定义变量直接报错;-o pipefail 管道中任何命令失败都让整体返回失败;-E 让错误处理覆盖到函数和子 shell。这四件套加上之后,大多数“静默失败”的机会都被堵住了。另外,脚本开头用 shellcheck 做一次静态检查也是个好习惯。
还有几个细节值得提。路径用双引号包裹,防止空格和特殊字符出问题;临时文件用 mktemp 生成而不是固定写死路径,避免多个实例运行时互相覆盖;脚本结束前用 trap 清理临时资源和句柄,防止异常退出时留下脏文件;变量拼接路径时使用 ${VAR:-default} 形式提供默认值,避免环境变量未设置时报错。这些习惯看起来小,累积起来能避免很多线上事故。
#!/usr/bin/env bash set -Eeuo pipefail LOG_DIR="${LOG_DIR:-/var/log/myapp}" TMP_FILE="$(mktemp)" trap 'rm -f "$TMP_FILE"' EXIT if [[ ! -d "$LOG_DIR" ]]; then echo "日志目录不存在: $LOG_DIR" >&2 exit 1 fi4.4 效率工具与真实排查案例复盘
Linux 的日常工作里,除了命令本身,还有几个思路层面的提升。
一个是别名机制,把高频、易敲错的命令收成短别名。比如 alias gs='git status'、alias ll='ls -alF'、alias dc='docker compose',别名的价值在于把长串的参数打包成肌肉记忆,减少每天的重复输入。当然别名只针对个人习惯,团队共享的脚本里不要依赖任何别名,保证脚本在干净环境里也能跑。
另一个思路是用函数封装复杂逻辑。比如我需要频繁查看某个日志里某个用户的完整行为链路,就写了一个函数,接收用户 ID 作为参数,自动组合 grep 和 awk 输出时间线。这个函数放在 .bashrc 或 .zshrc 里,每次执行都能省掉一长串命令。
下面分享一个真实排查案例的复盘。某天同事反馈某服务响应缓慢,我按部就班做排查。先 free -h 看内存,发现交换分区使用率很高,内存明显不足;再用 top 查看进程,发现两个 Java 服务在同一台机器上,其中一个堆内存配置过大;再看日志,发现大量文件句柄相关的报错,判断是文件描述符限制或打开文件过多;最终定位为服务 A 的堆内存上限设置过高,挤占了服务 B 的可用内存,导致服务 B 频繁换页。解决办法就是调整 JVM 参数,同时检查两个服务的负载部署规划。整个排查思路就是从系统资源到进程,再到应用日志,逐层逼近,每一步都用命令验证,而不是靠猜。
“无监控,不排查”是我一直强调的观点。生产环境里如果没有系统性的监控和日志采集,每次出问题都靠临时登录机器去看,等于火情发生后才找消防水带。时间充裕的话,优先把基础监控搭起来:CPU、内存、磁盘、进程存活、核心业务接口的时延和错误率,这些都值得有长期曲线。有了曲线,很多问题的排查从“现在发生了什么”变成了“从什么时候开始变成这样”,而后者定位起来要快得多。
写在最后的一点个人体会
这个系列笔记写到这里,我很想把“工程实践”这个词落到更实处。它不是某个庞大的理论体系,而是每天在命令行、调试器、测试代码和故障排查之间来回打磨出来的手感。Git 的那套分支规范也好、Debug 的二分思路也好、测试的分层设计也好、Linux 的排查套路也好,单独拿出来都不难理解,真正难的是在一次次项目压力下仍然坚持执行,并不断地从错误中修正自己的判断。
如果你刚接触这些内容,我的建议很简单:先从一到两个点入手,比如这个月强制自己按规范写 Git 提交信息,下个月试着在修复每个 Bug 前先写一条复现记录。这些小实践会在之后某次线上问题里派上用场,那是所有资料和课程都替代不了的经验积累。下一篇笔记我会继续沿着工程实践的路径展开,聊聊和代码评审、协作流程相关的内容,也欢迎你先在底下回复你在实际工程里遇到的问题,我们接着聊。