opensergo-java-sdk
opensergo-java-sdk copied to clipboard
[DISCUSSION] Best practice of shared/individual client between different integrations in the same process | 同个应用进程多个框架对接 OpenSergo 时 client 的最佳实践
我们的微服务通常会使用好几种框架,比如 RPC 使用的 Spring Cloud Alibaba,流量治理使用的 Sentinel,数据库中间件使用的 ShardingSphere。在这些框架都对接了 OpenSergo 的情况下,在接入到 OpenSergo 控制面,对 OpenSergo client 的使用会有几种方式:
- 每个框架对接 OpenSergo SDK 的时候,分别创建各自的 client 来接入控制面,各自监听自己需要的配置。这样做,不同传输链路不会相互影响;弊端就是同个进程多条长连接,不利于控制面连接管理,以及整体的性能。
- 不同框架对接 OpenSergo 时 share 同个 OpenSergo client,也即 share 同个长连接(正常情况下连接的控制面都是一个)。不同框架只需要往同个 client 注册不同 subscriber 即可。这样的好处是只需要一个长连接,可以比较好地进行复用。
目前的实现未预留 shared 的设计,社区可以一起讨论下这几种情况的最佳实践,以及 shared client 的设计。
关于 client 模型 的设计,需要关注以下几点: 1、shared 模型下,同一应用不同框架所引入的 OpenSergo SDK 对接的是否为同一个 OpenSergo Control Plane。 2、用户是否希望使用 share 模型
从这几点出发,share 模型 大致需要如下功能:
1、实例化 OpenSergoClient 实例的时候,需要辨别所要实例化的 OpenSergoClient 是否已存在(根据实例化参数判断),如果存在,则进行复用,不存在则进行实例化。
2、添加 client 模型
的可选配置项,供用户自行选择是否启用 share 模型。(资源开销角度我们建议 shared 模型,若用户有其他考量可不使用shared 模型)。
因此,得到初步设计如下: 1、多个 OpenSergoClient 实例之间需要互相隔离,需要管理好 OpenSergoClient 之间的资源问题。确定每个 OpenSergoClient 自己独有的属性对象有哪些:
- gRpc 通讯的长连接
- OpenSergoClientStauts: OpenSergoClient 当前的运行状态
- SubscribeDataCache:
OpenSergoClient
向OpenSergo Control Plane
订阅资源在本地的缓存 - SubscribeRegistry :
OpenSergoClient
需要向OpenSergo Control Plane
订阅资源的Subscriber
的本地缓存 - ConfigKindMetadataRegistry: OpenSergo 定义的 ConfigKind 元数据与gRpc对象的映射关系的缓存。(这一属性是否需要
OpenSergoClient
独自管理,需要社区进行讨论。)
2、在 OpenSergo SDK 定义一个 OpenSergoOptions 的类, 这个类里包含整个SDK生命周期所需要的配置(例如 client 模型
,Logger 日志输出
,version 管理机制
等,)。OpenSergoOptions 可作为一个扩展点,后续如果有新的配置项,均可在其中定义
- OpenSergoOptions 必选项 在 SDK 中应当设置默认值
- 应用程序可以设置全局 OpenSergoOptions, 应用程序设置的 OpenSergoOptions 覆盖 SDK 的原有默认值
- OpenSergoClient 实例化方法的参数,在原有的host和port参数基础上,新增一个 OpenSergoOptions。不同框架在进行调用实例化方法时,可指定 OpenSergoOptions 属性。
- OpenSergoClient 实例化方法中的第一步,先对
应用程序设置的全局 OpenSergoOptions
和OpenSergoClient实例化参数中的 OpenSergoOptions
进行合并。(如果未指定则使用默认值,如果应有程序
和框架
都指定了,优先级需要社区进行讨论后再做决定,推荐“就近原则”,以框架的在实例化参数中指定的为准) - 合并完成之后,再根据 OpenSergoOptions 进行 OpenSergoClient的实例化步骤。

伪代码如下:
// 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 的管理
}
是否可以考虑:保持 OpenSergoClient 的接口不变,内部实现增加两种客户端:一个连接原生客户端 GrpcClient,一个是资源客户端 ResourceClient。连接客户端可以支持多个资源客户端,也可以只支持一个资源客户端。 创建时,不同的控制面,可以通过注册的方式来创建资源客户端,可以达到复用连接客户端的目的。
你说的资源客户端,我理解的是在 OpenSergoClient 内部维护一个类似于缓存的结构,将不同数据面的订阅信息( SubscribeTarget 与 ConfigSubscriber )按数据面分组保存,与控制面交互的时候,将订阅信息遍历出来,但还是沿用原先的订阅通道进行交互,是这样吗?
对差不多这个意思,这样可以保持 OpenSergoClient 本身的易用性,将复杂性隐藏到实现内部。 从内部结构上看,确实存在 资源端与连接端两种类型,资源端依赖连接端获取数据。 OpenSergoClient 负责将数据以统一的方式提供给使用方昂。
嗯。我们 的 SDK 目前在 OpenSergoClient 内部本身已经维护了一个 SubscribeTarget 与 ConfigSubscriber 的缓存,在与 控制面交互的时候,也确实是 将其逐个来订阅的。只是这个模式其实只要做个简单改进:把 OpenSergoClient 做成单例 即 shared模式
,就可以简单实现 单 OpenSergoClient 的复用了,但这样又会产生另一个问题,尽管shared模式
性能表现良好,但并不是所有接入方都希望使用shared模式
的 OpenSergoClient。
因此我们现在考虑的是:
-
OpenSergoClient 模式多样化
。如果同个应用中的不同接入方有自己的需求,用户不希望与某个接入方与其他接入方共用一个 OpenSergoClient 时,怎么去实现。因此我们后续的规划是,默认采用并且也推荐用户使用shared
模式,同时也提供individual
模式。 -
OpenSergoClient 模式
引出的如何提高 OpenSergoClient 扩展性以及如何增强版本兼容
。客户端模式只是目前发现的其中的一个可配置项,后续OpenSergo继续推进,版本升级时会有更多功能点的可配置项。如何使 SDK 提供方与使用方都能够顺滑的升级,也是我们考虑的点。SDK 提供方,只需要增加 SDK 配置项以及相关逻辑代码,尽量少的修改 OpenSergoClient 暴露出去的接口或者方法。SDK 使用方 升级时只需要 添加相关配置即可。
至于你提到的思路中,保留 OpenSergoClient 的接口不变,在 OpenSergoClient 中维护多个资源客户端,本质上也是将 OpenSergoClient 做成单例进行复用。而维护多个资源客户端应该是不必要的,因为对于 控制面来说,数据变动只会对 OpenSergoClient 发送一次数据,而数据面对于数据的处理是通过 ConfigSubscriber 来进行的(也就是说,不同资源客户端只需要 向 OpenSergoClient 添加不同的 ConfigSubscriber即可)。
同时你的方案已经是默认了 OpenSergoClient 是shared模式
,无法满足特殊情况下用户不希望采用单例 OpenSergoClient 与其他接入方共用的需求。
以上是我的理解 😃 ,如果我阐述的哪里不合适或者有其他更好的建议,欢迎详细展开,我们继续讨论哟 ~
OpenSergoClient 本身不做成单例,用户依然可以根据场景和需要创建多个 OpenSergoClient,满足特殊场景的用户需要。 OpenSergoClient 本身没有 shared 模式和 individual 模式,新的设计会按需自动共享连接,来处理不连接下的不同类型的资源更新,某种意义上这确实是 shared 模式。不过,这个 shared 是由用户的定义情况产生的,比如针对不同的资源,使用了相同的连接客户端,那么这就是 shared。 本质上,既然是相同的连接ip和 port,使用共享是合理的,极端情况下,确实不想共享,可以自行创建新的 OpenSergoClient 客户端。
Some of my ideas, just for reference:
- OpenSergoClient 作为基础的客户端,完全由使用者创建和管理,这个是没问题的;
- 我们现在需要的是 OpenSergo SDK 自身有一套管理机制,能够复用同个 endpoint 下的 client 连接。这里面是不是可以抽象出一种
OpenSergoClientManager
,用来自动创建和复用托管 client。如果用户不需要复用连接,则仍可以自主创建 OpenSergoClient,而不需要关注 manager。
-
getOrCreateClient(host, port)
在首次获取某个 endpoint 时创建并缓存对应 client,后续再取相同 endpoint 时直接返回对应的 client - 需要考虑 client config 怎么提供;是否支持不同 endpoint 不同 config 的场景;
- 异常场景考虑
- 是否需要做回收
@sczyh30 我对上述几点,逐点进行考虑,得出如下:
- OpenSergoClient 作为基础的客户端,完全由使用者创建和管理,这个是没问题的;
- 我们现在需要的是 OpenSergo SDK 自身有一套管理机制,能够复用同个 endpoint 下的 client 连接。这里面是不是可以抽象出一种 OpenSergoClientManager,用来自动创建和复用托管 client。如果用户不需要复用连接,则仍可以自主创建 OpenSergoClient,而不需要关注 manager。
- 框架作为直接接入OpenSergo的用户,可以 OpenSergoClientManager 和 OpenSergoClient 构造两种方式进行实例化,从而达到 复用与独立使用的目的。
- 应用作为间接使用OpenSergo的用户,则需要依托于框架来进行 OpenSergoClient 的管理,如果应用方想要 复用或者独立使用 Client ,则需要框架是否提供接口或者配置进行支持。
- 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 断线重连机制 进行考虑
@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?
@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?
这种场景不正是我们希望的复用的最佳场景么:joy:,如果 endpoint 存在就缓存里取client,不存在就实例化
OpenSergoClientManager 主要解决的就是 不同 endpoint
场景的client 实例化,以及 相同 endpoint 相同 config
场景的 client 复用
@jnan806 相同 endpoint 同个 config 的场景,用户在 OpenSergoClientManager 里面怎么提供?
这种场景不正是我们希望的复用的最佳场景么😂,如果 endpoint 存在就缓存里取client,不存在就实例化
OpenSergoClientManager 主要解决的就是
不同 endpoint
场景的client 实例化,以及相同 endpoint 相同 config
场景的 client 复用
这里指的是 这个通用的 config 怎么提供?比如可以通过环境变量来配。
这个我们可以在 https://github.com/opensergo/opensergo-java-sdk/issues/22 里面进一步展开
这里指的是 这个通用的 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 进行讨论。
OpenSergoClientManager
需要保证单例,这样无论间接创建还是直接创建多份 OpenSergoClient
实例,都不会造成资源的浪费。