架构设计背后的哲学

如果说软件开发的本质是不断挖掘问题领域中隐藏的错综复杂性,那么架构解决的问题就是如何管理这些复杂性。而在软件领域,最为复杂的软件实体莫过于软件操作系统。从数以千计的工程师参与开发的UNIX操作系统到Linux开源系统的成功,越来越多的人开始关注和思考UNIX技术背后隐藏的设计哲学。

UNIX设计哲学概括为一句话就是“小而专注”。可以说,微服务架构理念和UNIX设计哲学一脉相承,微服务将UNIX设计哲学中的核心准则通过概念的抽象,描述成了更加通用的架构风格和设计原则。下面,让我们跟随经典重新认识在AT&T公司诞生的UNIX操作系统和它背后的设计哲学,所谓“温故而知新”,这些经典思想能加深我们对微服务架构的认识和理解。

1、小即是美

在软件的设计开发过程中,软件系统的规模很容易膨胀,工程师喜欢将纷繁复杂的功能全部堆积在一个程序中,这样的好处是代码唾手可得。然而,根据二八法则,实际运行中的代码其实往往只占到我们代码量的20%。所以,切忌将大而全的工程作为我们的目标,相反,我们应该将功能设计成为实用且简洁的小程序。

在我的职业生涯中,经历过很多设计复杂、规模庞大的单体系统。有的是基于C++编程语言的后端大型系统,有的是使用J2EE-XML编程风格、交织着庞大功能模块的巨石型应用,这些项目通常由10人以上的研发团队负责,动辄百人/月的开发计划。这种大型单体系统在上线后,时常发生系统内存溢出、系统宕机等问题,让开发人员心惊胆战。往往一个小问题会影响整个系统的正常运行,排查解决过程需要检查整个工程的代码逻辑,开发人员疲于解决生产上线后的各种问题和Bug修复。

事实证明,我们需要将庞大、复杂的系统分解成小程序。正如UNIX系统中强调的KISS(Keep it Simple and Stupid)原则,可以说整个UNIX系统都是由数目众多的小程序组合完成各种复杂的操作系统功能的。每个小程序只完成其中一小部分功能。表面上,这些小程序都很低效,但是正是通过像搭积木一样将不同功能的小程序通过变换顺序和组合的方式,完成了各种意想不到的丰富而强大的功能。

小程序之美体现在响应变化上。通过小程序可以将这种改变控制在足够小的范围,保证不会给整个系统带来巨大的影响。

小程序之美体现在工程结构的简化和完备上。当你的程序充满了个性化的反射调用和程序员独特的编程思维痕迹,那么应该反省你的代码是不是已经脱离了小程序应有的代码简洁和易于理解。

小程序之美体现在性能优势上。小程序对外部的依赖少,从而可以快速启动,基于资源收敛式的反应式编程模型可以提升性能并减少资源浪费。

小程序之美体现在团队合作和独立演进上。清晰的边界划分是团队协作的有效手段,而体现在微服务架构上就是服务提供者和消费者可以预先订立契约,可以根据契约独立、并行开发各个服务,这样就实现了服务之间的解耦,使其功能能够独立进化。

2、做好一件事

小程序应该只做好一件事,应该保持对一件事的专注力。在UNIX设计哲学中,解决一个问题并将问题解决到完美,比同时解决多个问题更为重要。然而在我们的工程中,随着项目的进展,我们很难将庞大的代码库进行清晰的模块划分,更糟糕的是我们很多时候并不知道这些模块之间的服务界限。

在UNIX操作系统中,我们可以发现很多命令的强大之处正是只有单一的功能,并将这件事干好,也就是所谓的“Do one thing,do itwell”。而且UNIX首创的管道可以把这些命令任意地组合,以完成一个更为强大的功能。这些哲学到今天都在深深地影响着整个计算机产业,下面我们罗列一些经典的UNIX命令程序,如下表所示。

架构设计背后的哲学插图

微服务的目标是独立地完成一件事并做到最好。微服务可以根据业务的边界来确定应用的行为,这样它不仅可以很容易地确定某个功能逻辑对应的代码,而且由于该服务专注在某个边界之内,因此可以更好地避免由于代码库过大衍生出的复杂性问题。

3、快速建立原型

只做好一件事的小程序可以让我们的项目轻装前行,我们不必再担心系统会成为一个庞然大物而无法控制。对于单体巨石应用,需要制定周密的计划去编写设计文档,以及为聚合不同模块之间功能而准备脚本和代码,相比之下,自治的小程序更加清楚自己的业务职责和业务范围。开发人员可以对小程序快速建立原型,缩短服务的交付周期,迅速构建可以供用户使用的程序和服务,并从结果中得到反馈,向最终的目标前进。

在UNIX设计哲学中,软件发生变化是不可避免的,这种变化来源于沟通的失败、需求理解的差异、知识经验的限制等,所以软件工程相比任何其他工程都更加容易返工,需要软件从业者不断试错、总结、验证,并根据期望重新建立共识。尽快建立原型是一个重要的步骤,有了具体的原型,可以降低项目的风险,可以给客户展示和进行可视化跟踪。往往这个过程是伴随着系统的迭代和演进的。

回到微服务,我们说微服务架构在建立原型上有天然的优势,微服务架构的很多特性能让我们快速落地和逐步地独立完善和迭代项目。

  • 微服务架构强调细粒度的服务,在服务规模上尽量精简和务实。
  • 微服务架构只做好一件事,没有过多的其他因素干扰,我们可以将注意力集中在完成这件事情上,尽量排除干扰因素。
  • 微服务架构提供轻量级的通信集成方式,有利于集成测试和验证结果。
  • 微服务架构建立在已有的技术基础之上,能够更加快速地构建、发布和体验应用。

4、软件的复利效应

在软件工程实践中,在项目的启动或者早期阶段,经常会面临技术方案的选择问题。一些软件工程师会陷入自我保护的状态,认为别人的方案存在缺陷,自己如果重新做会比现有方案做得更好,但其实可能是因为他不了解“每个软件可能都是在某种约束条件下工作的,而且适用于某些特定的场景”。我们把这种现象称为“NIH(NotInvented Here)综合征”。NIH综合征的结果就是重新发明轮子,患有NIH综合征的人相信内部开发更安全、更高效、速度更快、维护成本更低,然而,其实他们并不一定能够使用创新的方案来解决实际的问题。目前,软件正朝着规模化和标准化的方向发展,标准驱动下的软件开发和集成方式,要求你的工作能够集成到标准中,而不是另起炉灶。NIH综合征的危险之处就是,你的软件、你的服务无法与标准对接,你的系统可能成为一个孤岛系统。例如,在你提供了私有协议的RPC方式暴露服务的情况下,你的服务只能生存在自己的闭环体系中,而且基于标准的HTTP接口API的调用方式很难与你的服务集成。

项目的生命周期受很多因素的影响,预算、人员成本、推广等都会给项目的持续发展带来风险,NIH综合征会给项目的持续发展带来额外的成本问题,因为你缺少相关人员、资料、生态的支持,而采用标准化的技术可以从互联网中得更多的技术、社区和人员的支持。

下面让我们看看UNIX的实践,所谓“前人种树、后人乘凉”。查阅UNIX坎坷的发展历程,我们就会发现,UNIX操作系统是在数千名工程师的辛勤努力的基础上发展起来的。UNIX中复杂的逻辑都是由小的程序累积而成的,聪明的程序员总是可以借用前人写的优秀代码实现自己的功能,这样才能更快、更好地扩大软件的影响力和威力,放大自己的工作成果。

在微服务领域,我们已经看到非常多优秀的微服务技术框架,例如本文后续会详细介绍的Spring Boot微服务脚手架。它是在Spring生态的基础上发展起来(Spring Boot也是软件复利的成果)的开发框架。在Java世界,Spring Boot带给广大开发者的是更加简化的工作流程,以及更高的开发效率。这也是Spring出色的地方,弥补了Java在简化工作上的空白,让开发者开发者避免了大量的重复工作。SpringCloud则降低了建立分布式系统的复杂度,Spring的忠实热爱者可以充分享受软件复利效应带来的福利。

5、可移植性优先

可移植性在软件工程中的重要性无论怎么强调都不过分,因为这个哲学正是UNIX操作系统能够成为“常青树”的秘诀。在UNIX环境下,Shell脚本具备更好的可移植性,Shell脚本通常由多个UNIX命令组成,Shell可执行文件间接由UNIX命令解释器解释执行。如果你对效率要求不高,可以尽量使用Shell脚本执行,Shell脚本语法简单、使用方便、运行之前不需要编译、具备很强的文本处理能力,开发效率也比较高。

但是Shell归根结底还是一种弱类型语言,没有严格的数据类型检查。Shell的缺点是I/O性能不高,同时因为是解释性语言,对于值计算类型的问题也没有很好的支持。如果你想让程序具有更好的可移植性,可以选择具备跨平台能力的编程语言。Java便是一种跨平台语言,基于JVM虚拟机平台,Java制定了字节码执行引擎规范,可以满足程序的可移植性要求。这也是Java语言发展这么多年依然具有强大生命力的重要原因。

在微服务时代,容器的隔离性和可移植性可以说为软件开发带来了革命性的颠覆,Docker技术通过采用LXC虚拟化手段,利用Linux系统的Namespace和Cgroup技术确保了应用程序与资源的隔离。Docker通过和各大厂商联合发起OCI(开发容器标准),规范了应用运行时的容器镜像标准。镜像的打包、构建、部署、命名过程都按照统一规范进行,进而标准化了底层运行时支撑环境,这样你就可以在统一的容器环境下灵活地交付、部署和移植代码。通过采用Docker容器技术,我们将不需要再关注操作系统的特殊性和差异性,可以更关注应用程序本身,底层多余的环境因素可以通过容器提供的虚拟环境来屏蔽。

也就是说,我们的微服务具备了更好的可移植性,真正做到了“一次构建,随处运行”。

发表评论