开发人员,千万不要去碰那该死的业务参数,无论什么时候!

你好呀,我是歪歪。

前几天发了一个牢骚:

本来只是单纯的吐槽一下,但是好多人对其中的细节比较感兴趣。

大家都是搞技术的嘛,对于"踩 BUG"这种喜闻乐见的事情,有兴趣是很正常的。

其实我这个 BUG,其实严格意义上不能叫做 BUG,因为和程序无关,甚至和技术的关系都不算大。从标题上你也能猜出来,是和一个业务参数相关。

但是在这个过程中,因为我是整个事件全程的亲历者,所以现在回看这个事情,我还是有一些思考在里面的。

我觉得这是一个程序员会遇到的"典型事件"。

那就用这篇文章一起复盘一下吧。

背景

要说明这个问题的背景,甚至不需要一个具体的业务场景,只需要围绕着以下这个非常常见的利息计算公式,就可以说明问题的起因:

利息=计息金额*日利率。

日利率=年利率/360

由于日利率的计算,涉及到除法,在对应需求第一次开发时,业务的要求是日利率保存 7 位小数。

在程序中,年利率和日利率是两个字段分别保存的,日利率在初始化的时候就算好落库了,后续程序直接取这个算好的日利率就行了。

系统上线,相安无事。

跑了一段时间后,业务又提来一个需求:当前的精度不够,需要调整到 11 为小数。

你不用好奇歪师傅这边到底是什么业务场景,反正我去看了业务数据,需求是合理的,那就把需求接过来干就行了。

保存 7 位小数和 11 位小数,大家都是搞开发的,肯定也知道这个就是一个小改动,很快就能搞定。

事实也是如此,虽然之前的需求对应的代码不是我写的,但是我看过代码,清楚的知道改动点在哪,所以很快就开发完成。

前面说了,这个需求之前在线上按照 7 位小数跑了一段时间,所以存在一些存量配置。

针对这些存量数据,在需求评审会议上的时候,我提了一句:存量配置怎么处理呢?

业务答复:这次需求上线的时候,你按照 11 位小数重新算好,然后写 SQL 更新一下就行。

我心里一盘算:计算公式明确,年利率我也有,算一把,没啥问题。

就答应了。

然后,不出意外的出意外了。

假设年利率是 2.5%,除以 360 之后,保留 11 位小数,应该是 0.00006944444。

而我不知道当时为什么手抖了,在 SQL 里面写成了 0.00069444444。

我给你对比一下:

0.00006944444

0.00069444444

相当于我写出来的日利率被扩大了十倍。

然后再回头看看这个公式:

利息=计息金额*日利率

日利率被扩大十倍,那么对应的计提金额也会被扩大十倍。

这就是问题的背景。

一个单纯的人为失误,和程序没有任何关系,所以严格意义上不属于程序 BUG。

但是这个问题确实是足够低级。

为什么没被发现?

那么这个错误的 SQL 是怎么通过代码评审、测试验证这两道关卡被带到生产环节的呢?

首先,这一次提交的代码,压根就没有评审环节。

我有代码提交权限,也有代码审核权限。所以我自己提交,自己就审核通过了。

其实这个需求应该是组里面另外一个小伙伴来做,但是当时他被调到其他组了。

他还在我们组的时候,我们的合作模式是他提交代码,我进行审核。

如果有这个环节,我觉得我有 50% 的几率发现问题。

为什么是 50% 呢?

因为这取决于我审核代码时是否有正在处理其他的事情,如果有其他事情处理,我可能会形式主义的看上几眼。如果没有其他事情,而这次提交的代码量又不大的话,我基本上都会认真的过一下提交的内容。

通过代码评审之后,接下来就应该是测试环节。

测试主要关注的是精度从 7 位变成 11 位之后,最终计算出来的利息是否符合预期。

他测试时是走了整个业务的全流程。

在"全流程"中,这个 11 位精度的日利率,是在页面配置年利率的时候通过程序自动计算出来的,不会错的。

而他在验证 SQL 语句的时候,测试环境又没有生产环境的配置,所以他拿着我提供的 SQL,只能保证写的语法没问题,能正常执行,并不能确保里面数据的正确性。

而我也记得很清楚,我当时给他说过:你执行一下 SQL 不报错就行,值的正确性,我来保证。

而且戏剧性的是,测试同事很仔细的去看了值,他去数了确实是 11 位小数。但是可惜,站在他的视角,他发现不了值被扩大了十倍。

所以,测试环节也没有发现这个问题:

0.00006944444

0.00069444444

就带着上生产了。

一个问题正常来说不应该被带上生产,但是我们确实不能保证测试环节一定能把所有问题都测出来,所以新项目、新迭代的生产验证也是非常有必要的。

这个我们也做了。

按理来说,生产上的数据已经是错误的了,而且是一个"利息金额扩大十倍"的明显的错误,如果主动去做了数据验证,应该能被发现才对。

那为什么做了生产验证,却没有发现问题呢?

因为当时存量配置有三条,我提供了 3 个 SQL,其中有一个是算对了的。

每一条存量配置都对应着大量的利息数据,而算对了的这个对应的数据更多,在比例上超过 60%。

我进行生产验证的时候,在大量的利息计提数据中随机抽选了两条,选中的这两条,恰好都是正确的 SQL 对应的数据。

所以我发现符合预期,得出了生产验证通过的结论。

站在这个节点,回顾整个事件,这个时候应该是最有可能发现问题的时候。

但是没发现。

根本原因是验证方案不严谨,玄学原因是运气不站在我这边。

怎么暴露的?

你想想,这种业务参数配置错误的问题你能通过什么监控规则监控到吗?

其实很难的。

我们一般来说做技术层面的监控,都是监控程序是否按照预期正常运行。比如在计算的过程中出现异常,那我们是可以监控到的。

但是在这种只是参与计算的值不对,但是能正常计算出一个值的情况,并不会报错。

这种问题通过技术手段很难监控到。如果硬要去做监控,肯定是能做的,比如从异常浮动的维度、横向数据对比的维度,但是配套的开发成本又上去了。

我是怎么发现这个问题的呢?

也是纯粹的运气。

是一个周五的晚上,我做另外的一个和本问题毫无关系的场景下的数据验证的时候,偶然间看到了一笔数据的金额和前几天比,明显大了很多。

这是不符合业务规律的。

然后进一步跟踪,最终定位到了前面的问题 SQL。这个时候距离这个 SQL 上线,已经过去了三天,已经产生了一批错误数据了。

如果我没有偶然间看到这个问题数据,那么这个问题会在什么环节暴露呢?

就是在业务使用这个数据做核对的时候。

那个时候整个问题的性质就变了。不仅是处理时间来不来得及的问题了,而是这个问题是由"开发自主发现"还是由"外部反馈发现"这两个完全不同的性质了。

一般来说,不管是什么问题,先抛开严重程度,只要是开发自主发现的,都能一定程度上让事情变得不那么难堪。

所以我们才一度强调"可监控"的重要性。

随后,我联系了业务,反馈了这个情况。他表示在他下次使用这批数据之前,把数据修复好就行。大概一个月后,他会用到这批数据。

这样,我有接近一个月的时间来处理这个问题,防止问题扩大化。

时间非常充足,站在这个角度,我运气还不错的。

问题已经暴露出来了,随后就是制定针对这批错误数据的修复方案了。

修复方案就和业务场景相关了,属于多个业务场景叠加在一起,所以修复方案其实是比较复杂的,涉及到"修数"和"补数",没有展开描述的必要了。

只是想简单提一句,这个修复方案是我利用周末的时间想出来的,很多细节问题我都需要考虑到,甚至在心里写了一遍伪代码。

确实是浪费了周末的时间,但是这是为自己的错误买单,半点不怨别人,就是活该。

而对于参与后续方案讨论的同事来说,在这件事情上付出的时间,才是属于无妄之灾。

这就是整个事情的过程,一个小数点引发的血案。

再回首

现在整个事情的全貌都在你眼前了,你得到了什么经验教训?

因为手抖了,写错了一位小数,这确实是直接原因,所以是想着下次再处理这种数据的时候,更加小心一点吗?

我觉得不是这样的。

我得到的经验教训就是我的标题:开发人员,千万不要去碰那该死的业务参数!

如果在最开始需求评审会,我们讨论到存量数据的时候。

业务说:这次需求上线的时候,你按照 11 位小数重新算好,然后写 SQL 更新一下就行。

我说:不行,这个属于是业务参数,我不能去动。上线完成后,就具备这个功能了,你可以通过页面配置去修改。

我知道他们修改业务参数的流程,很长很复杂。

首先业务需要发起一个参数变更的 OA 流程,然后走到他的部门负责人审批。

业务部门负责人审批完成后,会到具体负责业务参数配置的人员手里,还需要该人员对应的部门负责人审核。

审核完成后有权限的人员才会去修改这个业务参数,而这个参数的修改,在对应的系统功能上还有两级甚至三级审核。

整个完成之后发起 OA 的人员还需要进行变更确认,看看页面上是否是自己想要的配置。

这一套流程走下来,你觉得还会出错吗?

很难出错了。

你可以批判这个流程过于臃肿,但是你最终总是会认识到,这个流程其实是在保护打工人。

我知道他流程比较复杂,而我写个 SQL 几乎是没有成本的,但是这是在 SQL 正确的前提下。

如果当时不答应通过 SQL 的方式帮他处理存量数据,他其实有更加正规的流程去处理这些数据,而且不会出错。

事后我们复盘的时候,也有同事私下向我提出了这个的问题:为什么不走 OA 流程去调整这个参数?

另外,关于流程,我给你举一个程序员方面的例子。

一个核心开发人员拥有线上数据库的操作权限,我们先假设这个人绝对忠诚、绝对可以信赖、绝对恪尽职守、绝对不会删库跑路。

某一天,他收到一个预警信息,经过排查发现需要去修改数据库里面某个数据的状态,他直接就去修改了。

这个操作非常常见,特别是在小公司或者在一些在快速发展阶段的公司。

后来这个公司成长起来了,开始更加注重操作风险了,回收了所有人员的数据库权限,以前的事儿既往不咎,以后想要修改数据库数据,必须要发起一个审批流程,经过层层审批之后才能执行。

这个流程和"直接去修改"这个动作比起来,就重了无数倍了。

站在程序员的角度,前几年都是可以直接操作生产数据,突然这个制度出来了,极大的影响了之前的开发惯性。所以刚刚开始执行的时候,你可能会骂一句:xxx。

但是长远来看,这个流程其实是在保护你。

当你有数据库权限的时候,操作对了,没有人会夸你。操作错了,你就是罪魁祸首。

有了一个审批流程,在加重了操作成本的同时,也降低了错误成本。

处理问题的时长可能增加了,对于问题处理的敏捷度可能降低了,但是站在公司的角度,随着公司的发展"稳定"才是永恒的主旋律,在稳定面前,敏捷度反而是可以牺牲的。

歪师傅在第一家公司业务野蛮发展的时代,曾经就有这样的权限,那个时候刚刚参加工作两年多的时间,觉得事情就应该是这样的,这样才是正确的,可以足够敏捷,足够迅速的处理问题。

后来权限回收了,当时我也在私底下骂骂咧咧了几句。

再回来,随着经验和在职场上见过得事儿越来越多,才渐渐认识到:蛮荒时代确实出英雄,但是我没有把握好机会成为英雄。蛮荒时代之后的流程规范,规章制度其实是在保护那批没有成为英雄的人,其中就有我。

最后,给你,也给我自己一个忠告:开发人员,你最好要知道你数据库里面每一个业务参数背后的业务含义,但是千万不要去碰那该死的业务参数。也轮不到你碰,该碰的人会在正确的流程下去碰。

无论什么时候,心中都要绷着这根弦。