4.1寻找理想的集成技术
集成是微服务相关技术中最重要的一个,做得好,你的微服务可以保持自治性,独立地修改和发布他们;但如果做得不好的话会带来灾难。
4.1.1避免破坏性修改
应该尽量避免服务提供者的修改导致消费方也随之发生改变。比如,如果一个微服务在一个响应中添加了一个字段,那么已有的消费方不应该受到影响。
4.1.2保证API的技术无关性
保持开放心态的人一定会喜欢微服务,因为微服务特别提倡服务之间的通信方式的技术无关性!这就意味着,不应该选择那种对微服务具体实现技术有限制的集成方式。
HTTP协议的流行,前端技术的流行,跨平台,兼容,生态开放,这些特性都起到了巨大的作用。现在,如果还用限定性的技术来做系统,后边一定会遇到很多痛苦的问题,甚至刚开始就会遇到。一位同事关于跨平台的例子,曾经的U8的关于跨平台的例子,拐着弯儿实现和直接到达。
4.1.3使你的服务易于消费方使用
让你的服务便于消费方使用。以前遇到过这样来方便消费方使用的办法---提供客户端库,这样在传统软件行业也许值得提倡,但是在微服务架构下绝对不应该被提倡,因为它会造成耦合的增加。(当年HTTP客户端的事儿,软件领域是OK的,微服务领域,除非定位是SDK,否则就是不OK)
4.1.4隐藏内部实现细节
我们不希望消费方和服务内部实现的细节绑定在一起,因为这会增加耦合。所以,所有倾向于暴露内部实现细节的技术都不应该被采用。
4.2为用户创建接口
4.3共享数据库
业界所见到的最常见的集成形式就是“数据库集成”。这种方式看起来非常简单,而且可能是最快的集成方式,这也是它这么流行的原因。
首先,这使得外部系统能够查看内部实现细节,并与其绑定在一起。各个服务各自对表结构的修改很容易导致其他服务的不可用,每次修改都需要做大量的回归测试来保证功能的正确性。
其次,因为与数据库绑定在了一起,所以需要使用一个合适的驱动。一段时间后,可能想要更换数据库(关系->非关系),很难替换到已经使用了很久的数据库。再见,松耦合。
最后,哪个服务都可以修改数据库,如果有一些相同的修改逻辑排布在不同的服务中,当修复一个bug时,你需要修改多个地方,然后对这些修改的服务分别部署。再见,松耦合,再见,微服务。
4.4同步与异步
同步通信听起来合理,因为可以知道事情到底成功与否。异步通信对于运行时间比较常的任务来说比较有用,否则就需要在客户端和服务器之间开启一个长连接,而这是非常不实际的。
同步:请求/响应
异步:请求,注册一个回调,服务端操作结束后,调用回调。
基于事件的系统天生就是异步的,整个系统都很聪明,业务逻辑不会集中于某个核心大脑,而是平均地分布在不同的协作者中。基于事件的协作方式耦合性很低。客户端发布一个事件,但并不需要知道谁或者什么会对此作出响应。你可以在不影响客户端的情况下对该事件添加新的订阅者。
4.5编排与协同
编排:让客户服务作为中心大脑,创建时,它会跟积分账户,电子邮件服务以及邮政服务通过请求/响应的方式进行通信。假如使用的是同步的请求/响应模式,我们甚至能知道每一步是否都成功了。
缺点
会让少量的服务成为“上帝”服务,而与其他打交道的那些服务通常都会沦为贫血的,基于CRUD的服务。
协同:仅仅从客户服务中使用异步的方式触发一个事件,该事件名可以叫做“客户创建”。电子邮件服务,邮政服务以及积分账户可以简单地订阅这些事件并且做响应的处理。
协同的方式可以降低系统的耦合度,并且你能更加灵活地对现有系统进行修改。使用协同方式,在这种方式下,每个服务都足够聪明,并且能够很好地完成自己的任务。
不同的场景选择不同的方式,所以,需要了解不同技术的实现细节,从而更好地做出选择。
针对请求/响应方式,可以考虑两种技术:RPC(Remote Procedure Call,远程过程调用)和REST(Representational State Transfer,表述性状态转移)。
4.6远程过程调用
RPC的种类繁多,其中一些依赖于接口定义(SOAP,Thrift,protocol buffers等)。
有很多技术 本质上是二进制的,比如Java RMI,Thrift,protocol buffers等,而SOAP使用XML作为消息格式。有些RPC实现与特定的网络协议相绑定(比如SOAP名义上使用的就是HTTP)。根据自己的使用场景来选择不同的网络技术。
4.6.1技术的耦合
有些RPC机制,如Java RMI,与特定的平台紧密绑定,这对于服务端和客户端的技术选型造成了一定限制。Thrif和protocol buffers对于不同语言的支持很好,从而在一定程度上减小这个问题的影响。
4.6.2本地调用和远程调用并不相同
RPC的核心想法是隐藏远程调用的复杂性。但是很多RPC的的实现隐藏的有些过头了。简单地把一个本地的API改造成为跨服务的远程API往往会带来问题。开发人员会在不知道该调用是远程调用的情况下对其进行使用。
封装和隐藏带来的效率和生产力不必说,但同时也带来了“无知”,这也是为什么很多公司注重员工对源码的研究能力和探究兴趣。
分布式计算中一个非常著名的错误观点就是“网络是可靠的”。
4.6.3脆弱性
生产者和消费者共用模型,如果服务端修改了模型,那么,对象反序列化的时候就会出问题。需要把生产者和消费者的部署绑定在一起。
4.6.4RPC很糟糕吗
如果你决定要选用RPC这种方式的话,需要注意:不要对远程调用过度抽象。
RPC是请求/响应协作方式中的一种,相比使用数据库集成的方式,RPC显然是一个巨大的进步,但是,我们还有其他的选择。
4.7REST
REST是RPC的一种替代方案。
REST本身并没有提到底层应该使用什么协议,尽管事实上最常用的是HTTP。HTTP的一些特性,比如动词,是的在HTTP之上实现REST要简单的多,而如果使用其他协议的话,就需要自己实现这些特性了。
4.7.1REST和HTTP
REST架构风格声明了一组对所有资源的标准方法,而HTTP恰好也定义了一组方法可供使用。GET使用幂等的方式获取资源,POST创建一个新资源。
HTTP也可以用来实现RPC,比如SOAP就是基于HTTP进行路由的,但不幸的是它只用到了HTTP很少的特性,而动词和HTTP的错误码都被忽略了。
4.7.3JSON,XML还是其他
目前来说,JSON更加流行。
JSON也有一些缺点。XML使用链接来进行超媒体控制。JSON标准中并没有类似的东西。
JSON的流行与互联网的流行和普及分不开,但XML也有其适用的场景。比如,很多地方的导入导出就用的XML,iUAP设计器的导入导出就用的是XML。
4.7.4留心过多的约定
我们很容易把存储的数据直接暴露给消费者,那如何避免这个问题呢?
一般先设计外部接口,等到外部接口稳定之后再实现微服务内部的数据持久化。在此期间,可以简单地将实体持久化到本地磁盘的文件上。这并非长久之计,但这样做可以保证服务的几口是由消费者的需求驱动出来的,从而避免数据存储方式对外部接口的影响。
设计的一些事儿,可以从全局出发,可以为很多的场景做预备,扩展性,松耦合等等,但也同时要避免过度设计~
4.7.5基于HTTP的REST的缺点
虽然HTTP可以用于大流量的通信场景,但对于低延迟通信来说并不是最好的选择。相比之下,有一些构建于TCP(Transmission Control Protocol,传输控制协议)或者其他网络技术之上的协议更加高效。比如WebSockets更加高效,在初始的HTTP握手之后,客户端和服务端之间就仅仅通过TCP连接了。
对于服务和服务之间的通信来说,如果低延迟或者较小的消息尺寸对于你来说很重要的话,那么一般来讲HTTP不是一个好主意。你可能需要选择一个不同的底层协议。
有些RPC的实现支持高级的序列化和反序列化机制,这部分可能会成为服务端和客户端之间的一个耦合点,因为实现一个具有容错性的读取器不是一件容易的事情,但从快速启动的角度来看,它们还是非常有吸引力的。
综上,基于HTTP的REST仍然是一个比较合理的默认选择。
4.8实现基于事件的异步协作方式
4.8.1技术选择
微服务发布事件的机制和消费者接收事件的机制。
像RabbitMQ这样的消息代理能够处理上述两个方面的问题,而且有很好的可伸缩性和弹性,但也同时会增加开发流程的复杂度,需要一个消息系统(即消息代理)才能开发以及测试服务,也需要额外的机器和知识来保持这些基础设施的正常运行。但是,一但做好这些,它会是实现松耦合,时间驱动架构的一种非常有效的方法。
但是,要注意一点:尽量让中间件保持简单,而把业务逻辑放在自己的服务里。
4.8.2异步架构的复杂性
异步的问题:
一个耗时的异步请求/响应,需要考虑响应返回时需要怎么处理。
- 返回到发送请求节点,如果节点服务停止了怎么办?
- 不返回发送请求节点,是否需要把信息事先存到某个地方,以便后续处理
对于习惯进程间同步调用的程序员来说,使用异步模式也需要思维上的转换。
注意设置作业最大重试次数。必要时需要实现一个消息医院(或者叫死信队列),所有失败的消息都会被发送到这里。可能还需要做一个界面来显示这些消息,如果需要的话还可以触发一个重试。
还要注意,需要确保各个流程有很好的监控机制,并考虑使用关联ID,这种机制可以帮助你对跨进程的请求进行追踪。
4.9服务即状态机
服务应该根据限界上下文进行划分。每个服务应该拥有与这个上下文中行为相关的所有逻辑(高内聚)
4.10响应式扩展
响应式扩展(Reactive extensions,Rx)提供了一种机制,你可以把多个调用的结果组装起来并在此基础上执行操作。调用本身可以是阻塞或者非阻塞的。
RxJava
很多RX实现都在分布式系统中找到了归宿,因为调用的细节被屏蔽了,所以事情也更容易处理。其漂亮之处在于,可以把多个不同的调用组合起来,这样就可以更容易地对下游服务的并发调用做处理。
https://zhuanlan.zhihu.com/p/27678951
https://juejin.im/post/5b8f536c5188255c352d3528
https://www.jianshu.com/p/0cd258eecf60
4.11微服务世界中的DRY和代码重用的危险
DRY(Don't Repeat Yourself),如果不遵守,可能会导致代码规模变大,从而降低可维护性。
经验:在微服务内部不要违反DRY,但在跨服务的情况下可以适当违反DRY,服务之间引入大量的耦合回避重复代码带来更糟糕的问题。“但这的确是一个值得进一步探索的问题!!!!”
客户端库
如果你想要使用客户端库,一定要保证其中只包含处理底层传输协议的代码,比如服务发现和故障处理等。千万不要把与目标服务相关的逻辑放到客户端库中。
4.12按引用访问
数据一直在变动,你现在拿到的这个实体不一定就是之前的那个实体,这个实体的很多属性可能都已经发生了变化,时间越长,概率越大。很多时候,为了保证数据的实时性,我们可能拿到的应该是一个引用,而非副本。这样,在需要查看时,获取最新的信息,这样就可以保证时效性。但这同时带来了查询的压力。这时,可以引入“时效性”缓存,这样,可以在请求压力和数据时效性之间达成较好的平衡。
上边说的不是一定的,要根据不同的业务场景采用不同的方案。
4.13版本管理
4.13.1尽可能推迟
减小破坏性修改影响的最好办法就是尽量不要这样做。
Martin Fowler->容错性读取器 http://martinfowler.com/bliki/TolerantReader.html
客户端尽可能灵活地消费服务响应。系统中的每个模块都应该“宽进严出”。在请求/响应的场景下,该原则可以帮助我们在服务发生改变时,减少消费方的修改。
比如对实体序列化的控制,序列化id.
4.13.2及早发现破坏性修改
针对所有消费者的测试,尽早发现问题。
契约的定义,当服务提供者和服务消费者约定好接口后,契约就已经形成,当契约不更改的情况下,服务提供者的所有提交修改都必须符合契约的定义。
4.13.3还有那个语义化的版本管理
语义化版本管理:MAJOR.MINOR.PATCH MAJOR的改变意味着其中包含向后不兼容的修改;MINOR的改变以为这有新功能的增加,但应该是向后兼容的;最后,PATCH的改变代表对已有功能的缺陷修复。
4.13.4不同的接口共存
希望微服务可以独立于彼此进行发布,有一种办法是同时包含新老接口的版本。这样,既更新了接口,也给消费者时间做迁移。一旦所有的消费者不再访问老的接口,就可以删除该接口以及相关的代码。
同时维护多个版本接口不推介这样,因为维护负担重。为了使其更可控,可以在内部把素有对V1的请求进行转换处理,然后去访问V2,继而V2再去访问V3,使用这种方式后,以后需要删除哪些代码就比较清楚了。
场景:有团队正在使用你们团队编写的当前接口,而且顾不上升级。
4.13.5同时使用多个版本的服务
短期内同时使用两个版本的服务是合理的,尤其是当你做蓝绿部署或者金丝雀发布时。如果升级消费者到新版本的时间过程,就该考虑是否应该在微服务中暴露两套API的做法了。
4.14用户界面
4.14.1走向数字化
从组合的角度来考虑用户界面,如果把我们提供的能力看成是不同的绳索,组合就是把它们编织起来,可以为桌面应用程序,移动端设备,可穿戴设备的客户提供不同的体验。
React的成功不是偶然,Redux更加确定了这一点的必然性。
4.14.2约束
桌面Web应用---需要考虑与用户浏览器以及屏幕解析度相关的约束
移动端----移动网络的带宽,交互方式可能会导致电量消耗过快,从而导致客户流失
尽管我们的核心服务可能是一样的,但需要对不同的场景的约束进行考虑。
4.14.3API组合
UI如何聚合呢?使用API入口(gateway)可以很好的缓解这一问题,在这种模式下多个底层的调用会被聚合成为一个调用。
4.14.4UI片段的组合
让服务主动暴露出一部分UI,然后只需要简单地把这些片段组合在一起就可以创建出整体UI。可以使用类似服务端模板的技术来实现拼装。但也有问题:
- 保证用户体验的一致性比较困难
- 原生应用和胖客户无法消费服务端提供的UI组件(展示没问题,想使用其中的很多功能与原生应用交互却很难)
- 服务提供的能力难以嵌入到小部件或者页面中,联动也不容易做。
4.14.5为前端服务的后端
问题在于,如果该入口变得太厚,包含的逻辑太多,就会难以维护,它们会被逐渐交由单独的团队来管理,并且因为它们变得太厚,很多功能的修改都会导致这部分代码的修改。
一个后端只为一个应用或者用户界面服务(BFF(Backends For Frontends),为前端服务的后端)。允许团队在专注于提供给定UI的同事,也会处理与之先骨干的服务端组件。
4.14.6一种混合方式
一个组织可能会选择基于片段组装的方式来构建网站,但对于移动应用来说,BFF可能是更好的方式。关键是要保持底层服务能力的内聚性。
4.15与第三方软件集成
组织经常面临的问题:
- 组织对软件的需求几乎不可能完全由内部满足
- 自研对于大多数商业公司来说是非常低效的。
应该自己做,还是买?
如果某个软件非常特殊,并且它是你的站战略性资产的话,那就自己构建;如果不是这么特别的话,那就购买。
4.15.1缺乏控制
如何与现有购买的产品集成?厂家决定的。使用什么语言进行扩展?厂家决定的。能否引入针对该工具配置文件的版本控制?厂家决定的。
---尽量把集成和定制化的工作放在自己能够控制的部分。
4.15.2定制化
销售口中的深度化定制一定要小心。针对一个巨无霸,N久年前的工具做定制化开发,你会明白什么是绝望。。。
扩展开发的一些思考:
1.支持扩展,但是需要绕一大圈,甚至需要去弄懂计算机基础的很多东西,需要会N多年前的老技术,绕大圈解决问题完全是情非得已,而不应该成为第一拨人的首选。
2.简单,易懂,直接,这应该是扩展支持该有的姿态。
4.15.3意大利面式的集成
服务之间的集成是一件非常重要的事情,理想情况下应该存在一些为数不多的标准化集成的方式。
很多时候,我们总是喜欢将就一下,从第一个人将就到最后一个人不得不将就。很多时候,我们面临的不是这样可以做到,那样也可以做到的问题,而是标准的问题。把你丢进一个集成方式有十几种的组织中,你再牛逼也觉得苦逼。
4.15.4在自己可控的平台进行定制化
SAAS当然是有用的,但不适用于从头开始构建的系统。关键是把事情移到自己可控的部分做。
如果我没理解错的话,上图应该是峰哥当年给U8云化指的出路。
理出来该有的基础接口(现代化,精简化,清晰化,JSON化):
领域元数据接口,表单元数据接口,参照接口,增删改查公共接口。这是我们当年走过的路子。。。
4.15.5绞杀者模式
其实个人感觉这种方式自己玩就可以了,或者在领导不知道的情况下,自己在长久待的组织中一点一点啃。大多数情况下,你跟领导说我要一点一点绞杀,领导会跟你说“这事儿咱别干了,太慢”
绞杀者模式,绞杀者可以捕获并拦截对老系统的调用,进而决定把调用路由到遗留代码中还是导向新写的代码中。逐步对老系统进行替换。
老手和新手的区别
《Effective C++》作者Scott Mayer ,C++老手和 C++新手的区别就是前者手背上有很多伤疤
过去的事情如果就这么简单的过去了,那未来只会更糟 --《驴得水》