三年前旧代码的重构、总结与反思

最近在维护一个三年前的旧代码,用的是laravel框架。

从某些方面来讲,这个代码算是比较标准为了实现“在规定的时间内完成相关功能”,同时“程序员水平不高”、“经过大量优化”之后,变地特别烂的。但是其中,程序员的水平和态度是最主要的,其他相对于而言都是次要的。

当然,我就是那几个程序员之一,所以我可以放心大胆地说自己的坏话

另外本文会多次提到语言间的对比,当然本文的目的并不在此。

框架之争

无论是当年来看还是现在来看,Laravel框架思想还是结构,都算得上是“Modern PHP”的典范。

Laravel之于php,就相当于springboot之于java。

Laravel针对http请求引入了中间件,稍微配置一下便可以很方便地使用类似servlet的拦截器,功能还远比servlet强大。

针对ORM类的需求,自创了eloquent框架,使用的简洁性上也算得上是一流。

至于安装、配置、部署、依赖等等,laravel也都提供了完全通用的方案,这就很可怕了。

可以这么说,如果我们完全按照laravel的架构,完全遵照laravel的文档,写出来的代码即便不会很优雅,但是也绝对不会特别坑。

当然,laravel缺点也是很明显的。最重要的一个缺点,性能。

作为PHP框架,laravel的性能无意识特别拖后腿的地方。我们这里已经没有详细数据,但是大概的数据我们可以提供一下:一个最简单的路由,里面只有Redis::set这一个操作,并且没有任何中间件或者计算逻辑,24C64G的机器,只能支撑到大约300+QPS,即便开了opcache等也没有质的提升。至于php7,第一个7.0版本是发布在15年12月,项目上线四个月后,不要说还要等laravel支持PHP7,更不要说php7也满足不了性能需求。

这也就是我们第一版代码就已经是两种语言异构的原因。

PHP部分用于处理正常业务请求,Go部分用于处理心跳等其他请求。

技术和能力

在后续的几年内我们也在反思这个问题。

如果我们当初做的是采用tcp协议进行传输数据,服务端也用Go,那么我们还可以做很多“看上去很酷”的事情,例如:我们可以实现并到处宣扬C10K、C100K,可以到处宣扬实现了十万百万MPS(Message per second),可以将所有的任务结果流式传输到服务端,可以做到同步返回结果,可以让用户体验上更好。

但是如果这样做的话,无状态、流量、日志存储等便是要考虑的新问题。这可能会把我们培养成技术流,但是也可能把我们的项目变成新的“技术瘤”。

现在回想起当年,更重要的问题在于,我们确定当时参与者没有人敢提出这种方案,更没有人能驾驭住这个方案

维护成本

维护成本无疑是后期最大的成本。

早期我们依赖supervisord,在最早期我们经历过supervisord和docker的supervisord冲突的故障,后期我们也经历过其他项目也依赖supervisord、因为配置原因导致其他项目被停止的故障。故障么,自己的锅自己背,也没什么好说的。

但是其他的维护成本是比较多的。

你所能想到的,例如agent的保活,算是比较常见的问题,几十上百个agent总会有一两个出问题,这个我们也都习以为常了,甚至自己做一做自动修复也能解决问题。

你所不能想到的,有些人将环境相关的任务也给算到你的头上。有些人会因为“你这个系统怎么在这个环境出现了这个问题”,查了半天,对方端口没有打开。

这点在我们中间件相关的项目比较常见。通常他们会上来就问这个中间件怎么出这种问题了,实际上呢,让他们把堆栈完整地发出来之后,告诉他们“caused by里写明了,unknown host,就是你的XXX域名没配嘛”。每天都有四五个人问这种问题,也会给答疑方带来很大的压力。

在我们的其他工作中,也在不断地探索如何减少教育成本,FAQ、培训似乎收效都不高。

假装喷人似乎有效,例如“你XX的是不是开远程调试了(此处请脑补意大利炮)”,但是这种操作也不能每天都能做的。

如何降低教育成本、让开发者自己拥有自己解决问题的能力,一直是我们工作的重点。但是目前看起来,我们在这方面收效甚微。

需求、功能、BUG和变迁

这个项目我们也算是顶住了很大压力,没有接新的需求,也没有再去增加新的功能。

我们当时的理由是“他仅仅是个任务的服务端”,“你只要如此这般写这个任务便可以实现这个功能了”。

但是后续系统变迁是我们当时所没有考虑的。

后续我们有了一个新的任务管理界面,有了新的统一登录接口,CMDB的接口也几经变化。

最终这个系统只剩下API每天任劳任怨地工作着。

按理来说没人访问的接口和界面就应该直接下掉。可是至少这也是亲生的bug,我也心软,没办法下手。

根据其他系统的经验,有时候我们是不得不添加部分功能的,而这部分功能我们可能会引入很多问题。

举个例子,某些人吐槽为什么大公司的代码如此之烂,一个项目中httpclient就有四个版本。

这件事情可以理解,例如早期可能只用了HttpURLConnection进行get请求,中期为了支撑post请求,支持参数,支持超时,分别对HTTPClient了封装,后期因为引入JWTs,又封装了一次。代码冗余度变高,但是既然“系统跑得很好”,也就“没有精简的必要”。

可以理解,但是不代表可以接受。

代码冗余一直是内部项目重构时常见的问题,通常表现为为了不影响原有代码的执行,把现有的代码拷贝一份,换个名称,修改一下交给新接口来调用。

Java等静态语言合并冗余代码比较简单,编译成功即可保证大部分功能可用。但是php等动态语言我们则不敢这么做。PHP做不到“编译成功便保证基本没问题”。

就这个例子来看,一方面是开发对HttpClient的认知不足,另一方面则是开发对代码的抽象能力不够,也未留下适当的接口满足未来的需求,才会出现“一个项目中httpclient就有四个版本”的噩梦。

有些内部系统也会和早期的我们一样,首先为了做出成果,然后才是追求更高层次。

但是这并不是一个做技术的人应该有的态度。

优秀程序员的价值,不在于其所掌握的几招屠龙之术,而是在细节中见真著。

如果我们可以一次把事情做对,并且做好,在允许的范围内尽可能追求卓越,为什么不去做呢?

成果是要有的,但是一个做技术的人,应该有对职业的自我尊重、对自我价值的追求和对卓越的理解和渴求。

完美有多远?不知道,但是我以后肯定会多走几步。

单元测试和语言

并发控制实际上是个蛋疼的问题,夸张一点说,当时的PHP并不能特别轻松地实现并发,甚至不能实现并发。我们目前的服务端实际上只是做了任务转发,采用了一些取巧的方法实现并发(curl_multi),但是我们并不能实现并发控制等功能。至于说多线程(pthreads)和多进程(pcntl)的方案,实测下来也并不稳定,测试阶段便会产生coredump。

并且经过多次调优,我们也最终解决了curl_multi的性能问题,可以达到成千上万的并发,并且性能还算可以。

现在复盘一下,如果用的是Go的话,可以很轻松地用5-6行代码增加并发控制。Go语言自身性能不错,并发也很好。

Go语言的功能之一就是自带单元测试。这点和maven差不多,但是Go是少数几个语言层提供测试工具链的语言之一。

相比于动态语言,静态语言的优势之一便是安全。

可以稍微夸张点讲,静态语言一旦编译成功,除非有RuntimeExcetion,不然基本不会出问题。

而PHP这种动态类型的语言,就比较蛋疼了:不仅写的时候可能会有问题,很多IDE也无法意识到你到底是不是写了个bug,甚至过几年回来阅读代码,即便是自己参与过的项目,读起来代码也很蛋疼。PHP也意识到了这一点,从PHP7引入了类型声明,也能缓解这个问题。

用Go语言之前,我的习惯是不写单元测试。用了Go语言之后,我开始养成对所有函数都写单元测试的习惯。

我们本文中提到了很多次Go语言,实际上语言对项目的影响并不大,真正起主导作用的,还是人。

规范

运维规范对本项目的影响并不大,主要是开发规范。

后续的工作中,我不止一次·告诫业务开发,我们目前所有的规范,无论是运维规范、数据库开发规范,或者任何代码开发规范,都是我们一次一次地踩坑铺出来的路。

如果当初我们有数据库开发规范的话,表结构也不会这么坑。就像laravel框架一样,我们按照规范来写,不至于让代码上升一侧层次,但是也不至于让代码烂出水平。

在此我们强烈对小公司和开发人员推荐《阿里巴巴Java开发手册》,不仅有开发规范、还有表结构规范,无论是对开发或是对公司都有好处。

语言对项目的影响并不大,真正起主导作用的,还是人。

如果人的平均素质并不能达到优秀的话,那么完善的流程和规范将能很大程度上影响一个项目的质量。

总结

教育成本是后期维护的主要成本之一,我们也一直尝试赋予开发者自己解决问题的能力,虽然很难。

人的素质无疑能直接决定一个项目的质量。

当然,对于普通公司、新人这种平均素质达不到优秀的情况,完善的流程和规范将很大程度上保障一个项目的质量。

静态语言、单元测试等手段是保障项目稳健性的重要方式。

    原文作者:大舒
    原文地址: https://segmentfault.com/a/1190000016179093
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞