十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从背锅侠到香饽饽:开发者如何系统化拦截Bug、告别半夜救火

从背锅侠到香饽饽:开发者如何系统化拦截Bug、告别半夜救火 刚从睡梦里被电话拽起来那会儿我整个人是懵的。手机屏幕上闪烁着同事的名字接起来对面一句“线上出问题了你负责的那块报错了”瞬间清醒。打开电脑、连上内网、翻日志、看监控折腾到凌晨三点多最后发现是数据边界没处理干净一条脏数据钻进来了。修完补丁躺回床上翻来覆去睡不着脑子里只有一个念头为什么又是我为什么我总是那个半夜爬起来改 bug 的人那时候我在团队里的位置很尴尬说难听点就是个“背锅侠”。只要线上出问题第一反应就是找我因为代码是我写的业务是我接的文档是我补的好像所有事都跟我有关所有问题也都该我负责。白天被需求追着跑晚上被事故拽着走那段时间整个人都是绷紧的手机一响就心悸。后来我下定决心要改变这种状态不是靠跳槽也不是靠转行而是从工作方式本身入手一步步把“背锅”的局面扭转成“香饽饽”的定位。这个过程大概花了一年多今天想把其中最关键的方法论和实操经验完整地分享出来希望能帮到那些还在“背锅”循环里挣扎的开发者。1. 那个凌晨两点的故障电话是我职业转折的起点先说那次让我彻底下定决心的故障。不是多复杂的技术问题却折腾了我半宿问题本身反而成了最好的教材。1.1 一次典型的“背锅”事故复盘当时我负责的是一个订单查询接口逻辑不复杂根据用户 ID 查出订单列表然后做分页返回。上线之前自测过单测也过了联调也OK根本没想到会出问题。事故现象是某个用户反馈订单列表页直接白屏刷新也没用。同事一查监控发现那个接口的响应时间从平均 80ms 飙升到了 3 秒多数据库连接池被打满了。我爬起来排查先从慢查询日志入手发现有一条 SQL 特别奇怪 ——WHERE user_id ? ORDER BY create_time DESC LIMIT ? OFFSET ?在数据量只有十几万的情况下居然扫描了全表。再看执行计划索引没失效但排序字段没走索引导致每次请求都要做 filesort。数据量一大再加上 OFFSET 深分页性能直接崩了。更讽刺的是这个深分页问题我其实在技术文章里看到过但当时写代码的时候完全没往心里去觉得“才十几万数据能有多慢”。结果就是被现实狠狠教育了一顿。1.2 复盘时发现的问题远不止技术层面修完代码已经是凌晨三点半躺下之后我脑子里没有马上放松反而开始复盘整个过程越想越清醒第一我的开发流程里有明显的漏洞。自测范围太窄只验证了功能正常没考虑数据量增长后的性能衰减。单测只覆盖了 happy path边界条件基本没测。第二我对线上环境的感知是断层的。平时开发用的是本地库几十条数据随便查根本没意识到线上数据量已经是几十万级别。我没有主动去了解线上数据分布、索引情况、访问峰值这些都是“别人告诉我”而不是“我自己去看”。第三定位问题的方式非常原始。半夜爬起来没有一套清晰的排查链路看到慢查询就去改 SQL改完就完没有去思考为什么这种问题会在上线一个月后才爆发。第四整个修复过程是“人肉驱动”的。没有人通知我是我自己刷手机看到同事在群里说线上有问题才主动请缨。没有报警、没有监控、没有应急预案所有事情都靠临场反应。那天晚上我想通了一件事如果我一直用这种“被动救火”的方式工作那半夜爬起来改 bug 就是我的宿命换成任何一家公司都一样。问题的根源不在公司不在同事在于我自己的工作方式。真正的自救不是换环境而是改变自己应对问题的方式。2. 为什么你总是那个“被锅砸中”的人很多人觉得自己“背锅侠”是因为运气差、团队差、领导PUA但冷静下来看这里面其实有一套可以拆解的逻辑。想摆脱这个状态必须先搞清楚“锅”是怎么飞过来的。2.1 背锅的本质你踩中的不是偶然事故而是必然风险“锅”通常不是凭空掉下来的它往往是你之前某次技术债的利息。一次图省事的写法、一个没覆盖到的边界条件、一份没更新的接口文档当时看上去无关紧要后来都会变成事故现场的证据。我做了一个统计把自己半年内处理的所有线上问题列了张表按原因归类结果非常有说服力问题类型占比典型场景边界条件未处理35%空值、超长字符串、异常状态码性能瓶颈25%深分页、N1查询、索引失效并发/竞态15%重复提交、库存超卖、缓存穿透依赖服务异常15%第三方接口超时、返回格式变化其他10%配置错误、环境差异、人为误操作当时我看到这个表还挺惊讶的因为 90% 的问题都不是什么高深的技术难题而是“本该想到但没想到”的疏漏。换句话说我不是能力不够而是排查路径不对、预防意识不够。2.2 “被动救火”式开发的五个致命习惯对比身边那些很少被线上问题缠住的同事我发现被动救火的人普遍有几个共同习惯一是把“能跑就行”当成完成标准。只要本地跑通了、测试环境通过了就认为任务完成至于性能、安全、边界、兼容性统统不在考虑范围内。等线上数据一多、并发一高问题全暴露了。二是从不主动看监控和日志。开发环境、测试环境、生产环境完全是三套逻辑本地跑得好不代表线上没问题。很多问题在线上环境是有征兆的——响应时间变长、错误率缓慢上升、磁盘空间慢慢减少但这些指标平时没人看等出了问题才去捞日志。三是对依赖系统的行为不了解。现在很少有系统是完全独立的总要调别人的接口、用第三方 SDK、连公共数据库。如果只关心“调用成功返回什么”不关心“异常时会返回什么”、“超时多久算超时”那依赖系统一出幺蛾子你这边就跟着遭殃。四是修复问题只打补丁不做根因分析。遇到线上 bug 第一反应是“赶紧堵住再说”堵完之后没有复盘、没有写测试、没有更新文档。结果就是同类问题换个姿势再来一次你还是第一个被找上的人。五是沟通只做“被动汇报”不做主动同步。出问题了才在群里发“我在排查了”修复了才补一句“已修复”。平时不主动同步风险、不主动反馈进度领导对你的印象就是“总在救火”久而久之你就成了背锅侠的最佳人选。2.3 身份定位的转变你的价值不是“能修 bug”而是“让 bug 不发生”想明白了背锅的根源之后我做了一个思维上的转变不再把自己定位成一个“能快速修 bug 的人”而是努力变成“让 bug 在源头就被拦住的人”。这个转变非常关键。因为“能快速修 bug”意味着你要持续待命、持续救火修得越快别人越会依赖你锅也越来越重。而“让 bug 不发生”意味着你要把精力投入在预防、设计、监控、复盘这些前置环节上从源头上减少故障发生的概率。想通这一点之后我给自己定了一个目标把 80% 的精力放在“让 bug 不发生”上只留 20% 的精力处理“实在拦不住的突发问题”。接下来我会按这条主线来分享具体的实操方法。3. 一套从源头拦截 bug 的开发习惯让我摆脱了“24小时待命”既然要“让 bug 不发生”就得从代码生产的第一道工序开始改造。这部分是我实际践行效果最明显的也是后来能扭转局面的关键。3.1 写代码之前先写“边界清单”我以前写代码的习惯是拿到需求直接开写写到某个函数要处理参数时才临时想“这里会不会有异常”。现在我的做法完全反过来动手写代码之前先打开一个文档列出这个需求涉及的边界条件。边界清单包含但不限于输入参数为空、为 null、超长、包含特殊字符、格式错误时怎么处理数据量数据量为 0、1、大量、超大时性能有没有问题并发场景同一用户重复提交、多个用户操作同一资源、缓存过期瞬间的并发穿透依赖系统第三方接口超时、返回异常值、参数变化、版本升级兼容环境差异本地、测试、预发、生产环境的配置差异、数据差异权限边界普通用户、管理员、未登录用户能访问到哪些资源写完边界清单之后再根据清单写代码。每个边界分支要么显式处理要么明确注释说明“此处不考虑该场景因为……”。这样写出来的代码天然就比“凭感觉写”的要健壮得多。有人说这样做会不会太慢了我的经验是前期多花半小时列清单后面能省下好几个半夜。而且这半小时花得很值因为你是在用系统的暴力清单去对抗记忆的不确定性。3.2 防御性编程不要相信任何输入包括你自己的输出边界清单是“事前规划”防御性编程则是“事中兜底”。我总结了一套实用的防御性写法不需要掌握什么高深技巧只需要改掉几个习惯第一个习惯是统一封装外部接口调用。比如项目里对接了十几个第三方接口以前是各自处理超时和异常后来我抽了一个统一的 HTTP 客户端所有外部调用都必须经过它。这个统一客户端做了三层兜底超时控制、重试机制带退避策略防止雪崩、异常转换把第三方异常统一转成项目内异常避免调用方拿到的错误五花八门。第二个习惯是关键数据校验放在入口处。接收外部请求的接口在进入业务逻辑之前先做参数校验不合法直接返回错误码不让脏数据流进链路深处。这里我用了参数校验注解 自定义校验器的方式比在业务代码里写一堆 if/else 清爽得多。第三个习惯是对不可控的资源做保护。包括连接池设置合理上限防止数据库被打满、线程池拒绝策略要明确不要默认抛出吓人的异常、缓存设置合理的 TTL防止缓存无限增长、对超大结果集做硬性限制防止内存溢出。这三个习惯加起来能挡住一大半“本来可以避免”的线上问题。我自己实践了大半年最直观的感受是线上报警少了很多即使出了问题也不再是那种让人头秃的“灵异 bug”而是有明确根因的“业务逻辑问题”。3.3 单元测试的“反向思维”先写会出错的用例再写实现很多人不爱写单测觉得写单测是因为代码写完了再补一遍纯属浪费时间。我以前也是这么想的直到我发现单测真正的价值不在于“证明代码能跑”而在于**“迫使你把代码想清楚”**。我的做法是写代码之前先写一个会失败的测试——假设输入是一个超长字符串期望返回什么假设传入空对象期望走哪个分支假设依赖服务超时期望调用方看到什么这些用例写出来之后你会发现代码的骨架已经在脑子里成型了写实现只是把骨架填充完整而已。具体到技术上我用的是“测试金字塔”的思路底层纯函数和工具类写密集的单测中层业务逻辑写核心流程测试上层接口写集成测试。单测跑得快、定位准集成测试用于验证模块交互。还要强调一点不要为了覆盖率而写测试。单纯追求 100% 覆盖率很容易写出“为了跑通而跑通”的垃圾用例。我一般只给核心业务逻辑、工具函数、复杂分支写测试覆盖率高不高不重要重要的是每个测试都有明确的“保护目标”。3.4 代码评审真的能拦住问题但前提是你要认真准备以前我对 code review 的态度很敷衍觉得是走个流程代码扔上去同事随便看看点个“通过”就完事。后来我转变了观念把每次 code review 当成一次“免费的技术检阅”。要做到这一点提测之前我会自己做一遍“预评审”把这次改动的核心逻辑用一段话写清楚把涉及到的边界条件列出来把容易踩坑的地方主动标出来作为 PR 描述附上去。这样评审的同事能快速理解你的思路也更容易发现真正的问题。评审时我特别关注三个方面一是并发安全有没有共享可变状态、有没有需要加锁或用原子操作的场景二是异常路径正常流程看着没问题但异常分支处理得对不对三是可维护性这代码三个月之后我自己还能不能看懂。有一句话我记得很牢“如果你不想让别人 review 你的代码那项目出问题的时候你就别怪别人不帮你”。认真对待每一次评审其实是在积累职场上最实在的人脉和信任。4. 线上问题排查的“三板斧”十分钟定位取代一晚上瞎猜即使做了再多的预防措施线上也会出现意想不到的问题——这是程序开发的客观规律。关键不在于“绝对不出 bug”而在于“出了 bug 之后你能不能尽快定位、尽快解决、尽快复盘”。这部分我总结了一套自己的排查链路把平均定位时间从小时级压缩到了分钟级。4.1 建立分层监控体系让问题“自己报上来”我以前是“等人通知有问题”现在是“让系统先报警”。这一步转变的核心是搭建一套分层监控体系每一层负责发现不同类型的问题。我实践中的分层方案是监控层级核心指标报警阈值示例基础设施CPU、内存、磁盘、网络CPU 使用率持续 5 分钟超过 85%应用性能响应时间、错误率、QPS错误率超过 1% 持续 3 分钟业务指标订单量、支付成功率、转化率订单量环比下降 30%日志告警ERROR 日志关键字、异常堆栈出现 NPE、OOM、连接池耗尽关键词搭建这套体系我用的是开源方案Prometheus Grafana 做指标监控ELK 做日志聚合再用 Alertmanager 做告警通知。这套组合在中小团队里非常常见而且都是成熟组件不会踩太多坑。4.2 定位问题的心法先看数据再看代码最后才动“手术”出问题时最大的陷阱是“还没看清楚就急着改”。我踩过很多次这个坑看到某个错误日志顺手就改了对应的代码结果没解决根本问题反而引入了新问题。我的排查链路已经固化为一套标准流程第一步看监控面板。先看整体趋势是突然飙升还是缓慢上涨是单机故障还是集群性故障影响的接口是哪个依赖的下游服务有没有异常这一环节的核心目标是“把问题的范围圈定住”。第二步捞日志。根据监控圈定的时间窗口和服务列表去日志平台里捞对应的请求 ID、错误堆栈、慢查询记录。日志平台要有聚合检索能力能按接口名、错误码、用户 ID、请求 ID 过滤否则你会被海量日志淹没。第三步对照代码验证假设。拿到错误日志之后找到对应的代码路径看看哪里可能抛出这个异常。不要直接改先在本地复现——写一个最小的测试用例构造出同样的输入看能不能复现同样的错误。能复现说明你找到了根因不能复现说明你的假设可能是错的。第四步止血。如果问题影响线上业务先做一个安全的止血措施比如切流、降级、回滚保证用户体验不受影响然后再慢慢修根因。这一步非常重要——半夜爬起来改 bug 的悲剧很多时候就是在没有止血措施的情况下直接在现场做复杂修改导致时间被无限拉长。4.3 用好“二分法”和“git blame”快速圈定嫌疑代码定位问题除了靠监控和日志还有两个非常实用的辅助工具二分法和 git blame。二分法适用于以前功能是好的最近一次改动后出了问题。比如最近一周发过 5 次版本用户报障说现在不能登录了那就先回滚到第 3 次版本试试如果第 3 次版本正常说明问题出在第 4 次或第 5 次改动里。这种办法能快速缩小范围避免在一大堆代码里瞎找。git blame则适用于线上某个报错位置在很老的一段代码里想知道为什么当初要这么写。直接对着报错行号git blame能看到每一行是谁、在哪个 commit、为什么这么改。很多时候你看一眼 commit message 就明白了——不是“开发者水平差”而是“当初的业务场景就是这样”。4.4 应对“AI 改半天 bug”的场景不要被工具带偏现在很多人会用 AI 辅助排查 bug但我在实践中发现AI 改 bug 往往很慢——它会一遍遍分析、一遍遍猜测给出一些看起来很合理但方向不对的建议。我总结的经验是AI 更适合做“信息检索”和“代码解释”不适合做“根因定位”。比如你给它一个错误堆栈问“这个异常可能由哪些原因导致”它能列出十条可能很有参考价值但你让它“修复这个 bug”它可能给你一个没考虑上下文、跟业务逻辑完全脱节的方案。我自己用 AI 的方式是先用上面的排查链路圈定问题的 90% 范围再用 AI 去验证“我猜的这个原因是否常见”、“有没有我没想到的边界情况”。用 AI 加速信息获取而不是把决策权完全交给 AI。5. 从“修完就完”到“bug 资产化”让同一类问题只发生一次很多人的状态是修完一个 bug如释重负赶紧合代码、发版本然后继续下一个需求。这个状态最大的问题是——同样的坑下次还会踩。为了彻底摆脱“总是半夜被叫醒”的命运我把 bug 当成一种“资产”来管理建立了一套让同类问题不再发生的机制。5.1 每次事故后必做的三步复盘过程还原、根因定位、改进项跟踪事故复盘不是“写个报告发群里”就完事而是要形成一个可执行的改进闭环。我实践下来的方法是三步第一步过程还原。用时间线把事故从发生到解决的完整过程记录下来什么时候报警、谁第一个发现、什么时候开始排查、哪个环节耗时最长、什么时候修复上线。这一步的目的是找出流程中“卡点”和“延迟”。第二步根因定位。用 5 Why 法一层层往下挖为什么接口返回错误因为用户传了一个 null 参数。为什么要传 null因为前端没有做非空校验。为什么前端没做校验因为接口文档里没有说明此字段必填。为什么文档没说明因为当时排期太紧接口文档直接照着老接口抄的。挖到第五个 why你会发现根因根本不是“技术 bug”而是“文档管理不规范”或“需求评审缺环节”。第三步建立改进项清单。根因找到之后把需要做的改进动作列成清单补文档、加校验、写测试、加监控、改流程。每个改进项指定负责人和截止时间下次复盘时逐条检查。没有跟进的复盘就是走形式。5.2 搭建自己的“故障知识库”从个人的经验变成团队的能力在复盘过程中我发现一个现象同样的 bug很多人都在踩但每个人的排查路径都是从零开始。现在信息是流通的但经验往往只存在于个别人的脑子里。所以我做了一个“故障知识库”其实就是一个用 Markdown 维护的目录文档放在团队知识库里结构很简单故障知识库/ ├── README.md // 索引页按系统模块归类 ├── auth/ │ ├── 2023-05-21_登录态失效问题.md │ └── 2023-07-14_验证码过期时间过短.md ├── order/ │ ├── 2023-06-02_深分页导致慢查询.md │ └── 2023-09-11_库存扣减并发超卖.md └── payment/ └── 2023-08-19_回调重复通知处理.md每篇故障文档的格式也固定包含事故时间、影响范围、根因、排查过程、修复方案、预防措施、参与人。这个知识库不是为了写给别人看的而是为了“下次遇到类似问题我可以直接搜索到上次的处理方式而不是重新从头开始查”。5.3 顺手帮测试补上边界用例是预防 bug 最高杠杆的动作每次修完一个线上 bug我都会顺手做一件事把这个 bug 对应的测试用例补到自动化测试套件里。这个动作听起来很简单但价值很大。因为线上 bug 是最真实的测试用例——它经过了真实用户、真实数据、真实环境的考验优先级远高于你自己臆想出来的测试场景。把它补进去相当于用真实事故给代码库“上保险”。我一般会在修复代码的同时写一个针对这个场景的回归测试放在测试目录里注释写明“对应 issue #123防止深分页导致慢查询回归”。这条测试能保证即使以后有同事重构了这段代码改坏了这个逻辑CI 也会首先报警而不是等线上用户发现。辅助测试也不是越多越好这里要注意控制粒度。只补核心业务逻辑和容易回归的边界场景避免把测试套件搞成一个几千用例的“僵尸库”。5.4 分享和复盘不只是在帮别人更是在为自己树立“靠谱”标签建立知识库里我发现一个很有意思的现象每次我在团队内做技术分享讲最近遇到的一个有意思的 bug 和排查过程同事们都会觉得“这个人很专业”。一开始我以为这是错觉后来我意识到原因在于大多数人解决问题后是不会分享的你愿意把踩坑的经验讲出来本身就是一种稀缺价值。而且分享的过程中你会被迫把自己的思路理顺发现自己到底有没有真正理解这个问题。这种分享带来的隐性收益是巨大的领导觉得你靠谱、同事遇到问题会主动找你交流、新同学入职后会把你当成“导师”。你不再是那个“总是被锅砸中”的人而是成了“有问题值得去问问”的人。这种身份的转变是“背锅侠”变成“香饽饽”最核心的助推器。6. 让别人看到你的价值从“背锅侠”到“香饽饽”的身份转变前面讲的技术和流程层面的改进解决的是“怎么让 bug 少发生”的问题。但还有一个很现实的问题即使你把 bug 都解决了如果没人看到你的努力和成果你依然可能在别人眼里是个“默默干活的人”。这部分我讲讲怎么在职业身份上真正完成转变。6.1 把“解决问题的过程”讲成“一个有价值的故事”同样是修一个线上问题有两种表达方式“昨天线上出问题了我修了一下已经好了”和“昨天发现了一个线上数据边界问题花了一个小时定位到根因是某个接口对超长参数没有做校验我们补了一层统一校验并且加了对应的回归测试顺带更新了接口文档”。第一个说法别人听到的只是“你处理了一个杂事”。第二个说法别人听到的是“你有系统化解决问题的能力”。我认识到这个区别之后凡是处理完问题都会顺手整理一份简洁的记录发给相关同事和领导。不是写长文就是几行字包含问题是什么、影响范围、根因、处理方式、后续预防。这个习惯帮我积累了大量可见的“战果”也让“靠谱”这个人设越来越牢固。6.2 主动承担“看不见的工作”让自己成为流程中的关键节点除了解决具体问题我还主动承担了很多“看不见的工作”——比如优化 CI 流程、完善代码规范、搭建监控面板、整理部署文档、给新人做 onboarding 培训。这些工作没有具体的业务 KPI不直接产生收入但它们的杠杆极高做好了能减少整个团队的返工率、提升交付速度、降低新人上手成本。而且这些工作有一个特点做得越好大家越依赖你但因为你解决的问题都前置了所以你不会被“半夜叫醒”。我举个例子有一次我花了两天时间把团队从手工部署改成自动化流水线顺便加了版本回滚按钮。上线后发版时间从 40 分钟缩到 5 分钟回滚从“手工改代码再重新部署”变成“一键完成”。后来线上出问题需要紧急回滚时其他团队还在手忙脚乱我们这边一键搞定。领导看在眼里从此提到我都是“那位很稳的同学”而不是“那位很忙的同学”。6.3 学会说“不”但用事实和数据说话以前我是一个不会拒绝的人。不管谁来问“这个功能能帮我加一下吗”“这个接口能帮我看看吗”我都说“行”。结果就是自己的活儿干不完别人的问题也处理不好两头不讨好。后来我学会了一种更成熟的拒绝方式——不是生硬地说“不行”而是用数据和优先级说话。比如同事来找我加需求我会说“我现在手里有 A、B、C 三件事A 和 B 都是这周上线C 是下周二交付。你要的这块要实现的话得花大概一天时间你希望我砍掉哪个”这种方式把“应不应该做”的问题变成了“优先级怎么排”的问题。你不再是那个“什么都答应但什么都做不好”的人而是一个有主见、有条理、知道什么该做什么不该做的合作伙伴。这种工作方式也让领导对你更放心因为他知道你不会因为乱接需求而耽误主线任务。6.4 把自己当成“问题的终结者”而不是“问题的传递者”最后想分享一个思维方式上的转变。以前遇到自己解决不了的问题我的反应是“这个问题我不会去找别人”然后就把问题抛出去了。但“背锅侠”之所以被叫去背锅往往就是因为在“责任”的传递链条上你总处于“接收”的位置。现在我的做法是遇到问题先不急着向外抛而是问自己三个问题第一我自己能不能查资料解决第二如果不能谁能帮我解决第三我能不能在找他之前先准备好足够的上下文和复现步骤这样做的结果很有意思很多你以为只能找别人的问题在自己查资料的过程中就解决了而真正需要找别人的问题因为你有足够的上下文和复现步骤对方一看到就愿意帮你——因为他知道跟你配合不费劲。大概半年之后我明显感觉团队里的氛围变了。以前同事找我都是“你快看看这是不是你的问题”现在更多是“这个需求要动你的模块提前跟你对齐一下”。领导给我排需求时也更倾向于让我负责那些“跨模块、复杂、需要设计”的活而不是零散的小修补。这种从“救火队员”到“关键路径上的设计者”的转变才是“香饽饽”真正的含义。最后再分享一个小技巧我每周五下午会花半小时做一次“本周回顾”只回答三个问题——本周解决了哪些问题哪些是重复发生的我可以用什么机制让它们不再重复这个习惯成本极低但它是让我从“被动救火”走向“主动预防”的根基也是我这套自救体系里最重要的一环。
返回列表