Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nacos Client Failover Function Enhancement #11053

Closed
nkorange opened this issue Aug 30, 2023 · 16 comments
Closed

Nacos Client Failover Function Enhancement #11053

nkorange opened this issue Aug 30, 2023 · 16 comments
Labels
area/Client Related to Nacos Client SDK area/Naming kind/feature type/feature kind/proposal
Milestone

Comments

@nkorange
Copy link
Collaborator

nkorange commented Aug 30, 2023

需求背景

目前Nacos客户端有一个FailoverReactor来进行容灾文件的管理,可以通过在指定磁盘文件里写入容灾数据来进行客户端使用数据的覆盖。FailoverReactor目前会拦截Nacos客户端查询接口调用,以getAllInstances接口为例,目前FailoverReactor的工作流程如下图:

这里主要涉及到两个组件:

  • FailoverReactor:容灾数据管理类,负责容灾开关和数据的加载和刷新;
  • ServiceInfoHoler:默认的服务数据管理类,持有一份内存服务数据缓存,负责处理Nacos服务端的推送的最新数据;

FailoverReactor和ServiceInfoHoler的交互机制如下:

这里和客户端容灾的相关逻辑主要是3个:

  1. FailoverReactor会定期读取磁盘容灾开关文件;
  2. 当容灾开关打开时,FailoverReactor会从磁盘里加载容灾数据文件,同时用户调用查询请求时就会优先使用容灾文件里的数据;
  3. FailoverReactor会定期从ServiceInfoHolder拿到最新的内存数据,保存到容灾磁盘数据文件里;

目前的方式有四个问题:

  1. 无法人工覆盖容灾数据,当前是定期将ServiceInfoHolder里的数据进行持久化作为容灾数据。即使手动修改了容灾数据磁盘文件内容,也会被覆盖为ServiceInfoHolder里的数据;
  2. 基于磁盘的容灾缓存有一些限制,比如服务的订阅者有多个机器实例时,如果需要打开容灾开关,需要运维批量修改机器的文件;
  3. subscribe接口不会使用FailoverReactor里的数据;
  4. 容灾数据的可观测性也需要优化。

实现方案

上面提到的问题1、3和4都比较好解决,下面会一一阐述。针对问题2,一般在生产环境中,我们会考虑使用中心化的数据存储来进行容灾数据的存储和管理。我们可以将FailoverReactor依赖的数据 来源抽象为一个SPI接口FailoverDataSource,这个接口默认实现还是本地磁盘,但是用户可以实现这个SPI接口来使用自定义的容灾数据源。

接口和数据结构定义

FailoverDataSource的定义为:

public interface FailoverDataSource {

     FailoverSwitch getSwitch();

     Map<String, FailoverData> getFailoverData();
}

各个方法的作用为:

  • FailoverSwitch getSwitch():获取当前的容灾开关;
  • Map<String, FailoverData> getFailoverData():获取当前的容灾数据,返回一个map,key是服务名,value为对应服务的容灾数据;

FailoverSwitch的定义建议如下:

public class FailoverSwitch {

    private boolean enabled;
}
  • enabled:容灾是否打开;

FailoverData的定义建议为:

public abstract class FailoverData {

    private DataType dataType;
    
    private Object data;
    
    protected FailoverData(Object data, DataType dataType) {
        this.data = data;
        this.dataType = dataType;
    }

    enum DataType {
        naming,
        config
    }
}
  • dataType:容灾数据类型,这里因为需要综合考虑配置模块的容灾,所以使用抽象类定义了naming和config两种容灾数据类型;
  • data:容灾数据,子类设置具体类型;

以naming模块为例,NamingFailoverData扩展FailoverData:

public class NamingFailoverData extends FailoverData {

    private NamingFailoverData(ServiceInfo serviceInfo) {
        super(serviceInfo, DataType.naming);
    }

    public static NamingFailoverData newNamingFailoverData(ServiceInfo serviceInfo) {
        return new NamingFailoverData(serviceInfo);
    }
}

NamingFailoverData里的容灾数据类型为ServiceInfo。

交互流程

FailoverReactor内部流程

FailoverReactor和FailoverDataSource、ServiceInfoHoler的交互机制优化为:

image

各个组件职责说明如下:

  • FailoverReactor:存储容灾缓存数据,管理容灾开关定时任务和容灾数据更新定时任务;
  • FailoverSwitchRefresher:定时(每5秒)从容灾数据源查询容灾开关,根据容灾开关的值进行相应操作:
    • 若容灾打开:从容灾数据源查询容灾数据FailoverDataSource,保存到FailoverReactor的内存容灾数据map里;
    • 若容灾关闭:清空FailoverReactor的内存容灾数据map;
  • FailoverDataSource:存储容灾数据,前文已经提及;
  • ServiceInfoHoler:默认情况下服务数据的管理,前文已经提及;

客户端查询请求流程

对于客户端的查询请求,其流程优化为:

这里的流程和之前的变化为:当从failoverReactor里拿不到容灾数据的时候,还是会去serviceInfoHolder里读取数据。这么做的目的是因为我们可能只配置部分服务进行容灾,其他的服务还是走serviceInfoHolder。

订阅接口事件通知流程

对于订阅接口,之前是不会受到容灾开关的影响,现在则也会在容灾开启时停止数据更新通知:

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
        ...
        if (changed) {
            NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                    JacksonUtils.toJson(serviceInfo.getHosts()));
            // 判断容灾开关是否打开,打开时不发布事件:
            if (!failoverDataSource.getFailoverSwitch().isEnabled()) {
                NotifyCenter.publishEvent(new InstancesChangeEvent(notifierEventScope, serviceInfo.getName(), serviceInfo.getGroupName(),
                        serviceInfo.getClusters(), serviceInfo.getHosts()));
            }
            DiskCache.write(serviceInfo, cacheDir);
        }
        return serviceInfo;
    }

同时,在容灾关闭时,我们需要根据容灾期间数据是否发生变化来决定要不要触发订阅事件通知,我们可以把这个逻辑驾到上面提到的FailoverSwitchRefresher里:

image

在这里当FailoverSwitchRefresher轮询发现容灾关闭时,在清空FailoverReactor的内存数据之前,会触发FailoverReactor和ServiceInfoHolder的数据比较,如果发现数据不一致,则会触发ServiceInfoHolder发布对应的服务变更事件。

可观测性

我们可以定义个MultiGauge来存储FailoverReactor里目前生效的容灾数据内容,统计粒度为每个服务当前有多少个实例:

MultiGauge failoverInstanceCounts = MultiGauge.builder("nacos_naming_client_failover_instances").register(Metrics.globalRegistry);

Set<String> serviceNames = failoverDataSource.getSwitch().getServiceNames();
Map<String, FailoverData> failoverDataMap = failoverDataSource.getFailoverData();

failoverInstanceCounts.register(serviceNames.stream().map(serviceName -> MultiGauge.Row.of(Tags.of("service_name", serviceName), ((ServiceInfo)failoverDataMap.get(serviceName)).ipCount())).collect(Collectors.toList()), true);

测试用例

以下场景需要进行测试:

  • 打开容灾开关后,对于包含在容灾的服务列表里的服务,Nacos服务端数据变化不影响客户端查询和订阅;
  • 关闭容灾开关后,对于所有服务客户端查询到最新数据;
  • 关闭容灾开关后,若订阅的服务数据在容灾期间有变化,会触发一次订阅通知;
  • 设置容灾的服务列表,不在服务列表里的不会使用容灾数据;
  • 使用自定义的容灾实现,可以被加载并运行;
@KomachiSion
Copy link
Collaborator

You mean you want to do failover extension for nacos-client?

@nkorange
Copy link
Collaborator Author

nkorange commented Aug 31, 2023

It can be implemented as an extension to support multiple kinds of failover data sources.
Other than that, there are also new features to the failover solution:

  1. the registered listeners via subscribe method won't receive notifications in failover mode.
  2. we can choose only a subset of the consuming services to use failover data.

@YunWZ
Copy link
Contributor

YunWZ commented Sep 5, 2023

我们是通过redis缓存示例数据...

@MajorHe1
Copy link
Collaborator

MajorHe1 commented Sep 7, 2023

我们是通过马上重启解决服务端不可用问题……

@KomachiSion
Copy link
Collaborator

问题:

  1. 流程图中是先判断是否订阅,再判断是否failover,疑问是failover的位置,failover是最后手段还是最初手段?我觉得应该是最初判断比较好
  2. FailoverDataWriter我觉得是否没有必要, 客户端的failover应该是只读的,客户端测不应该写入。

@nkorange
Copy link
Collaborator Author

问题:

  1. 流程图中是先判断是否订阅,再判断是否failover,疑问是failover的位置,failover是最后手段还是最初手段?我觉得应该是最初判断比较好
  2. FailoverDataWriter我觉得是否没有必要, 客户端的failover应该是只读的,客户端测不应该写入。
  1. 这块是图有误,确实应该failover先判断,已更新;
  2. FailoverDataWriter目前客户端里就是有的,去掉的话就需要人工复制数据到文件里,这个如果机器比较多的情况下还是比较麻烦;

@KomachiSion
Copy link
Collaborator

image

我在最新的develop分支中检索,没有发现, 可能是类名不正确,但是从我理解的failover设计中, failover文件就是用户人工创建的,不同于snapshot文件。

@nkorange
Copy link
Collaborator Author

目前的实现在:

我们可以加个开关,容灾打开时,就不写,容灾关闭时,就定时写,保持容灾数据的更新。

@KomachiSion
Copy link
Collaborator

我觉得没必要, disk就是一个插件的默认实现, 默认实现不从client测写入failover,只有用户创建了对应文件才生效。

如果其他的自定义插件要写磁盘, 你就在自定义插件中自己写就好了,不要加开关侵入到默认插件里面。

@stone-98
Copy link
Contributor

stone-98 commented Oct 13, 2023

当failover发生改变时,为什么不进行通知listener呢?单单只作用于查询接口。

对于用户来说,如果订阅对应的实例,那么一定是想要监听这个实例的变化,如果failover内容发生了变化,查询相关实例明明发生了变化,但是listener却没有接收到回调,这是否有些不合常理呢?

@nkorange
Copy link
Collaborator Author

当failover发生改变时,为什么不进行通知listener呢?单单只作用于查询接口。

对于用户来说,如果订阅对应的实例,那么一定是想要监听这个实例的变化,如果failover内容发生了变化,查询相关实例明明发生了变化,但是listener却没有接收到回调,这是否有些不合常理呢?

是的,也需要通知

@stone-98
Copy link
Contributor

目前这个设计好像是没有考虑这点的是吗?我看目前的话只有当关闭开关时才会进行通知~

@stone-98
Copy link
Contributor

或许我可以等该设计的pr合并后,再支持这个特性?

@nkorange
Copy link
Collaborator Author

@stone-98 更新了设计图,PR也会跟进更新

@stone-98
Copy link
Contributor

@stone-98 更新了设计图,PR也会跟进更新

okk

@KomachiSion
Copy link
Collaborator

Merged, will be added as a pre beta feature in 2.3.1

@nkorange nkorange changed the title Nacos注册中心客户端容灾 Nacos Client Failover Function Enhancement Feb 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/Client Related to Nacos Client SDK area/Naming kind/feature type/feature kind/proposal
Projects
None yet
Development

No branches or pull requests

5 participants