微服务(二 三)
使用API网关(二)
本书关于设计,构建和部署微服务的第一章介绍了微服务架构模式。 讨论了应用这种架构的优缺点,以及抛开微服务的复杂性,为什么说微服务架构对于复杂应用来说是理想的选择。 本部分是这个系列的第二章,会讨论用API网关构建微服务。
当你选择用一组微服务去构建你的应用的时候,你需要确定,微服务应该如何和应用的客户端打交道。 对单体应用来说意味着仅仅是一组服务端点(endpoints),他们通常是多个副本(replicated),在不同副本之间通过负载均衡服务请求。
介绍
让我们假设一下,你正在为购物应用开发一个移动客户端应用程序。你可能需要实现一个产品细节的页面显示特定的产品的信息。
举例说来,图2-1 是Amazon 安卓移动应用程序,当你移动页面时看到的产品的细节信息。
图2-1 一个购物应用尽管这是个智能手机应用,产品信息页展示了很多的信息。例如,不仅仅是产品的基本信息如名字,描述和价格,还展示:
1. 购物车数量
2. 订单历史
3. 客户评价
4. 低库存警告
5. 装运选项
6. 各种推荐,包括和本产品经常一起购买的其他产品,购买这个产品的客户也购买的其他的产品,购买本产品的用户同时查看了其他的产品。
7. 其他的购买选择。
当使用一个单体应用架构的时候,移动客户端通过调用一个REST服务获取这些数据,比如:
Get api.company.com/productdetails/productid
一个负载均衡器路由这个请求到相同应用实例中一个上去。 这个应用然后查询数据库的不同表并返回响应数据到客户端。
相反地,当应用微服务架构时,现在在产品细节页面上的数据将会来自于多个不同的微服务实例。以下是一些可能的服务,这些服务拥有显示在产品细节页面上的数据:
购物车服务-购物车购物数量
订单服务-订单历史信息
产品目录服务-基本产品信息,比如产品名称,描述和价格等
产品评论服务-客户评论
库存信息服务-低库存报警
装运服务-装运选项,期限和价格,从装运服务提供的API获取。
推荐服务-建议的其他产品。
图 2-2 移动客户端需要微服务的映射我们需要确定移动客户端如何调用这些服务。 让我们来看看有哪些选项或方法。
直接客户端到微服务通信
理论上来说,一个客户端可以直接请求任何一个微服务。 每个微服务有一个公共的服务端点:
https://serviceName.api.company.name
这个服务链接会映射到微服务的负载均衡器,然后有负载均衡器选择并分配这些请求到可用的服务实例上去。 为了获取产品页面的信息,移动客户端会请求每个以上列出的微服务。
不幸的是,这样做会有些限制和挑战。一个问题是客户端需求和每个微服务暴露的细粒度API不匹配的问题。例子中,客户端不得不分别请求各个(7个)微服务。 在更复杂的应用里,它或许不得不请求更多。比如,Amazon描述的为了渲染一个产品页面,会涉及到上百个服务。 尽管客户端可以通过本地网发起多个请求,但是如果通过公共互联网效率会很低下,这种做法绝对不是好的实践方法。 这种方法也会是客户端代码实现异常复杂。
另一个客户端调用微服务的问题是有些服务协议可能不是web访问友好的。 一个服务可能是用Thrift 二进制RPC协议,同时另一服务或许是用AMQP消息协议。 这两种协议都不是浏览器或防火墙友好的协议。 所以这种协议最好是内部使用。一个应用应该使用Http或是Websocket,这两种是web或防火墙友好的协议(注:都是80端口,公司等一般会开放这两个端口)。
应用这种方法的另一种缺点是微服务会不好重构。随着时间的推移我们或许会重新调整服务分区(partitioned)。比如,我们或许会合并或拆分一个服务成两个或多个服务。 如果客户端直接调用微服务,这种类似的重构(refactoring)实施起来会非常的困难。
使用一个API网关
通常一种更好的方法是使用API网关技术。 一个API网关是一个服务器,它是整个系统的唯一的入口。这个概念和面向对象(OOD-Object-Oriented-Design)的门面模式(Façade Pattern)相似。这个API网关封装了内部系统架构并且提供给每个客户裁剪后的API。 它或许会提供其他的功能如身份验证,监控,负载均衡,缓存,请求整形和管理,和静态请求处理等等。
图2-3 在微服务中使用API网关API网关负责路由请求,组合,和协议转换。 所有的来自客户端的请求都会经过API网关。然后被路由到合适的微服务。 API网关通常将会通过调用多个微服务和聚合结果来处理一个请求。 它会为不同的协议提供翻译,比如HTTP,WebSocket和一些内部用的Web 非友好协议等等。
API网关也提供给每个客户端一个定制的API。 它通常会提供给移动客户端一个粗粒度的API。 想想,比如,产品细节的情况。API网关能提供一个服务端点(/productdetail?productid=xxx),它使移动客户端仅调用一次就可以获取所有的产品细节信息。 这个API网关通过调用各种不同的服务(产品信息,推荐,评价等等)组合结果。
一个很好的例子是Netflix的API网关。 Netflix的流服务被各种不同的设备使用,包括电视,各种盒子(set-top boxes),智能手机,游戏系统,平板电脑等等。起初,Netflix试着提供一组适用所有需求(one-size-fits-all)的流请求的API。然而,他们后来发现这是有问题的。 因为各种不同的设备会有他们独特的需求。现在他们使用API网关技术,它对每种设备通过适配器的方式提供API裁剪。 一个适配器(Adapter)通常是通过调用平均地6-7个后端服务来处理每个请求。 Netflix的API网关,一天大约处理10亿级别的请求。
API网关的优缺点
正如你所期待的,使用API网关技术有缺点也有优点。 一个主要的好处是它封装了应用的内部结构。 不是非得调用特定的服务,客户端简单地和API网关交互。 API网关为每类客户端提供一个特定的API 。 这减少了客户端和应用的服务的交互次数。 也简化了客户端代码。
API网关也有些缺点。 它必须要有一个高可用(Highly Available)组件被开发,被部署和被管理。另一个风险是API网关变成开发的瓶颈。 开发者为了暴露每个微服务端点必须更新API网关。
更新API网关的过程越轻量越好。否则,开发者会被迫排队等候更新网关。 尽管有这些缺点,但对现实世界的应用而言,使用API网关技术是非常合理的选择。
实现API网关
现在已经看到了使用API网关的动机和权衡它的优缺点。 让我们看看那些需要深思熟虑的设计问题。
性能和扩展能力
仅有少数的公司运营在像Netflix这样的规模,它需要处理每天10亿级别的请求。 尽管如此,对于大多数的应用来说,API网关的性能和扩展能力通常也是非常重要。 因此,构建一个在支持异步的,非阻塞I/O平台上的API网关是非常合理的。有很多不同的技术可以被用于实现这种可扩充的API网关。 在JVM上,你可以使用基于NIO的框架如Netty,Vertx,SpringReactor 或是JBoss Undertow. 一个流行的非JVM选择是Node.js ,它是构建在Chrome JavaScript引擎上的平台的。还有一个选择是NGINX Plus。
NGINX Plus提供一个成熟的,可扩充的,高性能的Web服务器和反向代理(Reverse Proxy),它易于部署,配置,编程。 NGINX Plus可以管理认证,访问控制,负载均衡请求,缓存响应和提供application-aware的健康检查和监控。
使用反应式编程模型(Reactive Programming Model)
API网关通过简单地路由请求到合适的后端服务来处理一些请求。 它通过调用多个后端服务和聚集结果处理其他请求。 一些请求,比如产品细节请求,这些后端服务相互独立。 为了最小化响应时间,API网关应当并行地(Concurrently)执行这些独立的请求。
然而,有时候,请求之间是有依赖的。API网关或许首先需要通过认证服务校验请求,然后路由给后端服务。 相似地,为了获取在客户愿望单里关于产品的信息,API网关必须首先获取用户的信息,然后获取每个产品的信息。 另一个有趣的组合的例子是Netflix Video Grid。
用传统异步回调方法写API组合的代码会很快把你引导到回调函数的地狱中。 这种代码一般会纠结在一起,很难被理解,并且容易错误。一个更好的方法是采用声明式,反应式方法写代码。反应式抽象体(reactive abstractions)包括Scala中的Future,Java中的CompletableFuture和Javascript的Promise。 也有被微软发起和开发的反应式扩展库 Reactive Extensions 。 Netflix创建了在JVM中的RxJava,主要用于他们的API网关。 JavaScript有RxJS,它能运行在浏览器和Node.js环境中。 采用反应式方法将使你写简单但高效的网关代码。
服务调用
一个基于微服务的应用是一个分布式系统,它必须使用一个进程内部通讯机制。 有两种类型的进程间通讯机制。一种是使用异步的,基于消息机制方法。 一些实现采用消息代理器(Message Broker),比如JMS或是AMQP。其他的,像ZeroMQ,是无代理的并且服务是直接通讯的。
其他风格的进程间通讯方法是同步机制比如HTTP或许Thrift。 一个系统通常同时采用这两种方法,异步的,或同步的机制。 它或许甚至采用每种风格的多个实现。API网关将需要支持多个不同的通讯机制。
服务发现
API网关需要知道每个服务与其通讯的地址(IP地址和端口)。在传统的应用中,你或许可以硬编码这些地址。但是在现代,基于云的微服务应用中,发现需要的地址并不那么简单。
基础架构服务(Infrastructure Service),比如,消息代理器,通常会有一个静态的地址,它可以通过操作系统的环境变量指定。 然而,确定一个应用服务的地址并不那么容易。
应用服务有一些动态分配的地址。 也有些服务的实例地址因为扩容和更新会动态更改。因此,API网关,像系统中的其他服务客户端一样需要采用系统的服务发现机制:要么是服务器端服务发现(Server-side discovery)或是客户端服务发现(Client-Side Discovery)。 第四章会详细地描述服务发现。现在,值得注意的是 ,如果系统采用客户端服务发现,那么API网关必须能查询服务注册表(Service Registry),服务注册表是个数据库,它记录了所有服务实例和他们的地址。
处理局部故障
当实现API网关时,另外一个你需要处理的问题是局部故障。 这种情况会发生在分布式系统中是指,无论何时一个服务调用其他服务时,这个服务要么反应很慢,要么就直接不可用。 API网关绝不应该无限期阻塞等待下游服务的响应。然而,如何处理故障依赖于特定的失败服务场景。 比如,如果推荐服务,在产品细节展示时,没有响应。这个API网关应当返回其他的产品细节信息。 因为这时的其他产品信息对用户依然有用。 这个推荐信息要么被置空,要么用其他信息替换掉。 比如,硬编码的前10个推荐列表。 然而,如果产品服务信息不可用,那么API网关应该返回错误信息给客户端。
如果缓存数据可用,API网关也可以返回缓存的数据。 比如,因为产品价格频繁改变,如果价格服务不可用,这个API网关应当返回缓存的价格数据。数据可以被API网关缓存,或是它被保存在外部缓存系统中,比如,Redis或是Memcached。 通过返回要么是默认数据,要么是缓存数据,API网关确保系统故障尽量不影响用户体验。
Netflix Hystrix是一个非常有用的库,用于代码中调用远程的服务。 Hystrix实现了超时机制。它实现了一种叫做“短路器模式”(Circuit Breaker Pattern)能避免等待没有响应的服务。 如果错误率超过了指定的门槛,Hystrix触发短路器并且所有的请求在一定时间内立即失败,比如从缓存中读数据或是返回一个默认值。 如果你使用JVM,你应当考虑使用Hystrix。 如果你应用运行在非JVM环境上,你应该用类似的库。
总结
对多数的基于微服务的应用,实现API网关是合理的选择,它作为唯一访问系统的入口。 API网关是负责路由请求,组合和协议转换。它为每个应用的客户端提供了一个定制的API。 API网关也能通过返回缓存的数据,或默认的数据屏蔽后端服务的故障。 下一章,我们将会看服务间通讯。
进程间通讯 (三)
这是本系列的第三章,关于用微服务架构构建应用。 第一章介绍了微服务架构模式,比较了它和单体架构模式的区别,并且讨论了使用它的优点和缺点。第二章描述了一个应用的客户端如何通过中间媒介API网关和微服务通讯。 本章,我们来看看系统内部的服务间如何通信。
介绍
在一个单体式应用中,组件间通讯通过语言级方法或功能函数调用实现。 基于微服务架构应用则不同,它是运行在多个机子上的分布式系统。每个服务通常是一个进程。
因此,如图3-1,服务间通讯必定是采用进程间通讯IPC(Inter-process communication)机制。
以后我们会研究特定的IPC技术,但是现在,让我们探索一下各种设计问题。
图 3-1 微服务采用进程间通讯交互交互形式
当为一个服务选择IPC机制时,非常有益的事情是先想想服务是如何交互的。 有很多的客户-服务交互方式。他们可以被按两个维度分类。 第一类是是否交互是一对一或是一对多的:
一对一 : 每一个客户端请求是精确地被一个服务实例处理。
一对多:一个客户端请求被多个服务实例处理。
第二个维度是交互是异步的还是同步的:
同步交互:客户端期望服务能及时响应请求,并且可能会等待中阻塞。
异步交互:客户等待服务响应时不阻塞,并且如果有相应,响应也不必立即发送出去。
下表3-1,展示了各种交互形式。
表3-1 进程间通讯方式有各种类型的一对一交互,同步的(请求/响应)和异步的(通知和请求/异步响应):
请求/ 响应(Request/Response)-一个客户端发出请求并且等待一个响应。这个客户端期待响应能及时返回(timely fashion)。 在一个基于线程的应用中,线程发起请求或许会在等待中阻塞。
通知(Notification)(例如:单向请求)-客户端发送一个请求给服务但是不期望会有应答。
请求/异步响应(Request/async response)-一个客户端发送一个请求给服务,服务的应答是异步的。客户端在等待时不阻断。客户端是以应对”响应不能及时到达“这种假设方式设计的。
以下是各种类型的一对多的交互方式,两种都是异步的:
发布/订阅(Publish/Subscribe) –一个客户端发布一个通知消息,这个消息被零个或多个对此消息感兴趣的消费。
发布/异步响应(Publish/async response)-一个客户端发布一个请求消息,然后等待一段时间,感兴趣的服务会发出响应。
每个服务通常会组合使用这些交互方式。 对一些服务,单个IPC机制时足够的,另一些服务或许需要采用组合的IPC机制。
图3-2显示了出租车呼叫程序如何和请求一段行程的顾客交互。
图3-2 在服务交互中采用多种进程间通讯机制服务采用通知,请求/ 响应,和发布/订阅机制。 比如,乘客的智能手机发出一个通知给行程管理服务要求接车。行程管理服务(Trip Management Service)通过调用乘客管理服务(Passenger Management Service)去验证乘客是否是激活状态。 行程管理服务然后创建行程,用发布/订阅机制通知其他服务,包括分配器(Dispatcher),它定位一个可用的司机。
看完了交互形式,让我们看看如何定义API。
定义API
一个服务API是服务和其客户端的协议合同。 不管你的IPC机制的选择是啥,很重要的事是采用一些接口定义语言(Interface Definition Language)准确地定义接口。 有些很好关于API-first 定义API的讨论。 你通过先写API接口定义并且一起和客户端开发者讨论这些定义,然后才开始正式开发API。 经过几轮的API定义的讨论,然后去实现这个服务。这样设计优先(Design Up Front)的方式增加了构建服务并满足客户需要的机会。
正如你将要在本书后面看到的,API定义的本质依赖于你使用哪种IPC机制。 如果你使用消息机制,API就应该包括消息的通道和消息类型。 如果你采用HTTP,API定义就要包含URL和请求响应格式。 以后我们会详细描述IDL。
API演进
一个服务的API会随时间持续改变。 在一个单体应用中,更改API定义通常是很直观的,也就是更改API定义并且更新所有的API调用者代码。在基于微服务的应用中,即使你的API的消费者调用的服务都在同一个应用中,这种更改也是很困难。通常来说,你不能强制所有的客户端和服务同步更新(lockstep)。同时,你或许会增量部署服务的新版本,比如 一个服务的新版和旧版同时运行。 这种情况下最好有个策略来处理这些问题。
如何处理一个API的更新依赖于更改的大小。 一些更改很小和上一个版本是兼容的。 比如,你或许增加一个属性到请求或是响应中。 设计客户端和服务端要遵循鲁棒原则(robustness principle)。 使用老版本API的客户端应该继续能和新版本服务工作。服务提供缺失属性的默认值,客户端忽略任何额外的响应属性。 选择一个IPC机制和消息格式使你可以轻易地演进API是很重要的设计技巧。
然而,有的时候,你必须对API做一些主要的,不兼容的更改。 由于你不能强制客户端立即更新,在一定时间内,一个服务必须要支持老版本的API。 如果你正在使用基于http的机制比如REST,一种方法是其服务URL中嵌入版本号。每个服务实例或许同时处理多个版本的服务。 另外一个选择是你可以部署不同的实例处理一个特定的版本服务。
处理局部故障
正如在第二章提到的关于API网关,在一个分布式系统中,会有经常存在的局部故障的风险。 因为客户端和服务端是分离的进程,一个服务或许不能及时响应客户端的请求。 一个服务或是因为局部错误或运维而挂机。或是一个服务过载而对请求响应出奇的慢。
比如,考虑一下,第二章产品细节的场景。让我们想象推荐服务没有响应。客户端一个幼稚的实现是为等待响应会永久阻塞。这种做法不仅是用户体验很差,而且在很多应用中它会消耗掉宝贵的资源比如线程。 最终运行时态会消耗掉所有的线程而使系统变得没有响应。如图3-3
图3-3 线程因无响应而阻塞为了防止这个问题出现,设计服务时,要慎重考虑处理局部故障。 一个好的途径可以参考是Netflx的设计策略。这些处理局部故障的策略包括:
- 网络超时-绝不永久阻塞,并且总是在等待响应时采用超时机制。 采用超时机制确保了资源绝不会被永久绑定。
- 限制特殊请求的数量-为客户端能访问特殊服务请求的数量设置上限,如果达到上限,或许再继续请求是没有意义的,对这些尝试的请求需要立即返回失败。
- 断路器模式(Circuit Breaker Pattern)-记录成功和失败的次数,如果失败率超过配置的门槛值,触发断路器以便于使将来的尝试立即返回失败。 如果失败的次数很多,意味着服务不可用并且继续发送请求是无意义的。请求超时过后,客户端应该重试,并且如果成功,关闭断路器设置。
- 回退-当请求失败时,执行回退逻辑。例如返回缓存的数据或是默认值,比如 置空推荐集。
Netflix Hystrix 是一个开源的库,它实现了这些和那些模式。如果你在用JVM,你应该考虑采用Hystrix 。 如果你在用非JVM环境,你应该采用类似功能的库。
IPC技术
有很多不同的IPC技术可以选择。 服务可以是同步基于请求/响应通信机制,比如基于HTTP的REST或是Thrift。 另外的选择,他们可以用异步的,基于消息的通信机制,比如AMQP或是STOMP。
消息的格式也有多挣选择。 服务可以用可读的,基于文本的格式,比如JSON或是XML。他们也可以采用二进制格式(更高效)比如Avro或是Protocol Buffer。 以后我们将会研究同步的IPC机制,但是首先让我们讨论异步IPC机制。
异步的,基于消息的通信
当采用消息机制,进程通过异步交换消息的方式通信。 一个客户端通过发送一个消息请求服务。如果这个服务被期待应答,它通过发送另外一个消息给客户端。 因为通信是异步的,客户端不会因为等待应答而阻塞。相反地,这个客户端被设计时会假设不会立即收到应答。
一个消息包括头部信息(元数据,比如发送者信息)和一个消息体。消息通过信道(Channels)相互通信。 任意数量的生产者会发生消息到信道。 相似地,任何数量的消费者会从信道收到消息。 有两种类型的信道,点到点(Point-to-Point)和发布/订阅(Publish-subscribe)。
- 点到点信道传递一个消息到一个消费者。服务采用点到点信道实现之前提到的一对一交互方式。
- 一个发布-订阅信道传递每个消息到多个订阅的消费者。服务采用发布-订阅信道实现之前提到的一对多交互方式。
图3-4 展示了出租车呼叫应用或许采用发布-订阅信道。
图 3-4 在出车呼叫应用中采用发布-订阅信道行程管理服务通知感兴趣(subscribed)的服务,比如分配器(Dispatcher),关于一个新的行程,它通过写一个创建行程的消息到发布-订阅信道。 分配器通过写一个司机提议消息到一个发布-订阅信道找个一个可用的司机并且通知其他服务。
有很多消息系统可以选择。 你应该选择一个支持很多语言的消息系统。
一些消息系统支持标准的协议比如AMQP和STOMP。 其他一些消息系统有专用和文档的协议(proprietary and documented protocols)。
有非常多的开源消息系统可以选择,包括RabbitMQ(http://www.rabbitmq.com),Apache
Kafka,Apache ActiveMQ 和NSQ。 更高的抽象级别,他们都以一定的形式支持消息和信道。 他们都力争可靠,高效和可扩容。然而,他们的每个代理器消息模型在细节上都有很大的区别。
采用消息机制有很多的优点:
-解耦服务和其客户端--一个客户端简单地通过发送一个消息到合适的信道发起请求。客户端完全意识不到服务实例的存在。它不需要任何的发现机制去确定服务实例地址。
-消息缓存-同步的请求/响应协议,比如HTTP,客户端和服务端双方必须在消息交换时可用。相反地,一个消息代理器排队把消息写入信道直到消费者能处理他们。 比如说,这意味着,在线商店可以从消费者哪里接收消息,即使是订单完成系统缓慢或不可用。 订单仅仅是排队等候处理。
- 灵活的客户端-服务交互-消息支持所有的前面提到的交互方式。
-明确进程间通讯-基于RPC的通讯机制尝试使调用远程服务看起来像调用本地服务一样。然而,因为物理原因和可能的局部故障,事实上,他们非常的不同。消息机制使这些区别更明确,以此避免开发者被虚假的安全性麻痹。
当然了,消息机制也有其缺点:
-额外的运维复杂性-消息系统是另一个必须安装,配置,运维的系统组件,另外消息代理器必须高可用(HA-Highly Available),否则话,系统可靠性会受影响。
-实现基于请求/响应的交互复杂性-请求/响应风格的交互必须要一定的工作量去实现。每个请求消息必须包含一个应答信道和一个相关的ID。服务响应消息内必须包含这个ID和应答信道。 客户端使用这个相关的ID去匹配请求响应。 通常更容易些,使用直接支持请求/响应的IPC机制。
我们已经看到了使用基于消息的IPC机制。让我们看一下基于请求/响应的IPC机制。
同步的,请求/响应IPC
当使用一个同步的,基于请求/响应的IPC机制,一个客户端发送一个请求给服务。这个服务处理请求然后返回一个响应消息。
在很多的客户端,发送请求的线程阻塞以等待一个响应。其他的客户端或许会用异步的,事件驱动的客户端代码,这些代码或许是用Future或是Rx Observable。 然而,不像采用消息,客户端期待响应将会及时返回。
有很多的协议可以选择。 两个比较流行的协议是REST和Thrift。 让我们先来看REST。
REST
现在开发采用RESTful的方式开发API是很时尚的。 REST是一种IPC机制,它总是采用HTTP。
REST的一个关键的概念是资源(Resource),它通常是指一个商业上的对象(Object)比如,客户(Customer)或产品,或是这些商业对象的集合。 REST采用HTTP的标准动词(Verbs)来操作资源,它们可以通过URL被引用。比如 用Get 请求返回一个资源的表述,这个返回的资源或许是xml,文档或是JSON对象。 POST请求创建一个新的资源,PUT请求更新一个资源。
REST创建者Roy Fielding ,是这样描述REST的:
“REST 提供了一组结构化的限制,当作为整体应用时,强调组件交互的库容能力,接口的泛化程度,组件的独立部署和中间组件的减少交互延迟,强化安全性和封装陈旧系统。(REST provides aset of architectural constraints that, when applied as a whole, emphasizesscalability of component interactions, generality of interfaces, independentdeployment of components, and intermediary components to reduce interactionlatency, enforce security, and encapsulate legacy systems.)” --Roy Fielding ”基于网络软件架构的架构风格和设计“
图3-5 展示了出租车呼叫系统应用或许可以采用REST的一种方法
图3-5 出租车呼叫系统采用REST交互方式乘客的智能手机通过POST发送一个请求到行程管理服务/trips,这个服务通过GET请求从乘客管理服务获取关于乘客的信息,验证乘客有权创建行程后,行程管理服务创建一个行程并返回201的响应给智能手机。
许多开发者声称基于HTTP的API是RESTful。 然而,按照Fielding在这个博客中的描述,实际上,并非如此。
Leonard Richardson (作者强调,和他没有关系 ,注:和作者第二个名字相同 Chris RichardsonJ )定义了成熟度模型(Maturity Model for REST) ,它包括下面这些级别:
- Level 0 –满足级别0 REST API客户端通过HTTP POST 请求它的专有的(Sole)服务端点(Endpoint)。 每个请求指定执行的动作,动作的目标(比如,商业对象)和任何的参数。
- Level 1-满足级别1 REST API支持资源的概念,在资源上执行一个动作,一个客户端用POST请求指定一个执行的动作和任意的参数。
- Level 2 –满足级别2REST API采用HTTP动词(Verbs)执行动作,GET是获取,POST是创建,PUT是更新。如果有,用查询参数(Query Parameters)和请求体(body)指定动作参数。 这样可以使服务利用Web基础架构,比如对GET请求缓存。
- Level 3- 级别3 REST API的设计基于HATEOAS(Hypertext As The Engine Of Application State)命名规则。基本的概念是通过GET返回的资源包含可以执行的动作链接(Links)。比如,一个客户端能用从GET获取到的资源中的一个链接取消一个订单。一个HATEOAS的好处是硬编码URL到代码中。另外一个好处是,因为一个资源中包含可以执行的链接,客户端不必猜测对这个资源可以做哪些操作。
用基于HTTP协议有很多的优点:
- HTTP简单而熟悉。
- 在浏览器中,你可以测试HTTP AP ,比如用Postman,或是用命令工具curl (假设用JSON或一些其他文本格式)。
- 它直接支持请求/响应风格的通讯。
- HTTP是防火墙有好点协议。
- 它不需要中间代理器,简化了系统架构的设计。
当然了,用HTTP也有其缺点:
- HTTP仅仅支持请求/响应方式的交互。你能用HTTP作为通知(Notification),但是服务器必须总是发送一个HTTP响应。
- 因为客户端和服务直接通讯(没有中介缓存消息),他们必须在交互消息时可用。
- 客户端必须知道每个服务实例的地址,正如第二章描述的关于API网关那样,在现代应用中,这不是一个很小的问题。 客户端必须采用服务发现机制定位服务实例。
开发社区最近重新发现了对RESTful API的接口定义语言的价值。有些选项包括RAML和Swagger工具。 一些接口定义语言IDLs(Interface Definition Language),比如Swagger,允许你去定义请求和响应的格式。 其他的,比如RAML,需要你使用另外的规范如JSON 模式(JSON Schema)。 关于描述API,IDLs通常会有些工具帮助生成客户端Stubs和服务器端结构代码。
Thrift
Apache Thrift 是有趣的REST替代工具。它是一个写跨语言RPC客户端和服务器端工具。Thrift提供了C语言风格的接口定义语言,用于定义你的API。你使用Thrift编译器生成客户端和服务器端结构代码。编译器可以产生很多语言的代码,包括C++,Java,Python,PHP,Ruby,Erlang和Node.js.
一个Thrift结构包括一个或多个服务。一个服务定义类似于Java接口定义。它是请类型化方法的集合。
Thrift方法既能返回(或空)一个值又能,如果他们被定义为单项,不返回值。返回值的方法实现了请求/响应风格的交互;客户端等待响应消息,或是抛出一个异常。单项方法类似于通知风格的交互; 服务器不用返回响应。
Thrift支持很多的消息格式: JSON,二进制码或是压缩的二进制码。二进制码比JSON高效因为它更快解码。并且,正如名字暗示的,压缩的二进制码是空间高效(Space-efficient)格式。JSON,当然是,可读且浏览器友好的格式(Human-and-browser-friendly)。Thrift也给你选择选择传输协议的选项,协议包括纯TCP和HTTP。TCP可能比HTTP更高效,但是,HTTP是防火墙友好,浏览器友好和可读的协议。
消息格式
现在我们研究过了HTTP和Thrift,让我们来消息格式的一些问题。 如果你在使用消息系统或是REST,你要选择你的消息格式。 其他的IPC机制比如Thrift或许仅支持少量的消息格式,或是仅仅一种。 在任何一种选择下,支持跨语言的消息格式是很重要的。即使你现在正在用一种语言写微服务程序,在将来,你可能会其他的语言。
主要有两种消息格式:文本和二进制码。文本格式的例子有JSON和XML。这些格式的优点是,它不仅可读,他们也是自我描述的。 在JSON中,一个对象的属性是用名值对(name-value pairs)的集合来表示的。这使一个消息的消费者可以选择自己感兴趣的而忽略其他的。 结果是,很小的消息格式的更改能向后兼容。
XML文档的结构是通过XML 模式(XML Schema)。随着时间的推移,开发社区意识到JSON也需要类似的机制。一种选项是使用JSON模式(JSON Schema),或是独立使用,或是作为IDL的一部分使用比如Swagger。
基于文本的消息格式的缺点是消息倾向于冗余。特别是XML。因为消息是自我描述的,每个消息包括属性的名字和他们的值。另外的缺点是分析文本的成本。由此,你或许想考虑使用二进制格式。
有些二进制格式可选。如果你使用ThriftRPC,你能使用二进制码Thrift。 如果你需要选择消息格式,流行的选项包括Protocol Buffer 和Apache Avro 。这两种各种都提供了定义你的消息格式结构类型化的IDL。然而,一种区别,是Protocol Buffer采用加标签的字段(Field),而Avro的消费者需要知道其模式(Schema)用于解释消息。 这样的话,API演进采用Protocol
Buffer比采用Avro容易。 这个博客有非常好的对Thrift,Protocol Buffer 和Avro的对比分析.
总结
微服务必须采用进程内通讯机制。 当设计你的服务如何通讯式,你需要考虑各种不同的问题:服务如何交互,如何为每个服务指定API,如何演进API,还有如何处理局部故障。有两种类型微服务可以使用的RPC机制:异步消息和同步的请求/响应。为了通讯,一个服务必须能发现其他的,下一章,我们看一下在微服务架构下,服务发现的问题。
翻译自“Microservice -from design to deployment" by Chris Richardson with Floyd Smith.
第四章待续 (服务发现-Service discovery)。。。