在分布式系统和微服务架构中,系统的能力来自服务与服务之间的交互和集成。为了实现这些过程,就需要服务提供者对外暴露可以访问的入口,而服务消费者就基于这些入口对服务提供者发起远程调用。
我们来举一个例子,如果我们想要发布一个 DemoService,那么可以使用这样的代码。
ini
DemoService service = new...;
RpcServer server = new...;
server.export(DemoService.class, demo, options);
而对 DemoService 服务进行导入的实现过程,可以采用如下所示的代码风格。
ini
RpcClient client =
DemoService service = client.refer(DemoService.class);
service.call("how are you?");
显然,就这两段代码而言,看上去很简单。但事实上,想要实现这样的效果,开发人员要做的事情非常多。今天,我们就将从这个简单的示例出发,探讨背后的服务发布和引用流程。
服务发布和引用
在当前主流的分布式服务框架中,无论是 Dubbo 还是 Spring Cloud,都提供了类似前面介绍的服务发布和引用功能。通过对这些框架的实现机制进行抽象和提炼,我们实际上可以梳理出一套统一的设计和开发流程。接下来,让我们先来看服务的发布流程。
服务发布
先抛开具体的技术和框架,我们可以简单抽象出如图所示的服务发布整体流程。
上图中包含了服务发布过程中的各个核心组件,包括发布启动器、动态代理、发布管理器、协议服务器和注册中心。我们先来一一展开这些核心组件。
- 发布启动器
发布启动器(Launcher)的核心作用有两点,一个是确定服务的发布形式,一个是启动服务发布过程。在目前主流的开发框架中,配置化、注解和 API 调用是最常见的三种发布形式。
以上三种方式各有利弊,在日常开发过程中,配置和注解比较常用,而 API 调用则主要完成服务与服务之间的集成。
讲完发布形式,我们来讨论如何启动服务发布过程。
可以看到,我们能够使用 Spring 容器来完成基于配置化和注解形式下的服务启动过程。而对于 API 调用而言,由于不一定会借助容器,所以可以直接使用 main 函数来实现这一目标。
- 动态代理
动态代理是远程过程调用中非常核心的一个技术组件,在服务发布和服务引用过程中都会用到,其主要作用就是为了简化服务发布和引用的开发难度,以及确保能够对整个过程进行扩展和定制。我们在后面介绍到服务引用时还会看到动态代理。
- 发布管理器
服务发布过程需要使用专门的组件来进行统一管理,这个组件就是发布管理器。该组件需要判断本次发布是否成功,然后在服务发布成功之后,把服务的地址信息注册到注册中心。而这里的服务地址信息则来自协议服务器。
- 协议服务器
在服务发布过程中,在物理上真正建立网络连接,并绑定或释放网络端口的组件是协议服务器。协议服务器还会对网络连接进行心跳检测,以及在连接失败之后进行重连操作。
用于发布服务的常见协议包括 HTTP、RMI、Hessian 等。我们也可以自己定义这样的协议,例如 Dubbo 框架就实现了一套定制化的 Dubbo 协议。
- 注册中心
注册中心的作用是存储和管理服务定义的各类元数据,并能感知到这些元数据的变化。注册中心的核心机制就是服务注册和发现,业界也存在一批主流的注册中心实现工具。
上述的服务发布流程有一定的共性,可以通过转化映射到某个具体的框架上。事实上,基于 Dubbo 的服务发布流程与上述过程非常类似。我们在后面的内容中会做进一步的分析。
服务引用
相较服务发布,服务的引用是一个导入(Import)的过程,整体流程如下图所示。
从图中,我们可以看到服务引用流程与服务发布流程呈对称结构,所包含的组件有:
- 调用启动器
调用启动器和发布启动器是对应的,这里不再重复介绍。
- 动态代理
在服务引用过程中,动态代理的作用就是确保远程调用过程的透明化,即开发人员可以使用本地对象来完成对远程对象的处理。
- 调用管理器
和发布管理器相比,调用管理器的核心功能是提供了一种缓存机制,从而确保服务调用者可以根据保存在本地的远程服务地址信息来发起调用。
- 协议客户端
和协议服务器相对应,协议客户端会创建与服务端的网络连接,发起请求并获取结果。
- 注册中心
注册中心在这里的作用是提供查询服务定义元数据的入口。
上述的服务引用流程同样有一定的共性,可以通过转化映射到某个具体的框架上。事实上,基于 Dubbo 的服务引用流程与上述过程也比较类似。
相比于服务发布,服务引用的实现过程通常会更加复杂一点。这点在 Dubbo 框架中体现的就比较明显。接下来,我们就以 Dubbo 框架为例,分析它的服务发布和引用流程。
Dubbo 中的服务发布和引用
Dubbo 中的服务发布
Dubbo 中的服务发布基本上遵循了我们前面所抽象的服务发布流程,但也添加了一些优化措施。体现在两方面,一方面是发布的时效性,另一方面是发布的作用范围。
我们先来讨论 Dubbo 暴露服务的两种时效,一种是延迟暴露,一种是正常暴露。
你可能会问,Dubbo 为什么要考虑发布时效这个问题呢?主要是为了提供平滑发布机制。如果 Dubbo 服务本身还没有完全启动成功,那这时候对外暴露服务是没有意义的,我们可以通过使用 delay 参数来设置延迟时间,从而确保服务在发布的时间点上就是可用的。
另一方面,Dubbo 中提供了四种发布作用范围的选项。
可以看到,如果我们把 scope 配置为 none,则意味着不会发布这个 Dubbo 服务;如果配置成 local,则说明该服务只会在当前 JVM 中进行暴露,从而可以提高服务调用的效率;如果配置 scope 为 remote,那么该服务就会进行远程暴露;而如果不配置为以上任何一种情况,那么 Dubbo 既会暴露本地服务又会暴露远程服务。
Dubbo 中的服务引用
正如前面所提到的,与服务发布相比,Dubbo 等分布式服务框架中的服务引用整体过程会更加复杂一点。原因在于,在服务引用过程中,因为所调用的服务一般都会部署成集群模式,势必会涉及负载均衡。而如果调用超时或失败,还会采用集群容错机制。
接下来,我们来看 Dubbo 中如何实现服务引用。我们在 ReferenceConfig 的 init 方法中找到了如下所示的 createProxy 方法。这个 createProxy 方法是理解 Dubbo 服务引用的关键入口,我们梳理了它的代码结构。
typescript
private T createProxy(Map<String, String> map) {
if (isJvmRefer) {
//生成本地引用 URL,使用 injvm 协议进行本地调用
} else {
//URL 不为空,执行点对点调用
} else {
//加载注册中心 URL
}
if (urls.size() == 1) {
//单个服务提供者,直接调用
} else {
//多个服务提供者,则构建集群
if (registryURL != null) {
// 如果注册中心链接不为空,则将通过注册中心执行集群调用 } else {
//反之,直接执行集群调用
}
}
// 生成服务代理类
return (T) proxyFactory.getProxy(invoker);
}
在上述流程中,我们明确了 Dubbo 中服务引用的几种不同场景,这些场景对调用管理器的功能做了扩展,但整体流程是一致的。
总结
在远程过程调用的实现思路上,主要包括服务发布和服务引用两大维度。今天我们围绕远程服务的发布和引用流程展开了详细的讨论,这部分内容是我们构建分布式系统的基本前提。同时,基于这套服务发布和引用流程,我们对 Dubbo 这款主流的分布式服务框架如何进行远程/本地服务暴露、如何实现对远程服务的调用过程也进行了分析。