服务发现之所以重要,是因为它解决了微服务架构最关键的问题:如何精准的定位需要调用的服务ip以及端口。
在实施微服务之后,我们的调用都变成了服务间的调用。服务间调用需要知道IP、端口等信息。
在没有微服务之前,我们的调用信息一般都是写死在调用方的配置文件里(当然这话不绝对,有些公司会把这些信息写到数据库等公共的地方,以方便维护)。
又由于业务的复杂,每个服务可能依赖N个其他服务,如果某个服务的IP,端口等信息发生变更,那么所有依赖该服务的服务的配置文件都要去修改,这样显然太麻烦了。
有些服务为了负载是有个多个实例的,而且可能是随时会调整实例的数量。如果每次调整实例数量都要去修改其他服务的配置并重启那太麻烦了。
为了解决这个问题,业界就有了服务注册发现组件。
假设我们有服务A需要调用服务B,并且有服务注册发现组件R整个大致流程将变成大概3部:
服务B启动向服务R注册自己的信息
服务A从服务R拉取服务B的信息
服务A调用服务B
有了服务注册发现组件之后,当修改A服务信息的时候再也不用去修改其他相关服务了
需求分析:服务注册和发现
rpc 项目要实现的第一个功能模块就是:服务注册和发现
,这个功能也是整个框架非常核心和关键的。
我们的 rpc 项目不用于生成环境,造个轮子嘛,只需要实现最基础的功能即可:
- 服务实例注册自己的元数据到注册中心,元数据包括:实例 ip、端口、接口描述等;
- 客户端实例想要调用服务端接口会先连接注册中心,
发现
待调用的服务端实例; - 拿到多个服务端实例后,客户端会根据负载均衡算法选择一个合适的实例进行RPC调用。
需求很明确了,下面开始写代码。
引入三方依赖
市面上靠谱的注册中心还是很多的,这次打算同时兼容两种注册中心:Zookeeper
和 Nacos
,是不是很良心?在使用前需要先引入以下依赖。
与 Zookeeper 交互可以引入对应的 SDK,zkclient 是个不错的选择;
JSON 序列化和反序列化可以引入 fastjson
,虽然经常爆漏洞,但是国产还是得支持下(虽然本人还是喜欢用jackson
):
1 | <!-- Zookeeper 客户端 --> |
至于 Nacos,可以直接引入官方提供的 SDK:nacos-client:
1 | <dependency> |
服务端实现服务注册
服务注册和发现分为两块功能:服务端注册和客户端发现,我们先来实现服务端注册功能。
定义服务注册接口
在日常的工作或者学习编码过程中,我们一定要习惯面向接口编程,这样做有助于增强代码可扩展性。
根据前面的需求描述,服务注册只需要干一件事情:服务注册
,我们可以定义一个接口:ServiceRegistry
,接口中定义一个方法:register
,代码如下:
1 | public interface ServiceRegistry { |
服务向注册中心注册,注册的内容定义一个类ServiceInfo
来封装。
1 | public class ServiceInfo { |
Zookeeper 实现服务注册
我们尝试用 Zookeeper 来实现服务注册功能,先新建一个类实现前面定义好的服务注册接口:
1 | public class ZookeeperServiceRegistry implements ServiceRegistry { |
接下来重写register
方法,主要功能包括调用 Zookeeper 接口创建服务节点和实例节点。
其中服务节点是一个永久节点,只用创建一次;实例节点是临时节点,如果实例故障下线,实例节点会自动删除。
1 | // ZookeeperServiceRegistry.java |
代码非常简单,大家看注释就能懂了。
Nacos 实现服务注册
除了使用 Zookeeper 来实现,我们还可以使用 Nacos,跟上面一样我们还是先建一个类:
1 | public class NacosServiceRegistry implements ServiceRegistry { |
接着编写构造方法,NacosServiceRegistry 类被实例化之后 Nacos 客户端也要连接上 Nacos 服务端。
1 | // NacosServiceRegistry.java |
获得NamingService类的实例对象后,就可以调用实例注册接口完成服务注册了。
1 | // NacosServiceRegistry.java |
注意:NamingService 类提供了很多有用的方法,大家可自行进行尝试。
客户端实现服务发现
定义服务发现接口
前面已经将服务实例注册到Zookeeper 或者 Nacos 服务端,现在客户端想要调用服务端首先得获得服务端实例列表,这个过程其实就是服务发现。
我们先定义一个抽象的接口,这个接口主要的功能就是定义一个获取服务实例的接口:
1 | public interface ServiceDiscovery { |
随机挑选一个实例是为了模拟负载均衡,尽量使请求均匀分配到各实例上。
Zookeeper 实现服务发现
前面使用 Zookeeper 实现了服务注册功能,这里我们再用 Zookeeper 来实现服务发现功能,先定义一个类实现 ServiceDiscovery 接口:
1 | public class ZookeeperServiceDiscovery implements ServiceDiscovery { |
下面实现核心方法:selectOneInstance
Zookeeper 内部是一个树形的节点,通过查找一个指定节点的所有子节点即可获得服务实例列表。拿到服务实例列表后如何挑选出一个实例呢?这里就可以引入负载均衡算法了。
1 | // ZookeeperServiceDiscovery.java |
注意:当前项目仅仅用于学习用,这里没有引入复杂的负载均衡算法,有兴趣的同学可自行补充,欢迎提交 MR 贡献代码。
Nacos 实现服务发现
最后来到 Nacos 的实现,话不多说先定义一个类:
1 | public class NacosServiceDiscovery implements ServiceDiscovery { |
同样也需要实现核心方法:selectOneInstance
。Nacos 的实现就比较简单了,因为 Nacos 官方提供的 SDK 功能太强大了,我们直接调用对应的接口就可以了,Nacos 根据算法会随机挑选一个健康的实例,我们不用关注细节。
1 | // ZookeeperServiceDiscovery.java |
完整的源码可以自行去 Github 上取:
1 | https://github.com/CoderLeixiaoshuai/easy-rpc |
小结
本文以较少的代码实现了 RPC 框架实现服务注册发现功能,相信大家对这个流程已经全面掌握了。
客户端与服务端通信的前提是需要知道对方的 ip 和端口,服务注册就是将自己的元信息(ip、端口等)注册到注册中心(Registry),这样客户端就可以从注册中心(Registry)获取自己”感兴趣”的服务实例了。
服务注册和发现机制可以通过一些中间件来辅助实现,如比较流行的:Zookeeper或者 Nacos 等。