Skip to content

单体系统时代:应用最广泛的架构风格

Published: at 11:45:18

为什么单体架构能够在相当长的时间里成为软件架构的主流风格?

大型单体系统

单体架构是绝大部分软件开发者都学习和实践过的一种软件架构,很多介绍微服务的图书和技术资料中,也常常会把这种架构形式的应用称作“巨石系统”(Monolithic Application)。

整个软件架构演进的历史进程里,单体架构是出现时间最早、应用范围最广、使用人数最多、统治历史最长的一种架构风格。但“单体”这个名称,却是从微服务开始流行之后,才“事后追认”所形成的概念。在这之前,并没有多少人会把“单体”看成一种架构。

如果你去查找软件架构的开发资料,可以轻轻松松找到很多以微服务为主题的图书和文章,但却很难能找到专门教我们怎么开发单体系统的任何形式的材料。

这一方面体现了单体架构本身的简单性;另一方面也体现出,在相当长的时间里,我们都已经习惯了,软件架构就应该是单体这种样子的。

那在剖析单体架构之前呢,我们有必要先搞清楚一个思维误区,那就是单体架构是落后的系统架构风格,最终会被微服务所取代。

因为在许多微服务的研究资料里,单体系统往往是以“反派角色”的身份登场的,比如著名的微服务入门书《微服务架构设计模式》,第一章的名字就是“逃离单体的地狱”。而这些材料所讲的单体系统,其实都有一个没有明说的隐含定语:“大型的单体系统”。

对于小型系统,也就是用单台机器就足以支撑其良好运行的系统来说,这样的单体不仅易于开发、易于测试、易于部署,而且因为各个功能、模块、方法的调用过程,都是在进程内调用的,不会发生进程间通讯,所以程序的运行效率也要比分布式系统更高,完全不应该被贴上“反派角色”的标签。要我说的话,反倒是那些爱赶技术潮流,却不顾需求现状的微服务吹捧者更像是个反派。

进程间通讯:Inter-Process Communication,IPC。RPC 属于 IPC 的一种特例。

所以,当我们在讨论单体系统的缺陷的时候,必须基于软件的性能需求超过了单机,软件的开发人员规模明显超过了“2 Pizza Teams”范畴的前提下,这样才有讨论的价值。那么,在咱们课程后续讨论中,我所说的单体,都应该是特指的“大型的单体系统”。

可拆分的单体系统

尽管“Monolithic”这个词语本身的意思“巨石”,确实是带有一些“不可拆分”的隐含意味,但我们也不能简单粗暴地把单体系统在维基百科上的定义“All in One Piece”,翻译成“铁板一块”,它其实更接近于自给自足(Self-Contained)的含义。

单体系统 Monolith means composed all in one piece. The Monolithic application describes a single-tiered software application in which different components combined into a single program from a single platform. —— Monolithic Application,Wikipedia

当然了,这种“铁板一块”的译法也不全是段子。我相信肯定有一部分人说起单体架构、巨石系统的缺点,脑海中闪过的第一印象就是“不可拆分”,难以扩展,所以它才不能支撑起越来越大的软件规模。这种想法我觉得其实是有失偏颇的,至少不完整。

我为什么会这么判断呢?

因为从纵向角度来看,在现代信息系统中,我从来没有见到过实际的生产环境里,有哪个大型的系统是完全不分层的。

**分层架构(Layered Architecture)**已经是现在几乎所有的信息系统建设中,都普遍认可、普遍采用的软件设计方法了。无论是单体还是微服务,或者是其他架构风格,都会对代码进行纵向拆分,收到的外部请求会在各层之间,以不同形式的数据结构进行流转传递,在触及到最末端的数据库后依次返回响应。

那么,对于单体架构来说,在这个意义上的“可拆分”,单体其实完全不会展露出丝毫的弱势,反而还可能因为更容易开发、部署、测试而更加便捷。比如说,当前市面上所有主流的 IDE,如 Intellij IDEA、Eclipse 等,都对单体架构最为友好。IDE 提供的代码分析、重构能力,以及对编译结果的自动化部署和调试能力,都是主要面向单体架构而设计的。

2d270f8e1a96f8f3764d05127bc1d6aa

而在横向角度的“可拆分”上,单体架构也可以支持按照技术、功能、职责等角度,把软件拆分为各种模块,以便重用和团队管理。

实际上,单体系统并不意味着就只能有一个整体的程序封装形式,如果有需要,它完全可以由多个 JAR、WAR、DLL、Assembly 或者其他模块格式来构成。

使是从**横向扩展(Scale Horizontally)**的角度来衡量,如果我们要在负载均衡器之后,同时部署若干个单体系统的副本,以达到分摊流量压力的效果,那么基于单体架构,也是轻而易举就可以实现的。

非独立的单体

不过,在“拆分”这方面,单体系统的真正缺陷实际上并不在于要如何拆分,而在于拆分之后,它会存在隔离与自治能力上的欠缺。

在单体架构中,所有的代码都运行在同一个进程空间之内,所有模块、方法的调用也都不需要考虑网络分区、对象复制这些麻烦事儿,也不担心因为数据交换而造成性能的损失。可是,在获得了进程内调用的简单、高效这些好处的同时,也就意味着,如果在单体架构中,有任何一部分的代码出现了缺陷,过度消耗进程空间内的公共资源,那所造成的影响就是全局性的、难以隔离的。

我们要怎么理解这个问题呢?

首先,一旦架构中出现了内存泄漏、线程爆炸、阻塞、死循环等问题,就都将会影响到整个程序的运行,而不仅仅是某一个功能、模块本身的正常运作;而如果消耗的是某些更高层次的公共资源,比如端口占用过多或者数据库连接池泄漏,还将会波及到整台机器,甚至是集群中其他单体副本的正常工作。

此外,同样是因为所有代码都共享着同一个进程空间,如果代码无法隔离,那也就意味着,我们无法做到单独停止、更新、升级某一部分代码,因为不可能有“停掉半个进程,重启 1/4 个进程”这样不合逻辑的操作。所以,从动态可维护性的角度来说,单体系统也是有所不足的,对于程序升级、修改缺陷这样的工作,我们往往需要制定专门的停机更新计划,而且做灰度发布也相对会更加复杂。

补充:这里我说的“代码无法隔离,无法做到单独停止、更新……”,其实严谨来说还是有办法的,比如可以使用 OSGi 这种运行时模块化框架,只是会很别扭、很复杂。

这里就涉及到一个需要权衡的问题:如果说共享同一进程获得简单、高效这些优势的代价,是损失了各个功能模块的自治、隔离能力,那这两者孰轻孰重呢?这个问题很有代表性,我们还可以换个角度思考一下,它的潜台词其实是在比较微服务、单体架构哪种更好用、优秀?

在我看来,“好用和优秀”不一定是绝对的。

我们看一个例子吧。

比如说,沃尔玛将超市分为仓储部、采购部、安保部、库存管理部、巡检部、质量管理部、市场营销部,等等,来划清职责,明确边界,让管理能力可以支持企业的成长规模;但如果你家楼下开的小卖部,爸、妈加儿子,再算上看家的中华田园犬小黄,一共也就只有四名员工,也去追求“先进管理”,来划分仓储部、采购部、库存管理部……的话,那纯粹是给自己找麻烦。

在单体架构下,哪怕是信息系统中两个毫无关联的子系统,我们也都必须部署到一起。当系统规模小的时候,这是个优势;但当系统规模扩大、程序需要修改的时候,相应的部署成本、技术升级时的迁移成本,都会变得非常高。

就拿沃尔玛例子来说,也就是当公司规模比较小的时候,让安保部和质检部两个不相干的部门在同一栋大楼中办公,算是节约资源。但当公司的人数增加了,办公室已经变得拥挤不堪的时候,我们也最多只能在楼顶加盖新楼层(相当于增强硬件性能),而不能让安保、质检分开地方办公,这才是缺陷所在。

另外,由于隔离能力的缺失,除了会带来难以阻断错误传播、不便于动态更新程序的问题,还会给带来难以技术异构等困难。

技术异构:它的意思是说允许系统的每个模块,自由选择不一样的程序语言、不一样的编程框架等技术栈去实现。单体系统的技术栈异构不是一定做不到,比如 JNI 就可以让 Java 混用 C/C++,但是这也是很麻烦的事,是迫不得已下的选择。

不过,在我看来,我们提到的这些问题,还不是我们今天以微服务去代替单体系统的根本原因。我认为最根本的原因是:单体系统并不兼容“Phoenix”的特性

单体这种架构风格,潜在的观念是希望系统的每一个部件,甚至每一处代码都尽量可靠,不出、少出错误,致力于构筑一个 7×24 小时不间断的可靠系统。

从“追求尽量不出错”,转变为“出错是必然”。这才是微服务架构能够挑战,并且能逐步开始代替运作了几十年的单体架构的根本驱动力。

不过,即使是为了允许程序出错,为了获得隔离、自治的能力,为了可以技术异构等目标,也并不意味着一定要依靠微服务架构。在新旧世纪之交,人们曾经探索过几种服务的拆分方法,把一个大的单体系统拆分为若干个更小的、不运行在同一个进程的独立服务,这些服务拆分的方法,后来导致了面向服务架构(Service-Oriented Architecture)的一段兴盛期,我们把它称作是“SOA 时代”。

总结

  1. 单体架构也是有优点的,比如易于分层、易于开发、易于部署测试、进程内的高效交互。
  2. 单体架构的缺点也很明显,比如系统中有任何一部分的代码出现问题,就可能影响到整个系统,造成全局性的影响。
  3. 系统规模小的时候,单体架构也是优势