良性循环 -- 软件开发中的一些小故事
2021-11-21
1. 重构 lexer 的小故事
刚来这家公司不久,很早期的时候,做过一件小事情是把 lexer 重构写了,主要是出于性能上面的一些考量。
我事先做了一些 clean up 的工作,把 lexer 的接口拆出来定义好,让新的实现和老的实现都是以同一个接口暴露给外界。
接下来,开始补测试,并且让新的旧的实现共存了一段时间。补测试花了不少功夫,有好多 corner case 并不容易暴露出来,如果没有足够的测试,是不敢让新实现的代码直接替换旧的的。
等测试补得足够完善,在某一个时间点,将新的 lexer 设置成默认的实现,再过一段时间,开始清理掉老的实现的代码,这个事情就算做完了。
我觉得这个事情能做成,最关键的一步,是补测试。这是一个不那么大的工程,后面了在公司也经历过一些大的工程,观察到,当工程大到一定程度后,用一套全新的东西替换掉旧的,基本上都不怎么成功。 一般都卡在新的实现替换掉旧的实现这一步,新的实现 bug 一大堆,没有能力替换掉旧的,导致两者长期共存,再到后来反而是旧的一直在更进,新的却无力维护,最终是被废弃状态。
若想重构,先补测试
2. 数据分析平台的故事
这是来自于我一个朋友的故事。在他们公司里面有一个做数据分析的组,他的职责就包括数据的入库和清洗这块的事情。数据来源自公司和其它各个部门。
他所遇到的问题是,各个部门吐出来的数据格式不统一,不便于入库分析,他处理起来特别头疼。开始尝试一个部门一个部门地接入,但是这样做很快他就崩溃了,工作量很大,就他自己一个人,要负责所有的对接,这个方式很不靠谱。
然后他换了一个方式,把大家拉到一起来开会,定制标准,约定好数据的协议格式,形成内部规范。然而,这一套也没能玩下去,其它部门的同事都不太愿意配合他。本来用得好好的代码,现在多了所谓标准限制,又要改东西,任谁都不喜欢。 还有一个让他崩溃的地方,即使公司内已经开会对数据格式达成了一致,他看到入库过来的数据还是经常有不合规范的。"这群王八蛋哪天又改了东西,然后他这一块就又坏掉了,又得跨部门去沟通"。
其实换位思考一下,会这样子很正常。假设另一个部门的人,凭空给你制造一些输入日志统一规范的需求,多了一堆的格式规范,并且你又看不懂这么干有什么直观的好处,从这些数据分析出什么有用的价值,自然是不愿意去改改改的,也不会愿意去遵守这些格式规范。更不论说做好完善的测试,覆盖输出日志,确认它是符合规范并阻止后续有改动破坏...那这个事情必然难以推动和落地呀!
最后,这个朋友的终极解决方案,是由他自己实现日志库,把公司层面的日志的库给统一,然后替换掉了。这样他就可以很方便控制入库的数据格式,其它部门也没有太多额外工作量,也就愿意配合了。
只有代码层的控制粒度,才是掌握事实标准
3. CI 稳定性的故事
在我们公司,研发负责写单元测试和集成测试,有另外一个部门会负责所有的从编译,测试,打包这一系列的持续集成(CI)相关的任务。测试环境是由他们来维护的。因为有很分支,也有很多个版本,又有很多套测试要跑,所以这个组合下来,想让 CI 每天自动化跑着不出问题,也是一个挺辛苦的活儿。
问题出在哪儿呢?写测试的人和具体负责让测试跑起来的人跨部门了。研发天天抱怨测试不稳定,影响到合 PR,体验非常不友好。于是让 CI 稳定成一个 KPI 指标了。这个事情其实并不太好做,体验过了才知道。
让 CI 不稳定的原因其实是两块的,一块是测试环境的锅,测试集成的资源不够了,磁盘满了,网络挂了,机器断电了各种都有可能发生。另一块是测试本身的不稳定,有些测试 case 跑一次没事,跑很多次就有可能概率性地挂掉;有些 case 单独跑的时候怎么都跑不挂,但是和其它测试 case 一起并发跑的时候就概率性挂;有些 case 在机器性能好的时候可以跑过,机器很慢就随机挂掉;这些都是测试本身写得有问题。
当写测试的人和跑测试的人是两波人以后,就容易相互推诿了。测试挂了?重跑...跑过就行。但是如果不修复问题,不稳定测试的欠债越来越多,有时候重跑好多次都不过。环境问题是测试部门的锅,而测试不稳是研发部门的锅,但是 CI 不稳定最后就变成薛定谔的锅了。
我们早期的时候人不多,基本上谁遇到自己的 PR 有测试挂了,不管是不是自己弄挂的,都会去修复掉不稳定的 case,每个人都能理解所有模块。但是项目越来越大,这一套行不通了,因为不一定是自己的提交导致 CI 挂掉的,而挂的那个测试的代码模块可能跟自己毫无关系,修复不熟习的测试会比较费劲。
关于 CI 的,还有一个问题是 CI 跑得太慢。测试越加越多,PR 提交之后测试跑好久都没完,也是挺影响开发体验的。之前都采取一些运动式治理模式,“我们的单元测试需要 3 分钟能跑完,CEO 很重视”。然后加机器,加并发,重构代码,终于 KPI 搞定了。然后 ppt 宣传一波,这个事情就过去了。 要说完成这个 KPI,投入还是很多的,但是最后的效果,过了当时的考核,慢慢又回到了原点,CI 随着测试的不停加入又慢了下来,而机器资源总是越来越不足...
只要源头没有管控起来,新加个单元测试跑 120s 还能往仓库合并的,那政治任务能完成才有了鬼了!哪怕当时辛苦搞一波,最后还是会慢慢回到原点。
4. 构建性能测试体系
有时候看到代码写得非常不讲究,就想顺手改掉,但是这样改是改不完的,因为随时会有其它人不停地提 PR。有时候会做一些重用对象减少分配之类的小优化,或者至少自己在写代码的时候会注意这些,但是这种治标不治本,不是每个人都重视这些的。看着 repo 的代码腐烂,痛心疾手,但又无力回天。
周sir跟我讲,像这样代码级别的优化,能达到个 50% 的提升几乎是上限了,也许就 10%,20% 而且做起来还很累;优化器那边一个查询计划是否最优,那可能是上百倍的性能差异;把注意力放在某一个算法的改进上面,也可能有甚至十倍的差距;大方向上面把架构处理好,也可能有一两倍的性能差异出来。我是深表赞同...于是直接放弃治疗了,还敦敦教导小朋友们,周sir讲得没错。
但是这种日常的代码,哪怕一个影响了 0.5% 性能的 PR 合进来,堆积多了,也是会有影响的,尤其是热点执行的代码路径上面的。
于是我没有直接去做一些代码级别的优化的事情,而是让一些代码库的 benchmark 每天跑起来。目的很简单,就是构建性能的测试体系。只有监测每个 PR 的这些小的变化,才是正确的方向。只有保证不会一些日常的小改动,让代码性能回退,下一步再去做一些小的优化点,才有可能持续进步。目前它已经捕获过几次性能回退了。
想做优化,先建立性能评估体系
5. 良性循环
最后是一则虚拟的故事,故事可能发生在每一家公司。我把这一则故事叫,良性循环。
这家公司快速发展,客户越来越多,客户的需求越来越多,研发接了很多新的功能。新加的功能越来越多,软件越来越复杂,bug 也越来越多了。由于产品的质量没把控好,客户抱怨很多,必须去救火。公司有专门 oncall,去处理各种客户的问题。
软件的复杂度并没有受到控制,也就是产品本身并没有改善,于是随着客户越多,oncall 也越来越多。人力都被这一层占用了,于是也就研发没有精力去优化产品本身。一些资深的研发全部投入到处理客户问题上面去了。代码呢,都是实习生写出来的。
实习生写的代码 bug 也多呀,于是研发更苦了,还得为新加的 feature 新引入的 bug 擦屁股。问题太多了,处理不过来,公司觉得,得加人。加人才能支持更多的客户。加的人越多,写的 bug 越多,产品是越来越复杂了,但是质量却每况愈下。 但是需求有那么多呀,不接客户的需求,就赚不了钱,养不了研发了呀。怎么搞呢?怎么样才能进入到良性循环?
哦,对了,我想到一个良性循环的反义词,叫“破窗效应”...