ChengXuYuan.com
程序员的职场第一站

技术债:the good, the bad, and the tao

安姐最近一篇文章『关于技术债务』阐述了如何避免和处理技术债(techdebt),值得一读。也许是年关将至,出来混,欠的账都要还了,我正好也打算写写技术债,干脆趁热打铁,也来一篇。本文从另外一个视角看技术债。

首先,什么是技术债?通常意义上的技术债是指我们在开发产品或者功能过程中,快速(往往伴随着混乱和各种限制)地在时间和资源受限的情况下完成正常情况下无法完成的工作时引入的技术问题。这些问题可能会导致将来引入新的功能变得很困难,或者开发出来的功能很快到达容量上的瓶颈。这种由于时间和资源受限的情况下不得不牺牲质量引入的技术债是最主要的一种技术债。

第二种技术债是因为开发团队未能事先进行合理的设计,导致架构混乱或者过度设计,从而引发在未来时刻爆发的技术问题。

第三种技术债是随着时间的推移,一个原本设计良好的架构不断叠加第一种技术债,导致代码是casebycase堆叠起来的。老的case不敢删除,新的case不断增长,使得代码膨胀到一个无法控制的规模。

还有一种技术债是因为开发团队的能力和水平有限,写出来的代码质量低下,从而引发在未来时刻爆发的技术问题。

大部分软件开发团队同时背负以上几种技术债。所有的技术债最终不得不被架构更加合理,接口更加清晰,效率更优,容量更大的方案取代,也就是所谓的还债。

注意技术债都是形容完成了工作但留下了遗憾或者隐患,完不成工作的那种叫技术败(techfailure),不在本文讨论当中。

The good

债务是人类经济活动中最伟大的发明之一,其重要性可以和纸币等量齐观。可以负责任地说,有经济的地方,就有债务。作为锄禾日当午的农民大军中的一员,我们码农最熟悉的债务就是房贷。房贷可以让我们在当下现金流短缺的情况下购买到我们将来才能负担得起的房子。如果没有房贷,我们是无法过上居者有其屋的生活的。

技术债既然被称之为一种债务,虽有债务的缺点,但也连带债务的好处。序员们并非活在乌托邦中,可以无所顾忌地追求极致的架构;我们生活在一个实际的,血淋淋的商业世界里。销售要某个产品和别人对标打单,市场要编制一个美丽的五彩缤纷的故事来应付发布,客户要求在限期之内完成某个他们自己也不知道什么时候才使用的功能(通常只是为了彰显甲方那种「我所说的,你都要照做」的气势),工程师就必须在限期之内完成。完不成,产品卖不出去,市场推广不开,客户和你一拍两散。还房贷?娃的奶粉钱?想都别想。所以这种时刻,引入技术债是不得已的聪明的做法。

这种做法在创业圈里有一个美丽冻人的名字:MVP(minimalviableproduct)。技术圈里已经快被人遗忘的,令人尊敬的老司机盖子同学就是个中好手,basic解释器如此,DOS系统也是如此。如果他老人家不付出技术债,恐怕还没拿到第一桶金就挂了,软件世界的版图也就可能没微软什么事了。

另一个经典的同时也令人扼腕的「举债经营」成功,不愿负债失败的例子是mongodb/rethinkdb。做为一个database,mongodb在其前两个版本的打法是骇人听闻的:一个数据库竟然靠mmap来提高效率,通过fsync来保证持久化(如果你不打开oplog的话,就只能听天由命了),然后还好意思发布1.0一路到2.x——有木有搞错!技术老司机们纷纷看不下去了,各种质疑雄文层出不穷,比如说经典的callmemaybe(请自行googlecallmemaybemongodb)。可以看得出mongodb把技术债用到了极致,而且是用在了对于其他软件公司性命攸关的部分:数据库。而rethinkdb则走向了另一个极端,为了技术正确,一路闷头苦练内功。两个同样在2009年发布第一个版本的产品,2011年mongodb就已经有人在production里使用了,而直到2015年,rethinkdb才遮遮掩掩地宣称它们productionready。结果是rethinkdb错失了NoSQL的红利期,赚不到也筹不到足够的钱维持其运营,不得不解散团队。而mongodb在购买了wiredtiger引擎后,基本解决了最让人诟病的技术债,在软件服务的路上走得还算顺畅。

所以,不要怕引入技术债,相反,要敢于引入技术债,举债经营,尤其是新的产品和功能未能证实其价值的时候。通过引入技术债,我们以软件日后腾挪的空间换取了发展的时间。从这个意义上讲,MVP不仅仅适用于创业公司,也适用于任何大小的公司。

The bad

债务的缺点自然是利息。房贷固然让你圆了一个安居乐业的梦,但经济不景气时可能导致的失业一下子会让你失去了偿债能力。银行会收了你的房子,残酷无情地敲碎你「人生蒸蒸日上」的黄粱美梦,让一切归零。所以有了房给贷这样的债务,一定要关注短期偿债能力,把流动比率或者速动比率控制在一个合理的范围内,同时要握有足够的自由现金流应对不时之需。

对于公司的运营来说,短期负债过多会影响正常运营甚至导致公司没有偿债能力从而不得不破产或者进行债务重组。对于软件开发而言,技术债所带来的利息是新功能更长的开发周期,或者老功能很快触碰天花板。这会影响公司接下来的发展。如果累积一段时间技术债的利息无法清偿,公司可能会技术破产,进而导致运营上的破产。twitter在早期开发时,MVP选用了rails。快速的开发能力带来了快速的产品验证,然而rails的低效使得twitter很快在技术上触及了天花板(尽管twitter针对其做了无数优化):2007-2008年,twitter动不动就挂了,并且一度挂了三天(https://techcrunch.com/2008/04/22/twitter-may-not-have-to-care-about-uptime-any-longer/)。后来在技术主管换血之后,它们痛定思痛,大刀阔斧做了债务重组,摒弃rails,拥抱java生态圈,用scala重写很多核心服务,终于把服务稳定下来。

技术债一个很要命的问题是债务的叠加。生活中我们很少叠加债务,如果你有了房贷,你会减少各种开销,避免同时背上那些APR极高的信用卡债务。软件开发中我们却很容易叠加债务。这些叠加的技术债会使利息指数增长,从而反噬我们。

技术债还有一个很严重的问题是backwardcompatibility。软件的有些接口设计上的缺陷被使用者当成了功能去使用,使用的范围之广以至于开发者在新版本中无法还债,只能被动地一路保有这样的债务。windows系统在其演进过程中,有大量的缺陷和设计失误被开发者当成了功能,使得微软无法改变这些不合理的接口。这是历史的教训,我们需要小心这类技术债。一个很重要的原则是对外的接口(API,SDK等)一定要小心设计,如果要负债,让接口背后的实现去负债。

The tao

技术债并非洪水猛兽,我们要要合理控制,让其发挥债务的优势。处理之道:

1)拥抱MVP。先解决温饱问题,再考虑还债。

2)把技术债外包出去,套句时髦的话,就是techdebtasaservice。infrastructure交给aws/azure,monitor交给datadog/newrelic,search交给elk/algolia,analytics交给mixpanel/神策数据(好友的公司,免费硬广),支付交给stripe/微信/支付宝。。。将自己的非核心技术问题/难题转嫁给更合适的团队。不过这种债务除了需要支付月度的服务费作为利息外,还有另一种隐性的,代价不菲的利息:vendorlock-in。

3)雇佣你所能获得的最优秀的人,给予她们你所能给予的,最能发挥她们能力的权限。由人引发的技术债是最让人痛心疾首无语凝噎的技术债。这条不解释。

4)拥抱匡威定律。你的组织架构决定了你的代码结构。想要快速独立的功能交付能力,你要有包含所有角色,拥有直接决策权的端到端的功能团队,而不是开发,测试,运维等彼此独立,组织上汇报给不同VP,优先级完全不在一个调子上,各自为战的团队;想要使用microservice,组织结构上就要打造彼此平行的serviceteam。很多技术债的出现是由于技术方向和组织结构不协调导致的。

5)在实现上可以多些负债,在接口上尽量减少负债。你有没有问过自己,除了从别人那里学习撰写代码的艺术和教育新人的目的外,为什么我们需要codereview?为什么我要关心别人的代码写成什么样子?我要关心memcpy的实现么?我要关心twillio究竟在代码级怎么把短信发给我的用户么?我不关心。我只关心interface和SLA。而软件中重要的恰恰是interface。为什么我们没有interfacereview?为什么如此重要的interfacereview要混在并不那么重要的codereview中?code可以被抛弃,被重写,但interface一旦确认,修改的代价会很大,尤其对外的interface。

说句题外话。如果我们只做interfacereview,不做codereview,有什么严重后果么?貌似没有——如果code写的不好,扔了重写就成。如果我们以这种思维打造软件,我们还需要整个开发团队使用一种或者少数集中语言么?我们还需要为每个工程师设置peerbackup么?一旦一个工程师离职,他的工作在需要的时候能够被重写,是不是就足够了?对于这一点,不懂软件的Bezos有着远见卓识——他在02年提出了一系列苛刻的关于serviceinstance的构想,并强制研发团队照做。见我的文章:拥抱约束

6)意识到软件是个有新陈代谢的有机体,并围绕这个思想来打造软件。代码是需要新陈代谢的,这意味着新的代码不断加入,旧的代码要不断删除。生物体里细胞坏了会重新造,组织死了会重新生成。生物体不会试图「修复」生老病死的细胞,只会将其杀死并代谢掉,同时重新生成功能一样的细胞。我们写代码时也要使每个部分保持独立(像一个个完整的细胞或者组织),留有日后自己或者别人将其完全删除重写的余地。这样,当我们主动或者被动引入技术债时,便心中不慌了——因为我们知道,我们保留了删除和重写的权利。

为什么解决技术债时不是考虑重构,而是考虑重写?重构技术债太多的代码有重构的代价——能重构代码,你得先把前任(或者几个月前的自己)的古怪逻辑搞清楚,还得小心不要让自己引入问题。很多时候,技术债累积过多的代码叠加了难以伸缩和扩充的问题,与其小心翼翼,瞻前顾后地重构不如重写来的干脆,也避免了半吊子工程。当然,如果能达到每个功能或者组建可以不费太大代价重写的理想,那么要选用以此为核心的架构,比如说”microservice”,并且制定相关的规则,比如说,一个service不超过一个程序员一周的工作量,以便于随时重写。

重写代码而非重构是足够疯狂的想法,但我们身边有个最好的例子:unix。有人反对unix是一系列microservice组成的系统么?估计没有。我们应该把我们打造的系统组织成围绕着一个核心的一个个ls/grep/awk/vim…。vim作为一个伟大的软件,其代码质量实在不敢让人恭维,所以有人干脆另起炉灶,照着vim的接口做了一套neovim。

先写这么多。回头我就软件的新陈代谢,生老病死再写篇文章。

作者:陈天

文章出处:程序人生(programmer_life)

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址