layotto
layotto copied to clipboard
配置下发通道与配置热加载
希望提供一个通用的配置下发的grpc协议通道,用于layotto与控制面的交互(可以参考go-control-plane), 可以随意定义用户可以,随意的定义下发配置的策略内容
用途: 1.redis、kafka客户端流量管理控制,可能需要容灾切换、灰度发布策略切换 2.redis连接参数调整、安全弱密码自动切换
Cool. 个人一直期望在layotto runtime 层支持 流量治理规则、支持规则动态下发(像 envoy 那样),这样能帮助所有组件都获得强大的流量治理能力。
不过您的描述比较简单,我有一些疑问,比如:
- 是希望下发 key-value 结构的“配置”,还是类似于 xds 那样、有一定结构的“规则”呢?
- redis 的组件现在有一些failover之类的配置,这些配置项能否满足您的需求,见 https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-redis/
- 启动配置文件里,现在有一些key/value配置项(见下图),layotto 会用这些配置项去初始化组件。如果加入动态规则下发后,您希望layotto 启动后,用哪里的配置做初始化,是启动后立刻读控制面的配置、用控制面的配置覆盖掉配置文件里的配置、然后再初始化么?
所以,您能否写个设计proposal,写下“您希望这个功能被设计成什么样”,比如写下您希望 layotto 对控制面开什么样的 api 出来、api 字段有哪些。因为由别人来设计的话,很可能设计出来的字段/功能满足不了您的需求~
Cool. 个人一直期望在layotto runtime 层支持 流量治理规则、支持规则动态下发(像 envoy 那样),这样能帮助所有组件都获得强大的流量治理能力。
不过您的描述比较简单,我有一些疑问,比如:
- 是希望下发 key-value 结构的“配置”,还是类似于 xds 那样、有一定结构的“规则”呢?
- redis 的组件现在有一些failover之类的配置,这些配置项能否满足您的需求,见 https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-redis/
- 启动配置文件里,现在有一些key/value配置项(见下图),layotto 会用这些配置项去初始化组件。如果加入动态规则下发后,您希望layotto 启动后,用哪里的配置做初始化,是启动后立刻读控制面的配置、用控制面的配置覆盖掉配置文件里的配置、然后再初始化么?
![]()
所以,您能否写个设计proposal,写下“您希望这个功能被设计成什么样”,比如写下您希望 layotto 对控制面开什么样的 api 出来、api 字段有哪些。因为由别人来设计的话,很可能设计出来的字段/功能满足不了您的需求~
如果要抽象到xds级别的结构的规则,可能相对困难 比如不同的mq,协议结构都不太一样,需要设计人员对每种mq的配置属性十分熟悉,才能抽象好 所以能否先以key-value形式发布,主要是针对企业自定义的组件api功能的规则
redis这种简单的容灾切换应该能满足,但是可能会考虑的更多
通常来说,layotto本地只会有控制面地址配置,其他配置均可以替换,当然也可以从本地配置获取 优先级为:控制面>本地配置
方案 v0.1:
需求
组件能查询、订阅配置中心的 kv 配置(比如 apollo 中的配置)。
产品设计
User story
- 用户在 apollo 页面改一下 Redis 的容灾切换配置,Redis 组件就能接收到新配置,把流量切到灾备集群
编程界面
允许组件依赖 config_store
组件,通过 config_store
组件查询、订阅配置数据。
组件之间的依赖关系如图:
如果组件有这种需求,需要做以下改动:
- 组件的配置文件里,添加
import
配置项,声明希望引用的config_store
组件名:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"import": {
"config_store": {
"name": "xxx"
}
}
}
},
- 组件要实现
configstores.Setter
interface, 以便 runtime 把config_store
注入进去
type Setter interface {
SetConfigStore(store Store)
}
方案设计
- 启动时,优先初始化所有
config_store
组件、再创建其他组件 - runtime 创建组件时,如果组件配置了
import
,则检查组件有没有实现Setter
接口,实现了的话就注入对应的config_store
组件 - 调组件的 Init 接口,初始化组件
生命周期为:
其他
- 有没有密钥动态下发的需求?
@unionhu 看下这样能否满足需求~以及是否有密钥动态下发的需求?
@unionhu 看下这样能否满足需求~以及是否有密钥动态下发的需求?
好巧,今天团队内部讨论到,真好我们也有apollo,也希望config组件能在启动过程中最先加载拉取配置,然后提供给其他组件用 这个特性对我们十分重要,你们优先安排吗?
有密钥动态下发的需求,而且我们可能希望密钥能动态更换,因为存在弱密码场景,安全要求整改
建议config和secret都放在 import标签里做,大一统
建议config和secret都放在 import标签里做,大一统
ok 关于 json 配置文件的结构,这么设计如何:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"ref": [
{
"type": "component",
"subType": "config_store",
"name": "apollo"
},
{
"type": "secret",
"name": "xxx",
"key": "yyy"
}
]
}
},
@unionhu @ZLBer @zhenjunMa @JervyShi 帮忙把把关
或者用 kind 标识种类, type 标识子类
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"ref": [
{
"kind": "component",
"type": "config_store",
"name": "apollo"
},
{
"kind": "secret",
"name": "xxx",
"key": "yyy"
}
]
}
},
不过我个人觉得配置结构怎么定都行,上面这种结构可能反序列化代码写起来麻烦点。 或者这样,反序列化代码写起来简单一点:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"componentRef": [
{
"type": "config_store",
"name": "apollo"
}
],
"secretRef": [
{
"name": "xxx",
"key": "yyy"
}
]
}
}
@unionhu 这个 feature 实现起来应该很简单,你们有开发同学感兴趣认领么~
@unionhu 这个 feature 实现起来应该很简单,你们有开发同学感兴趣认领么~
感兴趣,只是我们最近忙着上线mosn以及配套设施,估计要下下周,我周末有空的时候看看应该怎么搞
默认secret是只获取一次? config是获取一次+订阅的吗?
标记一下,就叫这版是 proposal v0.2:
@ZLBer 以这个配置结构为例:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"componentRef": [
{
"type": "config_store",
"name": "apollo"
}
],
"secretRef": [
{
"name": "xxx",
"key": "yyy"
}
]
}
},
-
secretRef
代表“启动时只获取一次这个secret” -
componentRef
代表 “需要在启动时注入组件”,组件type 是 "config_store",name 是"apollo" 而且 redis 组件要实现 configstores.Setter interface, 以便 runtime 把config_store 注入进去
type Setter interface {
SetConfigStore(store Store)
}
至于是“获取一次+订阅”,还是“获取一百次、不订阅”,全看 redis 组件的实现,看写redis 组件的人怎么写了~ redis 组件的开发者可以自由使用该 config_store 组件
- 假如我们以后有新需求"redis 组件想要动态下发的密钥,密钥托管在 AWS key vault 之类的密钥存储中"(只是举个例子,aws这个服务好像不支持监听密钥变更),实现方案是支持“在 redis 组件启动时,注入 secret_store 组件”,配置结构像这样:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"componentRef": [
{
"type": "config_store",
"name": "apollo"
},
{
"type": "secret_store",
"name": "foo"
}
],
"secretRef": [
{
"name": "xxx",
"key": "yyy"
}
],
"configurationRef": [
{
"name": "apollo",
"key": "xxxxx"
}
]
}
},
@unionhu 这个 feature 实现起来应该很简单,你们有开发同学感兴趣认领么~
感兴趣,只是我们最近忙着上线mosn以及配套设施,估计要下下周,我周末有空的时候看看应该怎么搞
@unionhu 欢迎~ 可以先看下,如果不太明白的话我可以投屏和你说下改哪
redis 组件
让我理解了半天... 就相当于 state.redis 持有了componentRef的引用,框架侧只需要启动的时候注入进去?(有spring那味了) 听起来还是不自动啊,那我只想注入点配置,你给我直接注入到meta里我直接用不行吗?
还有组件启动权重? 是我们手动调整下代码顺序?
@ZLBer
redis 组件让我理解了半天... 就相当于 state.redis 持有了componentRef的引用,框架侧只需要启动的时候注入进去?(有spring那味了)
是的,就是 spring 那味 👃
听起来还是不自动啊,那我只想注入点配置,你给我直接注入到meta里我直接用不行吗?
澄清一下,我们可以把下发的配置分为两类:
- 静态配置:启动时候用,启动完了就不再改动了
- 动态配置:比如 state.redis 组件监听某个配置项
openFeatureA
(用于开关某个功能),一但有变更,立刻能够获取到,然后打开/关闭相关功能
所以这里“注入一个组件”是为了满足“动态配置”的需求; 而“注入静态配置”可以这样:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"configurationRef": [
{
"name": "apollo",
"key": "xxxxx"
}
]
}
},
还有组件启动权重? 是我们手动调整下代码顺序?
是的,调整启动顺序,先启动、初始化 config_store 组件, 再启动别的类型的组件
@seeflood 动态配置这个,那用户还是需要修改layotto的代码吗 ?
我们把框架开发好(把依赖注入的逻辑开发好),用户不用修改layotto 代码,但是用户要写自己的组件、自己订阅配置变更、做打开开关之类的功能
这只是我目前想到的方案, 不代表就一定要这样哈,有更好的方案的话都可以讨论
忽然又意识到了sdk的好处,直接写回调函数 很cool的功能,没人写可以@我
@ZLBer 感觉你说的是个问题哦,想用这个功能必须写自己的组件,有点不通用?
5.13 社区会议讨论action:
- 需要“默认订阅”功能(某个租户/组 下面会默认订阅一批 kv), @unionhu 可以详细描述下场景、建议的设计; 这个功能应该有现成的,apollo 组件可以配 namespace 等参数,按参数取默认订阅的配置
- 先按当前方案做个 alpha版,后续 @unionhu 可以基于生产需求来改 alpha 版
干脆就静态配置我们就加载一次,动态配置搞成订阅塞到meta里,但这样缺点就是没法加用户自己的逻辑。
动态配置搞成订阅塞到meta里
这个是啥意思,是说组件配置的metadata 里,配想要订阅的 key,是这样么:
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"subscriptionRef": [
{
"name": "apollo",
"key": ["switchA","switchB"]
}
]
}
},
我意思是,就只是订阅,然后把监听到的配置更新再写回meta,比较粗犷
我想了一下,大概有两种设计:
思路A. 依赖注入 config_store
就是上面讨论过的设计,把 config_store 组件依赖注入进用户自己写的组件里
"state": {
"redis": {
"metadata": {
"redisHost": "localhost:6380",
"redisPassword": ""
},
"componentRef": [
{
"type": "config_store",
"name": "apollo"
}
],
"secretRef": [
{
"name": "xxx",
"key": "yyy"
}
]
}
},
优缺点分析:
- pros
- 灵活性高,适合大公司定制自己的组件。
- 完美复用 config_store 组件现成的功能,比如,如果 apollo 组件有“默认订阅”功能,那依赖注入的 config_store 组件就自带这个功能。
- cons
- 对于中小公司用户,如果想用“动态配置下发”功能,没法当伸手党,没有社区现成的组件用,得开发自己的组件
思路B. 配置变更时,刷新 metadata
这个应该就是 @ZLBer 说的方案,我详细描述下。
比如,现在 state.redis 的启动配置有下面这些(截图取自 dapr 文档 )
现状是:redis 组件启动时,用这些配置kv做初始化;所有配置都是静态配置、只在启动时取一次,不监听后续配置变更。
但是我们可以改成:
- 这些 kv 可以从 config_store 取, 或者做成 k8s 的 CRD
- layotto 监听这些 kv 的变更,一但有变化,用最新的配置重新初始化组件
优缺点分析:
- pros
- runtime 层可以做一些通用功能,“赋能”所有组件
- 方便用户当伸手党,社区有现成的组件,支持动态配置下发
- cons
- 实现起来复杂。比如重新初始化期间,怎么保证流量无损?
- 我不清楚这能不能满足用户生产需求。@unionhu 可以帮忙看看这种能满足需求么
选择思路 A 还是 思路 B
个人觉得:
- 一开始不适合脱离生产需求、上来就做复杂的功能,我怕做出来不接地气、没人用;
- 我们先按思路A 做个 alpha版,等用户尝试在生产落地时,再按照生产需求完善功能
- 在
我反而倾向思路B,在spring boot的设计思想是类似B方案, 这样有个好处,redis等组件只关注自建的配置数据结构模型,类似微服务设计的反腐层,不会跟Apollo强绑定,后续layotto需要对接其他配置中心会变得非常方便
能否评估方案B的工作量?若耗时比较久,可以先走A方案,后续支持B方案
5.13 社区会议讨论action:
- 需要“默认订阅”功能(某个租户/组 下面会默认订阅一批 kv), @unionhu 可以详细描述下场景、建议的设计; 这个功能应该有现成的,apollo 组件可以配 namespace 等参数,按参数取默认订阅的配置
- 先按当前方案做个 alpha版,后续 @unionhu 可以基于生产需求来改 alpha 版
目前看应该是没有问题的,我们生产的时间点是希望尽量六月底,先这么走吧,我也刚刚开始介入这块的事项
思路B 的缺点除了上面说的之外,还有:
- 改一个配置项就要重新初始化组件,比如只是动态下发一个开关,就重新初始化、重新建连,太浪费资源
解决“改个开关就要重新初始化”问题,一个优化方案是:
组件可以实现增量更新接口
updateConfig( configmap map[string]string) (success bool, err error)
每次配置变更时,runtime 先尝试让组件增量更新,如果失败再重新初始化组件。
其他需要考虑的设计点有:
- runtime 开放给控制面的动态下发配置接口是啥,怎么和控制面交互?
可以开个 gRPC 接口,用来更新配置:
rpc UpdateConfig( RuntimeConfig) returns (UpdateResponse)
可以基于 configuration api 封装个模块,订阅配置变更,有配置更新的话就调 UpdateConfig 接口
- 重新初始化过程中,怎么保证流量无损
- 怎么处理 readiness 状态
- 配置优先级:有一些配置是某个 app 定制的配置,有一些配置是所有 app 公用的通用配置,两者优先级是啥
这样有个好处,redis等组件只关注自建的配置数据结构模型,类似微服务设计的反腐层,不会跟Apollo强绑定,后续layotto需要对接其他配置中心会变得非常方便
恩恩,长期来看 B 更优雅
能否评估方案B的工作量?若耗时比较久,可以先走A方案,后续支持B方案
B 要设计的点比较多,我主要担心的还是“不想脱离生产需求做设计”,所以还是想先按方案A 做个简单的alpha版,等你们试用后的反馈,有生产场景了再来一起看该怎么做
思路B 的缺点除了上面说的之外,还有:
- 改一个配置项就要重新初始化组件,比如只是动态下发一个开关,就重新初始化、重新建连,太浪费资源
解决“改个开关就要重新初始化”问题,一个优化方案是: 组件可以实现增量更新接口
updateConfig( configmap map[string]string) (success bool, err error)
每次配置变更时,runtime 先尝试让组件增量更新,如果失败再重新初始化组件。其他需要考虑的设计点有:
- runtime 开放给控制面的动态下发配置接口是啥,怎么和控制面交互?
可以开个 gRPC 接口,用来更新配置:
rpc UpdateConfig( RuntimeConfig) returns (UpdateResponse)
可以基于 configuration api 封装个模块,订阅配置变更,有配置更新的话就调 UpdateConfig 接口
- 重新初始化过程中,怎么保证流量无损
- 怎么处理 readiness 状态
- 配置优先级:有一些配置是某个 app 定制的配置,有一些配置是所有 app 公用的通用配置,两者优先级是啥
这样有个好处,redis等组件只关注自建的配置数据结构模型,类似微服务设计的反腐层,不会跟Apollo强绑定,后续layotto需要对接其他配置中心会变得非常方便
恩恩,长期来看 B 更优雅
能否评估方案B的工作量?若耗时比较久,可以先走A方案,后续支持B方案
B 要设计的点比较多,我主要担心的还是“不想脱离生产需求做设计”,所以还是想先按方案A 做个简单的alpha版,等你们试用后的反馈,有生产场景了再来一起看该怎么做
好的,我这边会投入时间去熟悉layotto,有问题会给你们反馈
动态配置下发、组件热重载 Proposal v0.3
1. 解决的问题
- 现在生产用户有一套定制的启动时初始化配置的方案:有一些配置配在 app 里,app 启动后、调 sidecar,让 sidecar 基于这些配置做初始化。方案不够通用,想做的更通用些
- 支持配置动态下发。
- 一种思路是让配置文件和镜像解耦,通过磁盘挂载进容器。比如 Dapr 的配置项放进 Configuration CRD, CRD 变更后,需要运维人员通过 k8s 滚动重启集群。
- 另一种思路是把 config_store 组件注入进别的组件 ,但有一些缺点:
- 用户如果想用“动态配置下发”功能,没法当伸手党,没有社区现成的组件用,得开发自己的组件。 最好是 runtime 层做一些通用功能,“赋能”所有组件,社区维护现成的组件、支持动态配置下发,方便用户当伸手党,开箱即用。
- 另一种思路是像 envoy 一样,把配置分两类:bootstrap 配置(静态配置)、动态配置,前者放不放进镜像都行,后者支持配置动态下发、根据配置做热重载。
2. 产品设计
User story
- 用户在 apollo 页面改一下 Redis 的容灾切换配置,Redis 组件就能接收到新配置,把流量切到灾备集群
- 已有生产用户可以把初始化流程迁移到新的模型,向下兼容。
编程界面
比如,现在 state.redis 的启动配置有下面这些(截图取自 dapr 文档 )
现状是:redis 组件启动时,用这些配置kv做初始化;所有配置都是静态配置、只在启动时取一次,不监听后续配置变更。
但是我们可以改成:
- 这些 kv 可以动态下发
- layotto 监听这些 kv 的变更,一但有变化,用最新的配置重新初始化组件
- 如果组件觉得重新初始化太小题大做了,可以实现动态更新接口
优缺点分析:
- pros
- runtime 层可以做一些通用功能,“赋能”所有组件;方便用户当伸手党,社区维护现成的组件、支持动态配置下发,用户开箱即用
- cons
- 实现起来复杂。比如重新初始化期间,怎么保证流量无损?
- 我不清楚这能不能满足用户生产需求,担心过早设计、过度设计
3. High-level design
启动完成后,暴露 UpdateConfiguration API
Sidecar 启动还是用 json 文件,启动完成、readiness check 通过后,对外暴露一个新的 API,用于做配置热变更:
rpc UpdateConfiguration( RuntimeConfig) returns (UpdateResponse)
Agent 负责和控制面交互、调用 UpdateConfiguration API
也就是说,Sidecar 只是开个接口、等别人推配置。而具体和控制面交互、订阅配置变更的事情可以封装 agent 来做,比如图上的 agent 2,负责订阅 apollo 的配置变更,有变更了就调 Sidecar 的接口,让 Sidecar 热更新。
对于已有的生产用户,可以像图上封装 agent 1, 监听 app 喂的配置、dump 配置、重启时加载配置,然后把配置推给 Sidecar。
再比如可以写个 File agent 问题,监听文件变化,有变化就读取新配置、通知 Sidecar 热重载。
agent 不一定要单独进程,在 main 里启动一个独立协程也行。
组件热重载
Sidecar 被调 UpdateConfiguration API 后,会:
- 判断组件有没有实现"增量更新"接口:
UpdateConfig(ctx context.Context, metadata map[string]string) (err error, needReload bool)
- 如果组件有实现该接口,runtime 尝试让其增量更新
- 如果增量更新失败,或者没实现该接口,则 runtime 根据全量配置重新初始化组件
- 新组件重新初始化完成后(通过 readiness check),接管原组件的流量
4. 详细设计
4.1. gRPC API 设计
service Lifecycle {
rpc UpdateConfiguration(UpdateConfigurationRequest) returns (UpdateConfigurationResponse){}
}
// Component configuration
message UpdateConfigurationRequest{
ComponentConfig component_config = 2;
}
message UpdateConfigurationResponse{
}
ComponentConfig 字段设计
a. 设计一个通用的更新接口
message ComponentConfig{
// For example, `lock`, `state`
string kind = 1;
// The component name. For example, `state_demo`
string name = 2;
map<string, string> metadata = 3;
}
~~用 google/protobuf/struct.proto 描述动态json 见 https://stackoverflow.com/questions/52966444/is-google-protobuf-struct-proto-the-best-way-to-send-dynamic-json-over-grpc~~
用 map<string, string>
传配置。
-
优点 每次新加 API 或改配置结构时, 不用改每个语言的 sdk,让用户透传、sidecar 侧反序列化
-
缺点 字段格式没有显示定义,不明确,不够结构化
b. 结构化定义每类配置
// Component configuration
message ComponentConfig{
// For example, `lock`, `state`
string kind = 1;
// The component name. For example, `state_demo`
string name = 2;
google.protobuf.Struct metadata = 3;
oneof common_config {
LockCommonConfiguration lock_config = 4;
StateCommonConfiguration state_config = 5;
// ....
}
}
优缺点和上面相反
结论
选择 A,减少 SDK 维护成本
Q: 是单独写一个 API 插件,还是放进已有的 API 插件里
单独写一个 API 插件
Q: 等人推配置 vs 主动拉配置 vs 推了之后再反拉
等人推配置
Q: API 接受全量配置还是增量配置
a. 增量,顺序问题由 stream 保证
service Lifecycle {
rpc UpdateComponentConfiguration(stream ComponentConfig) returns (UpdateResponse){}
}
b. 全量
结论: b, 更简单
4.2. 组件 API 设计
type DynamicComponent interface {
UpdateConfig(ctx context.Context, metadata map[string]string) (err error, needReload bool)
}
4.3. 热重载
// TODO
- 重新初始化过程中,怎么保证流量无损
- 配置优先级:有一些配置是某个 app 定制的配置,有一些配置是所有 app 公用的通用配置,两者优先级是啥
- 配置事务读写,避免脏读