本篇文章整理自董艺荃在 Dubbo 社区开发者日上海站的演讲。
缘起
携程当初为什么要引入 Dubbo 呢?实际上从 2013 年底起,携程内主要使用的就是基于 HTTP 协议的 SOA 微服务框架。这个框架是携程内部自行研发的,整体架构在这近6年中没有进行大的重构。受到当初设计的限制,框架本身的扩展性不是很好,使得用户要想自己扩展一些功能就会比较困难。另外,由于 HTTP 协议一个连接同时只能处理一个请求。在高并发的情况下,服务端的连接数和线程池等资源都会比较紧张,影响到请求处理的性能。而 Dubbo 作为一个高性能的 RPC 框架,不仅是一款业界知名的开源产品,它整体优秀的架构设计和数据传输方式也可以解决上面提到的这些问题。正好在 2017 年下半年,阿里宣布重启维护 Dubbo 。基于这些原因,我们团队决定把 Dubbo 引入携程。
Dubbo 落地第一步
要在公司落地 Dubbo 这个新服务框架,第一步就是解决服务治理和监控这两个问题。
服务治理
在服务治理这方面,携程现有的 SOA 框架已经有了一套完整的服务注册中心和服务治理系统。对于服务注册中心,大家比较常用的可能是 Apache Zookeeper 。而我们使用的是参考 Netflix 开源的 Eureka 自行研发的注册中心 Artemis 。Artemis 的架构是一个去中心的对等集群。各个节点的地位相同,没有主从之分。服务实例与集群中的任意一个节点保持长连接,发送注册和心跳信息。收到信息的节点会将这些信息分发给其他节点,确保集群间数据的一致性。客户端也会通过一个长连接来接受注册中心推送的服务实例列表信息。
在服务数据模型方面,我们直接复用了现有 SOA 服务的数据模型。如图所示,最核心的服务模型对应的是 Dubbo 中的一个 interface 。一个应用程序内可以包含多个服务,一个服务也可以部署在多个服务器上。我们将每个服务器上运行的服务应用称为服务实例。
所有的服务在上线前都需要在治理系统中进行注册。注册后,系统会为其分配一个唯一的标识,也就是 ServiceID 。这个 ServiceID 将会在服务实例注册时发送至注册中心用来标识实例的归属,客户端也需要通过这个ID来获取指定服务的实例列表。
由于 Dubbo 本身并没有 ServiceID 的设计,这里的问题就是如何向注册中心传递一个 interface 所对应的 ServiceID 信息。我们的方法是在 Service 和 Reference 配置中增加一个 serviceId 参数。ArtemisServiceRegistry 的实现会读取这个参数,并传递给注册中心。这样就可以正常的与注册中心进行交互了。
服务监控
在服务监控这方面我们主要做了两部分工作:统计数据层面的监控和调用链层面的监控。
统计数据指的是对各种服务调用数据的定期汇总,比如调用量、响应时间、请求体和响应体的大小以及请求出现异常的情况等等。这部分数据我们分别在客户端和服务端以分钟粒度进行了汇总,然后输出到 Dashboard 看板上。同时我们也对这些数据增加了一些标签,例如:Service ID、服务端 IP 、调用的方法等等。用户可以很方便的查询自己需要的监控数据。
在监控服务调用链上,我们使用的是 CAT 。CAT 是美团点评开源的一个实时的应用监控平台。它通过树形的 Transaction 和 Event 节点,可以将整个请求的处理过程记录下来。我们在 Dubbo 的客户端和服务端都增加了 CAT 的 Transaction 和 Event 埋点,记录了调用的服务、 SDK 的版本、服务耗时、调用方的标识等信息,并且通过 Dubbo 的 Attachment 把 CAT 服务调用的上下文信息传递到了服务端,使得客户端和服务端的监控数据可以连接起来。在排障的时候就可以很方便的进行查询。在图上,外面一层我们看到的是客户端记录的监控数据。在调用发起处展开后,我们就可以看到对应的在服务端的监控数据。
初版发布
在解决了服务治理和监控对接这两个问题后,我们就算完成了 Dubbo 在携程初步的一个本地化,在 2018 年 3 月,我们发布了 Dubbo 携程定制版的首个可用版本。在正式发布前我们需要给这个产品起个新名字。既然是携程(Ctrip)加 Dubbo ,我们就把这个定制版本称为 CDubbo 。
CDubbo 功能扩展
除了基本的系统对接,我们还对 CDubbo 进行了一系列的功能扩展,主要包括以下这 5 点: Callback 增强、序列化扩展、熔断和请求测试工具。下面我来逐一给大家介绍一下。
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 了。
序列化扩展
携程的一些业务部门,在之前开发 SOA 服务的时候,使用的是 Google Protocol Buffer 的契约编写的请求数据模型。 Google PB 的要求就是通过契约生成的数据模型必须使用PB的序列化器进行序列化。为了便于他们将 SOA 服务迁移到Dubbo ,我们也在 Dubbo 中增加了 GooglePB 序列化方式的支持。后续为了便于用户自行扩展,我们在PB序列化器的实现上增加了扩展接口,允许用户在外围继续增加数据压缩的功能。整体序列化器的实现并不是很难,倒是有一点需要注意的是,由于 Dubbo 服务对外只能暴露一种序列化方式,这种序列化方式应该兼容所有的 Java 数据类型。而 PB 碰巧就是那种只能序列化自己契约生成的数据类型的序列化器。所以在遇到不支持的数据类型的时候,我们还是会 fallback 到使用默认的 hessian 来进行序列化操作的。
请求熔断
相信大家对熔断应该不陌生吧。当客户端或服务端出现大范围的请求出错或超时的时候,系统会自动执行 fail-fast 逻辑,不再继续发送和接受请求,而是直接返回错误信息。这里我们使用的是业界比较成熟的解决方案:Netflix 开源的 Hystrix 。它不仅包含熔断的功能,还支持并发量控制、不同的调用间隔离等功能。单个调用的出错不会对其他的调用造成影响。各项功能都支持按需进行自定义配置。CDubbo的服务端和客户端通过集成 Hystrix 来做请求的异常情况进行处理,避免发生雪崩效应。
服务测试工具
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 版本发布了。
堡垒测试网关
说完了单纯针对服务的测试,有些时候我们还希望在生产的实际使用环境下对服务进行测试,尤其是在应用发布的时候。在携程,有一个叫堡垒测试的测试方法,指的是在应用发布过程中,发布系统会先挑出一台服务器作为堡垒机,并将新版本的应用发布到堡垒机上。然后用户通过特定的测试方法将请求发送到堡垒机上来验证新版本应用的功能是否可以正常工作。由于进行堡垒测试时,堡垒机尚未拉入集群,这里就需要让客户端可以识别出一个堡垒测试请求并把请求转发给指定的堡垒服务实例。虽然我们可以通过路由来实现这一点,但这就需要客户端了解很多转发的细节信息,而且整合入 SDK 的功能对于后续的升级维护会造成一定的麻烦。所以我们开发了一个专门用于堡垒测试的服务网关。当一个客户端识别到当前请求的上下文中包含堡垒请求标识时,它就会把 Dubbo 请求转发给预先配置好的测试网关。网关会先解析这个服务请求,判断它对应的是哪个服务然后再找出这个服务的堡垒机并将请求转发过去。在服务完成请求处理后,网关也会把响应数据转发回调用方。
与一般的 HTTP 网关不同, Dubbo 的服务网关需要考虑一个额外的请求方式,就是前面所提到的 callback 。由于 callback 是从服务端发起的请求,整个处理流程都与客户端的正常请求不同。网关上会将客户端发起的连接和网关与服务端之间的连接进行绑定,并记录最近待返回的请求 ID 。这样在接收到 callback 的请求和响应时就可以准确的路由了。
后续功能规划
截止到今天, CDubbo 一共发布了27个版本。携程的很多业务部门都已经接入了 Dubbo 。在未来, CDubbo 还会扩展更多的功能,比如请求限流和认证授权等等。我们希望以后可以贡献更多的新功能出来,回馈开源社区。
本文作者:董艺荃,携程框架架构研发部技术专家。目前负责携程服务化框架的研发工作。