很多开发者都表示他们基于HTTP的API是RESTful的。但是,如同Fielding在他的中所说,这些API可能并不都是RESTful的。Leonard Richardson为REST定义了一个,具体包含以下4个层次(摘自):
- 第一个层次(Level 0)的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
- 第二个层次(Level 1)的 Web 服务引入了资源的概念。每个资源有对应的标识符和表达。
- 第三个层次(Level 2)的 Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用 HTTP 状态码来表示不同的结果。如 HTTP GET 方法来获取资源,HTTP DELETE 方法来删除资源。
- 第四个层次(Level 3)的 Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。
使用基于HTTP的协议有如下好处:• HTTP非常简单并且大家都很熟悉。• 可以使用浏览器扩展(比如Postman)或者curl之类的命令行来测试API。• 内置支持请求/响应模式的通信。• HTTP对防火墙友好的。• 不需要中间代理,简化了系统架构。不足之处包括:• 只支持请求/响应模式交互。可以使用HTTP通知,但是服务端必须一直发送HTTP响应才行。• 因为客户端和服务端直接通信(没有代理或者buffer机制),在交互期间必须都在线。• 客户端必须知道每个服务实例的URL。如之前那篇关于所述,这也是个烦人的问题。客户端必须使用服务实例发现机制。开发者社区最近重新发现了RESTful API接口定义语言的价值。于是就有了一些RESTful风格的服务框架,包括和。一些IDL,例如Swagger允许定义请求和响应消息的格式。其它的,例如RAML,需要使用另外的标识,例如。对于描述API,IDL一般都有工具来定义客户端和服务端骨架接口。Thrift是一个很有趣的REST的替代品。它是Facebook实现的一种高效的、支持多种编程语言的远程服务调用的框架。Thrift提供了一个C风格的IDL定义API。使用Thrift编译器可以生成客户端和服务器端代码框架。编译器可以生成多种语言的代码,包括C++、Java、Python、PHP、Ruby, Erlang和Node.js。Thrift接口包括一个或者多个服务。服务定义类似于一个JAVA接口,是一组方法。Thrift方法可以返回响应,也可以被定义为单向的。返回值的方法其实就是请求/响应类型交互模式的实现。客户端等待响应,并可能抛出异常。单向方法对应于通知类型的交互模式,服务端并不返回响应。Thrift支持多种消息格式:JSON、二进制和压缩二进制。二进制比JSON更高效,因为二进制解码更快。同样原因,压缩二进制格式可以提供更高级别的压缩效率。JSON,是易读的。Thrift也可以在裸TCP和HTTP中间选择,裸TCP看起来比HTTP更加有效。然而,HTTP对防火墙,浏览器和人来说更加友好。
消息格式
了解完HTTP和Thrift后,我们来看下消息格式方面的问题。如果使用消息系统或者REST,就可以选择消息格式。其它的IPC机制,例如Thrift可能只支持部分消息格式,也许只有一种。无论哪种方式,我们必须使用一个跨语言的消息格式,这非常重要。因为指不定哪天你会使用其它语言。
有两类消息格式:文本和二进制。文本格式的例子包括JSON和XML。这种格式的优点在于不仅可读,而且是自描述的。在JSON中,一个对象就是一组键值对。类似的,在XML中,属性是由名字和值构成。消费者可以从中选择感兴趣的元素而忽略其它部分。同时,小幅度的格式修改可以很容器向后兼容。XML文档结构是由XML schema定义的。随着时间发展,开发者社区意识到JSON也需要一个类似的机制。一个选择是使用JSON Schema,要么是独立的,要么是例如Swagger的IDL。基于文本的消息格式最大的缺点是消息会变得冗长,特别是XML。因为消息是自描述的,所以每个消息都包含属性和值。另外一个缺点是解析文本的负担过大。所以,你可能需要考虑使用二进制格式。二进制的格式也有很多。如果使用的是Thrift RPC,那可以使用二进制Thrift。如果选择消息格式,常用的还包括和。它们都提供典型的IDL来定义消息架构。一个不同点在于Protocol Buffers使用的是加标记(tag)的字段,而Avro消费者需要知道模式(schema)来解析消息。因此,使用前者,API更容易演进。这篇很好的比较了Thrift、Protocol Buffers、Avro三者的区别。总结微服务必须使用进程间通信机制来交互。当设计服务的通信模式时,你需要考虑几个问题:服务如何交互,每个服务如何标识API,如何升级API,以及如何处理部分失败。微服务架构有两类IPC机制可选,异步消息机制和同步请求/响应机制。在下一篇文章中,我们将会讨论微服务架构中的服务发现问题。
原文链接:(翻译:杨峰 校对:李颖杰)
或许很多人会说 Spring Cloud 和 Dubbo 的对比有点不公平,Dubbo 只是实现了服务治理,而 Spring Cloud 下面有 17 个子项目(可能还会新增)分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面,一定程度来说,Dubbo 只是 Spring Cloud Netflix 中的一个子集。但是在选择框架上,方案完整度恰恰是一个需要重点关注的内容。
根据 Martin Fowler 对微服务架构的描述中,虽然该架构相较于单体架构有模块化解耦、可独立部署、技术多样性等诸多优点,但是由于分布式环境下解耦,也带出了不少测试与运维复杂度。
根据微服务架构在各方面的要素,看看 Spring Cloud 和 Dubbo 都提供了哪些支持。
以上列举了一些核心部件,大致可以理解为什么之前说 Dubbo 只是类似 Netflix 的一个子集了吧。当然这里需要申明一点,Dubbo 对于上表中总结为“无”的组件不代表不能实现,而只是 Dubbo 框架自身不提供,需要另外整合以实现对应的功能,比如:- 分布式配置:可以使用淘宝的 diamond、百度的 disconf 来实现分布式配置管理。但是 Spring Cloud 中的 Config 组件除了提供配置管理之外,由于其存储可以使用 Git,因此它天然的实现了配置内容的版本管理,可以完美的与应用版本管理整合起来。
- 服务跟踪:可以使用京东开源的 Hydra
- 批量任务:可以使用当当开源的 Elastic-Job
- ……
虽然,Dubbo 自身只是实现了服务治理的基础,其他为保证集群安全、可维护、可测试等特性方面都没有很好的实现,但是几乎大部分关键组件都能找到第三方开源来实现,这些组件主要来自于国内各家大型互联网企业的开源产品。
RPC vs REST
另外,由于 Dubbo 是基础框架,其实现的内容对于我们实施微服务架构是否合理,也需要我们根据自身需求去考虑是否要修改,比如 Dubbo 的服务调用是通过 RPC 实现的,但是如果仔细拜读过 Martin Fowler 的 microservices 一文,其定义的服务间通信是 HTTP协议的 REST API。那么这两种有何区别呢?
先来说说,使用 Dubbo 的 RPC 来实现服务间调用的一些痛点:
- 服务提供方与调用方接口依赖方式太强:我们为每个微服务定义了各自的 service 抽象接口,并通过持续集成发布到私有仓库中,调用方应用对微服务提供的抽象接口存在强依赖关系,因此不论开发、测试、集成环境都需要严格的管理版本依赖,才不会出现服务方与调用方的不一致导致应用无法编译成功等一系列问题,以及这也会直接影响本地开发的环境要求,往往一个依赖很多服务的上层应用,每天都要更新很多代码并 install 之后才能进行后续的开发。若没有严格的版本管理制度或开发一些自动化工具,这样的依赖关系会成为开发团队的一大噩梦。而 REST 接口相比 RPC 更为轻量化,服务提供方和调用方的依赖只是依靠一纸契约,不存在代码级别的强依赖,当然 REST 接口也有痛点,因为接口定义过轻,很容易导致定义文档与实际实现不一致导致服务集成时的问题,但是该问题很好解决,只需要通过每个服务整合 swagger,让每个服务的代码与文档一体化,就能解决。所以在分布式环境下,REST 方式的服务依赖要比 RPC 方式的依赖更为灵活。
- 服务对平台敏感,难以简单复用:通常我们在提供对外服务时,都会以 REST 的方式提供出去,这样可以实现跨平台的特点,任何一个语言的调用方都可以根据接口定义来实现。那么在 Dubbo 中我们要提供 REST 接口时,不得不实现一层代理,用来将 RPC 接口转换成 REST 接口进行对外发布。若我们每个服务本身就以 REST 接口方式存在,当要对外提供服务时,主要在 API 网关中配置映射关系和权限控制就可实现服务的复用了。
相信这些痛点也是为什么当当网在 dubbox(基于 Dubbo 的开源扩展)中增加了对 REST 支持的原因之一。
小结:Dubbo 实现了服务治理的基础,但是要完成一个完备的微服务架构,还需要在各环节去扩展和完善以保证集群的健康,以减轻开发、测试以及运维各个环节上增加出来的压力,这样才能让各环节人员真正的专注于业务逻辑。而 Spring Cloud 依然发扬了 Spring Source 整合一切的作风,以标准化的姿态将一些微服务架构的成熟产品与框架揉为一体,并继承了 Spring Boot 简单配置、快速开发、轻松部署的特点,让原本复杂的架构工作变得相对容易上手一些(如果您读过我之前关于 Spring Cloud 的一些核心组件使用的文章,应该能体会这些让人兴奋而激动的特性,传送门)。所以,如果选择 Dubbo 请务必在各个环节做好整套解决方案的准备,不然很可能随着服务数量的增长,整个团队都将疲于应付各种架构上不足引起的困难。而如果选择 Spring Cloud,相对来说每个环节都已经有了对应的组件支持,可能有些也不一定能满足你所有的需求,但是其活跃的社区与高速的迭代进度也会是你可以依靠的强大后盾。
Round 4:文档质量
Dubbo 的文档可以说在国内开源框架中算是一流的,非常全,并且讲解的也非常深入,由于版本已经稳定不再更新,所以也不太会出现不一致的情况,另外提供了中文与英文两种版本,对于国内开发者来说,阅读起来更加容易上手,这也是 Dubbo 在国内更火一些的原因吧。
Spring Cloud 由于整合了大量组件,文档在体量上自然要比 dubbo 多很多,文档内容上还算简洁清楚,但是更多的是偏向整合,更深入的使用方法还是需要查看其整合组件的详细文档。另外由于 Spring Cloud 基于 Spring Boot,很多例子相较于传统 Spring 应用要简单很多(因为自动化配置,很多内容都成了约定的默认配置),这对于刚接触的开发者可能会有些不适应,比较建议了解和学习 Spring Boot 之后再使用 Spring Cloud,不然可能会出现很多一知半解的情况。
小结:虽然 Spring Cloud 的文档量大,但是如果使用 Dubbo 去整合其他第三方组件,实际也是要去阅读大量第三方组件文档的,所以在文档量上,我觉得区别不大。对于文档质量,由于 Spring Cloud 的迭代很快,难免会出现不一致的情况,所以在质量上我认为 Dubbo 更好一些。而对于文档语言上,Dubbo 自然对国内开发团队来说更有优势。
总结
通过上面再几个环节上的分析,相信大家对 Dubbo 和 Spring Cloud 有了一个初步的了解。就我个人对这两个框架的使用经验和理解,打个不恰当的比喻:使用 Dubbo 构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而 Spring Cloud 就像品牌机,在 Spring Source 的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。
从目前 Spring Cloud 的被关注度和活跃度上来看,很有可能将来会成为微服务架构的标准框架。所以,Spring Cloud 的系列文章,我会继续写下去。也欢迎各位朋友一起交流,共同进步。
原文链接:
如何将一个系统拆分成SCS(自包含系统)
在进行领域驱动设计(DDD)时,为了尽可能降低SCS之间的耦合,每个SCS应该实现一个 边界上下文 。每个系统不只拥有一个领域模型,事实上,一个系统可以包含多个不同的领域模型。每一个模型都有一个边界上下文。例如,在电子商务系统里搜索产品的当前价格时,产品的描述和数量是很重要的。而如果要向客户发货,则还需要其他的信息:产品的重量和客户的收货地址。将系统拆分成边界上下文是构建自包含系统最为有效的方式。
可以通过对用户故事进行分组来定义边界上下文。假设我们通过全文检索来搜索产品,那么通过分类和推荐来搜索也应该属于相同的边界上下文。当然,有时候拆分并不会有非常清楚的界线,这要取决于搜索的复杂性。
在将系统拆分成SCS时也需要考虑到 用户体验 。用户体验描述了客户与系统之间的交互步骤,比如搜索产品、结账或注册。每一个步骤都可能成为一个SCS。这些步骤之间一般只有很少的依赖。这些步骤之间有承上启下的关系:购物车在结账时就变成了一个订单,然后完成支付。
SCS不只处理某种特定的领域对象。例如,使用一个SCS来处理所有的客户数据就没有多大意义:很多不同的边界上下文都会用到客户数据。所以,为客户单独创建模型并在一个单独的SCS里实现是不可能的事情。如果真的这样子做了,那么每个需要用到客户数据的系统都会依赖它。这也就是为什么在将系统拆分成SCS时需要通过用户故事、边界上下文或用户体验来驱动,这种自上而下的方法会带来低耦合的系统。
虽然在后续有必要识别出公共部分,但这不应该成为关键点。公共逻辑可以被抽取到另一个系统里,但这意味着SCS会对这个系统产生依赖,它们之间就产生了耦合。
在文章中,我们说过服务间的消息传递有几种方式,一种是请求/响应技术,另一种是基于事件的机制。
RPC(远程过程调用)
RPC是Remote Procedure Call的简称。
这是请求/响应技术的一种,它使用本地调用的方式和远程进行交互,如SOAP、Thrift等,比如我们常使用的WebService和Java RMI,就是这种类型。它先在本地生成桩代码,然后通过桩代码进行远程调用。
RPC会带来一些问题,如Java RMI,其耦合性较紧,同时RPC会对调用进行大量的封装和解封装,同时修改接口时会造成服务的提供方和调用方都要修改。
REST
REST是受Web启发而产生的一种架构风格,REST风格包含的内容很多,Richardson的成熟度模型(http://martinfowler.com/articles/richardsonMaturityModel.html),其中有对REST不同风格的比较。
REST本身并没有提到底层应该使用什么协议,最常用的是HTTP,HTTP本身提供了很多功能,这些功能对于实现REST风格非常有用,比如HTTP的动词(GET、POST、PUT等)就能很好地和资源一起使用。
在使用REST时,传输的数据格式是XML还是JSON,这个没有一个定论。
基于HTTP的REST也有缺点:
1.它无法帮你生成桩代码(封装rest请求参数时需要),2.在要求低延迟的场景下,每个HTTP请求的封装开销可能是个问题,使用TCP、UDP可能更合适。
基于事件的异步协作
这种方式主要有两个部分需要考虑:微服务发布事件消费者接收事件机制。
消息队列(如RabbitMQ)可以同进处理上述两方法的问题。生产者使用API向代理发布事件,代理可以向消费者提供订阅服务,并且在事件发生时通知消费者。这种代理甚至可以跟踪消费者的状态,如标记哪些消息是该消费者已经消费过的。这种系统通常具有较好的可伸缩性和弹性,但这么做会增加开发流程的复杂度,因为你需要一个额外的系统(即消息代理)才能开发及测试服务。
另一种方式是使用HTTP来传播事件,ATOM是一个符合REST规范的协议,可以通过它提供资源聚合的发布服务,当服务提供方发生改变时,只需要简单地向该聚合发布一个事件即可,消费者会轮询该聚合以查看变化。它的缺点是:HTTP不擅长处理低延迟的场景,而且使用ATOM的话,用户还需要自己追踪消息是否送达及管理轮询等工作。
异步架构有其复杂性,比如,消息丢失了怎么办?消息重试失败了怎么办?消息重发了怎么办?消息请求崩溃了怎么办?我们可以通过设置最大重试、黑名单、白名单等措施来解决这些问题。但这也意味着复杂性的增加。
参考
《微服务设计》(Sam Newman 著 / 崔力强 张骏 译)
http://www.cnblogs.com/gudi/p/6624917.html