我工作的前 5 年,都是从事基础系统研发相关的工作,做过后台的接入层、后台的存储系统、RPC 框架。
说来不怕你笑话,那个时期,我对代码一直有一种恐惧感。这种恐惧是怎么来的呢?且让我慢慢说来。
我们所构建的基础系统,都是使用在亿级甚至十亿级用户产品的业务系统之上的。
从客户端(前端)到后台业务逻辑层,再到基础架构层,所写的代码是跑在整个调用链路的最后端。你可以认为,几乎每个用户的每个请求都会跑到我们写的那部分代码。
这个对系统带来的影响是:
- 代码出问题后,影响的用户范围会很大。
- 在这亿级甚至十亿级用户量的情况下,每天所带来的请求可能是千亿级,万亿级的。
在如此庞大请求量的情况下,几乎各种奇葩的异常,你都会遇到,代码要极其的健壮,一个小异常没处理好就会带来大麻烦。
这让我想起来,我们故障时候的情形。几年前,我们每做完一次版本变更,晚上基本都会睡不好,担心变更的代码有问题。
对手机的报警短信特别敏感,一有风吹草动,立马就会打开电脑 VPN 看看,即使是在深夜凌晨的时候。
我自己有个习惯,每次变更完,都要间隔几个小时去看看监控曲线和日志,看看有没有异常的苗头,一旦发现有不对劲地方,就会立即着手排查,直到确保没有问题为止。
不过即使如此,还是不可避免的会出现问题。半夜两三点的时候,你的手机突然响起,报警语音机器人跟你说,你有一个重要的监控曲线出现异常,请查看。
然后你的血压立马升高,心跳加速,你从床上,一跃而起,打开电脑,连上公司的 VPN,立马着手排查起来。
几分钟后,QA(质量工程师)电话过来,告知你,这个故障目前已经上报到部门故障系统,目前影响的用户有 XXX 的数量,请你加快处理的速度,然后你的心跳再次加速。
半小时后,终于有了眉目,这时,你的 Leader 电话过来,询问你是怎么回事,大概还需要多长的时间才能处理完毕。待你语焉不详地回复完你的 Leader,你又开始埋头,一行行的排查故障。
一个小时后,你终于将问题定位出来,执行了故障处理方案,例如回滚新的代码,或者屏蔽某些机器等。你才终于有了喘息的时间。
(PS:这里正确的流程是,出问题后,立马回滚代码,但存储系统因为数据的关系,在没有确定原因前不太敢回滚,怕对数据有影响。)
你赶紧爬上床去,睡上 2-3 个小时,因为第二天还要早起,赶到公司,去处理故障的后遗症,数据损坏和数据错乱。
那个时期,我们写代码都是特别小心的,变更,更是极度的谨慎。所以使得自己对代码变更有了一种焦虑和恐惧的心理。至少在那时候,写代码不是一件轻松的事情。
这个事情,我现在回过头来看。你可以认为有一部分是人的原因,但仔细的想想,写代码不出 Bug ,几乎也是极难做到,所以这里在研发流程上,其实也是有缺失的。
前期因为业务发展太快,团队的整体人力跟不上,所以,一开始很多流程,都是很原始的,那时候,是想做但客观条件不允许。
后来,业务稳定了,流程就规范了不少。比如引入了 Coverity 的代码检查,也推行过测试用例覆盖,持续集成等。
但最终,并不是所有的流程都延续了下去。比如,代码测试用例覆盖,有的团队到后面就放弃了,需求变化太快,测试用例成本太高。
Coverity 倒是自动化程度高,没啥人力投入,执行了下来。但我相信不是所有的公司,所有的团队,都会有这种规范的流程。一个是研发流程成熟度建设的问题,但除此,还有成本,业务迭代速度。
在互联网,产品高速迭代的时候,产品都还没有存活下来,成熟流程就更不太可能有了。
综合来看,一种规范,但相对较重的研发流程的建立,应该也是根据具体情况而定的。
需要考虑产品的形态,产品迭代的速度,团队的人力预算成本,产品的生命周期等等。
当然,无论怎么说,反正这不是个人可以决定的事情,如果你所在的团队有完善的研发流程,那是最好的事情,但如果没有那么完善,自己又能够做些什么呢?
我的经验来看,以下的一些措施,对于个人而言也有不错的效果。
测试驱动的开发(TDD)
有段时间,因为业务高速发展,对性能的要求不断提高,存储模型也跟随着不断迭代改进,所以那段时间的代码修改是比较多的,那个时间的焦虑感也特别重。
我记得是在 《重构:改善既有代码的设计》中了解到 TDD 的。简单来说, 就是先构建测试用例,再开始写你的功能代码。
在设计测试用例之前,你需要先定义好模块对外的接口,包括接口的种类,参数,返回值等。
然后,你针对定义好的接口,编写测试用例。这过程中,你可能会发现接口设计不合理的地方,也需要随着修改。
待你测试用例写完,基本你的接口也被修改的比较好了,所以 TDD 还能改善你的接口设计。
后续再为每个接口实现特定的代码逻辑。我当时将这种方法运用到了一个磁盘存储引擎中,发现相当不错。我特地花了一周左右的时间写测试用例。
后面,每天实现部分的功能后,都立马跑测试用例,每次跑完通过,你的心里都有稳的一 B 的感觉。有种妈妈再也不担心我写的代码有 Bug,被老板叼,导致扣工资了。
因为有了完善的测试用例,而且随着你测试用例不断的增加和覆盖,你的信心会越来越足,焦虑自然减少了很多。
不过这种方式,比较适合底层的系统和核心稳定的系统。对于需求多变的系统,构建测试用例的人力付出太大,而且需求一变,已有的测试用例可能失效,导致投入产出比不够高。
灰度发布
简单来说,就是一个特性要上线的时候,不是一下就开放给所有的用户使用。有点像产品上的内测,只不过是用在技术上。
比如我新增加了一个产品需求,例如就微信里面的 “看一看”入口,不是一开始就对所有用户开放的。
首先会上线一个新的客户端版本,代码逻辑已经预埋,但设计了一个开关,对所有用户都是关闭的。
前期,可能会找个千分之一,甚至万分之一的用户(随机或者特定的用户群体),让他们使用。
在这过程中,收集各种 Log,监控,用户的反馈,来确认和 Fix 系统存在的各种问题。
一般经过两三周后,如果没有大问题,就会进一步的放开使用的用户。比如变成百分之一,十分之一,一直迭代,直至覆盖全部用户。
灰度这个思想,在互联网是特别常用的。客户端,前端,后台都可以使用。比如后台,上线一个新修改后,也不是一下就开放给所有用户。
而是按照某种规则,例如以 QQ 用户为例,可能是这种规则:计算 Hash 值(QQ 号) % 1000 <= 灰度用户的比例(取 0 --- 1000)。
放量的最小力度就是千分之一,被灰度到的用户,看到新功能,没灰度到的用户不受影响。
这招用在新功能,用在系统优化,代码重构上都很不错。付出的额外成本不大,有的公司有自研的灰度系统,那最好。
没有的话,在重大且没有把握的功能上,自己加上几行灰度控制代码也不难。
监控和 Log
监控和 Log 不是什么新鲜的东西。工作第一年,我们的技术总监在一次会议上跟我们说:你写完的代码是死的,只有在线上跑的代码是活的。
监控和 Log(特别是监控),就像是你代码的体征信息,随时反应着你代码在实际环境中的运行情况,要高度的重视。
这段话,在后面,我深有感触。通过完善设计的监控和 Log,预先发现了很多的问题,也避免了很多,或代码 Bug,或系统设计缺陷导致重大故障。
后面,监控和 Log 的设计,也成了我们方案设计的一部分。一般都会在方案最后,加上必须的监控的点和 Log 点,例如请求数,成功数,失败数,各种异常数,极端逻辑执行次数等等。
你应该要意识到监控和 Log 的重要性,而且应该要花时间特别地设计。经过良好设计的监控和 Log,能发挥的价值,是那种凭感觉随便加的监控 Log 不可比拟的。
双写,双读验证
这招,新业务代码用的不多。更多用于基础系统或者核心系统的优化和重构上面。
而且有前置条件,需要一个操作可以重复执行(例如只读操作和幂等的数据操作)。
简单来说,就是将新旧代码,划分为两个流程(两个接口),上线到实际环境,然后在同个模块里面调用。
一个请求进来后,两个流程分别执行一次,逐字节做对比(例如 Memcmp)新旧流程的结果。新流程的结果只用于对比,返回得依旧是旧流程的结果,所以不影响线上业务。
如果对比失败,就可能存在异常,要查找并解决,在实际环境跑了几天后,都没问题,就可以采用灰度的方式,进一步放量。不过,一般业务不常使用,在基础系统上使用比较多,这里就不展开了。
另外,对于客户端,还有热补丁机制,客户端 Log 收集系统等。不过这种需要的开发量比较大,一个人不一定可以搞定,可能需要有个小团队来完成。
最后
软件工程是个庞大的话题,我也没能力论述这么大的话题。这里给大家讲了个以前的故事,并且分享了我常用的一些低成本,但可以提高线上代码质量的方法,给大家参考参考。大家有好的做法,也欢迎在留言里分享出来。