秦孝公在位期间致力于恢复秦国的霸业,他因此颁布著名的求贤令, 商鞅3次游说秦孝公,第一次讲的是尧、舜、禹、汤的帝道。上古时期,百姓安居乐业。秦孝公听着听着睡着了。商鞅离开之后,秦孝公向景监发火,说商鞅自大。景监把这个反馈给商鞅,商鞅没有气馁,请求再给他一次机会。5天后景监给他安排第二次朝见。这一次商鞅讲的是周文王、周武王的王道,讲的是礼治天下。秦孝公有点兴趣,觉得商鞅可以一起谈论,但没有打算重用商鞅。景监把秦孝公的意思回复给商鞅后,商鞅说:"我已经知道怎么游说他了,请再给我一次机会"。第三次商鞅讲的是春秋五霸的霸道。秦孝公这次听得津津有味,并几天几夜促膝长谈,不断向商鞅请教,并重用商鞅,才有了后来的商鞅变法,秦国一统天下。
在软件和编程领域有很多书籍会冠之以'某某之道','某某之美','某某之禅',软件领域也有自己的一套哲学思想。我今天想聊一聊我对软件架构的哲学思考。这是一个超大的命题,即使我在软件领域摸爬滚打了这么多年,也不敢说我已经窥其一二,今日胡说一通,希望得到大家的指正。
'道' 暨道路或者方向,是自然规律,是原则和目的,道路和方向是错的,无论你怎么努力,都很难成功。
'术' 是方法,手段和技巧,好的方法和工具可以让你在成功的路上事半功倍,前提是你走在正确的道路上。
'道' 是价值观,是主观的东西,也就是你认为什么是正确的事, 不同的人对什么是正确的事有着不同的认识。商鞅的霸道带领秦国统一六国,但是注定不能长久,秦国也未能如始皇的愿,千秋万代。
'术' 是方法论,相对比较客观,一个好的方法基本上是可以推广和重用。
软件架构之'道'
我个人认为,软件架构之道最核心的问题是解决复杂性的问题。如果说'道'是方向,那么软件架构之路应该带领码农走向简单。这里的简单应该包含:
- 软件分层应该简单,不应该引入不必要的层级
- 软件的模块应该简单,不应该引入不必要的功能
- 软件模块之间的关系应该简单,不应该引入不必要的交互和依赖
- 软件代码应该简单,应该容易理解和阅读
- 软件构建应该简单,应该容易搭建构建环境
- 软件测试应该简单,应该容易找出软件中的错误
- 软件调试应该简单,出错时应该容易定位错误源
- 软件运维应该简单,应该容易监控和管理
- 最重要的,软件的功能应该简单,应该容易让用户找到自己想用的功能,并且轻松的使用该功能,达成用户想要的目标。
那么我们想一想,软件发展到今天,我们上面所说的这些有变简单了么?似乎没有。那么是不是软件架构在背道而驰,变得越来越复杂了呢?我不这么认为。为了理解这个问题,我们来看看软件中的复杂性的元凶,是什么带来了软件的复杂性。
首先,软件的复杂性来自于功能。我们之前提到,软件架构的中心问题是满足功能要求。随着人们希望软件能够提供越来越多的功能,软件架构的设计必然会随之变得复杂。而这个复杂度的增加和功能之间的关系并不是线性的,而是几何级数甚至更高。因为软件开发是一个动态过程,新加入的功能必然会和已有的功能产生互动,有些是依赖,有些是制约,有些是干扰。例如系统最初有一个功能A,运作良好;当加入功能B的时候,A功能会制约B功能,所以在设计B功能的时候,除了要完成B本身的功能外,还要设计如何和A功能互动或者如何屏蔽A功能对B的限制。当C功能到来的时候,A和B同时要影响C,甚至可能要考虑ABC的联动,这样系统就会越来越复杂了。所以复杂的功能是当前软件复杂性的主要原因。
其次,软件的复杂性的另一个元凶是人为的。也就是由于开发软件的人和组织因为能力不足,或者因为懒惰,贪婪,傲慢等原罪,人为的使软件变得复杂。
我认为软件发展的大势仍然是向着简单性的方向在前进。因为对功能的要求越来越多,我们现在看到的复杂的软件架构实际支撑了更为复杂的诸多功能,所以从这个角度来看,软件架构实际上是向着简单的趋势发展。
我们所能做的是把复杂性封装在更低的层次。例如操作系统封装了对计算资源(内存,CPU,进程,文件系统)的使用,AWS云在基础设置infrastructure层次对网络,主机的使用进行封装,而Kubernetes利用容器集群,封装了应用的部署。操作系统,云和Kubernetes都是很复杂的,但是通过良好的封装和简单的接口,他们给使用者带来了便利。做为架构设计的一个原则,应该尽量把复杂性封装在更低的层次。
通常在分层架构中,越高的层次一般意味着更多的代码和更多的使用。当一个问题发生的时候,一般总是表现在最高层处(UI或者应用层),然而对这个问题的处理,可以在各个层次解决。例如为了提高访问效率,我们可以在UI或者数据层加入缓存,数据层的缓存可以支持所有的UI用户,而UI层的缓存只能针对使用该类型的UI的用户,例如web端和移动端的不用UI各自要实现自己的缓存。当然实际中,所有的层次都可以利用自己的缓存来解决问题,但是从解决问题的效率来看,在低层次上解决问题的效率要高于更高的层次。所以我们应该尽可能的把复杂问题的解决放在更低的层次上。
但是我们之前也提到过人是软件架构的一个基本点之一,分层的架构往往也意味着分层的组织。当UI的团队试图解决性能问题的时候,他们往往希望在组织可控的范围内解决问题。这并不是因为他们不明白在数据层解决问题的优势,而是因为要和另一个组织,数据库部门去沟通,带来的额外成本比自己加一个缓存可能还要高。做正确的事是有代价的。(参见康威定律)
爱因斯坦说: "Simple,but not simpler"。
建筑大师路德维希·密斯·凡德罗说:"Less is more。"
奥卡姆说:"如无必要,勿增实体"。
老子说:"大道至简"。
软件架构之道在于找到设计的平衡点,使得架构足够简单,但是能够满足需要。 (关于奥卡姆剃刀,请大家参考我之前的动图,)
除了简单性作为道之根本,软件架构设计还有一些常见的通用原则,我认为这些原则都是和降低系统复杂度一致的:
- KISS原则
Keep it simple and stupid 是我们之前的简单性原则的一种说法。我想说的是简单未必愚蠢,很多时候大智若愚。
- 最小代价(努力)原则
光总是走最短路径,人也一样。程序猿的惰性与生俱来,我们总是选择最容易行走的路径。这也是为什么我们应该尽可能在一开始的时候,作出正确的选择,因为一旦这个架构设计出现,后面的人很有可能不愿意为了更好的架构而改进,而是遵循已有的设计。这个和简单性原则一致,如果我们不能在一开始作出正确的选择,因为最小代价原则,系统必然会走向复杂。
- 最小意外原则
以前读过一本讲UX设计的书《点石成金》,英文名叫《Dont make me think》。和这个原则一致,UX设计应该自然,符合用户的常识和使用习惯。如果用户需要通过思考才能理解如何交互,那么一定是设计出了问题。同样的,架构设计也一样,好的设计应该避免意外,遵守通用的规范和习惯,代码也是。这些意外其实是软件架构和设计中的复杂因素。最小意外也就意味着尽可能的简单。
- DRY原则
重复是软件的原罪之一,"Dont repeat yourself" 告诉我们应该尽可能的消灭重复和冗余。重复使的软件的阅读,修改,测试变得复杂,消灭重复,是使软件变得简单的手段之一。
所有的这些原则都和降低复杂性不无关系。
拓展阅读:
软件架构之'术'
我们再来聊聊软件架构的'术'。在软件和软件架构设计领域,有很多方法和工具,我们来看看其中最常见的一些。我们可以把他们归为"术"。这些和软件架构都有着直接或者间接的关系。
数据结构和算法
数据结构和算法是软件编程领域里最重要的方法。数据结构,是抽象的表示数据的方式;算法,则是计算的一系列有效、通用的步骤。数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作。算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。 算法与数据结构是程序设计中相辅相成的两个方面,是计算机学科的重要基石。运用数据结构和算法,可以有效的解决编程中遇到的一些常见的复杂问题。
每一个程序猿从一开始学习编程就接触数据结构和算法。有人说,程序等于数据结构加算法。这有一定的道理,但是显然不够全面。数据结构和算法作为编程的基础方法,是各大软件厂商招聘考核的标杆,不管你是程序猿还是架构师,都需要投入精力于此,数据结构和算法是软件和架构设计的基础。
面向对象和设计模式
仅有数据结构和算法还不足以应对复杂的软件开发的需要。面向对象的设计成为了软件开发领域里最为流行的思想。面向对象是一种对现实世界理解和抽象的方法,利用的是隐喻(metaphor)的手段。隐喻其实是我们在软件设计中常用的一种手段,为了便于理解,我们把现实世界中的概念用软件中的概念的来模拟,这样做的好处是便于我们去思考,因为我们的设计是基于我们对现实世界的理解,所以可以重用我们现现实世界积累的成功经验。这样做的缺点是,软件世界有自己的特点,完全套用现实世界的偏见可能并非最为有效。现在虽然对面向对象的程序设计仍处于统治地位,但是它的声音已经渐渐变弱。与之对应的有面向过程,函数式编程,面向切面(Aspect Oriented)等思想。
设计模式(Design pattern)起源于《设计模式:可复用面向对象软件的基础》一书,提出和总结了对于一些常见软件设计问题的标准解决方案,称为软件设计模式。该书作者后以"四人帮"著称。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
软件设计模式和面向对象的设计都基于一些常见的原则:
· 单一职责原则 Single Responsiblity一个代码组件(例如类或函数)应该只执行单一的预设的任务。
· 开放封闭原则 Open Close程序里的实体项(类,模块,函数等)应该对扩展行为开放,对修改行为关闭。换句话说,不要写允许别人修改的类,应该写能让人们扩展的类
· 里氏替换原则 Liskov Substiution里氏替换原则的内容可以描述为: "派生类(子类)对象可以在程式中代替其基类(超类)对象。"
· 接口隔离原则 Interface Segregation指明客户(client)应该不依赖于它不使用的方法。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。
· 依赖倒置原则 Dependency Inversion程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
以上的五个原则构成了著名的SOLID,除此之外,还有一个著名的高内聚低耦合原则,是指具有相似功能的代码应该放在同一个代码组件里。一个代码片段(代码块,函数,类等)应该最小化它对其它代码的依赖。这个目标通过尽可能少的使用共享变量来实现。"低耦合是一个计算机系统结构合理、设计优秀的标志,把它与高聚合特征联合起来,会对可读性和可维护性等重要目标的实现具有重要的意义。"该原则和我们之前提到的把复杂性封装在更低的层次上是一致的,对复杂性的封装就是高内聚。
模块化和分层
模块化和分层是架构设计里最基本的方法。
如上图的一个典型的软件架构图中,不同的模块位于不同的层级,某些模块会跨一些层级。模块化和分层利用的是分而治之的方法,把复杂的软件系统划分为可控制,可管理的小单元。也即是说,它仍然是为了解决复杂性的问题。分层和模块化不是总能降低复杂度,过多的层次和冗余的模块会使系统变得更为复杂。
微服务和SOA
随着越来越多的软件应用走向云端,云原生的设计越来越多的被提起。微服务随之大行其道。Martin Fowler对微服务的定义是"微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务和服务之间采用轻量级的通信机制相互沟通(通常是基于HTTP的Restful API).每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构"
和之前的模块化和分层相比较,分层更多是关注在垂直方向上的责任划分,而微服务增加了水平方向的扩张,并实现去中心化的网络架构。关于去中心化的网络架构推荐阅读凯文·凯利的
优点
- 升开发交流,每个服务足够内聚,足够小,代码容易理解;
- 服务独立测试、部署、升级、发布;
- 按需定制的DFX,资源利用率,每个服务可以各自进行x扩展和z扩展,而且,每个服务可以根据自己的需要部署到合适的硬件服务器上;每个服务按
- 需要选择HA的模式,选择接受服务的实例个数;
- 容易扩大开发团队,可以针对每个服务(service)组件开发团队;
- 提高容错性(fault isolation),一个服务的内存泄露并不会让整个系统瘫痪;
- 新技术的应用,系统不会被长期限制在某个技术栈上;
缺点
- 没有银弹,微服务提高了系统的复杂度;
- 开发人员要处理分布式系统的复杂性;
- 服务之间的分布式通信问题;
- 服务的注册与发现问题;
- 服务之间的分布式事务问题;
- 数据隔离再来的报表处理问题;
- 服务之间的分布式一致性问题;
- 服务管理的复杂性,服务的编排;
- 不同服务实例的管理。
我们看到,微服务在某种程度上提高了系统的复杂度,天下没有免费的午餐,作为一个架构师,需要理解微服务带来的好处是否能够抵消复杂性提升的代价。
当今的微服务大多基于容器设计。
无服务设计 (Serverless)
Serverless 架构是指大量依赖第三方服务(也叫做后端即服务,即"BaaS")或暂存容器中运行的自定义代码(函数即服务,即"FaaS")的应用程序,函数是无服务器架构中抽象语言运行时的最小单位。在这种架构中,我们并不看重运行一个函数需要多少 CPU 或 RAM 或任何其他资源,而是更看重运行函数所需的时间,我们也只为这些函数的运行时间付费。
无服务的设计带来的优点有:
- 降低运营成本:Serverless是非常简单的外包解决方案。它可以让您委托服务提供商管理服务器、数据库和应用程序甚至逻辑,否则您就不得不自己来维护。由于这个服务使用者的数量会非常庞大,于是就会产生规模经济效应。在降低成本上包含了两个方面,即基础设施的成本和人员(运营/开发)的成本。
- 降低开发成本:IaaS和PaaS存在的前提是,服务器和操作系统管理可以商品化。Serverless作为另一种服务的结果是整个应用程序组件被商品化。
- 扩展能力:Serverless架构一个显而易见的优点即"横向扩展是完全自动的、有弹性的、且由服务提供者所管理"。从基本的基础设施方面受益最大的好处是,您只需支付您所需要的计算能力。
- 更简单的管理:Serverless架构明显比其他架构更简单。更少的组件,就意味着您的管理开销会更少。
- "绿色"的计算:按照《福布斯》杂志的统计,在商业和企业数据中心的典型服务器仅提供5%~15%的平均最大处理能力的输出。这无疑是一种资源的巨大浪费。随着Serverless架构的出现,让服务提供商提供我们的计算能力最大限度满足实时需求。这将使我们更有效地利用计算资源。
当然无服务设计也有一些限制,它更适用于大量,频繁而简单的,无状态的操作,如果服务之间的关系比较复杂,状态比较多,调用时间比较长,无服务设计并不是非常适合。
软件架构设计还有很多方法,这里不可能一一讨论,例如领域驱动的设计(DDD),重构技术,敏捷(更多是软件工程,但是和架构也会有密切地关系),希望以后是时间再和大家分享,讨论。
拓展阅读:
- 基于容器应用设计的原则,模式和反模式
- 微服务中的模式和反模式
- 高效微服务的10个最佳实践
- 关于Serverless无服务器架构的神话和误解
- 是时候放弃设计模式了吗?
- 微服务架构如何影响软件开发组织的文化
- 再见,面向对象编程
行走于软件江湖
如果软件行业是个江湖,那么我们程序猿就是行走于江湖的武林人士。每个人要做的就是找寻"道",研习"术",磨练"器"。
"道"是价值观,是你的江湖理想,是你的追求。就像《神雕侠侣》时期守襄阳,信奉"侠之大者,为国为民"的郭靖。
"术"是你的内功心法和拳艺招数。数据结构和算法就像是内功,帮助你提高对战的效率。而各种其它的方法就像是招数。你可以像是学了独孤九剑的令狐冲,仅凭招数就可以杀敌无数,但是要成为武林盟主,内功修养也是不可或缺。
"器"是工具,各种语言,IDE,可以划分到"器","工欲善其事,必先利其器"。各位大侠行走江湖,免不了要选几样趁手的兵器,有人喜欢剑走偏锋,有人喜欢暴力砍杀。什么,你说你喜欢九齿钉耙?我只能赞你一声神仙威武!(参考如果编程语言是种武器,你使用的武器是什么?)
江湖苦修非一日之功,希望各位大侠在软件江湖早日得道,成为武林至尊或者一方豪杰。
相关阅读: