SOA实践指南读书笔记
最近读完了《SOA实践指南——分布式系统设计的艺术 》这本书,以下是这次的读书笔记。
面向服务的架构(SOA)。它是一种帮助系统在增长的同时保持可扩展性和灵活性的方法,它也有助于填平“业务/IT”鸿沟。该方法由三个主要元素构成:
- 服务。服务一方面体现了自足的、能作为一个或多个流程一部分的业务功能;另一方面,服务能由任何技术、在任何平台上实现。
- 企业服务总线(ESB)。它是个专门的基础设施,它使我们能够用简单和灵活的方式来结合这些服务。
- 政策和过程。它们处理这样的事实——大型分布式是异质的、处于不断维护中的,其所有者各异。 SOA接受这样的观点:在大型分布式系统中,保持灵活性的唯一办法是支持异质、分散和容错。
大型分布式系统的特征
所有大型系统天生是异质的。这些系统的目的有别、实施时间各异、新旧程度差异悬殊。大型系统呈现出堆积了不同平台、编程语言、编程范式,甚至不同中间件的系统景观。异质性的一个原因是,大型系统和它们的数据有非常长的生命周期。在此生命周期中,通过加入新的系统和流程,不断开发出促进业务的新功能。大型分布式系统还有一个重要的附加特性:所有者各异。大型分布式的另一个关键系统时“不完美性”。尽善尽美的代价太高。
SOA的关键技术概念
- 服务
- 互操作性
- 松耦合
ESB
ESB的关键特性是,它使你能在异质系统间进行服务调用。它的职责包括数据转化、(智能)路由、处理安全和可靠性、服务管理、监测以及日志。
服务
服务的定义:一项“服务”是一个自足的、无状态的业务功能,通过定义良好的标准接口,它接受一个或多个请求,返回一个或多个应答。服务也可能执行离散的工作单元,如编辑和处理一个事物。服务应该不依赖于其他功能或过程。用于提供服务的技术,如编程语言,不构成本定义的一部分。
服务应该代表一项自足的功能,对应着一项真实世界的业务活动,换句话说,业务人员应该能理解服务干什么?
有一系列的性能指标关注于度量/性能参数,如可用性(组织中的成员应该何时能执行功能)、持续时间(执行功能需要多少时间)、频率(一段时间内功能被执行得有多频繁)。
松耦合
SOA松耦合可能的形式
| 紧耦合 | 松耦合 | |
| 物理连接 | 点对点 | 通过中介者 |
| 通信风格 | 同步 | 异步 |
| 数据模型 | 公共复杂类型 | 只有简单的公共类型 |
| 类型系统 | 强 | 弱 |
| 交互模式 | 通过复杂的对象树导航 | 以数据为中心、自足的消息 |
| 业务逻辑控制 | 集中控制 | 分布式控制 |
| 绑定方式 | 静态 | 动态 |
| 平台 | 强平台依赖性 | 平台无关 |
| 事务性 | 2PC(两阶段提价) | 补偿 |
| 部署 | 同时进行 | 非同时进行 |
| 版本划分 | 显示进行 | 隐式进行 |
异步通信:从消费者的角度来看,异步通信可能指的是消费者不用阻塞自己来等待答复,然而,从基础设置(ESB)的角度连看,异步通行可能指的是用来解耦消费者和供应者的消息队列。
异质数据类型:在面向对象成为主流以后,弄出一个公共的“业务对象模型”(BOM)就成了一个普遍的目标。但是,人们后来发现,对大型系统来说,这个方法时自求灾难。迟早有一天,一致话的代价会变得过高。从时间和资金的角度来看,保持所有系统同步的成本太高。即使你努力做到了,下一次公司合并又会引入异质。
通常的办法是,服务定义者定义自己提供的服务中所以用的数据类型。服务消费者必须接受这些类型。注意,服务消费者应避免在自己的源代码中使用供应者的数据类型。相反,消费者应该有一个薄薄的“映射层”,将供应者数据类型映射为自己的数据类型。
不用公用数据模型有优点也有缺点:
- 优点是,系统可以修改自己的数据类型,不会对其他系统造成直接影响(修改好的服务接口只影响相应的消费者)。
- 缺点是,你不得不从一个系统想另一个系统映射数据。 补偿:即保持总体异质性,同时耦合程度也更松的方法时补偿。使用这种方式时,你先修改第一个后端系统,再修改第二个后端系统;如果第一个修改是成功的,你就需要针对具体问题,适当的做出“补偿”。例如,你可以回滚成功的修改,,从而恢复修改前的一致状态,或者,向某个错误监控桌面机发出问题报告,这样,某人就可以检查问题的细节,手动处理问题。
松耦合有各种形式,你必须找到适合自己特定上下文和项目的耦合松紧程度。
服务的分类
- 基本服务。基础服务有两类,基本数据服务和基本逻辑服务。基本服务应该具备所谓的ACID特性。在大系统中,把一致性的责任交给更高的层,要它们确保针对多个后端的多个调用是一致的,这通常也并非易事。为了使事情简单些,应该明确哪个系统为主,哪个系统时派生的,然后应用相应的同步措施。 基本服务永远只应属于一个后端系统,只该对后端内的一致性负责。
- 组合服务
- 流程服务 组合服务与流程服务对多个后端的一致性负责。在多个后端间强制一致的通常办法是补偿,而不是事物安全或两阶段提交。
消息交换模式
| 模式 | 说明 | 优点 | 缺点 |
| 请求/应答 | 又分为阻塞和非阻塞的请求/应答模式(有时也称为“同步”和“异步”请求/应答模式) |
1.代码很简单。处理一个服务的调用与处理任何其他函数或过程调用一样。当你需要某些信息,或者需要完成某些事情时,你发出请求等待回到或确认,等问题被解决以后再继续自己的工作。 2.请求/应答模式的优点是,应答被递交到触发流程最初请求的同一个流程实例。 |
在你等待应答时,无法做任何事情。典型情况是,要么你需要一个快速的应答,要么对运行所耗时间要求不高。在实践中,处理这样的请求的供应者应该时常处于可用状态,并且能够在合理的时间内发出应答。 |
| 单程 | 如果你不需要应答,从消费者的角度来看,要做的事情甚至更简单:发送消息,然后就完事。这种单程模式通常被称为“发射后不管”。单程消息的序列带来了(业务)流程链的概念 | 消费者通常给供应者发送一个返地址”,供应者将其和应答消息一起送回。处理收到的应答消息的消费者进程/线程可以用这个地址信息,在内部将应答路由到正确的流程实例(即最初发送请求的那个实例) | |
| 请求/回调 | 这个模式通常被称为“观察者”或“发布/订阅”模式,它是基础的设计模式之一。一般来说,这个模式,这个模式允许多个“观察者”向一个系统”订阅”,而该系统会在发生特定情况时通知观察者。 |
出资模型
有一个集成的,可重用的服务并不会在供应者和消费者发生第一次通行时就产生回报,但是当许多供应者和消费者时就会产生回报。同样,松耦合对一个解决方案可能没有回报,但是,有许多解决方案要运行和扩大规模是,松耦合就产生回报。
多层验证
- 在后端验证。所有的验证只在后端进行。此时,前端不做任何预验证。使用这种方法时,用户填入大量数据,而后端因为最开始几个输入项有错拒绝接受数据,最终用户可能会感到非常受挫。及早发现问题会节省大量的时间和工作。
- 在前端验证。所有的验证只在前端进行。此时,后端假设服务的消费者不会发送任何错误的或者不一致的数据。当然,这种方法也有风险,因为前端可能犯错误或者漏过一些错误,就会导致后端的未定义行为,造成后端不一致和/或崩溃。
- 冗余。前后端都独立进行验证。前端会试图在前面找到可能的错误,后端将保证它接受的输入是有效的。然而,独立的验证可能导致不一致。
实践中的验证通常混合使用所有这些方法,后端永远都应该检查所有输入的有效性。 总体来说,使前端。使用前端预验证服务输入是个好主意。然而,预验证无法做到完美,否则前对就可以实现整个业务逻辑了(包括处理可能的竞争条件)。一般来说,你必须提供合适的流程来处理事情会出错这个事实。预验证的目的应该是保证服务流程“通常”会成功。小心点,不要再预验证上花不恰当的力气。
什么是SOA
在计算科学中,术语“面向服务的架构”(SOA)表达了一种软件架构的概念,它定义为使用服务来满足满足软件用户的需求。在SOA环境中,网络上的节点以独立服务的形式将自己的资源开发给网络上其他参与者,其他参与者按一种标准的方式使用资源。大多数对于SOA的定义都提到了实现SOA时使用Web Services(即使用SOAP或REST)。然而,可以使用任何基于服务的技术实现SOA。
……
与传统点对点架构不同,各种SOA都由松耦合、高度可以互操作的应用服务构成。这些服务基于某种格式定义进行互操作,该定义独立于底层平台和编程语言(例如,WSDL)。接口定义封装(隐藏)了供应商和语言相关的实现。SOA独立于开发技术(比如JAVA和.NET)。由此,软件组件将具备非常高的重用性,因为接口按与标准兼容方式定义。
基于SOA的逻辑架构模型
处理状态的方法对比
- 在服务中保持状态
- 在后端中保持状态
- 在前端/消费者中保持状态
在所有这3种选择有不同的优点和缺点:
- 将状态放入前端/消费者意味着,服务可以是无状态的,并且可以不牵涉后端。然而,当会话在运行时,你无法改变前端(如果要求支持多前端,或者如果你希望能够将会话转移到另一个桌面,这就是个缺点)。
- 将状态放入有状态服务需要在服务层上使用更多的资源,并且,要有办法将请求路由到同一个服务实例。然后,你可以改变前端,后端也不被牵扯进来。
- 将状态放入后端使你能使用无状态服务,前端也不必提供存储。然而,每一个会话都牵涉后端。
| 状态在前端 | 状态在服务 | 状态在后端 | |
| 在前端 | 所有状态 | ID | ID |
| 服务 | 无状态 | 有状态无状态 | 无状态 |
| 在后端存储 | / | / | 所有状态 |
| 支持多通道 | 否 | 是 | 是 |
无状态服务为什么更好
首先,对于无状态服务来说,在服务层做到负载均衡和失效备援相当简单。ESB仅需实现能使用下一个最容易获得的服务实例即可。它甚至可以是其他消费进程以前使用过的线程。这意味着如果一个系统失败了,其他系统可以轻易地接手,继续工作。如果服务层的通量不够高,你可以将服务实例的数量翻倍,从而是通量翻倍。这意味着解决方案能线性伸缩,并且,如果一个服务实例意外死亡,其他服务实例能投入进来。
而有状态服务的缺点是,它们绑定在会话上。这样一来,知道消费者会话结束(或者连接失效)之前,为它们分配的资源都绑定在一个特定的消费会话上。除此以外,你必须能让ESB把连续的服务调用路由到同样的服务实例上。
然而,要注意的是,有状态服务仍然可以线性伸缩。使用一种“粘性”路由的政策,根据可用的资源,你在第一个服务调用被执行时找到你的服务实例。这一来,对服务实例数量的翻倍就又能将可能的会话数量翻倍了。然而,此时不具备服务资源可以在不同消费者之间共享的优点。并且,如果一个服务实例意外死亡,会话状态就丢失了。
同时也要注意,有状态服务可以有失效备援机制(并且仍然可以线性伸缩)。此时,每个服务器不但返回它自己的会话ID,还返回失效备援会话的ID。只要这个失效备援会话不在一个中央服务器上,而是在邻近的服务器上(并且,每个服务器的邻近服务器各不相同),这样就不会造成瓶颈。
事件驱动架构
- 如果一个事件仅仅通知消费者客户数据有变化,需要处理,那就只需要发送少量数据。随事件发送的数据会帮助消费者判断更新是否与自己有关——例如,可能包含着新的常用合同信息或类似信息——但是,本质上,这些通知知识简单表明有更新发送,需要被处理。
- 如果一个事件包含了要处理的数据,该事件会更粗粒度一些。此时,事件的消费者会收到处理该事件所需的所有信息。
幂等性
等幂性是服务的一种可能的属性。在服务的上下文环境中,它意味着对同一服务调用的多次递交/处理不会引发问题。假设你有一个向银行中增加钱的服务。如果一个消费者调用这个服务(例如,为了处理银行转账)并且没有得到应答,消费者不会知道服务的请求还是服务的应答失败了。例如下图中,服务可能在请求处理之前或之后丢失。
在后一种情况下,从供应者的角度来看,服务成功了,指定数量的钱被增加到了银行账户上。在前一种情况下,服务没有影响(即没有增加钱)。
当服务的消费者没有得到应答时,它不知道服务是否成功了。为求确保,它可能重试服务调用,再次发出同样的请求。结果是,达到供应者的服务请求可能有一次或两次。如果服务是幂等的,那么,服务请求到达多少次都没有关系:同一服务调用的多次请求只产生一次的效果。
刚刚描述的银行服务不是幂等的,因为,它依赖于第一次服务调用失败发生在何处,客户的余额可能最终被增加了两次,而不是消费者想要的一次。为了避免这样的问题,通常说来,只要有可能就是服务幂等是个好主意。
所有的读取服务都是幂等的,因为一个服务供应者无论多频繁地执行返回数据的请求都没有关系。然而,要注意,一旦做了类似每个请求都写一个备忘录项这种事,读取服务就变成了写入服务。如果这个备忘录和业务有某种关联,这就可能成为问题(例如,对同一样的事项,最终可能有多个备忘录项)。
写入服务可能是幂等的,也可能不是。当一个服务调用的影响依赖于后端现有的状态时,写入服务不是幂等的。举个例子,在前面的例子中,银行账户的最终余额依赖与最初的余额。如果开始的余额是500美元,服务调用增加100美元,结果将是600美元。如果因为最初的应答丢失了,另一个请求到达并且得到处理,最后的余额将是700美元。
一个幂等的写入服务的例子是,一个通过发送所有地址来设定客户地址的服务。供应者处理多少次请求都没有关系,结果将是一样的。
注意:你总是可以避免让服务不幂等。例如,我们可以修改银行服务语义,发送新值而不是应该增加的值,从而使服务幂等。这就是说,不是调用:
addToBalance(100);
我们可以调用:
setBalance(600);
这个调用可以反复地发送和处理,余额的结果都会是600。当然,其他服务也能在相同的账户中增加或减少资金,这就带来了问题。这就带来了问题。你如何才能知道最终的余额应该是多少呢?正如你看到的,从业务的角度来看,让服务幂等可能变得复杂。
除此以外,从业务角度来看,有的服务内生就是不幂等的。所有创建某些东西的服务都没有办法和一个状态去比较(除非你知道创建新资源的工厂的状态)。例如,如果你有一个创建银行账户的服务,你就是在创建账户,仅此而已。
如果从业务的角度来看,引入幂等性是个难题的话,那么,你需要一些对幂等性的技术支持。实现幂等性的普通方法相当简单,消费者发送数据的方式必须能让供应者看的出两个技术性的请求其实是同一个。为此,消费者可以随着每次新请求发送一个唯一的ID。如果消费者没有得到应答,它的每次重试都是同样的ID。下图是对此的图解。
在处理每个进入的请求前,供应者应将请求及相关联的唯一ID存储到一个中央数据库中。如果供应者收到一个ID已经在存储库中的请求,它就知道该请求已经被处理过。于是,供应者就不会再处理它,而是向消费者重新发送最初的应答(应答在第一次被送出去之前,也保存下来)。如果第二次请求到达是,服务实现正处于运行中,则供应者可以回复一个应答,说服务调用在运行中,或者供应者可以等到第二次请求处理完毕以后,再次把应答发送出去。
作为一项优化,消费者能和请求一起发送一个标志,这样供应者就知道此请求是否是一个重试。这样能做到改善性能,因为如果调用没有被标记为重试的华话,供应者就不必验证请求是否已经在存储库中了。
