衔接上文[理解REST] 03 基于网络应用的架构,上文介绍了一组自洽的术语来描述和解释软件架构;如何利用架构属性评估一个架构风格;以及对于基于网络的应用架构来说,那些架构属性是值得我们重点关注评估的。本篇在以上的基础上,列举一下一些常见的(REST除外)的适用于基于网络应用的架构风格,并使用对比架构属性的方式对其进行评估。
# 1 架构风格所产生的架构属性 {#style-induced-architectural-properties}
架构设计的目的是为了满足或者超出应用的需求,而不是为了创造出一种特殊的交互拓扑或者一种特殊的设计方式。当设计一个系统时所选择的架构风格,必须与这些需求保持一致,而不是相抵触。因此应该依据这些架构风格所产生的架构属性来对架构风格进行评估。架构属性是相对的,如果添加以一个架构约束,增强了某一个架构属性,也可能会消弱另外一个架构属性;此外,一个架构属性是被增强了还是被消弱了,也会受到系统实现的的影响。
下面几个小节来评估每一种架构风格,以及它们都会产生那些架构属性。(+)表示增强改善,(-)表示消弱,(±)表示取决于具体的场景。
# 2 数据流风格(Data Flow Style) {#data-flow-styles}
## 2.1 管道和过滤器(Pipe and Filter = PF) {#pipe-and-filter}
在PF风格中,每个组件(过滤器)会从输入端读取数据,并在输出端输出数据(亦可以增量处理,而不必等到全部处理完再交给下一个过滤器)。这里的架构约束是一个过滤器必须完全独立于其他的过滤器(零耦合)。多个过滤器这样头尾相连组合起来,就像是一个管道,所以称之为管道和过滤器风格(简称PF)。这种风格可以产生如下几个的架构属性 :
1. 简单性(+) : 可以把过滤器简单的组合起来。
2. 可重用性(+) : 任何两过滤器都可以链接在一起。
3. 可扩展性(+) : 可以新增过滤器到已有的系统中。
4. 可进化性(+) : 旧的过滤器可以被新的替代,只要其行为不发生变化。
5. 可配置性(+) : PF风格就像有一个看不见的手在组合过滤器的链接方式,来调整其整体的行为。
6. 用户感知的性能(±) : PF天生可以支持并发的执行,所以对此有改善作用。但是如果管道过长,且过滤器不支持增量处理,那么则可能会增加延迟,降低用户感知的性能。
具体的例子 : 比如linux shell,asp.net core中的middleware和filter等。
## 2.2 统一管道和过滤器(Uniform Pipe and Filter = UPF) {#uniform-pipe-and-filter}
UPF在PF的基础上,增加了一个所有过滤器都必须具有相同接口的约束。这个约束会在PF的基础上,产生如下的架构属性 :
1. 简单性(++) : 相同的接口的约束可以进一步增强PF的简单性。
2. 可重用性(++) : 相同的接口的约束可以进一步增强PF的可重用性。
3. 可配置性(++) : 相同的接口的约束可以进一步增强PF的可配置性。
4. 网络性能(-) : 相同的接口的约束需要对数据的格式做统一的转换,可能会降低网络的性能。
具体的例子 : 比如linux的标准输入输出流,asp.net core中的middleware也算是(没有跨越网络)具有相同接口的约束的UPF。
# 3 复制风格(Replication Style) {#replication-styles}
## 3.1 复制仓库(Replicated Repository = RR) {#replicated-repository}
通过利用多个进程来提供相同的服务,即为RR风格。这些多个分散的服务对于客户端来说,就好像是只有一个集中的服务。这种风格可以产生如下几个的架构属性 :
1. 用户感知的性能(++) : 可以明显的改善用户感知的性能。
2. 可伸缩性(+) : 可以通多增加或减少服务来调整服务的伸缩性。
3. 可靠性(+) : 得益于多个分散的服务,当某一个服务宕机之后不会对整体的运行造成多少影响。
具体的例子 : 比如GIT(分布式的版本管理系统本地有着完整的仓库副本),CVS(集中式的版本管理系统本地也是有完整的仓库副本的,只是写操作是需要网络的)等。
## 3.2 缓存(Cache = $) {#cache}
缓存风格是复制仓库风格的一个变种 : 复制个别请求的结果,以便被后面的请求复用这些结果。这种风格可以产生如下几个的架构属性 :
1. 网络效率(+) : 因为某些请求就近即可处理,则可以改进其效率。
2. 用户感知的性能(+) : 改善了效率,则用户感知的性能亦可得到改善,但是不如复制仓库风格的改善明显,毕竟也会有不少的请求没有命中缓存。
3. 简单性(+) : 由于只是简单的复制请求结果,实现起来简单的多,则其具备简单性的特点。
4. 可伸缩性(+) : 降低了到达真正的服务那里的比例,相当于增强了服务的可伸缩性。
具体的例子 : DNS缓存,CDN等。
# 4 分层风格(Hierarchical Style) {#hierarchical-styles}
## 4.1 客户-服务器(Client-Server = CS){#client-server}
服务器组件提供了一组服务,并监听对这些服务的请求;客户端组件通过一个连接器把请求发送给服务器。服务器可以拒绝这个请求,也可以执行这个请求,并把响应发送给客户端。服务端通常来说是一个永不终止的进程,通常是为多个客户端提供服务的。这个约束背后的原则是分离关注点(但是并不关心会话状态是在服务端还是客户端),这种风格可以产生如下几个的架构属性 :
1. 简单性(+) : 适当的功能分离可以简化客户端和服务端组件,使其只关注自身的职责。
2. 可伸缩性(+) : 简化了服务端组件后,从而可以提供服务端组件的可伸缩性。
3. 可进化性(+) : 只要通信接口不发生变化,客户端和服务端组件都可以独立的升级进化。
具体的例子 : RPC等。
## 4.2 分层系统(Layered System = LS) {#layered-system}
按照层次来组织,每一层都使用下层的服务,并且为其上层提供服务。层内部的细节对于相邻的层而已是完全被隐藏起来的。这种风格可以产生如下几个的架构属性 :
1. 可重用性(+) : 降低了各层之间的耦合,使得每一层都可以被复用。
2. 可进化性(+) : 在上下层接口不变的情况下,每一层都可以独立进化。
3. 可移植性(+) : 每一层得益于只关心其下层是否可用即可被移植,所以对移植性有改善。
4. 可伸缩性(+) : 简化每层组件的实现,有利于提升可伸缩性。
5. 用户感知的性能(-) : 分层处理增加了数据处理的开销和延迟,会降低用户感知的性能。
具体的例子 : TCP/IP协议,分层的网络协议栈。
## 4.3 分层-客户-服务器(Layered-Client-Server = LCS) {#layered-client-server}
LCS是在LS和CS的结合体。可以理解为在CS的基础上增加了代理组件和网关组件,对于客户端而言,代理组件是一个共享服务器,它接受请求并把它转发给服务器。网关组件在客户端组件和代理组件看来就像是一个正常的服务器,只是网关组件内部只是将它转发给了内部的其他服务器。在CS和LS的基础上,LCS改善了如下的架构属性 :
1. 可伸缩性(++) : 可以添加负载均衡或者安全检查之类的监控,用来提高系统的可伸缩性。
2. 可进化性(++) : 得益于增加的网关和代理组件,可以使得客户端和服务端组件更容易升级部署。
具体的例子 : shadowsocks等。
## 4.4 客户-无状态-服务器(Client-Stateless-Server = CSS) {#client-stateless-server}
在CS的基础上,增加一个约束 : 服务端组件上不允许有会话状态。从客户端发给的服务器的每个请求都必须包含理解请求所必须的所有信息,即不能在服务端上保存请求上下文信息(比如上一个请求的信息),会话状态应该都保存的客户端。这个约束会在CS的基础上,产生如下的架构属性 :
1. 可见性(+) : 监视系统不必为了确定请求的全部内容而查看多个请求的数据。
2. 可靠性(+) : 各自独立的没有依赖的请求可以更简单的从故障中恢复出来。
3. 可伸缩性(+) : 服务端不必保存多个请求直接的状态,从而允许服务端简化组件的实现并且快速释放资源。
4. 网络性能(-) : 由于服务端不再保存共享的状态数据,则会使得每次请求都会发送重复的数据,从而降低网络性能。
具体的例子 : 大多数桌面(或者APP)应用。
## 4.5 客户-缓存-无状态-服务器(Client-Cache-Stateless-Server = C$SS) {#client-cache-stateless-server}
在CSS的基础上,增加缓存组件。缓存在客户端和服务器直接进行周旋 : 它可以复用早先的请求,用来响应后面的相同请求,从而避免向原始服务器发送请求(得到的响应是一样的)。增加的这个组件可以在CSS的基础上进一步改善以下的架构属性 :
1. 网络效率(+) : 复用之前的请求结果,避免了额外的网络请求,从而改善了效率。
2. 用户感知的性能(+) : 改善了效率,从而可以提升用户感知的性能。
具体的例子 : SUN的NFS。
## 4.6 分层-客户-缓存-无状态-服务器(Layered-Client-Cache-Stateless-Server = LC$SS) {#layered-client-cache-stateless-server}
合并了LCS和C$SS两种风格,其产生的架构属性为LCS和C$SS的组合(但是不会把重复的CS结算两次)。
具体的例子 : DNS系统。
## 4.7 远程会话(Remote Session = RS){#remote-session}
RS是CS的一种变体,它试图使客户端组件的复杂性最小化或者使它们的可重用性最大化。每个客户在服务器上启动一个会话,然后调用服务器的一系列服务,最后退出会话。应用状态被保存在服务器上。这种风格可以产生如下几个的架构属性 :
1. 简单性(+) : 简化客户端组件,集中维护服务端组件使得整体更容易维护。
2. 网络效率(+) : 利用服务器维护了会话上下文信息,可以提升效率。
3. 可进化性(+) : 继承自CS。
4. 可伸缩性(-) : 服务器维护了会话上下文信息,降低了可伸缩性。
5. 可见性(-) : 监控程序必须要了解整个上下文信息,才能得以监视。
具体的例子 : Telnet,SSH。
## 4.8 远程数据访问(Remote Data Access = RDA){#remote-data-access}
RDA是CS的一种变体,它把应用状态分布在客户端和服务端上。客户端发送一个标准的数据查询请求给服务端,服务端分配一个工作空间并执行这个查询,这可能会产生一个巨大的结果集。客户端可以在在结果集上进行进一步操作。这就需要客户端了解服务端的数据结构,以便构造依赖该结构的查询。这种风格可以产生如下几个的架构属性 :
1. 网络效率(+) : 可以在服务器上执行多次迭代,逐步缩小一个结果集,从而改善效率。
2. 可见性(+) : 标准的数据查询语言可以改善可见性。
3. 简单性(-) : 客户端必须像服务器实现那些理解相同的数据操作概念,因此降低了简单性。
4. 可伸缩性(-) : 在服务端保存应用上下文,降低了可伸缩性。
5. 可靠性(-) : 部分的故障会导致工作空间处于未知状态,也降低了可靠性。尽管可以和事物机制(例如两阶段提交)来修正可靠性的问题,但是其代价则是增加了复杂性和交互的开销。
具体的例子 : SQL。
# 5 移动代码风格(Mobile Code Style) {#mobile-code-style}
## 5.1 虚拟机(Virtual Machine = VM) {#virtual-machine}
所有的移动代码风格的基础都是VM(或解释器)风格。VM风格本身并不是基于网络的风格,但是它通常在REV和COD风格中于一个组件结合在一起使用。代码在一个满足了安全和可靠性的受控的环境中执行,VM通常被用作脚本语言的引擎,来执行特定的任务。这种风格可以产生如下几个的架构属性 :
1. 可扩展性(+) : 在一个特定的平台上将指令和实现分离,改善了可扩展性。
2. 可移植性(+) : 指令和实现的分离也提高了其可移植性。
3. 可见性(-) : 难以通过简单的查看代码来了解要它们要做什么事情,从而降低了可见性。
4. 简单性(±) : 需要对指令执行的环境进行关系,则降低了简单性;但是在一些情况下可以通过简化静态功能得到补偿。
## 5.2 远程求值(Remote Evluation = REV) {#remote-evluation}
REV风格来源于CS+VM风格,客户端组件必须知道如何执行一个服务,但是缺少执行此服务所必须的资源,而这些资源都位于一个服务端站点上。因此,客户端组件把如何执行服务的代码发送给服务端的一个服务端组件,由它来执行代码,然后把结果返回给客户端。这种REV会要求被执行的代码是在一个受保护的环境中,使其不会影响到其他的客户端。这个约束在CS+VM的基础上可以产生如下的架构属性 :
1. 可扩展性(+) : 可以定制服务器组件的服务。
2. 可定制性(+) : 可以定制服务器组件的服务。
3. 网络效率(+) : 可执行代码运行在服务器端,可以不必通过跨越网络进行多次交互来得到相同的结果,从而可以得到更好的效率。
4. 可伸缩性(-) : 服务端对代码执行环境的管理会降低可伸缩性。
5. 可靠性(-) : 服务端管理的代码执行环境本身会增加一些故障。
具体的例子 : redis可以执行lua脚本。
## 5.3 按需代码(Code On Demand = COD) {#code-on-demand}
COM风格来源于CS+VM风格(但是又不同于REV),客户端组件知道如何访问一组资源,但是不知道如何处理它们。客户端需要向服务端请求一份可以处理这部分资源的代码,这部分代码在客户端本地执行。这个约束在CS+VM的基础上可以产生如下的架构属性 :
1. 可伸缩性(+) : 可以为已经部署的客户添加功能。
2. 可配置性(+) : 可以为已经部署的客户添加功能。
3. 用户感知的性能(+) : 如果代码可以适应客户端环境,并通过本地的一些交互代替网络交互,则有助于提高用户感知的性能。
4. 可伸缩性(-) : 服务端不在管理代码的可执行环境,释放了服务端的压力,则改善了服务器的可伸缩性。
具体的例子 : 浏览器中的JS。
## 5.4 分层-按需代码-客户-缓存-无状态-服务器(Layered-Code-on-Demand-Client-Cache-Stateless-Server = LCODC$SS) {#layered-code-on-demand-client-cache-stateless-server}
把COD添加到签名所说的LC$SS风格上,这时候把代码被看作是另一种形式的数据元素,因此并不会妨碍LC$SS的优点,同时也会叠加COD的优点。
## 5.5 移动代理(Mobile Agent = MA) {#mobile-agent}
MA风格来源于REV+COD。在MA中,一个完整的计算组件,它的状态,代码、执行代码所需的数据都被一起移动到了远程站点。它是已REV和COM两种方式同时工作的。
# 6 点对点风格(Peer-to-Peer Style) {#peer-to-peer-styles}
## 6.1 基于事件的集成(Event-based-integration = EBI) {#event-based-integration}
基于事件集成的风格也被成为隐式调用风格或者事件系统风格。通过消除了解连接器接口的标识信息的必要性,它可以降低组件之间的耦合。此架构风格不是之间调用另外一个组件,而是通过一个组件发布或者广播一个或多个事件。然后由系统负责调用其他注册了对这些事件感兴趣的组件。这样的系统一般都会有一个事件总线,所有的组件都通过这个总线监听它们各自感兴趣的事件。这种风格可以产生如下几个的架构属性 :
1. 可进化性(+) : 可以替换现有的组件而不影响其他的组件。
2. 可扩展性(+) : 添加新的监听组件非常容易。
3. 可重用性(+) : 可以使用通用的事件接口和继承机制。
4. 可配置性(+) : 如同PF风格一样,EBI也有一个看不见的手在配置着整个系统。
5. 网络效率(+) : 基于事件的数据传递方式,使得普通的轮询机制不再需要,从而可以提高效率。
6. 可伸缩性(–) : 各组件依赖的事件总线是整个系统的瓶颈点 : 事件的数量,由事件广播引起的事件风暴等都会损害系统的可伸缩性。
7. 简单性(±) : 可伸缩性的问题可以通过添加分层系统和事件过滤来缓解,但是也会以损害简单性为代价。
8. 可见性(-) : 难以预料一个事件发生后会由什么样的结果,缺乏可理解性。
9. 可靠性(-) : 不支持大粒度的数据交换,也不支持从局部的故障中恢复。
具体的例子 : 发布/订阅的消息系统。
## 6.2 C2 {#c2}
C2是EBI和LCS的组合形成的风格。C2在EBI的基础上,支持大粒度的重用,并通过加强基础层独立性来支持系统组件的灵活组合。异步通知消息向下传递,异步请求消息向上传递,这是组件之间通信的唯一方式。这个约束加强了对高层依赖的松耦合(服务请求可以被忽略),并且于底层实现了零耦合(无需知道系统使用了通知),这样既改善了对整个系统的控制,又没有丧失EBI的大多数优点。
通知是对于组件中的状态变化的公告,C2并不会对通知中应该包含什么内容加以限制。连接器的首要职责是消息的路由和广播,其次是消息的过滤。引入对于消息的分层过滤,可以解决EBI的可伸缩性的问题,同时改善可进化性和可重用性。
## 6.3 分布式对象(Distributed Object = DO) {#distributed-object}
DO是CS和CS的组合。在单独的CS的风格总,客户端和服务端是相互独立的,各自只负责自己的部分。DO则是使一个组件既是客户端也是服务端。也就是说它既对外提供服务,同时也是服务的消费方。DO组件的内部是完全被隐藏和保护起来的,操作它的唯一办法是通过它公开的接口进行访问。一个DO为了要和另外一个DO交互,则必须知道另外一个DO的标识信息,当一个DO的标识信息发生变化的时候,则必须要修改所有显示调用它的DO。因此必须要又一些控制器对象来负责管理维护系统的状态。
## 6.4 被代理的分布式对象(brokered Distributed Object = BDO) {#brokered-distributed-object}
为了降低DO中受到对象标识信息的影响,通常会使用一种或者多种架构风格来辅助通信,比如EBI和被代理的CS风格。这样的目的在于引入一个名称解析组件,用来把一个通用的服务的名称解析为一个能够满足该请求的对象的特定名称,并使用这个特定名称的对象来处理请求。尽管它改善了可重用性和可进化性,但是额外的间接层会产生一定的网络开销,从而降低用户感知的性能。具体的例子 : CORBA,ODP。
# 7 总结 {#summary}
以上的每一种架构风格都在组件之间推崇一种特定的交互类型。当组件跨域广域网的分布的时候,应用的可以用就会取决于对于网络的使用或者误用。通过对已架构风格对于架构属性的影响来刻画架构,才能选择出更适合此类应用的架构设计。
但是以上的评估是有一些局限性的,这里的评估是特别为分布式超媒体系统的需求量身定做的。比如通信的内容是细粒度的控制信息,那么PF风格的很多优点就不复存在了;而且如果用户交互的通信如果是必须的,PF则根本就不适用。同样的,如果客户端没有对请求进行缓存,那么分层+缓存的风格则只会增加延迟,而不会带来任何好处。这样的问题需要针对每一种类型的通信问题进行单独对比。下面的表格总结一下上面介绍到的所有的架构风格。
| 风格 | 继承 | 网络性能 | 用户感知的性能 | 网络效率 | 可伸缩性 | 简单性 | 可进化性 | 可扩展性 | 可定制性 | 可配置性 | 可重用性 | 可见性 | 可移植性 | 可靠性 |
| ——– | ———- | ——– | ————– | ——– | ——– | —— | ——– | ——– | ——– | ——– | ——– | —— | ——– | —— |
| PF | | | ± | | | + | + | + | | + | + | | | |
| UPF | PF | - | ± | | | ++ | + | + | | ++ | ++ | + | | |
| RR | | | ++ | | + | | | | | | | | | + |
| $ | RR | | + | + | + | + | | | | | | | | |
| CS | | | | | + | + | + | | | | | | | |
| LS | | | - | | + | | + | | | | + | | + | |
| LCS | CS+LS | | - | | ++ | + | ++ | | | | + | | + | |
| CSS | CS | - | | | ++ | + | + | | | | | + | | + |
| C$SS | CSS+$ | - | + | + | ++ | + | + | | | | | + | | + |
| LC$SS | LCS+C$SS | - | ± | + | +++ | ++ | ++ | | | | + | + | + | + |
| RS | CS | | | + | - | + | + | | | | | - | | |
| RDA | CS | | | + | - | - | | | | | | + | | - |
| VM | | | | | | ± | | + | | | | - | + | |
| REV | CS+VM | | | + | - | ± | | + | + | | | - | + | - |
| COD | CS+VM | | + | + | + | ± | | + | | + | | - | | |
| LCODC$SS | LC$SS+ COD | - | ++ | ++ | ++ | +±+ | ++ | + | | + | + | ± | + | + |
| MA | REV+COD | | + | ++ | | ± | | ++ | + | + | | - | + | |
| EBI | | | | + | – | ± | + | + | | + | + | - | | - |
| C2 | EBI+LCS | | - | + | | + | ++ | + | | + | ++ | ± | + | ± |
| DO | CS+CS | - | | + | | | + | + | | + | + | - | | - |
| BDO | DO+LCS | - | - | | | | ++ | + | | + | ++ | - | + | |
做了4篇博客的前期准备工作,下一篇就开始介绍什么是REST了。以上 如有错误之处,欢迎指正!
# 8 参考资料 {#reference}
[理解REST] 00 参考资料
转自:https://linianhui.github.io/understand-rest/04-network-based-software-architecture-style/