[转]业务出海过程中,你有遇到过跨域RPC调用的问题吗?
在微服务架构中,一般都是通过 API 网关统一向外部系统提供 API 服务。熟悉 Spring Cloud 的同学知道,Zuul 在 Spring Cloud 中就起到了 API 网关的作用。同理,在菜鸟微服务架构中,菜鸟 API 网关作为对外暴露服务的网关。
菜鸟在面向全球 CP(Cainiao Partner)提供服务的时候,遇到了传统 API 网关在跨国网络上传输数据的瓶颈,如下图,Partner C(国外的 CP)调用 API Gateway 走了跨国的公网,该段网络的质量非常不稳定,严重影响了 Partner C 的服务体验。我们把这种由于网络延迟高、抖动严重而引起的网络质量问题称为 网络桎梏。
菜鸟 API 网关通过多域部署解决了这个问题。Partner C 可以接入同域的 API 网关,将 Partner C 的 API 服务调用转发到目标 API 网关,需要引入一种机制来完成调用转发,这里使用的就是 跨域调用。而前文提到的网络桎梏问题下沉到跨域调用解决。本文将会重点介绍菜鸟的跨域调用解决方案,希望能给你一些启发。
1. 再谈应用出海
很多菜鸟应用,最开始都部署在 IDC(中心机房),通过 SOA(比如 Dubbo)进行相互依赖,随着业务的发展,会有上云和出海的需求,但并非所有系统都会多域部署,从而存在了跨域依赖的问题,这个时候,就可以通过 跨域调用 解决。
例如:A、B、C 三个应用都部署在 A 域,A 和 B 都依赖 C,但是只有应用 A 和 B 会出海,从而 A 和 B 会跨域依赖应用 C。
为了让应用 A、B、C 尽可能少的改动甚至不改动就完成出海,要求跨域调用具备普通 SOA 框架(比如 Dubbo)的调用形式,具体来说,跨域调用要和 Dubbo 一样,支持基于接口的调用。这样,在接口不变的情况下,可以很方便的在域内调用和域见调用进行切换。
从 API Gateway 和应用出海的实例可以看出,需要沉淀一种跨域调用的能力。在菜鸟提供了专门用于跨域调用的项目:
CRPC(CrossDomain Remote Procedure Call)。CRPC 的设计目标总结成一句话:像调用本地服务一样解决网络桎梏调用跨域服务。
2. 双向代理模式:解决网络桎梏问题
网络桎梏是跨域调用要解决的首要问题,解决方式是通过双向代理的模式,代理之间通过专线打通。
如下图(C 指 Consumer,P 指 Provider):Consumer 和 Provider 在 CRPC 中都是 Client,代理层作为 CRPC 的 Server。对于同域的 Consumer 而言,Proxy 是正向代理;对于同域的 Provider 而言,Proxy 是反向代理。
CRPC 将一次调用链路分成了三段:两段同域调用,一段跨域调用。由于跨域调用仅存在于 Proxy 之间,而 Proxy 之间又是通过专线打通,从而能够解决跨域的网络桎梏。
当业务发生一次远程调用时,客户端会将请求发到同域的 Proxy(称为源 Proxy),源 Proxy 将请求转发到 Provider 同域的 Proxy(称为目标 Proxy),目标 Proxy 将请求推送给 Provider 端,Provider 端完成本地调用后,将相应返回给目标 Proxy,然后将该响应原路返回给源 Proxy,源 Proxy 然后将响应返回给 Consumer 端,通过图中标注的 6 个步骤,完成整个跨域调用链路。
由于网络被分成了三段,引入了一个新的问题:如何完成服务发现? 此外,由于 Proxy 和 Consumer/Provider 的结构是 C/S,从而引入了另一个问题:如果保证 Server 端的性能和可扩展性?最后,所有依赖跨域调用的域都需要部署 Proxy:如何完成 Proxy 的快速部署? 下面将分别解决这里提出的问题。
3. 服务发现
首先来解决跨域调用的服务发现。跨域调用的网络被分成了三段,其中 Consumer 到 Proxy,Proxy 到 Provider 这两段是同域调用,Proxy 到 Proxy 是跨域网络。
同域的服务发现采用 soa 的服务发现,如下,在这个模型中,服务提供者首先把服务注册到服务注册中心,然后消费者向服务注册中心获取服务元信息(如生产者地址),然后消费者向服务提供者发起一次服务调用。在 Spring Cloud Netflix 中,Eureka 作为服务注册中心。
为了实现跨域网络的服务发现,引入了一个新的思想:服务具有域的概念。CRPC 把域的概念提了出来,服务有域的概念,语义为某域提供的 xxx 服务。
举个例子,某个系统拥有自己的配置数据,通过暴露写配置服务来更改配置数据,对于不同域来说,比如上海和新加坡,配置数据是不一样的,所以在调用写配置服务时,需要明确指明是调用上海的写配置服务还是调用新加坡的写配置服务。
在这个思路下,在调用方调用一个服务时需要指定服务提供的域,该信息就可以用于跨域网络的路由。
从而,跨域调用解决了从 Consumer 到 Provider 的服务发现,如下图:
4. Server 端性能考虑
接下来解决 Server 端的性能问题,主要介绍 CRPC 使用的异步化和长连接方案。
4-1. 异步化:提升 Proxy 吞吐量
由于跨域调用有较长的 rt,所以如果每次调用都独占一个线程,可能导致线程资源紧张。因此,Proxy 使用了异步化架构,从而达到可观的吞吐量。Proxy 的入口和出口分为如下几种情况:
- Dubbo 进:同域内 Consumer 调用 Proxy 使用的入口
- Dubbo 出:同域内 Provider 调用 Proxy 的出口
- HTTP 进:跨域路由 Proxy 调用 Proxy 的入口
- HTTP/2 出:跨域路由 Proxy 调用 Proxy 的出口
需要针对每种入口和出口采用异步实现,Proxy 节点内部的线程情况如下,从而 SOA 和 Servlet 的线程资源不会受 Proxy 内部处理逻辑阻塞影响,Proxy 内部的线程资源也不会受调用下游 rt 长而影响。
4-2. 长连接:减少 Proxy 资源使用,提升 Proxy 转发性能
Proxy 站点间通过 HTTP/2 进行长连接,HTTP/2 使用二进制格式,多路复用以及 HPACK 压缩技术,性能较 HTTP/1.x 有很大提升,并且长连接能够减少 Socket 资源。
Proxy 节点间长连接如下,长连接建立在 Proxy 和目标域的 nginx 集群上,而不是建立在源 Proxy 和目标 Proxy 之间。通过双向建立长连接,完成 Domain A 调用 Domain B 以及反向调用。
5. Server 端水平扩展
由于采用了 C/S 架构,Server 端需要支持水平扩展。Proxy 的入口有两个,分别是 Consumer 调用 Proxy,以及 Proxy 调用 Proxy。只需要保证两种调用时负载均衡的,并且 Server 端是无状态的,就可以很方便的实现 Server 端的水平扩展。
其中,Consumer 调用 Proxy 是通过微服务框架实现的软负载均衡实现的负载均衡策略。而 Proxy 调用 Proxy 是通过 Nginx 将 HTTP/2 协议降级成 HTTP/1.x 反向代理实现的负载均衡。这里也解释了前面讲述异步化时为什么 Proxy 的入口是 HTTP 而不是 HTTP/2,以及介绍长连接时 Proxy 为什么是和 Nginx 集群做长连接的问题。
6. 多域对等部署
最后来解决 Proxy 快速部署的问题,为了能够使 Proxy 能够快速部署,使用了 Docker 的部署方式,并且多域的 Proxy 没有差异,完全对等部署。从而一个新的域需要生产 Proxy 时,可以快速部署。
7. 像调用本地服务一样使用 CRPC
在服务路由一节讲到,Consumer 调用服务时需要指定服务提供方所在的域,但是 CRPC 要基于接口的调用,这要求指定域的操作不能侵入接口,具体的 CRPC 调用如下。(此处代码仅做示例,无法调用)
1. 生成跨域 Provider Bean
1 2 3 4 5 6 7 8 |
// 本地接口实现通过 CRPCProvider 注解生成跨域服务的 Provider @CRPCProvider(serviceVersion = "1.0.0") public class TestServiceImpl implements TestService{ @Override public String test(String input) { return "hello " + input; } } |
2. 生成跨域 Consumer Bean
1 2 3 4 5 6 |
@Configuration public class CRPCConfig { // 通过在接口上增加 CRPCConsumer 注解生成跨域服务的 Consumer,指定服务提供方的默认域 @CRPCConsumer(version = "1.0.0", defaultDomain = "新加坡") private TestService testService; } |
3. CRPC 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public Class CRPCTest{ @Autowired private TestService testService; // 调用时使用 CRPCConsumer 配置的默认域 @Test public void test_0() { String result = testService.test("world"); } // 调用时指定服务提供方的域 @Test public void test_1() { String result = ConsumerUtil.forDomain("新加坡", testService).test("world"); } } |
本文首先介绍了 CRPC 的场景,由此引入了 CRPC 的核心设计目标:像调用本地服务一样解决网络桎梏调用跨域服务。在解决网络桎梏问题上使用了双向代理并通过专线打通的方式,由于 Consumer/Provider 和代理是 C/S 架构,本文随后介绍了在 Server 端设计时解决的四个问题:1. 跨域服务发现。2.Server 端性能。3.Server 端水平扩展。4. 多域对等部署。最后通过接入 CRPC 的示例代码介绍了如何像调用本地服务一样调用跨域服务。
[source]业务出海过程中,你有遇到过跨域RPC调用的问题吗?