万字长文探讨可信构架之道(下)
四. 典型架构对比分析
系统架构跟随技术的发展不断升级和改进,从传统的单一架构演变为分布式、微服务架构、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-服务注册发现的应用适配】
【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.....
架构设计的核心还是方法论,简单的设计并不等同于较少的付出。往往需要对现实世界的抽象,看似简单,但实现起来却需大量的业务和系统知识、很强的设计能力。因此,做到简单是程序员不断追寻的目标之一。
模式是一种指导,有助于做出一个优良的设计方案,达到事半功倍的效果。模式也是面向对象设计的基石,但模式常扮演着过度设计的角色。在设计之初少关注模式的适用,把精力放在如何满足需求上,而在设计迭代演进中重构到模式以扩展或演变为软件设计的基础,提升灵活性,避免导致过度或不充分的设计。
… …
八. 最 后
软件设计是门艺术,是门划分边界的艺术。
软件设计不只属于程序设计,更像是一种艺术创作的思维 …
优秀的架构设计需要架构的目的和方向,
需要架构设计师的统筹全局、深入需求,需要抽象、演化式架构设计思维,
需要架构设计师的不懈努力和对细节的把握,以及充分的、前瞻性的预见性,
需要数据模型的准确、完整、规范、一致以及标准化,
需要清晰的边界划分,需要模块的职责单一,需要信息专家模式,
需要系统的高内聚、低耦合,通讯链路关系的最简化,
需要致力于能量消耗低限度,将消耗单位能量产生的熵提高到最大限度,
需要面向系统化组织的设计,需要团队的设计、协作…
......
软件系统的品质,更多取决于架构的优劣;
决于思维设计的高度与深度:抽象分治,高聚低耦,简单至上,模式加持,演化迭代, ...
可信构架,架构至简,唯美艺术 !
- 点赞
- 收藏
- 关注作者
评论(0)