技术上解耦的手段:集成
1. 理想的集成技术
1) 避免破坏性修改:修改一个服务不会导致该服务的消费方随之发生改变。如:一个微服务在响应中添加一个字段,已有的消费方不会受到影响
2) 保证api的技术无关性:对微服务的实现没有限制
3) 易于消费方使用:消费方可以用任何技术实现。
4) 隐藏内部实现细节:服务内部实现细节对消费方不可见
2. 现在常用的集成技术:
集成即是为用户创建接口
1) 共享数据库:业界最常用的方式
a. 优点:
i. 直接操作数据库,最初的实现简单、快速
ii. 共享数据简单
b. 劣势:
i. 不同的上下文通过数据库耦合在一起;
ii. 内部暴露给消费者;
iii. 很难做到无破坏性修改,进而不可避免地不做任何修改
iv. 消费方与特定的技术绑在了一起;
v. 无法共享行为
2) 同步:发起一个远程服务调用后,调用方会阻塞自己到整个操作的完成。
同步可以使用的协作方式:请求/响应
a. 优势:
i. 可以知道调用成功与否
ii. 技术实现简单
b. 劣势:
i. 运行时间长的应用,需要客户端和服务器之间的长连接
ii. 高延迟
3) 异步:调用方不需要等待操作是否完成就可以返回,甚至可能不关心操作完成与否
可以使用的协作方式:请求/响应或者基于事件
a. 优势:
i. 对运行比较长的任务比较有用,否则客户端和服务器之间要开启长连接。
ii. 低延迟
iii. 基于事件的协作方式可以分布处理逻辑,低耦合
b. 劣势:
i. 处理异步通信的技术相对复杂
4) 编排(orchestration):使用某个中心大脑来指导并驱动整个流程,可以用管弦乐队的指挥来比喻
a. 优势:
i. 调用简单
ii. 客户服务本身可以对跨服务业务流程进行到哪一步进行跟踪
iii. 有工具帮助实现,比如一个合适的规则引擎,流程建模软件
iv. 如果使用的是同步的请求/响应模式,甚至能知道每一步是否成功
b. 劣势:
i. 作为中心控制点的客户服务会成为瓶颈:网状结构的中心枢纽及很多逻辑的起点
ii. 其它的服务通常都会沦为贫血的、基于crud的服务
iii. 重量级的编排方案都非常不稳定并且修改代价极大
5) 协同(choreography):告知系统中各个部分各自的职责,具体怎么做的细节留给自己,可以用芭蕾舞中的每个舞者来做比喻,通常使用事件驱动的方式
a. 优势:
i. 显著地消除耦合
b. 劣势:
i. 看不到业务流程的进展
ii. 需要额外的工作来监控跨服务的流程,以保证其正确的进行。实际的监控活动是针对每个服务的,但最终把监控的结果映射到业务流程中
相对于编排,在微服务架构里优先选择协同
3. 请求/响应模式相关的技术
同步或异步的协作方式都可以用到请求/响应模式,请求/响应模式有两个典型的技术:
1) 远程过程调用RPC:本地调用,远程服务器执行并返回结果
RPC类型:
a. 依赖于接口的,如SOAP,Thrift,及protocol buffers等。不同的技术栈可以通过接口定义轻松生成客户端和服务器端的桩代码。
例如:Java服务暴露一个SOAP接口,然后使用WSDL(Web Service Definition Language,Web服务描述语言)定义的接口生成.Net客户端的代码;
b. 其它的技术,不需要额外的共享接口定义,但是会导致服务端和客户端之间更紧的耦合。如Java RMI。
RPC的优势:
a. 易于使用:只使用普通的接口调用方式,而不用关注实现的细节。依赖于接口的RPC也会生成大量的桩代码
RPC的劣势:
a. 技术上的耦合:有一些RPC机制,如Java RMI,与特定的平台紧密绑定,对技术选型有限制。
这种技术的耦合也暴露了内部的细节
b. 对远程和本地的API,使用的思路不同,远程的要考虑对符合进行封装和解封装。
i. 简单地把本地api改装成跨服务的远程api会带来问题
ii. 开发人员很多时候也不知调用的是本地还是远程api
iii. 网络的不可靠性:即使客户端和服务器工作正常,调用也可能会出错。
c. 脆弱性:使用二进制桩生成机制的rpc(例如RMI)所普遍面临的挑战,当服务器端的数据类型里的部分字段被删除、新增字段或者修改字段,远程的客户端都要随之一起变化。如果远程的客户端不能一起调整,就会出错。这就带来了脆弱性。
更现代的RPC,如protocol buffers或者Thrift,会通过避免对客户端和服务端的lock-step发布(具体如何做的?)来消除脆弱性。
d. 没有充分应用HTTP:SOAP是基于HTTP进行路由的,但不幸的是它仅用到HTTP很少的特性,动词及HTTP的错误码都被忽略了,HTTP的潜力没有得到充分应用。
2) 基于HTTP的REST:RPC的一种替代方案。
最重要的概念是资源,服务可以根据请求内容创建资源的不同表现形式。
REST的优势:
a. 资源的解耦:资源的对外显示方式和内部存储方式之间没有什么耦合。例如:客户端可能会请求一个Customer 的JSON表示形式,而Customer 的内部存储方式可以不同。
b. HTTP的动词(GET、POST和PUT)和REST的资源一起使用,意味着可以只用资源(名词)作为入口,用动词进行操作:
i. 对于一个资源,接口只有一个,但可以通过HTTP协议的不同动词对其进行不同的操作。
例如,对Customer 资源的操作,避免了很多版本的createCustomer及editCustomer的方法,想法
c. 可以利用HTTP周边的大的生态系统。比如Varnish这样的HTTP缓存代理;mod_proxyzh这样的负载均衡器;HTTP的监控工具;HTTP的安全控制机制
d. 引入HATEOAS(Hypermedia As The Engine Of Application State,超媒体作为程序状态的引擎)原则,避免客户端和服务端之间的耦合
HATEOAS背后的原理:客户端和服务端通过指向其它资源的链接进行交互。通过HATEOAS,不需要一再调整客户端代码来匹配服务器端的改变。通过这些链接,客户端能自行获取相关API。
REST的劣势:
a. 使用HATEOAS时,客户端和服务端之间的通信会过多,因为客户端会不断发送链接、请求,直到找到自己想要的那个操作。
优化方案:让客户端自行遍历和发现api
b. 基于HTTP的REST无法帮助生成客户端的桩代码,要在客户端自己实现
容易犯的错是构建出一些共享库,在客户端和服务器端之间共享。这样就引起紧耦合的问题。
c. 有些Web 框架无法很好支持所有的HTTP动词。GET和POST请求都支持(等待验证),但是有的框架不能支持PUT和DELETE。
d. 不能很好地满足高性能和低延迟的要求:REST的JSON或者二进制,相对SOAP来说更加紧凑,每个HTTP请求的封装开销是个问题。
解决方案:用WebSockets替代HTTP,WebSockets更加高效。
4. 基于事件的异步协作方式
微服务架构通常选用事件驱动的方式。使用事件驱动的注意事项:
1) 确保各个流程有很好的监控机制
2) 考虑关联ID,可以对跨进程的请求进行跟踪
基于事件的异步协作方式有两种:
1) 消息代理
服务生产者使用API向代理发布事件,代理向服务消费者提供订阅服务,并在事件发生时通知消费者。
优势:
a. 能处理微服务发布事件机制及消费者接收事件机制的问题
b. 可以跟踪消费者的状态,比如标记哪些消息是该消费者已经消费过的了。
c. 具有较好的可伸缩性和弹性
劣势:
a. 增加开发流程的复杂度:
响应返回时如何处理?是否发到请求的那个节点?如果是,节点停止工作了怎么办?如果不是,是否需要把消息先存储起来再处理?
b. 需要一个额外的系统(即消息代理)才能开发和测试服务,需要额外的机器和专业知识
c. 消息代理是中间件的一部分,但是很多中间件厂家通常倾向于把很多的软件打包进去,比如企业服务总线(ESB)。
规避方式:尽量让中间件保持简单,而且把业务逻辑放到自己的服务中。
2) ATOM:一种符合REST规范的协议,通过它提供资源聚合(feed)的发布服务
优势:
a. 使用方便,很多现成的客户端库可以消费该聚合。客户服务发生改变时,只需简单向该聚合发布一个事件即可。
b. HTTP能很好处理伸缩行
劣势:
a. HTTP不擅长处理低延迟
b. 用户需要自己跟踪消息是否送达及管理轮询等工作。
c. 消费者竞争关系:用户需要到资源聚合里轮询,很多用户消费一个资源就会引起竞争关系。
5. 用户界面作为集成的组合层
用户界面支持服务之间的集成,连接各个微服务的工具
1) 约束:
a. 桌面网页:
i. 用户浏览器
ii. 屏幕解析度
b. 移动端:
i. 带宽
ii. 电池电量的消耗
iii. 单手操作
iv. 短信交互
2) API组合
a. 使用多个API来表示用户界面:UI主动访问所有API,然后再将状态同步到UI控件
问题:
i. 很难为不同的设备定制不同的响应
建议方案:允许客户指定它想要哪些字段,但这就需要每个服务都支持这种方式。
ii. 谁来创建用户界面?另一个团队会退回到分层合作方式。
b. UI片段的组合:服务直接暴露出一部分UI,然后只需简单地把这些片段组合在一起就可以创建出整体UI
i. 优势:服务团队可以同时维护这些UI片段
ii. 问题:
1) 保证无缝的用户体验:可以利用活样式指导解决
2) 原生应用和胖客户无法消费服务端提供的UI组件。解决方案:嵌入HTML插件
3) 界面的动态刷新是否可以做到?例如搜索时,键入关键字推荐信息自动刷新
c. 为前端服务的后端:适用于1)与后端交互比较频繁的界面;2)需要给不同设备提供不同内容的界面
解决方案:API入口
i. 优势:
1) API入口可以对多个后端调用进行编排;
2) 对多个后端调用进行编排
3) 为不同设备提供定制化内容
ii. 劣势:
1) API入口如果包含逻辑太多,就会难以维护
2) 失去不同用户界面之间的隔离性
3) 限制了独立于彼此进行发布的能力
iii. 建议:专用后端,一个后端只为一个应用或者用户界面服务,也叫做BFF(Backends For Frontends,为前端服务的后端)
API认证和授权可以处于BFF和UI之间
BFF的风险:包含不该包含的逻辑;业务逻辑应该在服务中,不应该在BFF里;BFF应该仅仅包含与实现某种特定的用户体验相关的逻辑
6. 集成的其它原则
1) 服务即状态机:
a. 微服务应该拥有所属限界上下文中行为相关的所有逻辑;
b. 客户服务控制所有与客户生命周期相关的事件。
c. 把关键领域的生命周期显式建模,作为唯一一个处理状态冲突的地方,并可以在这些状态变化的基础上封装一些行为。
2) 响应式扩展(Reactive extensions,Rx):
a. 可以把多个调用(阻塞/非阻塞)的结果组装起来,
b. 一些Rx实现对组装后的结果进行某种函数变换。如RxJava中就可以使用类似map或者fileter的经典函数
c. 需要做一些基于多个服务调用的操作时,使用微服务所选用的技术栈的响应式扩展。
3) DRY:避免系统行为和知识的重复
优势:
可以得到重用性比较好的代码,把重复代码抽取出来,然后在多个地方进行调用
劣势:
微服务和消费者之间的过度耦合,一处修改,处处调整
4) 资源的有效性:在处理请求的过程中,资源有可能会发生改变。
a. 按引用访问:发送资源的URI,而不是资源的具体信息,等请求处理完成后再查询URI
b. 基于事件协同的对位:资源发生变化后,发送事件通知,同时使用资源的URI
c. 提供资源的有效性时限信息:获取资源的同时,获取资源的有效性时限(即资源在什么时间之前是有效的)。可以利用HTTP的缓存控制
5) 版本管理
服务的接口难免会发送改变,通过版本进行管理。
a. 尽可能推迟破坏性修改:
i. REST对于内部的修改不大会引起服务接口的变化
ii. 避免将客户端和服务端紧密绑定:如服务端使用JSON发送响应,客户端利用XPath提取所需的信息(Martin Fowler称其为容错性读取器)。
iii. Postel法则:宽进严出,对自己发送的东西要严格,对自己接收的东西要宽容
b. 及早发现破坏性修改
i. 使用消费者驱动的契约来及早定位
c. 使用语义化的版本管理
MAJOR.MINOR.PATCH
i. MAJOR:包含不向后兼容的修改
ii. MINOR:有新功能的增加,但是是向后兼容的
iii. PATCH:对已有功能的缺陷修复
d. 不同的接口共存
i. 新接口和老接口同时存在:在发布一个破坏性修改时,同时部署一个包含新老接口的版本
ii. 需要对不同的请求进行路由:HTTP的系统,把版本信息添加到URI中;
e. 同时使用多个版本的服务
i. 同时运行不同版本的服务:老用户路由到老版本的服务,新用户路由到新版本的服务。
ii. 蓝绿部署或者金丝雀发布
6) 和第三方软件的集成
这里第三方软件特指不受控制的系统,例如COTS或SaaS
a. 在自己可控的平台进行定制化:任何定制化只在自己可控的平台上进行,并限制工具的消费者的数量
b. 绞杀者模式:捕获并拦截对老系统的调用,1)路由到现存的遗留代码;2)路由到新写的代码
通常使用一系列的微服务来拦截,而不是一个单一的单块应用。这时拦截并重定向会变得很复杂,可能要引入代理模式。