噢,你的代码像一坨翔。然后呢?

1,283 阅读9分钟

Big Ball of Mud,中文名称“一坨翔”。自打我入行以来,就一直在和这一坨翔做搏斗。此处略去一千字,反正相信我,哥肯定是吃过翔的。虽然不少翔是自己拉的。有太多文章谈论这个问题了,每次满怀热情的打开,然后看到结尾要么就是告诉你要拆微服务,要么就是告诉你我这有个什么样的中间件产品,你要不要用一用。你妹啊,如果知道怎么拆,需要来看你的文章么…… 咱不多废话了,严肃起来。

Big Ball of Mud 的系统有三种死法:

  • 因为性能不满足要求而死,这个时候需要懂内核懂数据结构的架构师来好好做一下拆分。让系统更符合计算机体系结构,最大化利用硬件资源。各种新式的中间件就是在这个领域不断革命。
  • 因为业务逻辑复杂得hold不住而死,开发效率慢到爆
  • 因为软件架构不符合组织架构,导致团队间吵架吵到死的,这个时候老板重新思考什么样的组织架构才是合适的(大boss通过组织架构划分,他绝对是最终极的架构师)。

计算机体系架构因为其是确定性的,这个结构调整相对好做。我们见过很多实际的例子,通过调整代码的结构,使得 cache miss, branch prediction rate,data locality 大幅优化,而性能数倍提高。

我的梦想就是,找到一种拆分系统的原则,使得其能够和业务架构非常贴合。从而减少让焦油坑一般的厚重的业务逻辑代码也可以变得充满美感。对计算机有兴趣的青年,不应该最后都到基础架构的领域里去造轮子。Eric Evans 对于 Core Domain 的说法深得我心。

然后就开始了找赤脚大夫,抓药方的不归路。

药方一:领域模型

在那个还有 CRC 卡的年代。面向对象设计,领域模型这些是被寄予厚望的。在曾经的 javaeye 论坛上,如红小兵一般的当年的我,也是很激情澎拜地参与着模型是应该贫血还是充血的争论


大部分尝试使用领域模型的项目,好一点的只是在代码里多了一个model目录,倒没有付出什么成本。差一点的是把整个数据表重新定义了一个xxxBO的对象,然后每次都要多一次对象的字段拷贝。逻辑写来写去只看见增删改查,哪里有什么领域模型可言?

领域模型说穿了就是一个AggregateRoot,就说要维护一个model的概念完整性(conceptual integrity)。比如一个账单上的账要是平的,某个条目上多了,另外一个条目就要少了。这些业务上的恒定规则(invariant)需要被封装到对象内部被保护起来,以免数据出现逻辑上的错乱(和并发无关)。有多少系统有复杂的业务规则需要这么封装的?其实很少。

面向对象流派发展到后来又出现了 qi4j (Qi4j Community)以及 DCI (Lean Software Architecture)。概念其实都是一样的,一个对象来承担不同上下文的职责是会膨胀的,完全没有必要把参与不同业务流程的不同职责,强行塞到同一个对象里。我们应该要把对象按照不同context,把逻辑拆出来。但是等 James Coplien 这些大牛们想明白的时候,微服务已经主宰世界了。在微服务的世界里,服务的拆分代替了类的设计,变成了主要的建模工具。至于一个服务自身是贫血还是充血的,已经没有人在乎了。

药方二:所谓服务化改造

最常见的药方长这个样子

这个药方在不同时期,被不同的大夫,以不同的名字开过。还记得当年的 .NET Web Service,SOA 吗?把各种 RPC 技术往DB前面一挡,咱就服务化了。

我一直就纳闷了,你一个远程调用的 DAO 有什么贡献?业务代码里写 mysql.xxx 和写 rpc.xxx 有本质区别?当然数据服务可以处理分库分表,缓存同步等问题。但是那些本质上都是非功能性需求,是一个业务无关的轮子。对于业务代码的贡献就是把非业务的逻辑给剥离出去了而已。

药方三:企业服务总线

比卖RPC方案(WS-xxx 标准当年也养活了不少人那)更可恶的厂商,是那些兜售 ESB 的厂商。也许这些所谓的流程引擎,可以在 OA,运维部署工具 等特定领域减少开发工作量。打着提高开发效率的做一个平台,来托管业务逻辑的行为是可耻的。Udi Dahan 的一个评价特别静精辟,这些做得好,最多就是一个VB6,做得不好就是场灾难

曾经自己拉一坨这样的翔,然后含着泪喂给伙伴们吃完之后,从此看见 BPM/ESB 这样的字眼就特别紧张。构造一个平台,和发明一门 PHP 这样的语言差不多复杂。上面需要有调试,编辑,版本管理,版本diff等一系列的支持。大部分的公司内部系统,没有足够的资源去把这些东西给做完善来。如果不是面向特定的场景(数据处理,OA,运维),而是一个非常通用的系统,其效果只能比 PHP 裸写逻辑还要糟糕。

安利一下,世界上最好的流程调度和编排工具:PHP,没有之一。

药方四:异步化

另外一个流派的口头禅是耦合。你看,RPC是邪恶的。服务之间调用耦合太严重了。我们需要引入队列来解耦。Fred George 同学甚至把这种通过消息异步解耦的系统架构推上了神坛的高度。

看架构图,我们都是兴奋的。这种太好了,我业务都不用感知到这些下游服务的存在了。你们自己去订阅我的消息队列就好了。但是一旦用到实践中,就发现你根本没法直接用起来。常见的借口是这样的:

  • 消息处理的可靠性:kafka 后面最常见的业务是什么?其实是一些看趋势,做智能分析的业务。如果你不用在乎少几条数据没处理,大可以用这种异步架构。如果你要出财务报表?那还是老老实实用DB吧。
  • 消息处理的及时性:你别把 kafka 用到主流程里。这个消息要是被延迟了,会死人的。RPC 还是“可靠”一点。
  • 业务本来就是同步的:我是一个手机 app,我的端的交互接口就是需要在操作了之后,同步得到服务甲乙丙N个服务的处理结果。没有这些数据,操作完了的界面上就看不到剩余配额,看不到账单,看不到xxx。你 mq 能给我返回值么?我擦……

当然做为一个理想追求的你来说,觉得这些都不是问题:

  • 消息处理的可靠性:消费的时候如果失败了不提交offset就行了嘛。挂了会从上次失败的地方重试的。最多重,不可能丢的。再说了,还可以实时对账,隔天对账嘛。
  • 消息处理的及时性:就好像 RPC 服务不会挂一样。异步无非就是多了 mq 这一跳而已嘛。我拍胸脯,保证不挂,行不行?我给你做压测,保证p99的延迟低于xyz毫秒。
  • 业务本来就是同步的:doXXX() 这样的接口就不应该返回具体的界面数据。应该是doXXX()给个成功和失败,然后 getX(),getY(),getZ()。如果消息还在异步处理中,大不了显示的counter还没有+1,或者我就干脆写一个“还在处理中”就好了嘛。

世界上的人都是你这样有理想的人该有多好哇。不过相信我,大部分同学会认同,异步化没有帮他们解决什么问题,反而搞出一堆麻烦事情来。你说让他们去找产品,把需求改成异步的?扯淡吧……

药方五:Event Sourcing

以 Greg Young 同学为代表的大夫,最喜欢的就是跟你说“我这有一个蓝色的小药丸,你吃不吃?”。他的药丸长这个样子

你们都做错了,业务逻辑要以 command/event 为中心,而不是以 state 为中心。难怪你们都写出来的是一坨翔。你看哥的这种 event handler 的模式,多么牛x。我们知道丝袜可以治疗静脉曲张。但是不代表人人都应该穿丝袜。到底 Event Sourcing 怎么就解决了复杂业务逻辑的问题?我就是不用 event sourcing,我也可以在业务代码里产生事件并写 mq 啊?

Greg Young 的说法是如果你的业务不是以 event 为中心的,那么这个 event 你怎么知道是对的呢?来来来,我们看看 accountant 都是怎么工作的

会计是从来不用橡皮擦的。只有我们的系统是以 append only 的 event 为基础的,我们才是可信的。我擦,好有道理的样子哦。

但是我相信你从激动里回过神来,尝试了三五天之后,肯定会放弃在你的生产环境里吞下这个小药丸。简单来说,就是不值得。大部分的人都不是在做高并发写入,同时又具有复杂业务逻辑的事情。大部分人的工作就是增删改查,把数据按照业务逻辑算对了而已。没有对冲基金,证券交易所这种的业务上下文,Event Sourcing 就是 over design。

事实上所有的 DB 都是 Event Sourcing 的。我们只是把这部分有挑战的工作,托管给了 Oracle 那帮聪明的家伙了而已。只有当悲观锁(事务),乐观锁(单行事务,update xxx=zzz where version=1)都玩不转了的时候。我们才值得把状态从 DB 迁移到业务代码里,自己来管理状态的并发写入。

刨去了 OLTP 部分用Event Handler的模式,Event Sourcing的另外一部分故事,不过是前面异步化方案的新瓶装旧酒而已。仍然有同样的问题,不是所有人“都愿意”被异步化的。这是一个技术问题,更一个团队合作方式的问题。

且听下回分解

下篇:噢,你的代码像一坨翔。甩锅呗!