REST?RPC?是时候改变你对微服务的认知了!

大部分时候,微服务都是创建在一种基于请求和响应的协议之上。好比,REST等。这种方式是天然的。咱们只须要调用另一个模块就是了,而后等待响应返回,而后继续。这样的方式确实也知足了咱们的不少的场景:用户经过点击页面的一个按钮而后但愿发生一些事情。react



可是,当咱们开始接触许多独立的service的时候,事情就发生改变了。随着service数量急速的增加,同步交互比例也随着service在急速增加。这时候,咱们的service就会遇到不少的瓶颈。数据库


因而,不幸的ops工程师们就被咱们坑了,他们疲惫的奔波于一个又一个的service,拼凑在一块儿的二手信息片断,谁说了什么,去往哪里,何时发生?等等。。。api

这是一个很是典型的问题。市面上也有一些解决方案。一种方案就是确保您的我的服务具备比您的系统更高的SLA。 Google提供了这样作的协议。另外一种方法是简单地分解将服务绑定在一块儿的同步关系。浏览器

上面的作法都没有从模式上根本解决问题。咱们可使用异步机制来解决这个问题。好比,电商网站中你会发现这样的同步接口,好比getImage()或者processOrder(),也许你感受蛮正常。调用了而后但愿立刻有一个响应。但当用户点击了“购买”后,触发了一个复杂且异步的处理过程。这个过程涉及到购买、送货上门给用户,这一切都是发生在当初的那一次的按钮点击。因此把一个程序处理逻辑切分红多个异步的处理,是咱们须要解决的问题。这也正符合咱们的真实的世界,真实世界原本就是异步的,拥抱异步吧。
缓存

在实际状况下,咱们其实已经自动拥抱了异步了。咱们发现本身会定时轮询数据库表来更改又或者经过cron定时job来实现一些更新。这些方法都是一些打破同步的方式,可是这种作法总让人感受有种黑客范儿,感受像是黑客行为,怪怪的。网络

在本文中,咱们将会讨论一种彻底不一样的架构:不是把service们经过命令链揉到一块,而是经过事件流(stream of events)来作。这是一个不错的方式。这种方式也是咱们以后要讨论的一系列的一个基础。架构


令(commands)、事件(events)和查询(queries)app

当咱们进入正式的例子以前,咱们须要先普及三个简单的概念。一个service与另一个service有三种交互方式:命令(Commands)、事件(Events)以及查询(Queries)。dom


事件的美妙之处在于“外部数据”能够被系统中的任何service所重用。异步

并且从service的角度来讲,事件要比命令和查询都要解耦。这个很重要。

服务之间的交互有三种机制:

  1. Commands 。命令是一个操做。但愿在另外一个服务中执行某些操做的一个请求。 会改变系统状态的东西。 命令期待有响应。

  2. Events 。事件既是一个事实也是一个触发器。 发生了一些事情,表示为通知。

  3. Queries 。查询是一个请求,是一个查找一些东西的请求(request)。重要的是,查询不会使得系统状态发生改变。


ps:这里注意 请求  事件 的区别。命令和查询都属于请求驱动。

一个简单事件驱动流程

让咱们开始一个简单的例子:用户购买一个小东西。那么接下来要发生两件事情:

  1. 支付。

  2. 统检查是否有更多的商品须要被订购

在请求驱动(request-approach)的架构中,这两个行为被表现为一个命令链条。交互就像下面这样:


首先要注意的问题是“购买更多”的这个业务流程是随着订单服务(Order Service)一块被初始化的。这就使得责任不独立,责任跨了两个service。理想状况下,咱们但愿separation of concerns,也就是关注隔离。

如今若是咱们使用事件驱动,而不是请求驱动的方式的话,那么事情就会变得好一些。

  1. 在返回以前,UI service 布一个OrderRequested事件,而后等待OrderConfirmed(或者Rejected)。

  2. 订单务(Orders Service存服Stock Service) react个事件。


仔细看这里,UI serviceOrders Service并无改变不少,而是经过事件来通讯,而不是直接调用另外一个。

这个Stock service(库存服务)颇有趣。Order Service告诉他要作什么。而后StockService本身决定是否参与本次交互,这是事件驱动架构很是重要的属性,也就是:Reciver Driven Flow Control,接收者驱动流程控制。一会儿控制反转了。

这种控制反转给接收者,很好的解耦了服务之间的交互,这就为架构提供了可插拔性。组件们能够轻松的被插入和替换掉,优雅!


随着架构变得愈来愈复杂,这种可插拔性的因素变得更加剧要。举个例子,咱们要添加一个实时管理订价的service,根据供需调整产品的价格。在一个命令驱动的世界里,咱们就须要引入一个能够由库存服务(Stock Service)和订单服务(Orders Service)调用的相似updatePrice()这样的方法。

可是在事件驱动(event-driven)世界更新价格的话,service只须要订阅共享的stream就是了,当相应的条件符合时,就去执行更新价格的操做。


事件(Events)和查询(Queries)的混合

上面的例子只是命令和事件。并无说到查询。别忘了,咱们以前但是说到了三个概念。如今咱们开始说查询。咱们扩展上面的例子,让订单服务(Orders Service)在支付以前检查是否有足够的库存。

在请求驱动(request-driven)的架构中,咱们可能会向库存服务(Stock Service)发送一个查询请求而后获取到当前的库存数量。这就致使了模型混合,事件流纯粹被用做通知,容许任何的service加入flow,但查询倒是经过请求驱动的方式直接访问源。


对于服务(service)须要独立发展的较大的生态系统,远程查询要涉及到不少关联,耦合很严重,要把不少服务捆绑在一块儿。咱们能够经过“内部化”来避免这种涉及多个上下文交叉的查询。而事件流能够被用于在每一个service中缓存数据集,这样咱们就能够在本地来完成查询。

因此,增长这个库存检查,订单服务(Order Service)能够订阅库存服务(Stock Service)的事件流,库存一有更新,订单服务就会收到通知,而后把更新存储到本地的数据库。这样接下来就能够查询本地这个“视图(view)”来检查是否有足够的库存。

纯事件驱动系统没有远程查询的概念 - 事件将状态传播到本地查询的服务

经过事件来传播( Queryby Event Propagation”)的查询有如下三个好处:

一、更好的解耦:在本地查询。这样就不涉及跨上下文调用了。这种作法涉及到的服务们远远不及那种”请求驱动”所涉及到的服务数量多。

二、更好的自治:订单服务(Order Service)拥有一份库存数据集的copy,因此订单服务能够任意使用这个本地的数据集,

而不是说像请求驱动里的那样仅仅只能检查库存限额,并且只能经过Stock Service所提供的接口。

三、高效Join:若是咱们在每次下订单的时候都要去查询库存,就要求每次都要高效的作join,经过跨网络对两个service进行join。随着需求的增长,或者更多的数据源须要关联,这可能会变得愈来愈艰巨。因此经过事件传播来查询(Query by Event Propagation)将查询(和join)本地化后就能够解决这个问题(就是本地查询)。


但这种作法也不是没有缺点。 Service从本质上变得有状态了。这样就使得他们须要被跟踪和矫正这些数据集,随着时间的推移,也就是你得保证数据同步。状态的重复也可能使一些问题更难理解(好比如何原子地减小库存数量?),这些问题咱们都要当心。可是,全部这些问题都有可行的解决方案,咱们只是须要多一点考虑而已。 

单一写入者原则(Single Writer Principle

针对这种风格的系统,也就是事件驱动风格的系统,一个很是有用的原则就是针对指定类型的传播的事件分配责任的时候,应该只分配给一个单一的service:单一的写入者。什么意思呢?就是Stock Service只应该处理库存这一件事情,而Order Service也只属于订单们,等等。

这样的话有助于咱们经过单个代码路径(尽管不必定是单个进程)来排除一致性,验证和其余写入路径(writepath问题。所以,在下面的示例中,请注意,订单服务(Order Service)控制着对订单进行的每一个状态的更改,但整个事件流跨越了订单(Orders),付款(Payments)和发货(Shipments),每一个都由它们各自的服务来管理。

分配“事件传播”(event propagation)的责任很重要,由于这些不只仅是短暂的事件,或者是那种无须保存短暂的聊天。他们表明了共同的事实(facts),以及“数据在外部(data-on-the-outside)“。所以,随着时间的推移,服务(services)须要去负责更新和同步这些共享数据集(shared datasets):好比,修复错误,处理schema的变化等状况。


上图中每一个颜色表明Kafka的一个topic,针对下订单(Order)、发货和付款。  当用户点击“购买”时,会引起“Order Requested”,等待“Order Confirmed”事件,而后再回复给用户。 另外三个服务处理与其工做流程部分相关的状态转换。 例如,付款处理完成后,订单服务(Order Service)将订单从“已验证(Validated)”推送到“已确认(Confirmed)”。


模式(Patterns和集群服务(Clustering Services)的混合

上面的说到的模型有点像企业消息(Enterprise Messaging),但实际上是有一些不一样的。企业消息,在实践中,主要关注状态的转换,经过网络有效地将数据库捆绑在一块儿。

而事件协做(Event Collaboration)则更偏重的是协做,既然是协做就不简单的是状态转换,事件协做是关于服务(service)经过一系列事件进行一些业务目标,这些事件将触发service的执行。因此这是业务处理(business processing)的一种模式,而不是简单的转换状态的机制。

咱们一般但愿在咱们构建的系统中这种模式具备两面性。事实上,这种模式的美妙之处在于它确实既能够处理微观又能够处理宏观,或者在有些状况下能够被混合。

模式组合使用也很常见。咱们可能但愿提供远程查询的方便灵活性,而不是本地维护数据集的成本,特别是数据集增加时。这样的话就会让咱们的查询变得更加的简单,咱们只须要轻松部署简单的函数就能够了。并且咱们如今不少都是无状态的,好比容器或者浏览器,在这种状况下也许远程查询是一种合适的选择。

远程查询设计的诀窍就是限制这些查询接口的范围,理想状况下应该是在有限的上下文中(context)。一般状况下,创建一个具备多个特定,具体视图的架构,而不是单一的共享数据存储。注意是多个具体的视图,而不是单一的共享数据存储。(一个独立(bounded)的上下文,或者说是偏向原子,这里说的原子不是侧重微服务中常说的那个“原子服务”。独立上下文,通常是指有那么一组service,它们共享同一个发布流水线或者是同一个领域模型【domain model】)。

为了限制远程查询(remote queries)的边界(scope),咱们可使用一种叫作“集群式上下文模式(clustered context pattern)”。这种状况下,事件就流纯粹是用做上下文之间的通讯。但在一个上下文里的具体service们则能够既有事件驱动(event-driven)的处理,同时也有请求驱动(request-driven)的视图(view),具体根据实际状况须要。

在下面的例子中,咱们有三个部分,三个之间只经过事件相互沟通。在每个内部,咱们使用了更细粒度的事件驱动流。其中一些包括视图层(查询层)。

仍是看下图吧:


集群上下文模型(Clustered Context Model)

事件驱动(event-driven)五个关键好处:

  1.  解耦:把一个很长的同步执行链的命令给分解,异步化。 分解同步工做流。 Brokers 或topic解耦服务(service),因此更容易插入新的服务(service),具备更强的插拔性。

  2. 离线/异步流:当用户点击按钮时,不少事情都会发生。 一些同步,一些异步。 对能力的设计,不管是之前的,仍是未来的,都是更自由的。提升了性能,提升了自由度。

  3. 状态同步更新:事件流对分布式数据集提供了一种有效的机制,数据集能够在一个有界的上下文里被重构(“传播”或“更新”)和查询。

  4.  Joins:从不一样的服务(service)组合/join/扩展数据集更容易。 join更快速,并且仍是本地化的。

  5. 可追溯性: 当有一个统一化的,中心化的,不可变的,保持性的地方来记录每一个互动时,它会及时展示,debug的时候也更容易定位问题,而不是陷入一场关于“分布式”的谋杀。(这里有点晦涩)

总结

Ok,在事件驱动的方法中咱们使用事件(Events)而不是命令(Commands)。事件触发业务处理过程。事件也能够用到更新本地视图上。而后咱们向你介绍了,在必要时,咱们能够再回到远程同步查询这种方式,特别是在较小的系统中,并且咱们还将远程同步查询的范围扩大到更大的范围(理想状况下,仍是要仅限于单个独立的上下文,也就是单个领域模型,不能再扩大了,刚恰好才是真的好)。

并且全部这些方法都只是模式(pattern)。模式就会有框得太死的问题。模式覆盖不到的地方,咱们就要具体状况具体对待了。例如,单点登陆服务,全局查询的service仍然是一个好主意,由于它不多更新。

这里的秘诀就是从事件的基准出发去考虑问题。事件让服务之间再也不耦合,而且将控制(flow-control)权转移到接收者,这就有了更好的“分离关注(separated concerns)”和更好的可插拔性。

关于事件驱动方法的另外一个有趣的事情是,它们对于大型,复杂的架构一样适用,就像它们对于小型,高度协做的架构同样。事件让service们能够自主的决定本身的全部事情,为服务们提供自由发展所需的自主权。

而后咱们向你介绍了事件和查询混合的场景。说到查询,在纯事件驱动方法中,查询彻底基于本地的数据集,而没有远程查询。本地数据集则是经过事件触发来更新状态。然而,不少时候,基于请求驱动的查询方式在不少时候也是比较方便的,由于本地数据集的方式,状态的同步更新确实是一件更加须要成本的事情。

而后咱们说到了单一写入z者原则。单一写入者让咱们数据更新有了统一的入口,有助于咱们经过单个代码路径(尽管不必定是单个进程)来排除一致性,验证和其余“写入路径(writepath)”问题。

而后咱们讨论了集群上下文模型。每一个领域模型组成一个独立的区域,而后再由多个区域共同组成一个领域模型集群,模型之间又经过Kafka来交互。每一个领域模型里又能够包含几种模式的混合,好比Events、Views、UI,这些里边能够既有事件驱动模式,又有请求驱动模式。

大致就这么多。

感谢Antony Stubbs,Tim Berglund,Kaufman Ng,GwenShapira和Jay Kreps,他们帮助咱们回顾了这篇文章。

译者曰:最近也刚好在作有关事件流的内容,对本文中讲到的异步解耦和拆解同步请求链条过长问题深有感触,也很是认同。另外最近有人聊到有关数据库查询效率问题,经过阅读本文也许会让你对查询有一个全新的认识。这些微服务理念看起来好像专属于“微服务”,好像其余人就不须要了解同样。其实也许微服务的这些先进理念就像其余任何的先进的架构理念同样,他们都是咱们软件架构知识体系的储备之一,也许在哪天你正在进行的项目遇到了瓶颈,没准本文讨论的这些内容就能派上用场了,不只仅限于本文举的那个例子。

微服务"交互方式"观念转变:

 是时候更新一下你对于构建微服务的一些知识体系了。若是你认为REST就是微服务构建的主要交互方式的话,那么也许你错了;若是你认为rpc就是构建微服务的的主要交互方式的话,那么也许你又错了。

由于这两种都属于一种类型,那就是他们都属于请求驱动(request-driven)模式,而这种模式不少时候是同步的,一条链上挂了不少的服务调用,势必在链条变长后,性能堪忧。

本文向你推荐了一个构建微服务的新的工具,或者说是向你补充了。那就是事件驱动(event-driven)的模式。它解耦、异步,带来了更好的扩展性和性能。不少时候,同步会让事情变得异常糟糕!

若是之后有人和讨论起微服务的模式的时候,你能够说REST、rpc(请求驱动)以及事件驱动共同混合使用才会构建出更好的微服务来!

ps:文中部分段落翻译用词略显晦涩,我曾尝试用大白话来翻译,但发现会损失原意,故请仔细斟酌消化。