微服务设计: RPC、REST以及异步通信

用《翻滚吧!阿信》电影的一句台词就是,“如果你一生只有一次翻身的机会,就要用尽全力!”

最近新的项目都是用微服务做实现, 一些在我看来可以放在一起的功能模块, 也被拆分成独立的进程, 每个人都给我大谈特谈微服务的划分, blashblash, 估计现在不说说自己在做微服务,都不好意思跟人家打招呼.

快餐时代, 大家都喜欢用厕所时间去阅读各种公众号上的文章, 但是较低概率遇见一篇系统的好文章, 于是安静打开了<微服务设计>这本书,哪怕里面翻译的流畅度比较勉强,但是本身内容的新颖度和信息量,完全遮盖了翻译上的不足.

首先谈谈困扰我非常久的RPC远程过程调用:
RPC也叫远程过程调用, 你以为你在调用本地的一个方法,但实际上该方法是远程服务器产生的.依赖借口定义(SOAP, Thrift, Protocol buffers等), 轻松生成客户端和服务端的桩代码. 例如, 我可以用一个Java服务暴露一个SOAP接口, 然后使用WSDL(Web Service Definition Language)定义的接口生成.NET客户端的代码.

这些RPC的实现会帮你生成服务端和客户端的桩代码,从而让你快速开始编码. 基本不用花时间, 我就可以在服务之间进行内容交互了. 这也是RPC的主要卖点之一: 易于使用.

有很多技术本质上是二进制的, 比如Java RMI, Thrift, Protocol buffers等, 而SOAP是用XML作为消息格式. 有些RPC实现与特定的网络协议相绑定(比如, SOAP名义上使用的就是HTTP). 不同网络协议可以提供不同的特性, 比如TCP提供保证送达, UDP虽然不能保证送达但协议开销小, 所以你可根据自己的使用场景来选择不同的网络技术.

RPC的缺点:
一些RPC机制,如Java RMI, 和特定的平台JVM绑定, 这对服务端和客户端技术选型上造成了限制. Thrift和Protocol buffers对于不同语言的支持很好.

使用本地调用不会引起性能和网络开销问题, 但是RPC会花大量的时间对调用方法进行封装和解封装.

可能因为服务端接口的变化而强迫调用方法的客户端需要重新生成桩代码.

REST
REST是受web启发而产生的一种架构风格. REST最重要的是资源的概念. 一个资源的对外显示方式和内部存储方式之间没有什么耦合. 例如, 当请求一个JSON表示形式的资源, 而该资源在内部存储方式可以完全不同. REST是RPC的一种替代方案.
实际上, REST并没有指定底层用什么协议, 只是HTTP本身提供的功能与实现REST风格非常接近, 比如GET, POST, PUT, DELETE等动作就能很好的和资源一起使用, HTTP恰好定义了这样的一组方法可以使用.

基于HTTP的REST支持不同的格式, 比如JSON或者二进制, JSON是一种比XML内容更加紧凑的文本方式. 需要注意的是: 基于HTTP的通信, 适合大流量的场景, 对低延迟通信并不是最好的选择(HTTP本身开销问题). 有一些构建于TCP或者其他协议更加高效, 比如WebSocket, 虽然名字中有Web, 但是在初始HTTP握手以后, 客户端和服务器端之间就仅仅通过TCP连接了. 对于向浏览器传输数据这个场景而言, Websockets更加高效.

RPC和REST的区别
上面关于RPC和REST的描述,你已经大概知道了他们的区别.

简单说, RPC是面向一个方法的调用, 比如SOAP, 所有要调用对方的方法封装在(只能是)POST消息里, 发往对方, 对方收到该POST请求, 通过解开封装调用该方法, 才可能知道该方法是做CRUD具体什么操作, 而且, 可能会因为服务提供方法的改变, 而重新生成客户端的代码. 而REST是面向一个资源的操作, 基于HTTP操作资源的时候,可以直接通过HTTP的各种方式GET, POST, PUT, DELETE等指出具体是什么操作. 在这种情况下, 如果, 我们在系统前端添加安全控制, 就可以通过指定某些敏感方法不可以(DELETE)被允许, 而SOAP方式是无法做到的.

但是像前面所述的,基于HTTP的REST在性能上与像Thrift这样的二进制协议是没法比较的。

最后,在序列化和反序列化支持上,RPC实现支持高级的序列化和反序列化机制,而对于REST而言,这部分工作需要自己做。

基于HTTP的REST虽然有缺点,但是选择服务之间的交互,它仍然是一个比较合理的默认选择。

基于事件的异步通信
前面讨论了一些与请求/响应模式相关的技术,下面简单介绍下耦合度很低的基于事件的异步通信(订阅服务)

关于这个我只想记录一点:
消费者竞争模式描述了一种使用多个工作者实例同时消费消息的方法,工作者实例的数量可以增加,而且他们可以独立于彼此工作。但是有一种场景要避免,即多个工作者处理了同一条消息,从而造成浪费。如果使用标准的消息(例如RabbitMQ)队列就可以很好的处理这种场景。

异步通信架构的复杂性

基于消息队列的服务运行起来了,我们感觉非常棒。但是在某一次发布后,我们遇到了一个令人讨厌的问题。我们的工作者不停崩溃,不停崩溃,不停崩溃。

最终我们发现了bug所在,代码中某个请求会导致某个工作者(消费者)崩溃,当时我们采用的是事物处理队列:当工作者崩溃后,这个请求上的锁会超时,然后该请求就会被放回到队列里,另外的工作者就会重新尝试处理该请求,然后它也会崩溃,这就是典型的灾难性故障转移。

最后,我们修复了这个bug,并且设置了最大重试次数。但是我们意识到需要一种方式来查看甚至是重发这些有问题的消息。所以,最后实现了一个消息医院(死信队列),所有失败的消息都会被发送到这里,并创建了界面显示这些消息,如果需要还可以出发一次重试。如果你只熟悉点到点的同步消息,就很难快速发现问题本质。

事件却动架构和异步编程会带来一定的复杂度,需要每个流程有一个较好的监控机制。

当你需要一个低延迟的时候,通常会选择异步通信,否则会因为阻塞而降低运行的速度。

所以,现在出门不谈异步处理,也不好意思跟人家尬聊。。。(逃

    原文作者:科学Jia
    原文地址: https://www.jianshu.com/p/c2ecdbe1184a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞