Loading

从零开始实现简单 RPC 框架 4:注册中心

RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到。
那么,Consumer 要从哪里获取 Provider 的地址呢?

能不能 Consumer 自己配置 Provider 的地址?
这种方式理论上是可行的,不过事实上没人这么做。这种方式有以下缺点:

  1. Consumer 每引用一个接口,需要配置一次 Provider 的服务地址,配置繁琐易错。
  2. Consumer 引用其他业务组的服务,需要跨团队沟通,沟通成本高。
  3. Provider 如果换服务器、挂掉、新增,都需要通知到 Consumer 去修改服务地址,配置修改可能不及时造成服务异常。
  4. Consumer 如果引用很多服务,那么配置会非常杂乱,管理起来非常麻烦。

从上面的缺点来看,最好的方式是找个地方把配置管理起来
例如,把配置放到统一的数据库中,Provider 启动的时候,把自己的地址和接口写到表中; Consumer 在请求接口之前,就可以从表里获取该接口对应的Provider地址。
其实,这种把配置统一管理的地方,就叫 注册中心

注册中心就像中间桥梁,连接ProviderConsumer。三方关系示意图如下:
RPC框架最简单的结构
注册中心 只是 Provider 感知 Consumer 的一种方式而已,最终 Provider 调用 Consumer 接口还是以直连的方式进行。
Provider 注册或者取消注册,注册中心会通知 Consumer,保证 Consumer 感知服务状态的及时性。

注册中心的特性#

一个合格的注册中心,需要有以下的特性:

1. 存储#

可以简单地将注册中心理解为一个存储系统,存储着服务与服务提供方的映射表。一般注册中心对存储没有太多特别的要求,甚至夸张一点,你可以基于数据库来实现一个注册中心。

2. 高可用#

注册中心一旦挂掉,Consumer 将无法获取 Provider 的地址,整个微服务将无法运转。
当然 Consumer 可以添加本地缓存,从某种角度上看,是允许注册中心短暂挂掉的。

3. 健康检查#

Provider 向注册中心注册服务之后,注册中心需要定时向 Provider 发起健康检查,当 Provider 宕机的时候,注册中心能更快发现 ,从而将宕机的 Provider 从注册表中移除。
这特性数据库、Redis 都不具有,因此他们不适合做注册中心。

4. 监听状态#

当服务增加、减少 Provider 的时候,注册中心除了能及时更新,还要能主动通知 Consumer,以便 Consumer 能快速更新本地缓存,减少错误请求的次数。
这一特性同样数据库、Redis都不具有。

目前主流的注册中心有:ZookeeperEurekaNacosConsul 等。
由于本文主要是讲注册中心的实现,就不详细讲各种注册中心的差异、优缺点了,有兴趣的同学可以看这里

下面我们来讲 ccx-rpc 的注册中心是如何实现的。

注册中心的设计与实现#

接口定义#

下面是注册中心的接口,最简单就包含两个方法:注册查找

public interface Registry {

    /**
     * 向注册中心注册服务
     *
     * @param url 注册者的信息
     */
    void register(URL url);

    /**
     * 查找注册的服务
     *
     * @param condition 查询条件
     * @return 符合查询条件的所有注册者
     */
    List<URL> lookup(URL condition);
}

本地缓存#

为了减缓注册中心的压力,需要加上本地缓存,减少请求。同时也可以增加可用性,当注册中心挂的时候,本地还可以使用缓存中的数据。这部分逻辑否装在 AbstractRegistry 中,其他的实现都继承 AbstractRegistry

变量 registered 将服务信息缓存在 Map 中,服务名为 Key,Value 则是该服务注册的 Provider 列表。

/**
* 已注册的服务的本地缓存。{serviceName: [URL]}
*/
private final Map<String, Set<String>> registered = new ConcurrentHashMap<>();

当注册的 Provider 增加、减少的时候,会全量更新该服务下的 Provider 列表。

/**
 * 重置。真实拿出注册信息,然后加到缓存中。
 */
public List<URL> reset(URL condition) {
    // 获取服务名
    String serviceName = getServiceNameFromUrl(condition);
    // 将原来注册信息本地缓存删掉
    registered.remove(serviceName);
    // 重新从注册中心获取
    List<URL> urls = doLookup(condition);
    for (URL url : urls) {
        // 将所有 Provider 添加到本地缓存
        addToLocalCache(url);
    }
    return urls;
}

/**
 * 添加到本地缓存
 */
private void addToLocalCache(URL url) {
    String serviceName = getServiceNameFromUrl(url);
    if (!registered.containsKey(serviceName)) {
        registered.put(serviceName, new ConcurrentHashSet<>());
    }
    registered.get(serviceName).add(url.toFullString());
}

Zookeeper 实现#

ccx-rpc 中,注册中心实现了 zookeeper,实现类是 ZkRegistry
Zookeeper 客户端使用的是 Curator 框架,比官方的好用多了。

1. 注册#

服务注册的时候,会在 /ccx-rpc/${serviceName}/providers 下创建一个临时节点
为什么是临时节点呢?临时节点有个功能就是,当客户端断开连接的时候,该客户端创建的节点都会自动删除,这个特性非常适合注册中心。

public void doRegister(URL url) {
    zkClient.createEphemeralNode(toUrlPath(url));
    watch(url);
}

创建的临时节点的内容是 Provider 的 URL 信息
示例:ccx-rpc://192.168.10.111:5525?interface=com.ccx.rpc.demo.service.api.UserService&version=
因为 URL 中包含 /,所以需要进行 url 编码,最终在 Zookeeper 存的是:
ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=

/**
 * 转成全路径,包括节点内容。
 * 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers/ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=
 */
private String toUrlPath(URL url) {
    return toServicePath(url) + "/" + urlEncoder.encode(url.toFullString(), charset);
}

/**
 * 转成服务的路径。
 * 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers
 */
private String toServicePath(URL url) {
    return getServiceNameFromUrl(url) + "/" + RegistryConst.PROVIDERS_CATEGORY;
}

2. 查找#

Consumer 直接获取服务路径下的所有子节点即可。

public List<URL> doLookup(URL condition) {
    List<String> children = zkClient.getChildren(toServicePath(condition));
    List<URL> urls = children.stream()
            .map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
            .collect(Collectors.toList());
    return urls;
}

3. 监听#

Zookeeper 还有一个很强的功能:监听。当监听的路径发生状态变化时,会全量更新(reset)对应的服务的本地缓存。reset 方法在上面的 AbstractRegistry 有讲到,这里就不重复贴代码了。

/**
 * 监听
 */
private void watch(URL url) {
    String path = toServicePath(url);
    zkClient.addListener(path, (type, oldData, data) -> {
        reset(url);
    });
}

那么,我们是如何知道要监听哪些路径的呢?当 AbstractRegistry 本地缓存不存在的时候,会请求到 ZkRegistrydoLookup,请求出来的 Provider 都进行监听。

public List<URL> doLookup(URL condition) {
    List<String> children = zkClient.getChildren(toServicePath(condition));
    List<URL> urls = children.stream()
            .map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
            .collect(Collectors.toList());
    // 获取到的每个都添加监听
    for (URL url : urls) {
        watch(url);
    }
    return urls;
}

总结#

注册中心的设计比较简单,一个注册register和查找lookup就能简单满足要求。
为了提高性能和可用性,AbstractRegistry 还增加了本地缓存,其他实现继承 AbstractRegistry
最后我们讲了 ZkRegistry 的实现,主要就是注册查找监听
其他类型的注册中心按照这个模板,实现起来就会非常简单啦,如果有童鞋想实现其他的注册中心,欢迎给 ccx-rpc 提 PR。

ccx-rpc 代码已经开源
Github:https://github.com/chenchuxin/ccx-rpc
Gitee:https://gitee.com/imccx/ccx-rpc

posted @   小新是也  阅读(2543)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
阅读排行:
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略
点击右上角即可分享
微信分享提示
主题色彩