【万字长文】探讨可信构架之道
摘要:软件架构是一个系统开发生命周期中最前端的部分,也是最关键、核心的部分。它决定了后续代码的走向,决定了项目的走向,有时候甚至能决定一家公司的成与败。
一.介绍
二.架构即未来
三.软件架构的灵活设计
四.典型架构对比分析
五.服务化构架之可信谈
六.架构设计衡量与愿景
七.构架系统的感与悟
最后
架构是什么?简单来说就是架子的结构,譬如动物、人的骨架,建筑物的结构框架等。架构影响什么?架构会影响最终物品的形态和质量。类比软件系统亦如此,在这个二进制的世界里,程序员扮演着上帝的角色,为这个世界创造出不同的东西。那么,作为根本,重要性不言而喻,这就需要我们思考如何合理地进行物体设计、架构设计。
软件技术领域的发展历程也是系统架构的演变过程,从单体、分布式系统、SOA 架构,再到更细粒度的微服务化、服务网格、云原生架构等。架构设计的初衷也是为了应对各类场景的特有的高并发、高性能、稳定、易维护及可扩展性等。架构是多维的,可以有多种方法对其进行描述。架构设计依赖人的思考和判断,是一种抽象的结构,它由软件的各个组成部分和这些部分之间的依赖关系构成。经验丰富的设计人员能把握更多构架细节,对于程序架构具有较强的可预见性,让设计更加合理、充分。
软件系统架构对功能性需求影响不大,但架构的优劣决定了系统的非功能性需求即质量属性或简称为“能力”,决定了系统品质的好坏,决定了软件交付的可靠、可测试、可维护、可扩展及可部署性等。事实上,在任何架构甚至是一团糟的架构之上,都可以实现应用的功能性需求。因此即便是成功的系统,其内部架构也可能往往是一个大泥球,不合理的架构通常导致系统紧耦合、玻璃心、难以改变、没有头绪,甚至关于架构的规模如何?系统的性能如何?程序容易修改吗?系统的部署模型是怎么样?系统的响应如何?一切都是那么难以回答、难以定论 … …
一. 介 绍
软件架构设计可从宏观上说明系统的组成与特性,是经系统性思考、权衡之后在现有资源约束下的最合理决策, 最终明确的系统骨架包括子系统、模块、组件,以及它们之间的协作关系、约束规范、指导原则。覆盖需求、技术栈、成本、组织及架构、可扩展性、可维护性等。
1) 系统性思考的合理决策:比如技术选型、解决方案等;
2) 明确的系统骨架:明确系统有哪些部分组成;
3) 系统协作关系:各个组成部分如何协作来实现业务请求;
4) 约束规范和指导原则:保证系统有序,高效、稳定运行。
软件架构设计要完成两项工作,一是分析,二是设计。分析是分析需求,设计则是设计软件的大致结构。很多的方法论把分析和设计两种活动分开,但其实两者是很难区分的,做分析的时候会想到如何设计,而思考如何设计反过来又会影响分析的效果。可以说,两者之间是相互联系和不断迭代的。
在架构设计范畴中对于业务的理解和经验积累同样重要,当短期的需求与整体设计冲突时,有些设计师会偏向于当前的设计,缺乏对整体的思考,对未来的思考,而腐化了系统整体应有的架构。当程序员跟产品经理开撕,这个需求不能做,那个需求调整需要很长时间。其中也折射出架构设计的不合理可能。
二. 架构即未来
软件架构是一个系统开发生命周期中最前端的部分,也是最关键、核心的部分。它决定了后续代码的走向,决定了项目的走向,有时候甚至能决定一家公司的成与败。架构的目标是保证系统的高可用、可扩展、可伸缩、安全等一系列的指标,好的开始相当于成功了一半。
软件架构是系统的顶层结构,要着眼于全局,包括硬件、操作系统、网络环境,以及从立项到维护之间的所有过程(需求、设计、编码、部署、维护及迭代)。架构决定子系统之间的关系、分层与通讯方式、公共设计原则/风格、功能需求与非功能需求的优先级与取舍原则等。架构是多种结构的体现,就像建筑物的结构会随着观察动机和出发点的不同而有多种含义一样,软件构架也表现为多种结构,常见的软件结构有:模块结构、逻辑或概念结构、进程或协调结构、物理结构、使用结构、调用结构、数据流、控制流、类结构等。
优秀的架构可促进项目的良性发展,设计之初将远的近的都考虑进来,最后会使得项目朝着好的方向发展,降低项目所投入的时间、金钱、人力成本。坏的角度来看,对于多变的软件而言,有一句话说的好,计划跟不上变化。需求不确定性与人,也与业务相关。架构的变化往往成本很高,好的架构可使变化发生在局部而不影响整个系统。架构和设计的可扩展决定了需求变更、业务增加时付出的成本;标准化、规范化、可传承有利于提升团队的效率;“播种,施肥,除草”,播种时的付出会影响除草的成本,而这个成本可能是数倍甚至数十倍的,把初始做的更好后面才能更省心。
架构的分类上可分为业务架构、应用架构、技术架构,部署架构。业务架构是生产力,应用架构是生产关系,技术架构是生产工具。业务架构决定应用架构,应用架构需要适配业务架构并随之不断进化,同时依托技术架构、部署架构而最终落地。
1. 架构设计的目标
架构设计的目的是为了解决软件系统的复杂度带来的问题,对系统的高可用、高性能、可扩展、安全、伸缩性、简洁等做系统级的把握。常规来说功能需求决定业务构架,非功能需求决定技术构架,变化案例决定构架的范围。功能需求定义了软件能做什么,根据业务需求来设计业务构架,以使得未来的软件能够满足客户的需要。非功能需求定义了一些性能、效率上的约束、规则,技术构架要能够满足这些约束和规则。变化案例是对未来可能发生的变化估计,结合功能和非功能需求就可确定一个需求的范围,进而确定一个构架的范围。
结构面的考虑因素
1) 需求的符合性:正确性、完整性;功能性需求、非功能性需求;
2) 总体性能(内存,数据组织和内容,任务,网络操作,关键算法,硬件和其他接口影响);
3) 运行可管理性: 便于控制系统运行,监视状态,错误处理,模块间通信与可维护性;
4) 与其他系统接口兼容性;
5) 与网络、硬件接口兼容性及性能;
6) 系统安全性;
7) 系统可靠性;
8) 业务流程及信息的可调整性;
9) 使用方便性;
11)架构样式的一致性.
注:运行时负载均衡可以从系统性能、可靠性方面考虑。
架构设计的目标
确定系统边界,确定系统在技术层面上的做与不做。
确定系统内模块之间的关系,依赖关系及模块的宏观输入与输出。
确定指导后续设计与演化的原则,使后续子系统或模块设计在规定的框架内继续演化。
确定非功能性需求目标,涉及如下:
可靠性:系统对于用户的商业经营和管理来说极为重要,因此必须非常可靠。
安全性:软件系统所承担的交易的商业价值极高,系统的安全性非常重要。
可扩展: 在用户的使用率、用户数快速增长下保持合理的性能,适应用户的市场扩展可能性,以及包括对系统进行功能和性能的扩展。
可定制: 同样的一套软件,可以根据客户群的不同和市场需求的变化进行调整。
可维护: 一是排除现有错误,二是将新的需求反映到现有系统中去。一个易于维护的系统可以有效地降低技术支持的费用。
客户体验:软件系统必须易于使用。
市场时机:以最快速度争夺市场先机非常重要。
组织结构方面
1) 开发可管理性:便于人员分工、利于配置管理、大小的合理性与适度复杂性;
2) 可维护性:与运行可管理性不同;
3) 可扩充性:系统方案的升级、扩容、扩充性能;
4) 可移植性:不同客户端、应用服务器、数据库管理系统;
5) 需求的符合性.
2. 设计的思维模式
架构设计的本质是管理复杂性。抽象、分层、分治和演化思维是架构设计师征服复杂性的四种根本性手段。
A.抽象化设计思维
抽象是对某种事物进行简化表示或描述的过程,抽象让我们关注要素,隐藏额外细节。抽象能力的强弱,直接决定我们所能解决问题的复杂性和规模大小。架构设计应摒弃具体细节,抓住软件最上层、优先级最高、风险最大的那部分需求。
合理地使用抽象可提升设计的简单性,改善软件开发的质量。通常使用到两种抽象方法,基于过程的抽象和基于数据的抽象。基于过程做抽象时,会将待解决的问题分解为一个个小的子问题,每一个子问题分别由一个独立的模块、函数、类等来完成。良好抽象的系统在其中某一个部分的实现被替换的情况下,不需要修改设计仍然能正常工作。基于设计良好的抽象可组合构建出功能更加强大和复杂的系统。基于数据抽象的方法可以将复杂数据结构的使用和其构造分离,通过使用“抽象数据”的方式,用户可以通过明确定义的一系列接口对其进行访问和操作,隐藏对象的内部特征,对外部环境透明。而不适合问题本质的抽象方式不仅会影响软件设计的简单性,也可能给软件的可维护性带来负面影响。
B.分层的设计模式
分层技术在计算机领域中有着悠久历史,分层优势在于:上层的逻辑无需了解所有底层逻辑,它只需要了解和它邻接层的细节。TCP/IP 协议栈就是通过不同的层对数据进行层层封包,不同层间的耦合度明显降低。通过严格的区分层次,大大降低了层间耦合度。分层原则对软件进行结构上的划分,定义了结构的不同部分的职责,总体思路并无特别,只是将系统进行有效组织的方式,在完成分层之后软件架构已经清晰化了。
为了构建一套复杂系统,可把整个系统划分成若干个层次,每一层专注解决某个领域的问题,并向上提供服务。有些层次是纵向的,它贯穿所有其它层次,称为共享层。分层也可以认为是抽象的一种方式,将系统抽象分解成若干层次化的模块。
C.问题分治化思维
分而治之也是应对和管理复杂性的一般性方法。对于一个无法一次解决的大问题,一般先把大问题分解成若干子问题,如果子问题还无法直接解决,则继续分解成子子问题,直到可以直接解决的程度,这个是分解(divide)的过程;然后将子子问题的解组合拼装成子问题的解,再将子问题的解组合拼装成原问题的解,这个是组合(combine)的过程。
D.演化式架构思维
架构既是设计出来的,同时也是演化出来的,在设计中演化,在演化中设计,一个不断迭代的过程。架构师除了要利用自身的架构设计能力,同时也要学会借助用户反馈和进化的力量,推动架构的持续演进,这个就是演化式架构思维。能够不断应对环境变化的系统,才是有生命力的系统,架构的好坏,很大部分取决于它应对变化的灵活性。所以具有演化式思维的架构师,能够在一开始设计时就考虑到后续架构的演化特性,并且将灵活应对变化的能力作为架构设计的主要考量。
软件架构需根据业务的发展而不断变化。如果没有把握这个本质,在做架构设计时就很容易陷入试图一步到位的误区,期望不管业务如何变化架构都稳如磐石。业务的发展和变化总是很快,实践中遵循演化优于一步到位的原则。实践中可以参考:首先设计出的架构要满足当时的业务需要。其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等却可以在新架构中延续。严格的对待每一次的迭代,确保计划的完成、确保软件的质量、确保用户的需求得到满足,这样才是正统的迭代之路。
最开始的设计一定只是一个原始架构,但对于后续的架构设计而言非常重要。迭代设计也可称之为增量设计,每一次的迭代都是在上一次迭代的基础上进行的,迭代将致力于重用、修改、增强目前的架构,以使架构越来越强壮,得到一个稳定的架构。
3. 典型设计实践
软件模式通过定义一组互相协作的软件元素来解决软件架构设计问题,是实现架构设计的具体手段。架构设计需以全局的视角统筹计算、数据、存储、通讯等来综合考虑与权衡。
架构模式有助于定义程序的基本特征和行为。譬如对于系统结构设计使用层模式,对于分布式系统使用代理模式,对于交互系统使用 MVC 模式。模式本就是针对特定问题的解,一些架构模式很自然让程序适应大规模,有些让程序变得灵巧敏捷。可结合需求的特点和目标来采用相应模式设计架构。设计模式是从代码层面提炼出来的一种总结,可使代码的耦合度达到最大限度的分离,从而可使代码更好的被复用,更易被替换,更好的拥抱需求的变化。
1) 设计简单化原则
【Keep It Simple】"简单要比复杂有效",这就是简单设计模式的基本思路。一个复杂的架构不论是测试还是维护、以及后期的开发迭代都是困难的,也会导致沟通成本的上升,架构应尽可能的简单明了,用最简单的方案来解决问题。
无论是结构还是逻辑的复杂都会存在各种问题。架构越简单,稳定性就越好,这也是行业的共识。简单的架构并不等于实现简单,简单的架构需要设计者花费大量的心血,也要求设计者对技术有很深的造诣。简单的架构设计有助于加快开发团队对架构的理解。简单意味着问题不会非常的复杂,架构是解决需求的关键,无论需求再怎么复杂多变,总可以找出简单稳定的部分,以稳定的部分作为基础,再根据需要进行改进扩展,以解决复杂的问题。其次,简单性还体现在表示的简单上。也体现在系统内层次、模型、动静、流量、链路、读写、时间(异步化)等不同维度、粒度上的抽象、设计的简化。
“弹性的设计”满足需求变更的背后往往所付出的是复杂的设计。解决问题的大方向应是将复杂问题简单化,但在具体实施设计过程中,很多人可能会将简单问题复杂化,在设计模式的运用上易犯这个错误,如何尽可能的做到设计的简单明,落实到每个类/模块的具体实现上要真正能体现系统事物的本质,本质特征只有一个,代码越接近它,表示设计就是简单明了,越简单明了,承载的系统就越稳定、可靠、可信。
2) 高内聚、低耦合
高内聚、低耦合是软件工程中的概念,是判断设计好坏的标准,是架构设计最主要的目标。具有更好的重用性、维护性及可扩展性,软件设计原则是其实现的指导方针。设计中通常用耦合度和内聚度作为衡量模块独立程度的标准,划分模块的一个准则是高内聚低耦合。从模块来看,高内聚是尽可能使类的每个成员方法只完成一件事,低耦合是减少类内部一个成员方法调用另一个成员方法。从类的角度看,是减少类内部对其他类的调用。从功能模块来看,是减少模块之间的交互复杂度即横向:类与类之间、模块与模块之间,纵向:层次之间;尽可能内容内聚,数据耦合。
降低依赖,解除耦合
反向依赖
只从上往下依赖,将公共的重复功能的模块抽取出来。公共模块必须足够的功能单一,不能有其他业务的逻辑判断在里面。在整个模块依赖关系里,应该是一棵树状结构的关系图,而不是一个网状的关系图。
配置解耦
每个模块的动态属性,设定配置项,可使用配置中心进行实时更新并生效。
权限解耦
功能的权限控制及安全验证,原来是与业务代码集成。在服务网格化架构中,权限功能划分归属到通讯边车模块,业务和技术面通过 API 接口交互。
流量解耦
服务网格架构下,流量的控制下沉到边车通讯模块。支持流量拆分,服务之间的流量支持隔离性。
数据解耦
保证模块之间的数据不相互影响,同一个模块的冷热数据解耦。系统运行时间长了后也会积累大量的数据,为保证系统的性能稳定,要减少数据量太大造成的性能降低。
扩容解耦
好的架构设计需具备横向扩展能力,只通过增加硬件的方式就能提高系统性能。
部署解耦
支持快速试错、灰度发布。同一个模块先部署升级几台服务器到新版本,重启完成后流量切入后即可验证当前的部署是否有问题,无问题就继续部署其他的节点,如有问题则马上回滚到上一个版本。
动静解耦
当同一个模块的瞬间有非常高并发时,纯粹的流量解耦仍不够。不能让前端流量冲击后面真正的关键处理功能,需要更细的流量解耦,实现静态、动态资源访问分离。
可靠的系统是高度模块化(不暴露无关接口),最小侵入(常规使用无需继承之类的强耦合关系),易集成到完全不同类型的代码库(比如尽可能使用可移植代码而非直接调用平台 API),对外部环境有极少的假定,极力与其他系统的实现细节解耦。
3) 接口的设计原则
接口,可以理解为契约,一种约定。系统各个模块之所以能组合工作,正是因为通过定义好的 API 来交互。在对外提供抽象 API 的同时,也可能需使用其他模块的 API 作为自身运行的基础。接口设计要职责单一,尽可能隐藏内部实现,避免通过继承导致行为扩散,命名及风格统一,增强可理解性,定义好版本等保障接口稳定性、兼容性,对接口进行验证的思路是保证接口的可测试性。
a) 封装原则
优秀的接口设计会隐藏实现的细节,仅把需要的接口呈现给关联方,而具体的实现则对外部透明。
b) 最小职责
一个类实现的功能应尽可能紧凑,只处理紧密相关的功能,一个方法更应该只做一件事情,需要设计人员发现类的不同职责并将其分离。譬如一个微服务应尽可能职责单一,提供的接口也尽可能单一。
c) 最小接口
暴露给用户使用的方法应尽可能的少,公布的方法可能被客户频繁使用,如设计上存在问题或是要进行改进,都会对现有方法造成影响,因此需要将这些影响减到最小。另外,一些较轻型的共有方法应组合为单个的方法,降低用户和系统的耦合程度,具体实现可以通过外观模式或委托模式。
d) 最小耦合
设计的类和其它类的交互应该尽可能的少,如果发现一个类和大量的类存在耦合,可以引入新的类来削弱这种耦合度。在设计模式中,中介模式和外观模式都是此类型的应用。
e) 分层原则
封装原则的提升。一个系统,往往有各种各样的职责,如有负责和 DB 打交道的代码,也有和用户打交道的代码,把这些代码根据功能划分为不同的层次,就可对软件架构的不同部分实现大的封装。
参考博客:【SOLID原则精解之接口隔离原则 ISP】
4) 最少知识原则
一种面向对象程序设计的指导原则,描述了一种保持代码松耦合的策略。每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;为软件设计带来了两个主要的益处:更好的信息隐藏和更少的信息重载。打个比方,人可以命令一条狗行走,但是不应该直接指挥狗的腿行走,应该由狗去指挥它的腿行走。应用该原则有利于降低模块间的耦合,提升软件的可维护性和可重用性,但可能会导致不得不在类中设计出很多用于中转的包装方法,提升类设计的复杂度。
5) 门面控制模式
把职责赋予系统、设备或子系统的表示类(门面控制器),或者某个用例的表示类,让控制器接收事件并协调整个系统的运作。两个或多个对象间有交互的情况下,为避免直接耦合,提高重用性,创建中间类并赋予职责,对象的交互交由中间类协调。系统之间亦类同。
外观模式,可将调用者和复杂的业务类隔离,调用者无须知道业务类之间的复杂关系就能进行业务处理,从而大大降低了调用者和业务类之间的耦合度,适合用在内部关系较为复杂的组件中,也适合用在业务层向表示层发布接口的情况中。通过参数来实现数据的传递。
6) 信息专家模式
为子系统/模块分配职责的通用参考原则,把职责分配给拥有足够信息可以履行职责的专家,形象化来说就是将数据、事件、状态等传递给承担该数据处理的单一职责模块进行处理,可作为职责边界的划分依据。合理的职责分配能力,也就是每个类/组件/子系统应该承担什么职责,如何保证职责单一,它们之间如何协作,需要职责边界清晰。打个比方,当不确定哪个团队应该负责某个微服务时,一般原则也是谁拥有数据谁负责,基于有界上下文(一般是边界比较清晰的领域数据源)构建微服务。
7) 数据的一致性
分布式体系结构中,要保证数据的一致性,可用性和分区容忍性。但最多同时满足这三项中的两项,BASE 理论思想是即使无法做到数据的强一致性,但可采用适当方式达到最终一致。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统的整体可用性。分布式存储中一般单份数据至少存三副本,允许不同节点间副本同步的延迟就是软状态的体现。系统中的所有数据副本经过一段时间后最终能达成一致状态为最终一致性。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
微服务体系架构中,状态管理也许会成为首要和中心的问题。提供连接性和集成意味着系统本质上要么是查询状态,要么是更改状态(或两者)。对于给定的实体或信息,查询和更改状态的方法通常并不唯一。为避免数据损坏或意外结果,显式声明状态并使用某种策略来处理更改和查询状态带来的副作用,通过这种策略,每个微服务组件可以实现更高物理级别上的自治,从而允许更快的更改速度。
8) 稳定、可靠原则
稳定、可靠性是系统在给定的时间间隔及给定环境条件下,按设计要求,成功地运行程序的概率。成功地运行不仅要保证系统能正确地运行,满足功能需求,还要求当系统出现意外故障时能够尽快恢复正常运行,数据不受破坏。架构设计及部署上要考虑 N+1 冗余度(中心/服务器/系统/中间件/服务/数据等),避免单点问题故障、宕机。面向服务化的系统架构可通过服务治理,譬如限流、降级等来保障系统的可靠、稳定运行, 要实现隔离故障设计,通过断路保护避免故障传播和交叉影响。
服务方面,网络拓扑中任意节点丢失都有可能导致服务不可用,如边缘系统能提前检测到具有高风险的节点那么就可避免。系统角度,节点之间能互通状态和诊断信息,使得在系统层面部署故障检测、节点替换、数据检测等十分方便。数据角度,指数据通信方面等的可靠性。
9) 可扩展、伸缩性
对扩展的渴求源于需求的易变,需要架构具有一定的扩展性以应对变化,适应未来可能的变化。但扩展性和稳定性,扩展性和简单性都存有矛盾面,因此需要权衡投入、产出比,以设计出具有适当延展性的架构,譬如系统能做到无缝升级、扩容、扩充的可行性和便利等。好的系统架构能获得低延迟和高吞吐量,扩展性的目标是用可接受的延迟获得最大的吞吐量。
扩展包含两个层面:一是功能的可扩展性,主要是针对平台框架是否设计并预留了足够的扩展点,后续可以方便的增加各种功能或有第三方实现各种插件。另一种是性能的可扩展性,系统的弹性扩容能力,即随着系统用户量、并发的增加是否可实现弹性扩容,通过增加硬件设备就能提供更强的处理能力,这种一般称为可伸缩性。可伸缩性是高性能、低成本和可维护性等诸多因素的综合考量和平衡,可伸缩性讲究平滑线性的性能提升,更侧重于系统的水平伸缩,通过廉价的服务器实现分布式计算。
10) 架构的重用原则
重用是为了避免重复劳动、降低成本。架构设计的过程中可将一些公共部分抽象提取形成公共类和接口,其它功能模块所需相关功能可以调用,以达到重用目的。现实中,已在架构重用上做了很多的工作,譬如框架。使用功能分解的规则有助于提高重用性,因为每个类和方法的精度都提高了。
微服务架构中,每个服务都必须实现许多跟基础设施相关的功能,包括可观测性和服务发现模式,还需实现外部化配置模式,以在运行时向服务提供数据存储凭据等配置参数。在开发新服务时,一种更好的方法是在处理这些问题时应用微服务基底模式,在现有成熟的基底框架之上构建服务。
11) 设计系统运行时
现在的系统趋于复杂化、大型化, 构架设计应使系统可以预测系统故障,防患于未然。因此通过合理的构架规划系统运行资源,便于控制系统运行、监视系统状态、进行有效的错误处理;为实现上述目标,模块间通信应尽可能简单,同时建立合理、详尽的系统运行日志,通过自动审计运行日志了解系统运行状态、进行有效的异常处理。
关注控制流、通讯机制、资源争用、锁/令牌环机制、同步异步、并发/串行,同时也要考虑质量属性。
基于系统运行时的质量需求考虑问题,关注于系统的非功能需求。譬如客户常常要求系统的功能画面的最长响应时间不超过 4 秒,能满足 2000 个用户同时在线使用,基于角色的系统资源的安全控制等等。
12) 监控、能观性
运维的一项重要工作就是搞明白应用在运行时的一些行为,同时能够根据错误的请求或者高延迟等故障进行诊断排错。设计具备可观测性的服务:
健康检查 API:可以返回服务健康状态的 API;
日志聚合: 将产生的日志写入集中式的日志服务器,提供日志搜索,也可据日志情况触发报警;
分布式追踪:为每一个外部请求分配一个唯一 ID,用于在各个服务之间追踪请求;
异常跟踪:将程序异常发送到异常跟踪服务,给开发者发送告警并且跟踪每一个异常的解决;
应用指标:供维护使用的指标,如计数器等,导出到指标服务器;
审计日志:记录用户的行为。
13) 系统安全性设计
随着应用的不断深入和扩大,涉及的场景和信息也越来越多,大量涉密信息在网络上传输,因此对系统安全性的考虑已成为系统设计的关键,需从各个方面和角度加以考虑来保证数据资料的绝对安全。譬如在 Web 应用中,面对各种安全风险(SQL 注入,XSS、CSR 攻击等),各种漏洞是否能堵住,架构是否可以做到限流作用,防止 DDOS 攻击等。在微服务架构体系中,用户身份验证的工作通常由 API 网关/数据面来完成,服务调用方必须将有关用户的信息(如身份和角色)传递给它调用的服务,常见的解决方案是应用访问令牌模式,将访问令牌(如 JWT)传递给服务,对令牌加以验证并获取有关用户的信息。
参考博客:【云原生零可信网络下-应用服务的安全】
4. 设计视图及架构
架构可从多视角来看,就像建筑架构,一般有结构、管线、电气等。4+1 视图是描述应用程序架构的绝佳方式,四个不同的软件架构视图,每一视图描述架构的一个特定的重要侧面,且包括一些特定的元素及它们相互之间的关系。
逻辑视图:在面向对象的语言中,这些元素是类和包。它们之间的关系是类和包之间的关系,包括继承、关联和依赖。
实现视图:构建编译系统的输出,由表示打包代码的模块和组件组成。它们之间的关系包括模块之间的依赖关系以及组件和模块之间的组合关系。
进程视图:运行时的组件,每个元素都是一个进程,进程之间的关系代表进程间通信。
部署视图:进程如何映射到机器,视图中的元素由(物理或虚拟)计算机和容器进程组成,机器之间的关系代表网络。该视图还描述了进程和机器之间的关系。
视图中的+1 是指场景,负责把把视图中的元素如何协作串联在一起。每个场景负责描述在一个视图中的多个架构元素如何协作以完成一个请求。例如,在逻辑视图中的场景展现了类是如何协作,在进程视图中的场景展现了进程的协作。
系统的物理架构
主要考虑硬件选择和拓扑结构,软件到硬件的映射,软硬件的相互影响。
5. 持续交付 & 部署
实现项目/系统的持续交付和持续部署是 DevOps 中的关键环节。持续交付能够以可持续的方式安全、快速地将所有类型的更改(功能、配置、错误修复和实验等)交付到生产环境或用户手中,其关键特征是软件总是随时可以交付,它依赖于高水平的自动化,包括自动化测试。持续部署把持续交付提升到了一个新的水准,实施持续部署的高绩效组织每天多次部署到生产环境中,生产中断的次数要少得多,且可以从发生的任何事情中快速恢复。
微服务架构天然支持持续交付和持续部署。
持续交付和部署的目标是快速可靠地交付软件,评估的四个有用指标如下:
部署频率:软件部署到生产环境中的频率;
交付时间:从开发人员提交变更,到变更被部署的时间;
平均恢复时间:从生产环境问题中恢复的时间;
变更失败率:导致生产环境问题的变更提交百分比。
系统要小构建、小发布、快试错、不断迭代与交付。传统组织中部署频率低,交付时间很长,特别是开发和运维人员通常都会在维护窗口期间熬到最后一刻。相比之下,DevOps 组织经常发布,通常每天多次发布,生产环境问题要少得多。如,亚马逊可做到秒级就将代码更改部署到生产环境,Netflix 的一个软件组件的交付时间为分钟级。
持续交付和部署缩短了产品的上市时间,使企业能快速响应客户反馈,提供其所期望的可靠服务。开发人员可因此花费更多时间来提供有价值功能,而不是四处担任救火队员。
三. 软件架构的灵活设计
软件组件模块之间松耦合常见有两种方式:接口和消息,这两者如同人的骨关节一样,连接着松耦合的双方。消息比接口更加松耦合,因为接口方法有可能改变,导致接口的使用者跟着变化,而通过消息,消息生产者只要发送消息到消息系统就可以了,不再管是谁来接受消费这个消息,消息生产者由此和消费者完全解耦了。设计示例:
复杂性
假设原来是只有两个组件进行交互:
需求不断扩展变化,增加了第三个组件,那么这三个组件之间任意交互,复杂度就会提高:
复杂度以指数级的增长是惊人的,当我们增加到六个组件,复杂度将是惊人的。
自然界是如何应对这复杂呢?在物理中被称为构造定律。构造定律致力于描述能量和物质在物理网络(如河流)和生物网络(如血管)中的流动,理论提出,如果一个流体系统要继续存在(比如,生存),那它必须始终提供更容易的方式来获得这个系统中的流体。换句话说,系统应该致力于将能量消耗减少到最低限度,而同时将消耗单位能量产生的熵提高到最大限度。进化实质上是这么一个过程,即生物体不断的重组他们自身,以使能量和物质能够尽可能迅速高效的通过他们。更好的流体结构,不管它们是动物还是河流,将取代那些较差的结构。
如何设计出一种结构以促成流经这个结构的用户请求能更有效地获得响应呢?很显然,多个组件发生任意关联的方式肯定是不经济的,熵值副作用很大,如果变成以下这种结构,组件能够实现分组,三个元素是一组,另外一组是四个元素,组与组之间通过一个代表元素关联:
单一职责功能是关键,意味着只做一件事。它与让事情 DRY 原则是一致的:每一个知识都必须有一个单一的、明确的、在一个系统内的权威明确表示。不要重复,需要干脆。程序中的每一个重要的功能都应该在源代码中的一个地方实现,将业务规则、长表达式,if 语句、数学公式和元数据等各自放在一个地方。单一职责也与委托原则有关,只有放弃一些才能获得一些,有舍有得,放弃的就是委托其他类实现:不要自己做所有的事情,可以委托给相应的类去完成。因为每个组件都秉持单一职责,组件之间才可能发生唯一的一个关联关系。
高凝聚意味着模块的复杂性降低。因为变得有条理,复杂性自然降低。组件之间的强关系用耦合表达,保留下好的强关系也就是高凝聚,去除不好的强关系也就是低耦合。高凝聚、低耦合是降低复杂性提高灵活性设计中重要的宗旨。
除了静态的结构关系以外,高凝聚、低耦合还表达为对象的方法行为上,这需要通过分配职责来实现。什么是职责?它包括三个方面:
1) 对象应该执行的动作;
2) 对象包含的知识如:算法 约束 规格 描述;
3) 当前对象影响其他对象的主要因素。
以报童向买报人收钱为例:报童应该向顾客收取两块钱的买报费,他是直接把顾客的钱包拿过来从中掏出两块钱,还是请求顾客自己从钱包里掏出两块钱呢?无疑是后者。
这两者的区别是什么?Tell, Don't Ask 原则:
报童只要告诉顾客做什么,而不直接参与怎么做(你的钱够吗?够才能买)。报童只给顾客一个命令,而不必关注顾客是如何执行这个命令。Tell, Don't Ask 原则能够让我们保证两个组件之间在动作上不会发生过于细节过多的耦合,而只是通过一个消息就能完成,通过发送消息告诉对方我需要什么,而不是直接把对方拎过来搜身。至此,完成了两个组件之间最大松耦合,实现了架构设计的最大可能的灵活性。
软件架构复杂性必然如自然界任何事物一样不断发展,作为软件架构师如何学习大自然的巧妙设计,如同庖丁解牛一样切分复杂性为单一职责,再从结构和行为的高凝聚关系方面进行组合或消息传递,从而真正实现高凝聚、低耦合的灵活性目标。
四. 典型架构对比分析
系统架构跟随技术的发展不断升级和改进,从传统的单一架构演变为分布式、微服务架构、Serverless 架构,以下列举了主要的四种软件架构以及它们的优缺点。
1. 单体应用架构
单体架构的应用开发简单,易于更改,测试、部署简单及便于横向扩展。但随着需求不增加,管理成本也不断提高,代码库飞速地膨胀。慢慢地单体应用变得越发臃肿、复杂、不可靠,可维护性、灵活性逐渐降低,维护成本越来越高。单体程序陷入单体地狱,开发变得缓慢和痛苦,敏捷开发和部署已经不可能。应用变更后,开发团队将其更改提交到单个源代码仓库,从代码提交到生产环境的路径漫长而艰巨。
问题点:
过度复杂:以一个百万行级别的单体应用为例,整个项目包含的模块非常多、边界模糊、依赖关系不清晰、代码质量参差不齐、混乱地堆砌在一起,可想而知整个项目非常复杂。每一次更改都会让代码库变得更复杂、更难懂。应用一步一步地成为一个巨大的、令人费解的“脏泥球”。
开发速度缓慢:IDE 工具使用变慢,构建一次应用需要很长时间,应用太大导致每启动一次都需要很长时间。从编辑到构建、运行再到测试这个周期花费时间越来越长,严重影响团队的工作效率。
部署频率低:代码增多,构建和部署的时间也会增加。每次功能的变更或缺陷修复都会导致需重新部署,全量部署耗时长、影响范围大、风险高,使得应用上线部署的频率较低,出错率较高。
扩展能力受限:单体应用只能作为一个整体进行扩展,无法根据业务模块的需要进行伸缩。
可靠性差:程序体积庞大而无法进行全面和彻底的测试,意味代码中的错误进入生产概率大。所有模块都在同一进程中运行而缺乏故障隔离,一个模块中的错误将导致所有实例崩溃。
阻碍技术创新:体现在团队必须长期使用一套相同技术栈,采用新的框架和编程语言变得极困难。采用或尝试新技术极其昂贵和高风险,因为应用必须被彻底重写。
2. 分布式体系架构
随着业务的深入,要求的产品功能越来越多,业务模块逻辑也变得更加复杂,使得单体应用变得越发臃肿,可维护性、灵活性降低,开发周期越来越长,维护成本越来越高。此时需要对系统按照业务功能模块拆分演变成一个分布式系统,将业务模块部署在不同服务器上,模块之间通过接口进行数据交互,通过负载均衡结构提升系统负载能力。
架构特点介绍
降低耦合:把模块拆分,使用接口通信, 降低了模块之间的耦合度;
责任清晰:把项目拆分成若干个子项目,不同的团队负责不同的子项目;
扩展方便:增加功能时,只需要再增加一个子项目,调用其他系统的接口即可;
部署方便:可以灵活的进行分布式部署;
提升代码复用:采用分布式服务方式构建公用服务层,减轻开发量。
缺点:系统之间交互使用远程通信,接口开发工作量增大。
3. 微服务化架构
参考博客:【微服务架构及ServiceMesh技术框架介绍】
由松耦合和具有边界上下文的元素组成,是将应用程序功能性分解为一组服务的架构风格。每个服务都是由一组专注的、内聚的功能职责组成,使用服务作为模块化的单元。服务的 API 为它自身构筑了一个不可逾越的边界,无法越过 API 去访问服务内部类。可为应用程序提供更高的可维护性、可测试性和可部署性,同时提升了开发效率、应用可扩展性等。服务的本质是围绕业务而非技术问题进行组织的,反应业务语义、自包含、无状态、跨边界。
定义微服务架构
使用更抽象的系统操作概念将应用的需求提炼为各种关键请求,系统操作是描述服务之间协作方式的架构场景;然后确定如何分解服务,有几种策略, 一种是定义与业务能力相对应的服务, 另一策略是围绕领域驱动设计的子域来分解和设计服务,每一服务都有它自己的领域模型。策略围绕业务而非技术概念分解和设计服务,策略的结果都是一样的:一个包含若干服务的架构,是以业务而不是技术概念为中心;最后是确定每个服务的 API。
将标识的每个系统操作分配给服务,服务可独立地实现操作或可能需与其他服务协作。服务的分解需考虑网络延迟, 自包含,跨系统边界的数据一致性等,使用领域驱动设计中的概念来消除所谓上帝类。每一个系统操作的行为都通过领域模型的方式来描述,每一个重要的系统操作都对应着架构层面的一个重大场景,是架构中需要详细描述和特别考虑的地方。
进行微服务拆分
领域驱动设计是构建复杂软件的方法论,这些软件通常以面向对象和领域模型为核心。领域模型以解决具体问题的方式包含了一个领域内的知识。它定义了当前领域相关团队的词汇表,DDD 也称之为通用语言。领域模型会被紧密地映射到应用的设计和实现环节。在微服务架构的设计层面,DDD 有两个特别重要的概念,子域和限界上下文。
传统企业架构建模方式往往会为整个企业建立一个单独的模型,会有适用于整个应用全局的业务实体定义,例如客户或订单。这类建模方式会带来譬如一致性、复杂性及不同团队使用上的混乱等问题。DDD 采取完全不同的方式,定义多个领域模型来避免,每个领域模型都有明确范围,领域驱动为每一个子域定义单独的领域模型。子域是领域的一部分,领域用来描述应用程序问题域的一个术语,识别子域的方式跟识别业务能力一样。DDD 把领域模型的边界称为限界上下文,限界上下文包括实现这个模型的代码集合。在微服务架构中,每一个限界上下文对应一个或者一组服务。每一个子域都有属于它们自己的领域模型。
微服务架构好处
1) 使大型应用可持续交付和部署
持续交付和持续部署是 DevOps 的一部分,DevOps 是一套快速、频繁、可靠的软件交付实践,高效能的 DevOps 组织通常在将软件部署到生产环境时面临更少的问题和故障。微服务架构通过以下三种方式实现持续交付和持续部署:
拥有 CI 和 CD 所需的可测试性: 自动化测试是持续交付和持续部署的一个重要环节。每一个服务都相对较小,编写和执行自动化测试变得很容易,应用程序的 bug 也就更少。
拥有 CI 和 CD 所需的可部署性: 每个服务可独立于其他服务进行部署。如负责服务的人员需要部署对该服务的更改,无需与其他人员协调就可进行。因此,将更改频繁部署到生产中要容易得多。
实现团队的自治(自主且松耦合): 将组织构建为一个小型团队集合。每个团队负责一个或多个服务的开发和部署, 可独立于所有其他团队开发、部署和扩展他们的服务,提升了开发效率。
2) 服务较小且易维护
每个服务都较小,开发者更易理解,较小规模的代码库可提升开发者的工作效率,而整个应用是由若干微服务构建而成,所以也会被维持在一个可控状态。快速启动的服务能提高效率,加速研发过程。
3) 服务可独立扩展
支持不论是采用 X 轴扩展的实例克隆,还是 Z 轴扩展的流量分区方式。每个服务都可部署在适合环境。
4) 更好的容错性
可实现更好的故障隔离。如某个服务中的内存泄漏不会影响其他服务,其他服务仍可正常地响应请求。
5) 技术栈不受限
可结合项目业务及团队特点合理选择技术栈。这跟单体架构是完全不同,单体架构之下的技术选型会严重限制后期新技术的尝试。
微服务架构弊端
服务拆分和定义的挑战:服务的拆分和定义更像是一门艺术。如果对系统的服务拆分出现了偏差,很有可能会构建出一个分布式的单体应用:一个包含了一大堆互相之间紧耦合的服务,却又必须部署在一起的所谓分布式系统。将会把单体架构和微服务架构两者的弊端集于一身。
分布式固有复杂性:系统容错、网络延迟、数据一致性、分布式事务等都会带来较大挑战,服务必须使用进程间通信机制。此外,必须设计服务来处理局部和远程服务不可用/高延迟的各种情况。
运维要求较高:单体架构中只需保证一个应用的正常运行,而在微服务中需要几十甚至几百个服务的正常运行与协作,给运维带来了挑战。要成功部署微服务,需高度自动化的基础设施。
协调更多开发团队:当部署跨越多个服务的功能时需要谨慎地协调更多开发团队,必须制定一个发布计划,把服务按照依赖关系进行排序。这跟单体架构下批量部署多个组件的方式截然不同。
接口调整成本高:服务间通过接口通信。如修改某一微服务,可能使用了该接口的微服务都需调整。
重复性工作可能:很多服务可能会使用到相同功能,而该功能并未达到分解为一个微服务程度,可能会导致对这一功能的重复开发,尽管可使用共享库来解决但需面对多语言环境问题。
微服务架构已成为任何依赖于软件技术的企业业务的重要基石,是一把好处和弊端共存的双刃剑。在使用微服务架构时,一些问题无法回避,每个问题都可能存在多种解决办法,同时伴随着各种权衡和取舍,并没有一个完美的解决方案。
4. Serverless 架构
低运营成本,简化设备运维,提升可维护性,更快的开发速度等。
参考博客:【云计算的可信下半场-无服务器 Serverless】
五. 服务化构架之可信谈
说明:对本章节的理解需要一定的微服务化架构基础知识。
参考博客:【微服务架构及ServiceMesh技术框架介绍】【ServiceMesh-服务注册发现的应用适配】
<a href="http://3ms.huawei.com/km/blogs/details/8396531?l=zh-cn">【ServiceMesh-Istio 流量速率限制介绍】 【云原生零可信网络下 - 应用服务的安全】
1. 注册中心与配置中心
很多人经常把注册中心和配置中心混为一谈,有这种观点的认为服务注册数据其实就是配置的一种,这样解释也不无道理,的确注册中心的数据是配置的一种。但注册中心之所以独立存在,那是因为注册中心的数据有一定的业务独立性,是为了描述微服务相关。注册中心完全不依赖配置中心,而是一个独立的、高可用、数据一致的系统。设计意识形态上要清晰注册中心和配置中心的用途实质,区分是服务的注册/发现,还是配置的登记与更新、通知。
注册/反注册:保存服务提供者和服务调用者的信息;
订阅/取消订阅:服务调用者订阅服务提供者的信息,支持实时推送。
2. 业务与技术面的解耦
模块间交互越多,其耦合性越强,同时表明其独立性越差。耦合性是对模块间关联程度的度量,是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。耦合的强弱取决于模块间接口的复杂性、调用方式以及通过界面传送数据的多少。
需要将业务和服务治理中间件能力分离,否则异构的业务必然带来服务治理能力的非标准化。应用与技术面的解耦可带来应用实现的语言无关性(Java/Go/Python/C++等), 应用升级维护的无感知,以及模块的扩展性、稳定性等。
应用与治理分离,业务应用和治理能力的就近物理切割。通过部署本地代理的方式来完成这个切割。
强调执行和控制的分离,也即控制平面和数据平面的切分。实现对业务进程的零/少侵入原则,将服务治理能力看待为协议栈的一部分。
实践准则:
业务应用侧与服务系统平台内的各个模块的关系应该保持松耦合。
避免业务应用侧与数据面通讯模块的耦合,与通讯相关的功能应交由数据面通讯模块负责。在服务交互层面上,保持业务应用的轻形态,深入贯彻好信息专家模式。
避免业务应用侧的服务交互策略处理模式与注册中心分离。
避免业务应用侧进程内状态事件监测与传播的能量消耗的放大。
业务应用系统的敏捷化催生了开发的语言、技术实现的无关性,保持接口的兼容性。
……
3. 服务化的数据模型
系统关键数据模型,数据对象的生命周期等在设计之初就需明确和具有较为清晰的定义。数据模型是系统架构设计的重要组成部分,对模型对象的定义和使用,反向亦可折射出系统的架构设计的优劣。
服务对象模型是对服务的自描述,包含服务基础信息,环境信息、服务能力及 Qos 等。一个服务对象代表了一个服务整体,尽可能保持其无状态、自包含特征。对于有状态的服务可以建立数据存储(内存/SSD 磁盘)关联。
服务对象模型为业务相关性,对于基础技术的能力也可发布成为一类服务,保持数据对象模型的一致,两者的区别在对象属性和操作特征上。
服务对象模型要保持原子性,如果硬性拆分成多个子对象且在系统内的不同场景使用,譬如拆分成:服务基础对象、服务交互对象、业务领域对象,控制策略对象等各个模型。必然给分布式系统的整体带来复杂性、稳定性问题。也使得模块间的耦合程度加大。
4. 服务的注册与注销
服务的本质反映业务语义,可包括多个方法,具有自包含特征。如订单服务包含有查询订单、新增订单等。每个服务实例需向注册中心来注册和解除注册,而注册中心主要起协调者的作用,可借助注册中心来发现已登记并可使用的服务。
服务的注册应该是极简模式, 意指服务实例只需完成注册、维持保活即可。需要对外提供服务的接口需要注册,进程内交互的无需对外发布。
业务应用侧将系统内的服务成功注册到注册中心,即代表了服务的生命周期的开始,宣告了业务应用侧的微服务接口的可用但并非可达。边车模式下还需等待进行服务发现。
对于服务使用方来说,需声明其需要的服务,及对其自身在该服务上的使用方式进行定义或默认策略。对于服务提供方,则需要定义服务的基础信息和能力模型(协议、并发、编解码,信息大小等等),消费方的能力受限于服务提供方的能力输出。
基于业务领域服务和服务的交互形态,要明确哪些服务需要注册,是否反映了业务语义。登记注册在注册中心的信息必定是需要发现的关键资源,如果是来自业务应用侧的技术性的登记注册,应该从对象模型形态上构建关联,作为服务的属性(静态、动态)。
注册中心属于关键核心基础件,保障注册中心的稳定、可靠、高性能不仅需要其自身的优化,也需要关联方的最佳使用方式,在最合理的方式上来产生交互和信息传递。
业务应用与注册中心的直接交互为最佳实践,其它则会以牺牲简易、扩展、稳定为代价。
……
5. 服务的平台发现模式
服务消费方与服务提供方具有动态特征,需要登记到注册中心,包括两者的地址等环境信息。
客户侧服务发现模式,业务应用侧既要进行服务的注册,也要承担服务发现的职责。在分布式体系的生产集群环境,所有的业务进程都会存在与注册中心的 Watch 订阅,获取注册中心的服务关联方信息的推送。缺陷为订阅/推送的点过多,注册中心也需要为此产生一定的资源消耗,关联的点越多,消耗越大。
在通讯边车模式下,服务交互、路由等需经过数据面来处理,可由控制面适配层来提供服务发现,服务端和客户端不必承担服务发现职责。相比客户侧发现模式,订阅及信息推送的量只有原来的 1/N,大幅、有效减轻了注册中心压力。(服务对象模型要保持唯一性,避免注册信息与通讯模型对象的分离),同时,也进一步解耦业务应用侧与其它模块。
对于需要从注册中心获取登记的基础服务(譬如存储服务), 可结合查询方式和保底原则,减少不必要的向注册中心的订阅与事件触发后的信息推送。
6. 控制面适配层的职责
控制面适配层的职责在于对接通讯边车模块,实现控制指令下发、路由、负载、流控、熔断、安全等与服务治理相关的策略的管理。
与数据控制面适配层交互的模块包括:注册中心,各 POD 内的通讯边车模块进程,状态监控模块,管控(控制面)模块。与注册中心交互进行服务发现及实现与服务生命周期相关的操作;从管理控制面获取管控策略及安全策略;从状态监控模块获取业务应用侧事件状态并联动业务侧边车模块。
控制面适配层模块,业务应用侧程序两者应无实质性直接交互,要实现解耦。如产生耦合则会增加系统的复杂度。解耦方式可通过借助 API 接口来实施或委托隔离。
订阅全局服务信息并进行集中控制,需确保控制面适配层的可靠、稳定性,无单点问题。
对于策略、控制信息等需要确保可靠下发。
7. 服务调用与数据通讯
业务应用程序将业务领域服务,包括服务基础信息、实例环境、服务能力(Qos 等)向注册中心登记,即开启了服务的生命周期。通讯的方式承载了服务与服务之间的交互,好的构架应该将数据面通讯模块的交互关系降到最低。
服务注册完毕即对外宣告了其微服务组件的声明式 API。在边车模式的体系架构下,服务的调用需要数据面提供的信息路由,将请求提交给服务提供方来处理。
服务的交互是面向接口的调用,实例名信息、类别(请求方|提供方等)、环境信息等用于跨进程的寻址,而接口方法名用于进程空间内的回调,名称的可理解性、辨识性要好于数字标识。简单原则,能只依赖静态信息的绝不使用动态特征,减少系统的可变性依赖、耦合,特别是分布式系统场景,要减少子系统与子系统,模块与模块之间的耦合依赖。
通讯链路的设计原则,应该致力于将能量消耗减少到最低限度,将链接精准建立在必须的场景。避免链路的密集交互,大量的文件句柄,取代那些较差的结构。提升系统的整体稳定性,以及减少了分布式系统整体的资源消耗,包括计算、内存及带来的过多消耗导致的垃圾回收异常问题等。
数据通讯模块要确保高内聚、低耦合,主要职责在服务信息路由、负载均衡调度等服务治理相关。确保职责单一,要与业务应用侧尽可能的解耦。如果业务应用侧兼有路由控制则导致通讯控制职责分散,给系统带来复杂性,同时不利系统横纵扩展、不利业务实现的敏捷、不利高效交付等。也要避免引入与注册中心、状态监控模块的强关联。
外部系统可通过在服务注册中心获取服务的基础信息,或面向契约式地址的交互。通过数据面的路由抵达业务应用侧,可集中实施安全、审计、负载、流控等控制操作。
服务交互,通讯模式需要依据场景来提供同步、异步,请求的单向与双向等,在协议上可以考虑 HTTP, gRPC, REST,TCP/IP 等的最佳适配,协议的使用做到可插拔。
通讯类型、模式、形态等尽可能保持一致,简化对外的操作形态,特别针对业务应用侧。提升系统对外接口操作的透明度,将处理细节封装在接口内部。
链路的可用性检测,隔离、熔断等服务治理操作职责尽可能限定在数据面的边车模块。
. . . . . .
8. 服务的治理、策略
服务治理是个非常大的话题,真的要铺开来讲,几篇文章的篇幅都谈不完。这里先简单地看一下服务治理要解决的问题。微服务化通过将复杂系统切分为若干个微服务来分解和降低复杂度,使得这些微服务易于被使用和维护。微服务的连接、服务注册|发现、路由、负载均衡、服务熔断、隔离,服务限流、降级,访问控制(认证、鉴权)、监控(日志、链路追踪、预警等)、AB 测试,金丝雀发布等等,这些即是服务治理的内容。
在这里想说的是,每项服务治理的内容,都需要进行充分设计。需要清晰定义治理的目标,数据模型,治理策略的下发,治理实施的最佳控制点(确保职责单一)及联动模块,最佳的控制流、事件信息流,治理的关键路径(降低耦合),治理期间的周期性活动,治理的边界,治理对各个模块的侵入最小化,治理对资源的消耗,治理的总体可控,接口使用透明等等。治理是服务的治理,从数据模型上尽可能与服务对象模型融合。
服务流控:流控的能力必须要有但要慎用。流控要体现在服务面上(某种程度上反映了服务提供方的能力限定),策略模型与服务领域模型应该是依附关系,策略(令牌、速率、并发度等)及管控指令的下发应该通过数据控制面适配层来推送;流控的最佳集中控制点在数据面的通讯边车模块(可感知提供方的受压-主动/被动,可反压传导至服务请求方);减少业务应用侧的耦合,应该通过接口形态反压传导到业务应用程序并联动控制;可以支持直接业务应用侧的流控手段,但前提是与平台内基础模块的解耦。
通过加入埋点、数据监控,能及时发现系统出现的问题,及时预警,及时介入处理。
链路检测、跟踪应以端到端的形态体现,需要解耦应用侧与通讯数据面。
日志处理以 Node 为汇聚点(Pod, Container,进程)统一归并后流式归集,实时分析。
以多点,多维度,多形式进行系统平台内的指标监控、收集,健康检查等。
监控系统(状态、事件),其它(存储服务):解除节点内与节点外监控点的关联耦合。
. . . . . .
9. 高可用状态监测
监测分布式系统内的各个子系统,模块,组件的状态,用以异常控制、预警联动,故障隔离与迁移恢复等等。
单一 Node 内的状态检测由 Node、容器级别的监测代理集中采集上报,同时避免状态信息在 Node 级代理间传播,基于结构化本质,也无需构建 Node 级的全互联链路。如此,加强了事件监测状态在 Node 内的内聚和 Node 间的松耦合,极大减少资源消耗。
Node 监测的事件状态上送到平台内状态归集中心集群节点(N+1), 并作异常事件检测和规则触发后的预警。对于异常事件状态,以及触发的点、面需综合权衡设计,将关联影响方降到最低以最小化事件扰动,原则应交由对异常事件具有绝对控制职责的模块且在关键点来决策联动。以致力于将能量消耗减少到最低限度,同时将消耗单位能量产生的熵提高到最大限度。
为区分事件的来源于 Node、Container、进程,可引用事件状态机与即时探测模式。
为进一步解耦应用与技术面,通讯路由、信息传输应集中在剥离的通讯子系统解决。
系统运行时关注在异常事件及处理,要规划好分布式架构内的操作控制流、数据流及其关键路径,避免把正常事件当作常态化处理模式,避免数据的全量广播,避免系统的设计过度、冗余化可能,避免给整体架构带来复杂性,以至降低了系统稳定性、可靠性。
. . . . . .
10. 服务发布版本与灰度
服务相对较小,以服务为单元的测试、发布、编排及部署、升级会变得更容易、可靠。每个服务可独立于其它服务进行部署,因此,将更改频繁部署到生产中要容易得多。确保服务的领域数据模型的唯一性,基于版本的控制可强化服务以业务为语义及利于其生命周期管理。服务发布,迁移控制等所涉及的数据对象模型需体现在服务对象模型上。
快速可靠地迭代与交付软件,提升交付的敏捷以及故障恢复效率。传统组织中部署频率低,交付的时间很长。相比之下,DevOps 组织经常发布软件,生产环境问题要少得多。
灰度发布,服务实例迁移尽可能实现自治,减少对其它模块的过多耦合依赖。
减少操作控制、目标状态的不必要的传播、交互的环节,贯彻简单至上原则。
降低控制流,策略传递,信息传递,事件通知等等的用力面、传播面,设定最佳处理点。
交互操作的用语尽可能与服务相关用语具有区分性。
11. 资源高利用率、低使用率
自身依赖组件/模块是否与系统自身集成部分重复,应以复用优先、资源利用最大化为原则。
平台内、模块间的操作控制、事件/状态触发、数据传输,接口调用等,交互环节简单化。
给予分布式系统内关键/核心模块、子系统充足的资源,避免成为全局热点。
资源的低使用率要从顶层架构上来设计,消除无用模块、组件或通过归并方式达到资源分配的最小化、利用率最大化。
分布式系统内运行时的资源消耗,要规划、建立运行时模型,涉及控制、数据、资源等等。
避免分布式集群环境内的全网/区域全交互的订阅,数据推送,事件广播模式等。
… …
六. 架构设计衡量与愿景
好的软件架构设计是产品质量的保证,特别是对于客户常常提出的非功能性需求的满足,不好的架构是对资源的浪费(人力、物力等)。成功的软件架构设计必须遵循一定的原则、模式,失败的软件架构设计总是由于一些不确定的因素导致。
如果一个软件开发程度在 70%以上的情况下,加入一个新功能还需涉及到大量的文件、代码的修改,那这个软件架构一定很烂,而好的架构此时应该已经完成大部分底层组件的开发且相互独立,加入的大部分新功能基本上是原有组件的功能组合(不涉及内部的修改),以及加入新功能特有的独立组件。
如果每加入一个功能就有大量的修改提交,那么这个架构的质量,你懂的!
1. 架构设计的衡量
架构为业务服务,没有最优的架构,只有最合适的架构。架构始终以高效、稳定、安全等为目标来衡量其合理性。
业务需求的角度
能解决当下业务需求和问题;
高效完成业务需求,能以优雅且可复用的方式解决当下所有业务问题;
前瞻性设计,在未来一段时间都能以高效方式满足业务,从而不会每次当业务进行演变时,导致架构翻天覆地的变化。
非业务需求角度
高可用:尽可能提高软件可用性,通过黑白盒测试、单元、自动化、故障注入测试、提高测试覆盖率等来一步一步推进;
文档化:整个生命周期内都需做好文档化,变动来源包括但不限于 BUG,需求等;
可扩展:软件的设计秉承低耦合的理念去做,在合理的地方抽象。方便功能更改、新增和运用技术的迭代,并且支持在适时对架构做出重构;
高复用:为了避免重复劳动,降低成本,希望能够复用之前的代码、设计。这点对于架构环境的依赖是较大的;
安全:组织运作过程中产生的数据都是具有商业价值的,保证数据的安全也是刻不容缓的一部分,以免出现 XX 门之类丑闻。加密、HTTPS 等为普遍手段。
软件架构应该是拥抱变化,性能稳定,易于维护。
可伸缩性: 服务是可扩展的,并且扩展的成本是比较合理的。当服务的负载增长时,系统能被扩展来满足需求,且不降低服务质量;
高可用性: 尽管部分硬件和软件会发生故障,整个系统的服务必须是 7 *24 小时可用,可通过软件和硬件的冗余来实现;
可管理性: 整个系统可能在物理上很大,但应该容易管理,需要开发对应的管理工具;
价格有效: 架构设计需要考虑 ROI 因素,整个系统的实现是经济的、易支付的。如果一个架构很好,但成本高得惊人,那不一定是合适的架构。
对系统架构优劣的评价参考
1) 系统性能
2) 可靠性(容错/健壮性)
3) 可用性
4) 安全性
5) 可修改性(可维护、可扩展性,结构重组,可移植性)
6) 功能性
7) 可交互性
2. 逆向的康威定律
对于大型和复杂的应用程序,微服务架构往往是最佳选择。然而,除了拥有正确的架构之外,成功的软件开发还需要在组织、开发和交付流程方面做一些工作。下图展示了架构、流程和组织之间的关系:
为了能在使用微服务架构时能有效地交付软件,需要考虑康威定律。组织架构和系统架构之间有一种隐含的映射关系,设计系统的组织其产生的设计等价于组织间的沟通结构。因此反向应用康威定律并设计你的企业组织,使其结构与微服务的架构一一对应。这样可确保开发团队与服务一样松耦合。
若干小团队的效率显然要高于一个单一大团队。微服务架构使得团队可以实现某种程度的“自治”。每个团队都可以开发、部署和运维扩展他们负责的服务,而不必与其他团队协调。更进一步,当出现了某个服务故障或没有满足 SLA 等要求时,对应的责任人也非常清楚。而且开发组织的可扩展性更高,可以通过添加团队来扩展组织。如果单个团队变得太大,则将其拆分并关联到各自负责的服务。由于团队松散耦合,可以避免大型团队的沟通开销。因此,也可以在不影响工作效率的情况下添加人员。
3. 架构设计的愿景
系统架构能描述软件的整体且包括软件的各个方面,但每一个设计细节总是需要单独考虑,这时候就会出现设计细节之间、以及设计细节和架构之间的不一致。架构设计的各个部分之间的设计冲突是很容易发生的,发生的概率及频率和团队的规模成正比、和沟通的频度及效果成反比。如不同模块间的设计冲突导致了软件无法正常运行时,我们就需要坐下来好好的审视,究竟发生了什么。
建立一个架构愿景,提供软件全局视图,包括所有重要部分,定义各个部分的责任和之间的关系以及设计需要满足的原则。而这个愿景的设计源自需求,那些针对系统基本面的需求。比如说,系统特点是一个交互式还是一个分布式系统,这些需求将会影响到架构愿景的设计。同时,架构愿景也要满足其它各种特点,譬如简单、可扩展性、抽象性。简单来说,把架构愿景当作一个 mini 的架构设计,由于是在单次迭代中讨论架构愿景,因此从整体上考虑,架构愿景是也是在不断的变化的。因为架构愿景代表了架构的设计,架构愿景的演进代表了架构设计的演进。
架构的愿景是相对于一个范围来说的,在一个特定软件功能范围之内谈架构愿景才有实际的意义,例如针对软件的全局或某个子模块。在这个特定的范围中,订立了架构愿景之后,这个范围内的所有设计原则将不能违背架构愿景。这是非常重要的,是架构愿景的最大的用处。有了这样的保证,就可以保证设计的一致性和有效性。任何一项设计的加入,都能够融入到原先的架构中,使得软件更加的完善,而不是更加的危险。
从整个开发周期来看,全局架构愿景是随着迭代周期的进行不断发展、修改、完善的。子模块级、或是子问题级的架构愿景本质上和全局的愿景制定差不多,不能够和全局愿景所违背
在操作上,全局愿景是设计团队共同制定出来的,而子模块级的架构愿景就可以分给设计子团队来负责,而其审核则还是要设计团队的共同参与。以确保各个子模块间不至于相互冲突或出现空白地带,且每个子设计团队可从别人那里吸取设计经验。一般来说,设计团队取得了一致的意见就可确定全局架构愿景工作的基本完成。
子模块(子问题)间的耦合问题。一般来说,各个子模块间的耦合程度相对较小,而子问题间的耦合程度就比较大,如权限设计、财务等功能会被每个模块使用。那么就需要为子模块制定出合同接口,意指接口是正式的,不能随意修改,会被其它设计团队使用,如修改将会对其它的团队产生无法预计的影响。合同接口的制定、修改都需要设计团队的通过。此外,系统中的一些全局性的子问题最好提到全局愿景中考虑。
七. 构架系统的感与悟
1. 架构设计的常见误区
架构专门由架构师来做,业务及开发人员无需关注;
过早做出关键性决策;
架构设计企图一步到位,世上没有最好的架构,只有最合适的架构,不要企图一步到位;
为虚无的未来埋单:某种程度上来说不要过多考虑未来的扩展,说不定功能做完后效果不好就无用了。如业务模式和应用场景边界都已比较清晰,那应该适当的考虑未来的扩展性设计;
遗漏关键性约束与非功能需求;
高开高走落不到实处;
埋头干活儿缺乏前瞻性;
为了技术而技术:技术是为业务而存在,除此毫无意义。在技术选型和架构设计中,脱离实际而一味追求新技术,可能会导致架构之路越走越难。成本、时间、人员等各方面都要综合考虑;
架构设计无需考虑系统可测性。
2. 架构大方向必须正确
架构设计是软件成败的关键环节之一,决定了软件的整体质量进而决定了客户的满意度。同时也决定了软件的可扩展性、可维护性、稳定性以及性能等方方面面。架构大方向是概念架构设计,设计了正确的概念架构,表明软件架构设计已经成功了一半。一个产品与其它同类产品在概念架构设计上的不同决定了软件架构后续的发展导向。概念架构设计不关注具体的接口定义和实现细节,主要工作在于总体架构模式、技术选型等方面,是软件架构设计轮廓性规划和总体指导策略。
架构的方向性错误将导致后续设计、开发迭代的不稳定及复杂性,稳定性、扩展性等的缺失,基础的重构也将导致资源的过多、不必要的消耗,这也是项目团队所不愿意面对的。
3. 关于系统的重构参考原则
重构是基于已发现问题和潜在问题进行修正,也可说是一种还债行为,用更好的方式、方法来纠正以前代码和设计中存在的问题,同时也是最大化减少产品发生问题的可能,让系统减负运行。如代码优化,剔除重复、违背代码规范、模块耦合问题等。可将整个系统分成很多个子模块,以对各个子模块各个击破,最终完成对整个系统的重构,分而治之。
确保重构行为仅针对那些有重构需要的设计,需求的变更或对原设计进行改进以得到优秀简洁的设计实现。对于一段凌乱的代码,如不需要修改它就不需要重构。只有当你需要理解其工作原理时,重构才变得有价值,如果重写比重构更加容易,那就无需重构。对于架构来说,可近乎等价的认为只是在外部接口不变的情况下对架构进行改进。而在实际的开发中,除非非常有经验,否则在软件开发全过程中保持所有的软件接口不变是一件非常困难的事情。
重构是一种优秀的代码改进方式,追求不重复的代码虽然难做到,但是其过程却可以有效的提高开发团队的代码质量,每次对代码进行的迭代改进,促进了系统简单的实现。在团队中提倡使用、甚至半强制性使用重构,有助于分享优秀的软件设计思路,提高软件的整体架构。重构还会涉及分析、设计模式、优秀实践的应用。同时,重构还需要其它优秀实践的配合,譬如代码复审和测试优先等等。
4. 系统重构的目标要清晰
重构的诉求、目标要明确,解决什么问题,为什么会出现这些问题,如何解决。解决的方式彻底吗? 要站在更高的角度看待问题,架构高度、能力决定思维、方式、方法。 如果解决一个问题会带来更多问题的设计和开发有必要做吗? 局部的优化可能招致全局受损,在瓶颈之外的任何优化提升都只是幻象。如果资源、人力的消耗在无用功上,表象上是大家多么努力,腐化架构、平台的工作,那是对公司的不负责、对个人人生的不负责。
5. 架构团队评审制度的必要性
团队设计的理论依据是群体决策,与个人决策相比,群体决策的最大好处就是其结论要更加的完整。群体决策需要额外付出沟通成本、决策效率低、责任不明确等。但群体决策如果能够组织得当,是能够在架构设计中发挥很大的优势的。
系统的设计需要召开团队评审会议进行评审,避免某个问题的设计改造腐化了系统并导致后续的开发、交付、稳定性存在更多隐患。复审是避免设计出现错误的重要手段,可在架构设计过程中引入复审的活动。复审应该着重于粗粒度模块/组件的分类和它们之间的关系。正如后续的重构和稳定性模式所描绘的那样,保持粗粒度模块/组件的稳定性有助于重构行为,有助于架构模型的改进。
6. 系统的重构是对原架构的检视
重构是对构架的合理性、前瞻性、业务敏捷适应性等的综合考量。对平台架构,特别是分布式系统架构,要站在整体、全局的视角高度来规划、重构。当发现系统关键基础模块、子系统出现问题(扩展性、稳定性、发布的简易性、资源消耗等等),要审视是否原有架构的设计是否合理,清楚在语言(C/C++/Go/Java/…)之上承载的架构的特征、需要规避的问题。架构问题要在架构层面去解决,宜早不宜晚。
7. 软件架构设计原则不容模糊
系统内各个代码模块的质量很重要,同样,分布式系统内各个子系统的职责边界、整体运行、协作效率,稳定性、扩展性、可靠性、容错性等也亦非常重要。软件设计原则及模式为最佳实践,要深入理解和掌握、遵守,但并非要盲从,原则的违背必然会有成本的付出,设计人员要意识这一点,并适时变通补偿。具体要结合实际工作中业务、时间、资源及团队情况来权衡。 另外,软件的设计原则是针对面向对象设计和编程提出,但并非只适合系统内部对象、结构,对于大规模系统架构仍然有其一定的适用性、可借鉴性。
8. 重构系统以减法思维优先
在软件系统设计、开发、重构等工程活动中,能做减法的绝对不要做加法。当系统存在问题、需求变化等,多挖掘系统本身的可能性,重构不等于添加模块,增加属于惯性思维,添加任何代码模块或其它都将导致新的问题出现和面对。要让系统的架构简单、清晰、可扩展、稳定、高内聚低耦合,资源利用率最大化,事件扰动最小化,....
要对代码做减法,要尽可能减少功能,如有疑问则将其删除/注掉。许多功能可能从未使用,只需为其留一个扩展接口即可。可结合系统需求,有效合理使用一些设计思想、模式可使得程序结构更加合理,代码更加清晰、消除冗余,减少代码的坏味道等等。
9. 减少平台架构内的动态性
动态的东西难于捕捉,系统运行时亦是动态。但对于初始能明确的使用方式,接口形态,基础数据、配置参数,交互方式等,要避免产生更多变化。譬如,系统在初始就可以声明约定明确某个信息的唯一性,但一定要在唯一基础之上产生唯一的 ID, 因为 ID 是动态生成的,后续对 ID 来产生强依赖必将导致系统可扩展性存疑,带来模块之间的强耦合性。
10. 规避系统架构的过度设计
架构设计时常会在某些方面过度设计,为了一些根本不会发生的变化而进行一系列复杂的设计,这样的设计就叫过度设计,往往会带来资源的浪费并且会增加开发的工作量或难度。系统需要考虑扩展性,可维护性等,但切忌过度设计。需要站在顶层设计高度来判断哪些设计是过度而规避之。
一个系统/平台的稳定常规来说都会经历一个震荡期,可能跨越数个迭代周期,但最后一定趋向平稳。如后续版本发布、商用投产仍未达到设计的平稳化,仍需不断进行重构才能适应需求,项目的失败注定只是时间问题。大的结构性方向错误必将导致后续的设计、开发迭代的复杂性,以及稳定、扩展性等的缺失,叠加更多的设计不合理可能。
11. 系统关联中间件的引入
项目引入中间件,不论该中间件是来自外部或内部自研,要对其功能有非常清晰的认识。在合理范围内使用,最小依赖原则,关键基础功能引入,对于高级特征的引入使用要权衡和综合评估。如果引入的中间件成为系统的关键支撑项,要最优化场景使用,同时要避免和警惕陷入使用误区(如某中间件提供核心功能为 A,却被弱化 A 而强化使用功能 B 职责)。中间件自身的依赖件是否与系统自身所集成部分重复,以复用为优先原则。
12. 重构的短期交付与演化方向
短期架构的重构如影响、腐化了整体架构,影响了扩展、稳定性等,要早发现早停止。架构的演变迭代要保持架构的大方向不变。架构的演变需要朝着职责单一、高内聚低耦合,以简单至上等原则去设计、演化和落地。
13. 架构的简单并非实现简单
说到这里,如果大家有一个误解,认为一个简单的架构也一定是容易设计的,那就错了。简单的架构并不等于实现起来也简单。简单的架构需要设计者花费大量的心血,也要求设计者对技术有很深的造诣。
14. 架构师职责并非止于蓝图交付
建筑设计师把设计好的蓝图交给施工人员,施工人员就会按照图纸建造出一模一样的大厦。可是,企图在软件开发中使用这种模式,这是非常要命的。架构师完全不去深入到第一线怎么知道“地”在哪?怎么才能将设计落的稳当。
架构设计师最易犯的一个问题就是设计和代码的脱离,即使在设计阶段考虑得非常完美的架构,在编码阶段也会出现这样或那样的问题,从而导致架构实现变得复杂。或者说,出现了坏味道,重构的技巧也同样有助于识别坏味道。让设计师参与核心代码的编写或进行代码审核,以确保编码者真正了解了架构设计的意图。
15. ....
架构设计的核心还是方法论,简单的设计并不等同于较少的付出。往往需要对现实世界的抽象,看似简单,但实现起来却需大量的业务和系统知识、很强的设计能力。因此,做到简单是程序员不断追寻的目标之一。
模式是一种指导,有助于做出一个优良的设计方案,达到事半功倍的效果。模式也是面向对象设计的基石,但模式常扮演着过度设计的角色。在设计之初少关注模式的适用,把精力放在如何满足需求上,而在设计迭代演进中重构到模式以扩展或演变为软件设计的基础,提升灵活性,避免导致过度或不充分的设计。
… …
八. 最 后
软件设计是门艺术,是门划分边界的艺术。
软件设计不只属于程序设计,更像是一种艺术创作的思维 …
优秀的架构设计需要架构的目的和方向,
需要架构设计师的统筹全局、深入需求,需要抽象、演化式架构设计思维,
需要架构设计师的不懈努力和对细节的把握,以及充分的、前瞻性的预见性,
需要数据模型的准确、完整、规范、一致以及标准化,
需要清晰的边界划分,需要模块的职责单一,需要信息专家模式,
需要系统的高内聚、低耦合,通讯链路关系的最简化,
需要致力于能量消耗低限度,将消耗单位能量产生的熵提高到最大限度,
......
软件系统的品质,更多取决于架构的优劣;
决于思维设计的高度与深度:抽象分治,高聚低耦,简单至上,模式加持,演化迭代, ...
可信构架,架构至简,唯美艺术 !
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/0f28cbac5c079d7c287ddb25c】。文章转载请联系作者。
评论 (3 条评论)