目录

中小型项目紧急维护血泪心得

目录

这两天临时紧急维护一大概三万人日常使用的系统,前前后后提交了十来个补丁,和几个万行以上的 rowsAffect 的数据库语句。最后算是终于稳当跑起来,没有人反馈系统问题了。回想整个惊心动魄的过程,收获颇丰,宝贵的经验源于生死一线的操作,源于危机时刻的灵光迸发,源于惨痛的经历。所以记录一下这些难得的经验,有紧急修复保证线上可用的小策略,也有增强可维护性的一些心得。水平有限,某些操作可能不是最佳或者不适合十万百万级别项目,请批判着阅读。

一、背景

一个报到系统,上学期可用并 hold 住了全校所有本科生与研究生的报到流程(虽然当时也出了不少问题,打了很多补丁)。加上当时设计的时候,貌似没有考虑某些表的复用,导致这学期的报到出现了很多 bug。原系统不是我写的,正好我前些天熟悉了整个系统,我又最空,所以主要负责了这些 bug 的修复。写这篇文章没有任何贬低的意思,之前的系统很多地方写得确实很好,比如自己实现了一套工作流,完成程序主体的两个大佬也是我望尘莫及的。只是报到这部分当时设计得时候确实考虑欠妥,所以有了这次宝贵的经验。

二、紧急修复新代码带来的线上问题

这个经验和本次的 bug 没关系,但是很重要,是某大佬传授的简单但又很真理的一个原则。如果最新的代码导致了线上的 bug,比如功能不符合预期,甚至直接崩溃,首先要保证的是系统可用,而不是找找是什么导致了 bug。即,如果可以,直接回滚软件版本,比如重跑上一次的 Github actions。

本次 bug ,准确地说是一堆 bugs,是由于旧代码与新业务的不匹配造成的,不符合这种情况。

三、项目初期就要注意可维护性

经此一役,逐渐认识到了可维护性的重要性,甚至可以说是最重要的。可维护性有非常多的东西可以说,例如:

文档、注释: 一些重要的甚至奇葩的代码段,要写上原因和思路。以及各种 fix,一定要写明,为什么 fix,fix 了什么,这个 fix 是否是永久的,什么时候应该删除 fix 代码。这次维护修的其中一个 bug 就是,几个月前的某个 fix 忘记删掉了。尽量不要出现魔法数。常用的重要的组合动作,尽量封装。

代码的前瞻性: 简单来说,就是,在写本次的业务时,考虑到后面表的可重用性,如写这学期报到的时候,想到下学期重用某些表,会不会出现问题。

状态更改的双向性: 有判断 false 成为 true 的逻辑,就一定不要忘了写 true 成为 false。比如,第一次使用的时候,只判断某个字段是否不为空。若不为空代表需要核酸,置标志为 true。但是下学期使用的时候,如果出现上学期需要核酸,这学期不需要核酸,就没有修改成 false 的逻辑,导致 bug。这里还涉及一个小细节,就是 gorm 的 Updates 函数,如果传的是结构体,是不能将字段更新成默认值的。比如 db.Model(data).Updates(Data{flag: false}) 是没有效果的。需要用 map[string]interface{}

状态的自动更改要做好: 比如,自动清除(通过程序)一些报到状态的标志,而不是手动(通过数据库语句)清,本次的 bug 大多是因为某些状态不该清,清了;某些状态该清的没清。

四、加强人员沟通,无论是部门内还是跨部门

部门有人接到留校生也要重新报到的通知,就一下子清除了所有人的某个标志,导致所有留校生,请不了假。想请假必须发起入校申请、报到申请、请假申请。而辅导员不可能一直监控待审批请求,所以导致大部分留校生出不去。

有部分同学向我反馈了这个 bug,我当时不知道是因为标志被清了,也不知道这个通知,就开始看代码,查数据库。发现有个标志被清了,才知道了老师的通知,以及数据库的批量改动。于是我查看数据库的更新时间,以及留校申请记录,统一恢复了这些人的状态。在答疑群发了已恢复可正常请假的通知。

结果,又有人反馈不能请假。我又发现某条记录,被硬删除了。后面得知,是老师要求删除旧的入校申请,而请假必须同时保证前面的标志为 true 以及存在入校申请记录。又因为沟通原因,导致了通知误发,与用户体验不佳问题。

跨部门协作的话,后面又因为对接没有一对一,太赶了,导致专门发通知的部门,在我们还在努力修复的时候,发了通知。

所以,在有重大改动前,尽量和部门内的相关人员说一声。也方便在你不在的时候,别人能马上顶上来,而不是从头开始挖宝。

五、尽量使用软删除

我们使用的是 gorm,程序上的删除一般是软删除。但上面的申请是硬删除,导致了数据很难恢复,binlog 也恢复不了。所以尽量使用软删除,一切都可挽回。

六、数据库重要操作前先备份

比如软删除前,或者重置 flag 前,保存受影响的数据的信息,保证能及时恢复。应该也有不少能直接恢复的数据库工具。

七、先尽量保证用户体验,再找 bug 或彻底修复

前面存在的清了入校申请导致不能请假的 bug,不应该想破头想数据怎么恢复,而是应该在尽量少暴露漏洞的情况下,保证用户的体验。我是这么处理的:因为请假会同时校验入校申请记录是否存在,以及某个标志,而这个标志除了留校生都已经清空了。所以可以暂时取消对入校申请记录的校验,等正常开学了再让他们和大部队一起报到。所以既能解决留校生无法请假的问题,又不会造成非法请假。

如果想着怎么完美恢复数据,那么会拖很久,影响用户体验。

还有一个 bug 是,许多同学是提前返校的,并且是 14 天有省外旅居史的,需要提交核酸检测报告。出现了大面积的核酸过期问题,我定位后发现,本学期所有提交入校申请的同学,「返校permission」的核酸有效期都未更新,一直是上学期的记录,所以全部都是过期的。而且我和部门另外一个人都没发现什么明显 bug。

后面惊奇得发现,工作流的某个阶段是额外单独存了一份核酸检测信息的,里面也有核酸有效期记录,而且是成功更新了的。

于是马上先用这份核酸表来校验核酸是否过期,先保证用户体验,再去找为什么permission没成功更新。

八、允许的话,某些数据可以单独多存存

这个经验可能会影响性能甚至无用,但是它确实在紧急时刻救了一命。某个表只记录了核酸的有效期信息,更新失效了;但另外有个单独的核酸表,记录了完整的核酸的各个信息,导致 bug 迅速得以临时修复。

九、慎重提前返回 error

Go 的逻辑是,尽早返回 error。但业务场景下,并不一定如此。看下面这段伪代码。

func process() error {
    // ...
    if err := 保存完整核酸信息(); err != nil {
        return err;
    }
    if err := 记录预期返校时间(); err != nil {
        return err;
    }
    if err := 更新返校交通工具(); err != nil {
        return err;
    }
    if err := 更新 permission 表的核酸有效期(); err != nil {
        return err;
    }
    // ...
}

之前未成功执行最后的核酸有效期的更新,是「更新返校交通工具」那里出现了问题,而那个函数上学期是没问题的,所以从来没有人想到这里会有问题。导致函数提前返回,最后一个函数直接不执行了。

所以要慎重考虑提前返回 error,或者说,评估 error 是否提前返回的标准是:当前操作出现了 error,下面的操作都不执行了。否则,应该只是记录 error,然后继续执行。

十、多人合作,求助伙伴

一个人代码写久了,很容易迷糊,多找个人看看,或许就有思路了。

重要的操作,如代码更改或数据库语句,找个人帮忙看看,能减少犯错的可能。

集思广益,别人可能提供给你很多新的解决思路或者帮助。如,上面的交通表的错误,是部门的一位人说用 sentry 的 CaptureMessage 功能主动捕捉信息获取到的。

十一、错误不能瞎忽略,要及时记录

一直没发现更新交通表出错了,很大程度上是它忽略了 gorm 执行的 error,没记录也没打印。

不过有点我们做得一直不错,就是 sentry 几乎给每个项目都上了,自动捕捉 panic 信息并发邮件。

十二、小心软删除与 unique 冲突

之前交通表出错是因为,数据库给学号设置了个 unique_index。更新数据库的时候,利用 gorm 通过学号查找是否存在旧记录,存在的话,就更新,否则执行插入。

但一直报了 键重复 的错误。看了半天代码,甚至找人帮忙看都没看出问题。后面才发现,交通表所有的数据,被人软删除了,所以在 gorm 看来,所有的交通信息都是空的,会执行插入。但是数据库插入的时候,只会看 学号 是否重复,而数据库存在一条 deleted_at 不为空,学号已有的数据,当然插不进去。

十三、关于检测与提示条件的一些小心得

有的时候,尤其是这种修复性的提交,可以采用检测条件宽于提示条件的小策略。

例如,误删了某些必要条件。可以先放宽对这些必要条件的检测,如入校申请没通过也能请假(还有报到申请和好几个flag卡着,不会有事)。但是,在提示是否可以申请入校或者是否需要申请入校的另外一个界面,检测条件严些。保证既能请假,又能在这个特殊时期,把该补的申请补上。

又比如说,之前的交通更新 bug,导致提前返校的同学,实际报到时间这个字段,都没更新。所以我在判断是否成功报到的时候,只检测报到标志;在判断是否可以申请报到的时候,检测报到标志与报到时间是否为本学期。使得这些提前返校的同学既能重新报到一下,修正报到时间,又不会影响正常请假。

最后:永远都不要摆烂,相信一定有办法能解决

虽然这个系统的报到模块打了一个又一个的补丁,有很多标志位,但在几天的齐心协力的努力下,现在异常稳健,甚至修复了上学期的许多细节 bug。截止我提交最后一个 fix commit,以及大半天没有人反馈报到系统有任何细微问题了。

以上的很多操作完全谈不上优雅,但在保证业务可用上,证明是很有效的。谢谢阅读,希望有所帮助,期待交流反馈。