opensergo / opensergo-java-sdk

OpenSergo Java SDK
Apache License 2.0
41 stars 24 forks source link

[DISCUSSION] Best practice of shared/individual client between different integrations in the same process | 同个应用进程多个框架对接 OpenSergo 时 client 的最佳实践 #8

Closed sczyh30 closed 1 year ago

sczyh30 commented 2 years ago

我们的微服务通常会使用好几种框架,比如 RPC 使用的 Spring Cloud Alibaba,流量治理使用的 Sentinel,数据库中间件使用的 ShardingSphere。在这些框架都对接了 OpenSergo 的情况下,在接入到 OpenSergo 控制面,对 OpenSergo client 的使用会有几种方式:

目前的实现未预留 shared 的设计,社区可以一起讨论下这几种情况的最佳实践,以及 shared client 的设计。

jnan806 commented 2 years ago

关于 client 模型 的设计,需要关注以下几点: 1、shared 模型下,同一应用不同框架所引入的 OpenSergo SDK 对接的是否为同一个 OpenSergo Control Plane。 2、用户是否希望使用 share 模型

从这几点出发,share 模型 大致需要如下功能: 1、实例化 OpenSergoClient 实例的时候,需要辨别所要实例化的 OpenSergoClient 是否已存在(根据实例化参数判断),如果存在,则进行复用,不存在则进行实例化。 2、添加 client 模型 的可选配置项,供用户自行选择是否启用 share 模型。(资源开销角度我们建议 shared 模型,若用户有其他考量可不使用shared 模型)。

因此,得到初步设计如下: 1、多个 OpenSergoClient 实例之间需要互相隔离,需要管理好 OpenSergoClient 之间的资源问题。确定每个 OpenSergoClient 自己独有的属性对象有哪些:

2、在 OpenSergo SDK 定义一个 OpenSergoOptions 的类, 这个类里包含整个SDK生命周期所需要的配置(例如 client 模型,Logger 日志输出,version 管理机制等,)。OpenSergoOptions 可作为一个扩展点,后续如果有新的配置项,均可在其中定义


image

伪代码如下:

  // OpenSergoOptions 类定义
  public class OpenSergoOptions {
        // shared/individual 可定义枚举类
    private String clientMode; 
    ...
  }
  // 默认全局配置
  public static OpenSergoOptions globalOpenSergoOptions = new OpenSergoOptions("shared");
  // 应用程序 APP 自定义全局配置
  globalOpenSergoOptions.setClientMode("individual");

  // sentinel 定义并初始化 OpenSergClient
  OpenSergoOptions sentinelOpenSergoOptions = new OpenSergoOptions("shared");
  OpenserClient sentinelOenserClient = new OpensergoClient(ip, port, sentinelOpenSergoOptions)
  sentinelOenserClient.start()

  // dubbo 定义并初始化 OpenSergClient
  OpenSergoOptions dubboOpenSergoOptions = new OpenSergoOptions("shared");
  OpenserClient dubboOpenserClient = new OpensergoClient(ip, port, dubboOpenSergoOptions)
  dubboOpenserClient.start();

  // Spring Cloud Alibaba 定义并初始化 OpenSergClient
  OpenserClient dubboOpenserClient = new OpensergoClient(ip, port, null)
  dubboOpenserClient.start();
  // OpensergoClient 有参构造
  public class OpensergoClient(String ip, int port,  OpenSergoOptions openSergoOptions) {
        // mergeOpenSergoOptions
        // 需要注意 全局OpenSergoOptions 与参数OpenSergoOptions 冲突时的优先级设计
        mergeOpenSergoOptions(openSergoOptions)

        // 实例化 OpensergoClient
        // 需要注意 OpensergoClient 的资源隔离,以及多 OpensergoClient 的管理
  }
richieyan commented 1 year ago

是否可以考虑:保持 OpenSergoClient 的接口不变,内部实现增加两种客户端:一个连接原生客户端 GrpcClient,一个是资源客户端 ResourceClient。连接客户端可以支持多个资源客户端,也可以只支持一个资源客户端。 创建时,不同的控制面,可以通过注册的方式来创建资源客户端,可以达到复用连接客户端的目的。

jnan806 commented 1 year ago

你说的资源客户端,我理解的是在 OpenSergoClient 内部维护一个类似于缓存的结构,将不同数据面的订阅信息( SubscribeTarget 与 ConfigSubscriber )按数据面分组保存,与控制面交互的时候,将订阅信息遍历出来,但还是沿用原先的订阅通道进行交互,是这样吗?

richieyan commented 1 year ago

对差不多这个意思,这样可以保持 OpenSergoClient 本身的易用性,将复杂性隐藏到实现内部。 从内部结构上看,确实存在 资源端与连接端两种类型,资源端依赖连接端获取数据。 OpenSergoClient 负责将数据以统一的方式提供给使用方昂。

jnan806 commented 1 year ago

嗯。我们 的 SDK 目前在 OpenSergoClient 内部本身已经维护了一个 SubscribeTarget 与 ConfigSubscriber 的缓存,在与 控制面交互的时候,也确实是 将其逐个来订阅的。只是这个模式其实只要做个简单改进:把 OpenSergoClient 做成单例 即 shared模式,就可以简单实现 单 OpenSergoClient 的复用了,但这样又会产生另一个问题,尽管shared模式性能表现良好,但并不是所有接入方都希望使用shared模式的 OpenSergoClient。

因此我们现在考虑的是:

至于你提到的思路中,保留 OpenSergoClient 的接口不变,在 OpenSergoClient 中维护多个资源客户端,本质上也是将 OpenSergoClient 做成单例进行复用。而维护多个资源客户端应该是不必要的,因为对于 控制面来说,数据变动只会对 OpenSergoClient 发送一次数据,而数据面对于数据的处理是通过 ConfigSubscriber 来进行的(也就是说,不同资源客户端只需要 向 OpenSergoClient 添加不同的 ConfigSubscriber即可)。 同时你的方案已经是默认了 OpenSergoClient 是shared模式,无法满足特殊情况下用户不希望采用单例 OpenSergoClient 与其他接入方共用的需求。

以上是我的理解 😃 ,如果我阐述的哪里不合适或者有其他更好的建议,欢迎详细展开,我们继续讨论哟 ~

richieyan commented 1 year ago

OpenSergoClient 本身不做成单例,用户依然可以根据场景和需要创建多个 OpenSergoClient,满足特殊场景的用户需要。 OpenSergoClient 本身没有 shared 模式和 individual 模式,新的设计会按需自动共享连接,来处理不连接下的不同类型的资源更新,某种意义上这确实是 shared 模式。不过,这个 shared 是由用户的定义情况产生的,比如针对不同的资源,使用了相同的连接客户端,那么这就是 shared。 本质上,既然是相同的连接ip和 port,使用共享是合理的,极端情况下,确实不想共享,可以自行创建新的 OpenSergoClient 客户端。

sczyh30 commented 1 year ago

Some of my ideas, just for reference:

  1. OpenSergoClient 作为基础的客户端,完全由使用者创建和管理,这个是没问题的;
  2. 我们现在需要的是 OpenSergo SDK 自身有一套管理机制,能够复用同个 endpoint 下的 client 连接。这里面是不是可以抽象出一种 OpenSergoClientManager,用来自动创建和复用托管 client。如果用户不需要复用连接,则仍可以自主创建 OpenSergoClient,而不需要关注 manager。
    • getOrCreateClient(host, port) 在首次获取某个 endpoint 时创建并缓存对应 client,后续再取相同 endpoint 时直接返回对应的 client
    • 需要考虑 client config 怎么提供;是否支持不同 endpoint 不同 config 的场景;
    • 异常场景考虑
    • 是否需要做回收
jnan806 commented 1 year ago

@sczyh30 我对上述几点,逐点进行考虑,得出如下:

  1. OpenSergoClient 作为基础的客户端,完全由使用者创建和管理,这个是没问题的;
  2. 我们现在需要的是 OpenSergo SDK 自身有一套管理机制,能够复用同个 endpoint 下的 client 连接。这里面是不是可以抽象出一种 OpenSergoClientManager,用来自动创建和复用托管 client。如果用户不需要复用连接,则仍可以自主创建 OpenSergoClient,而不需要关注 manager。
  • getOrCreateClient(host, port) 在首次获取某个 endpoint 时创建并缓存对应 client,后续再取相同 endpoint 时直接返回对应的 client
    
    // OpenSergoClientManager 管理复用的 Client
    public class OpenSergoClientManager {
    public OpenSergoClient getOrCreateClient(host, port) {
    return getOrCreateClient(host, port, defaultOpenSergoConfig);
    }
public OpenSergoClient getOrCreateClient(host, port, openSergoConfig) {
    // TODO 
    if (首次) {
         // 首次获取某个 endpoint 时创建并缓存对应 client,
        return new OpenSergoClient(...)
    } else {
        // 后续再取相同 endpoint 时直接返回对应的 client
        return getFromCache(endpoint);
    }
}

}

// 独立的 Client 则通过 Client 的构造方法进行创建 public class OpenSergoClient { public OpenSergoClient(...) { ...... } }



> - 采用OpenSergoClientManager,需要考虑 client config 怎么提供;是否支持不同 endpoint 不同 config 的场景;

- 不同 endpoint 不同 config 的场景:实例化 OpenSergoClient 时,传入不同的 config 即可
- 相同 endpoint 不同 config 的场景:我们进行约定,以第一次实例化 OpenSergoClient 时传入的 config 为准

> - 异常场景考虑  

暂未考虑

> - 是否需要做回收

首先,用户如果创建 Client 就代表着 Client 是有用的,如果 Client 的 keep-alive 机制能够确保断线重连,那么就没必要回收了。所以此处的回收机制要结合 Client 的 keep-avlie 断线重连机制 进行考虑
sczyh30 commented 1 year ago

@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?

jnan806 commented 1 year ago

@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?

这种场景不正是我们希望的复用的最佳场景么:joy:,如果 endpoint 存在就缓存里取client,不存在就实例化

OpenSergoClientManager 主要解决的就是 不同 endpoint场景的client 实例化,以及 相同 endpoint 相同 config 场景的 client 复用

sczyh30 commented 1 year ago

@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?

这种场景不正是我们希望的复用的最佳场景么😂,如果 endpoint 存在就缓存里取client,不存在就实例化

OpenSergoClientManager 主要解决的就是 不同 endpoint场景的client 实例化,以及 相同 endpoint 相同 config 场景的 client 复用

这里指的是 这个通用的 config 怎么提供?比如可以通过环境变量来配。

这个我们可以在 https://github.com/opensergo/opensergo-java-sdk/issues/22 里面进一步展开

jnan806 commented 1 year ago

这里指的是 这个通用的 config 怎么提供?比如可以通过环境变量来配。

配置的提供方式,我个人不建议以 OpenSergo 为主要的配置文件或者环境变量来提供。

我们仅仅是 SDK ,只需要提供接口,或者 构造方法,或是 Builder ,而配置文件或环境变量,最好是注入到框架中,由框架进行转换后调用 SDK 时提供 config。

// OpenSergoClientManager 获取 Client, config 暂时以参数形式传递,具体方式待定
public OpenSergoClient getOrCreateClient(host, port, openSergoConfig) {
  // TODO 在首次获取某个 endpoint 时创建并缓存对应 client,后续再取相同 endpoint 时直接返回对应的 client
}
// Sentinel 中的 的配置
public class SentinelOpenSergoConfig {
    public String host;
    public int port;
    ......
}
// 将 SentinelOpenSergoConfig 转为 openSergoConfig后,实例化 OpenSergoClient
public OpenSergoConfig convertToOpenSergoConfig(SentinelOpenSergoConfig config) {
    // TODO 转化逻辑
}
sentinel:
  opensergo:
    type: opensergo
    host: opensergo.svc.endpoint
    port: opensergo.svc.port
    TLS: ...

此处配置的提供方式,应该是接入框架需要考虑的问题


至于 SDK 中 OpenSergoClientManager 如何接受 config 的配置,在 #22 进行讨论。

panxiaojun233 commented 1 year ago

OpenSergoClientManager 需要保证单例,这样无论间接创建还是直接创建多份 OpenSergoClient实例,都不会造成资源的浪费。