《微服务设计》,Building Microservices,作者Sam Newman,译者崔力强、张骏,人民邮电出版社,2016年。
笔记中有些内容直接引用原书。
================================================================
第四章 集成
1. 寻找理想的集成技术
避免破坏性修改。例如,响应增加字段不影响服务方。
保证API技术的无关性。微服务之间通信方式的技术无关性很重要。
使你的服务易于消费方使用。利于消费方使用任何技术来用你的服务。
隐藏内部实现细节。暴露内部实现会导致消费方去耦合内部实现,因此当修改内部实现时造成消费方不必要的修改。不要采用倾向于暴露内部细节的技术。
2. 为用户创建接口
3. 共享数据库。基于表的共享,会使得服务消费者看到内部实现,无法实现隐藏内部细节。另外,服务实现绑定了数据库技术,导致服务消费者也必须要使用同样的数据库,无法带来技术开放性。因此,避免使用共享数据库。
4. 同步与异步
同步通信的协作风格是请求/响应式,异步通信的风格是基于事件。适合用异步通信的场景:运行时间长的任务、低延时的任务、移动网络及设备。基于事件的系统依赖于接收事件的系统自己判断该做什么,其协作逻辑是分布在不同的协作者中,因此耦合度低。
5. 编排与协同
编排,依赖于某个中心来驱动流程。而协同依靠各个部分主动协作共同完成。编排的好处在于系统实现简单,便于追踪问题。缺点是中心控制节点承担太多职责,其它服务沦为CRUD贫血服务。协同的优点在于能消除耦合。缺点是需要额外工作来监控流程,增加系统复杂度。另外,如果想用请求/相应风格的语义,又想避免耗时业务时的长等待,可以采用异步请求加回调的方式。
6. 远程过程调用
远程过程调用(RPC)种类很多,一些依赖于接口定义(SOAP、Thrift、protocol buffers等),容易生成客户端和服务端的桩代码,用户可以快速编程。但其代价大于快速启动的好处。
技术的耦合。Java RMI,双方必须使用Java。Thrift和protocol buffers支持不同语言,一定程度上减轻了该问题。有时候RPC技术对于互操作性有一定的限制。
本地调用和远程调用并不相同。RPC面临网络的不确定性,还要进行载荷消息的封装和解封装。用户在使用时需要额外考虑网络带来的问题,而当作本地调用时又会带来考虑不全的问题。在进行RPC调用的错误处理时,显然要麻烦的多。
脆弱性。以Java RMI为例,接口修改就要导致桩代码的修改。
RPC很糟糕吗。尽量避免使用RMI,转为使用现代的RPC如protocol buffers或者Thrift。使用RPC时,不要对远程调用过度抽象,以至于网络因素都被隐藏了;确保可以独立升级服务端接口而不用客户端强制升级;在客户端中不要隐藏是在做网络调用这个事实。
7. REST
REST使得资源在服务内和在服务对外提供的形式可以不一样,解耦了。建议看Richardson的成熟度模型(http://martinfowler.com/articles/richardsonMaturityModel.html)。 REST没有规定底层协议,常用的是HTTP,也可以使用其它协议如串口或USB。HTTP上实现REST简单。
REST和HTTP。REST声明了一组对资源的使用方法,HTTP中有方法能与其对应。HTTP有很多支撑工具和技术。比如Varnish是HTTP缓存代理,mod_proxy是负载均衡器,还有大量HTTP监控工具,还有安全认证机制和工具。
超媒体作为程序状态的引擎。超媒体是一块包含了其它内容链接的内容。客户端与服务端应该通过超媒体进行交互。通过超媒体可以隐藏服务端内部的更改,将客户端与服务端解耦。该方式的缺点是客户端和服务端之间通信次数较多。但还是建议客户端自行发现遍历和发现API,因为这样可以解耦,不要过早优化。
JSON、XML和其他。JSON简单,内容更紧凑,比XML流行。但XML中有超链接来进行超媒体控制,JSON中没有,于是JSON有不同的自定义方式,如HAL标准。XML工具有更好支撑,提取负载特定部分可以使用XPATH工具,挺多,CSS选择器也可以用。JSON可以使用JSONPATH。
留心过多的约定。有些工具使用RESTFul Web服务框架把内部存储暴露给消费者,并不好。
基于HTTP的REST的缺点。无法像RPC一样帮助生成客户端代码。不要回到基于HTTP进行RPC的老路去构建共享库。另外,性能上的问题:基于HTTP的REST支持多种格式,如JSON或二进制,比SOAP强,但没法和Thrift这样的二进制协议比。对于低延迟通信或较小尺寸的消息不是一个好选择。不支持高级的序列化和反序列化。建议阅读《REST实战》这本书。
8. 实现基于事件的异步协作方式
技术选择。需要考虑微服务发布事件机制和消费者接收事件机制。RabbitMQ这样的消息代理可以解决上述问题,是个好选择。但尽量让这种消息中间件简单,逻辑放在自己的服务中,企业级服务总线是个不好的反例。在HTTP上,有ATOM这个符合REST规范的协议,可以用来提供资源聚合的发布服务。但是有消息中间件的话,还是建议使用消息中间件。
异步架构的复杂性。事件驱动的异步系统耦合度低,伸缩性好,但需要程序员转换思维模式,而且复杂性更高。要考虑各个流程有很好的监督,并考虑使用关联ID,它可以对跨进程请求进行追踪。强烈推荐《企业集成模式》这本书。
9. 服务即状态机。要把关键领域的生命周期显式地用状态机建模出来,避免出现贫血服务。
10. 响应式扩展(Reactive extensions, Rx)。它提供了一种机制,可以把多个调用结果组装起来并在此基础上执行操作。调用本身可以是阻塞或非阻塞的。当需要做一些基于多个服务调用的操作时,可以尝试它,它让代码更加简单。
11. 微服务世界中的DRY和代码重用的危险。Don’t Repeat Yourself,DRY可以得到重用性比较好的代码,可以创建一个共享库。但这在微服务中会导致服务和消费者之间过度耦合。在微服务内部不要违反DRY,在跨服务的情况下可以适当违反DRY。
客户端库。客户端库可以对服务开发进行一些封装,提升开发效率,避免重复的与服务交互的代码。但当开发服务端API和客户端API是同一拨人时,存在将服务逻辑引入客户端的问题,带来了耦合性的问题。如果要使用客户端,让它只处理底层传输协议(服务发现、故障处理等),不要加入服务逻辑。另外,客户端库可能会限制不同技术的使用。
12. 按引用访问。如果对访问的资源有本地缓存,要考虑资源的过期失效问题,确保同时有一个指向原始资源的引用。
13. 版本管理
尽可能推迟。避免过早将客户端与服务端紧密绑定。客户端要尽可能灵活消费服务响应,这符合Postel法则(系统中的每个模块都应该“宽进严出”)。例如客户端可以使用XPath从服务响应中提取需要的字段,即使字段位置改变也能正确读取(容错性读取器)。
及早发现破坏性修改。建议使用消费者驱动的契约来及早定位对消费者产生的破坏性修改。
使用语义化的版本管理。语义化版本管理使得客户端仅通过查看版本号就能知道是否能与之集成。版本好:MAJOR.MINOR.PATCH。MAJOR改变意味着包含向后不兼容的修改,客户端就不能直接集成。MINOR变化意味着新功能增加,向后兼容,客户端可以直接集成。PATCH变化意味着功能缺陷修复。
不同的接口共存。接口不可避免要修改时,保留老接口,提供新接口,二者共存。给消费者时间将老接口替换为新接口的使用,然后再删除老接口。另外,可以通过将老接口的请求转换为新接口的调用。不同版本共存时,可以在请求信息中增加版本标识,也可以在URI中增加版本标识。
同时使用多个版本的服务。为了支持老用户,有时候会使用多个版本的服务共存(注意,这里不是指服务接口,而是指服务本身)。短期内合理,但更应该考虑一个服务暴露两套API,而不是两个服务共存。
14. 用户界面
走向数字化。通过微服务的不同组合为桌面应用、移动端设备、可穿戴设备提供不同的体验。
约束。不同平台(桌面端、移动端)有不同的约束,屏幕解析度、通信方式、带宽、电池电量、UI操作。
API组合。 不同平台的API可以使用API入口(gateway),多个底层的调用会被聚合成一个调用。
UI片段的组合。相比UI主动访问所有API,再同步状态到UI控件上,更好的方法可能是服务直接暴露一部分UI,然后将这些组合到一起形成整体UI。可使用服务端模板的技术将这些片段组装起来。优势是修改服务的同时可以维护这些UI片段。
为前端服务的后端。服务端的聚合接口或API入口不要太厚重,要分成不同的后端,每个后端只为一个应用或用户界面服务(BFF, Backends for
Frontends)。
一种混合方式。片段组装、BFF等可以权衡混合使用。
15. 与第三方软件集成。
使用一些商业的第三方软件会有如下问题:
缺乏控制。只有软件的厂家才能控制其发展和进行技术决策。
定制化。定制化很昂贵。
意大利面式的集成。一团乱麻的服务集成。
推荐的集成方式:
在自己可控的平台进行定制化。推荐这么做,可以使用自己的服务包住第三方的服务。
绞杀者模式(Strangler Application Pattern, http://martinfowler.com/bliki/StranglerApplication.html),拦截对老系统的调用,把调用路由到现存的遗留代码还是新写的代码,然后逐步替换老系统。一般使用一系列的微服务来拦截,而不是单一的单块应用。