干货 | 携程的 Dubbo 之路

做者简介前端

董艺荃,携程框架架构研发部技术专家。目前负责携程服务化框架的研发工做。本文来自董艺荃在 Dubbo 社区开发者日上的分享。c#

1、缘起


携程当初为何要引入 Dubbo 呢?后端

实际上从 2013 年末起,携程内主要使用的就是基于 HTTP 协议的 SOA 微服务框架。这个框架是携程内部自行研发的,总体架构在这近6年中没有进行大的重构。受到当初设计的限制,框架自己的扩展性不是很好,使得用户要想本身扩展一些功能就会比较困难。服务器

另外,因为 HTTP 协议一个链接同时只能处理一个请求。在高并发的状况下,服务端的链接数和线程池等资源都会比较紧张,影响到请求处理的性能。闭包

而 Dubbo 做为一个高性能的 RPC 框架,不只是一款业界知名的开源产品,它总体优秀的架构设计和数据传输方式也能够解决上面提到的这些问题。正好在 2017 年下半年,阿里宣布重启维护 Dubbo 。基于这些缘由,咱们团队决定把 Dubbo 引入携程。架构

 

2、Dubbo 落地第一步


要在公司落地 Dubbo 这个新服务框架,第一步就是解决服务治理和监控这两个问题。并发

 

2.1 服务治理 

在服务治理这方面,携程现有的 SOA 框架已经有了一套完整的服务注册中心和服务治理系统。框架

对于服务注册中心,你们比较经常使用的多是 Apache Zookeeper 。而咱们使用的是参考 Netflix 开源的 Eureka 自行研发的注册中心 Artemis 。微服务

Artemis 的架构是一个去中心的对等集群。各个节点的地位相同,没有主从之分。服务实例与集群中的任意一个节点保持长链接,发送注册和心跳信息。收到信息的节点会将这些信息分发给其余节点,确保集群间数据的一致性。客户端也会经过一个长链接来接受注册中心推送的服务实例列表信息。高并发

 

 

在服务数据模型方面,咱们直接复用了现有 SOA 服务的数据模型。如图所示,最核心的服务模型对应的是 Dubbo 中的一个 interface 。一个应用程序内能够包含多个服务,一个服务也能够部署在多个服务器上。咱们将每一个服务器上运行的服务应用称为服务实例。

 

全部的服务在上线前都须要在治理系统中进行注册。注册后,系统会为其分配一个惟一的标识,也就是 ServiceID 。这个 ServiceID 将会在服务实例注册时发送至注册中心用来标识实例的归属,客户端也须要经过这个ID来获取指定服务的实例列表。

 

因为 Dubbo 自己并无 ServiceID 的设计,这里的问题就是如何向注册中心传递一个 interface 所对应的 ServiceID 信息。咱们的方法是在 Service 和 Reference 配置中增长一个 serviceId 参数。ArtemisServiceRegistry 的实现会读取这个参数,并传递给注册中心。这样就能够正常的与注册中心进行交互了。

 

2.2 服务监控


在服务监控这方面咱们主要作了两部分工做:统计数据层面的监控和调用链层面的监控。

 

统计数据指的是对各类服务调用数据的按期汇总,好比调用量、响应时间、请求体和响应体的大小以及请求出现异常的状况等等。这部分数据咱们分别在客户端和服务端以分钟粒度进行了汇总,而后输出到 Dashboard 看板上。同时咱们也对这些数据增长了一些标签,例如:Service ID、服务端 IP 、调用的方法等等。用户能够很方便的查询本身须要的监控数据。

 

在监控服务调用链上,咱们使用的是 CAT 。CAT 是美团点评开源的一个实时的应用监控平台。它经过树形的 Transaction 和 Event 节点,能够将整个请求的处理过程记录下来。

咱们在 Dubbo 的客户端和服务端都增长了 CAT 的 Transaction 和 Event 埋点,记录了调用的服务、 SDK 的版本、服务耗时、调用方的标识等信息,而且经过 Dubbo 的 Attachment 把 CAT 服务调用的上下文信息传递到了服务端,使得客户端和服务端的监控数据能够链接起来。在排障的时候就能够很方便的进行查询。

在图上,外面一层咱们看到的是客户端记录的监控数据。在调用发起处展开后,咱们就能够看到对应的在服务端的监控数据。

 

2.3 第一版发布

 

在解决了服务治理和监控对接这两个问题后,咱们就算完成了 Dubbo 在携程初步的一个本地化,在 2018 年 3 月,咱们发布了 Dubbo 携程定制版的首个可用版本。在正式发布前咱们须要给这个产品起个新名字。既然是携程(Ctrip)加 Dubbo ,咱们就把这个定制版本称为 CDubbo 。

 

3、CDubbo 功能扩展


除了基本的系统对接,咱们还对 CDubbo 进行了一系列的功能扩展,主要包括如下这 5 点:Callback 加强、序列化扩展、请求熔断、服务测试工具、堡垒测试网关。

下面我来逐一给你们介绍一下。

 

3.1 Callback 加强 

首先,咱们看一下这段代码。请问代码里有没有什么问题呢?

 

这段代码里有一个 DemoService 。其中的 callbackDemo 方法的参数是一个接口。下面的 Demo 类中分别在 foo 和 bar 两个方法中调用了这个 callbackDemo 方法。

相信用过 Callback 的朋友们应该知道,foo 这个方法的调用方式是正确的,而 bar 这个方法在重复调用的时候是会报错的。由于对于同一个 Callback 接口,客户端只能建立一个实例。

 

但这又有什么问题呢?咱们来看一下这样一个场景。

一个用户在页面上发起了一个查询机票的请求。站点服务器接收到请求以后调用了后端的查询机票服务。考虑到这个调用可能会耗时较长,接口上使用了 callback 来回传实际的查询结果。而后再由站点服务器经过相似 WebSocket 的技术推送给客户端。

那么问题来了。

站点服务器接受到回调数据时,须要知道它对应的是哪一个用户的哪次调用请求,这样才能把数据正确的推送给用户。但对于全局惟一的callback接口实例,想要拿到这个请求上下文信息就比较困难了。须要在接口定义和实现上预先作好准备。可能须要额外引入一些全局的对象来保存这部分上下文信息。

 

针对这个问题,咱们在 CDubbo 中增长了 Stream 功能。跟前面同样,咱们先来看代码。

 

这段代码与前面的代码有什么区别?首先, callback 接口的参数替换为了一个 StreamContext 。还有接受回调的地方不是以前的全局惟一实例,而是一个匿名类,而且也再也不是单单一个方法,而是有3个方法,onNext、onError和onCompleted 。这样调用方在匿名类里就能够经过闭包来获取本来请求的上下文信息了。是否是体验就好一些了?

 

那么 Stream 具体是怎么实现的呢?咱们来看一下这张图。

 

在客户端,客户端发起带 Stream 的调用时,须要经过 StreamContext.create 方法建立一个StreamContext。虽说是建立,但实际是在一个全局的 StreamContext 一个惟一的 StreamID 和对应回调的实际处理逻辑。在发送请求时,这个 StreamID 会被发送到服务端。服务端在发起回调的时候也会带上这个 StreamID 。这样客户端就能够知道此次回调对应的是哪一个 StreamContext 了。

 

3.2 序列化扩展

携程的一些业务部门,在以前开发 SOA 服务的时候,使用的是 Google Protocol Buffer 的契约编写的请求数据模型。Google PB 的要求就是经过契约生成的数据模型必须使用PB的序列化器进行序列化。

为了便于他们将 SOA 服务迁移到Dubbo ,咱们也在 Dubbo 中增长了 GooglePB 序列化方式的支持。后续为了便于用户自行扩展,咱们在PB序列化器的实现上增长了扩展接口,容许用户在外围继续增长数据压缩的功能。

总体序列化器的实现并非很难,却是有一点须要注意的是,因为 Dubbo 服务对外只能暴露一种序列化方式,这种序列化方式应该兼容全部的 Java 数据类型。而 PB 碰巧就是那种只能序列化本身契约生成的数据类型的序列化器。因此在遇到不支持的数据类型的时候,咱们仍是会 fallback 到使用默认的 hessian 来进行序列化操做的。

 

3.3 请求熔断

相信你们对熔断应该不陌生。当客户端或服务端出现大范围的请求出错或超时的时候,系统会自动执行 fail-fast 逻辑,再也不继续发送和接受请求,而是直接返回错误信息。

这里咱们使用的是业界比较成熟的解决方案:Netflix 开源的 Hystrix 。它不只包含熔断的功能,还支持并发量控制、不一样的调用间隔离等功能。单个调用的出错不会对其余的调用形成影响。各项功能都支持按需进行自定义配置。CDubbo的服务端和客户端经过集成 Hystrix 来作请求的异常状况进行处理,避免发生雪崩效应。

 

3.4 服务测试工具

Dubbo 做为一个使用二进制数据流进行传输的 RPC 协议,服务的测试就是一个比较难操做的问题。要想让测试人员在无需编写代码的前提下测试一个 Dubbo 服务,咱们要解决的有这样三个问题:如何编写测试请求、如何发送测试请求和如何查看响应数据。

 

首先就是怎么构造请求。这个问题实际分为两个部分。一个是用户在不写代码的前提下用什么格式去构造这个请求。考虑到不少测试人员对 Restful Service 的测试比较熟悉,因此咱们最终决定使用 JSON 格式表示请求数据。

那么让一个测试人员从一个空白的 JSON 开始构造一个请求是否是有点困难呢?因此咱们仍是但愿可以让用户了解到请求的数据模型。虽然咱们使用的是 Dubbo 2.5.10 ,但这部分功能在 Dubbo 2.7.3 中已经有了。

因此咱们将这部分代码复制了过来,而后对它进行了扩展,把服务的元数据信息保存在一个全局上下文中。而且咱们在 CDubbo 中经过 Filter 增长了一个内部的操做,$serviceMeta,把服务的元数据信息暴露出来。

这部分元数据信息包括方法列表、各个方法的参数列表和参数的数据模型等等。这样用户经过调用内部操做拿到这个数据模型以后,能够生成出一个基本的JSON结构。以后用户只须要在这个结构中填充实际的测试数据就能够很容易的构造出一个测试请求来。

 

 

而后,怎么把编辑好的请求发送给服务端呢?由于没有模型代码,没法直接发起调用。而 Dubbo 提供了一个很好的工具,就是泛化调用, GenericService 。咱们把请求体经过泛化调用发送给服务端,再把服务端返回的Map序列化成JSON显示给测试人员。整个测试流程就完成了。顺便还解决了如何查看响应数据的问题。

 

为了方便用户使用,咱们开发了一个服务测试平台。用户能够在上面直接选择服务和实例,编写和发送测试请求。另外为了方便用户进行自动化测试,咱们也把这部分功能封装成了 jar 包发布了出去。

其实在作测试工具的过程当中,还遇到了一点小问题。经过从 JSON 转化 Map 再转化为 POJO 这条路是能走通的。但前面提到了,有一些对象是经过相似 Google Protobuf 的契约生成的。它们不是单纯的 POJO ,没法直接转换。因此,咱们对泛化调用进行了扩展。

首先对于这种自定义的序列化器,咱们容许用户自行定义从数据对象到 JSON 的格式转换实现。其次,在服务端处理泛化调用时,咱们给 Dubbo 增长了进行 JSON 和 Google PB 对象之间的互相转换的功能。如今这两个扩展功能有已经合并入了 Dubbo 的代码库,并随着 2.7.3 版本发布了。

 

3.5 堡垒测试网关

说完了单纯针对服务的测试,有些时候咱们还但愿在生产的实际使用环境下对服务进行测试,尤为是在应用发布的时候。

在携程有一个叫堡垒测试的测试方法,指的是在应用发布过程当中,发布系统会先挑出一台服务器做为堡垒机,并将新版本的应用发布到堡垒机上。而后用户经过特定的测试方法将请求发送到堡垒机上来验证新版本应用的功能是否能够正常工做。

因为进行堡垒测试时,堡垒机还没有拉入集群,这里就须要让客户端能够识别出一个堡垒测试请求并把请求转发给指定的堡垒服务实例。虽然咱们能够经过路由来实现这一点,但这就须要客户端了解不少转发的细节信息,并且整合入 SDK 的功能对于后续的升级维护会形成必定的麻烦。

因此咱们开发了一个专门用于堡垒测试的服务网关。当一个客户端识别到当前请求的上下文中包含堡垒请求标识时,它就会把 Dubbo 请求转发给预先配置好的测试网关。网关会先解析这个服务请求,判断它对应的是哪一个服务而后再找出这个服务的堡垒机并将请求转发过去。在服务完成请求处理后,网关也会把响应数据转发回调用方。

 

与通常的 HTTP 网关不一样, Dubbo 的服务网关须要考虑一个额外的请求方式,就是前面所提到的 callback 。

因为 callback 是从服务端发起的请求,整个处理流程都与客户端的正常请求不一样。网关上会将客户端发起的链接和网关与服务端之间的链接进行绑定,并记录最近待返回的请求 ID 。这样在接收到 callback 的请求和响应时就能够准确的进行路由了。

 

4、后续功能规划


截止到今天, CDubbo 一共发布了27个版本。携程的不少业务部门都已经接入了 Dubbo 。

在将来, CDubbo 还会扩展更多的功能,好比请求限流和认证受权等等。咱们但愿之后能够贡献更多的新功能出来,回馈开源社区。

【推荐阅读】


2019携程技术峰会

超值早鸟票倒计时3天

戳下方图片直达

↓↓↓