- 云原生
- API网关
- 微服务
- 服务网格
【免费下载链接】easegress
A Cloud Native traffic orchestration system. (CNCF Project)
导读:Easegress 的 Custom Data 特性为集群内的"任何"数据提供了一套统一、可校验、可订阅的持久化存储,供 Pipeline、过滤器等各类组件共享与复用。本文以官方文档为骨架,结合仓库源码,系统讲解CustomDataKind的类型定义、CustomData的数据约束、完整的 v2 REST API 与egctl操作命令,并深入到 etcd 存储布局、JSON Schema 校验与事务批量更新等底层实现,帮助你从使用到原理全面掌握这一开发扩展能力。
为什么需要 Custom Data
Easegress 是一个云原生流量编排系统,其内部存在大量"组件之外"的附属数据需要持久化:例如某个过滤器要保存自身的运行状态、某个 Controller 要写入业务自定义的元数据等。这些数据有两种特点:
- Schema 不固定:不同组件持久化的数据结构千差万别;
- 需要跨成员共享:集群中所有节点应当看到同一份数据。
为此,Easegress 提供了Custom Data特性:它实现了一个存储"任何数据"(any data)的通用能力,可以被其他组件直接用来做数据持久化。由于不同组件的数据结构不同,必须通过CustomDataKind(自定义数据类型)来区分彼此。整个特性由三部分构成:
- CustomDataKind:数据类型的"定义",规定了标识字段与数据校验规则;
- CustomData:实际存储的数据项,是与 Kind 关联的键值映射;
- API / egctl:对外暴露的增删改查与批量操作入口。
从源码看,存储层实现在 pkg/cluster/customdata/customdata.go,API 层路由注册在 pkg/api/customdata.go,客户端命令则位于 cmd/client 下。
CustomDataKind:自定义数据类型
CustomDataKind用于描述"某一类数据"的规范:它定义了这类数据项的标识字段(ID)以及可选的 JSON Schema 校验规则。
定义示例
下面的 YAML 定义了一个名为kind1的 CustomDataKind:
name: kind1 kind: CustomDataKind idField: name jsonSchema: type: object properties: name: type: string required: - name各字段含义如下:
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 该 Kind 的名称,也是后续数据项的"类型名",在 API 路径中作为{kind name}使用 |
kind | 是 | 固定为CustomDataKind,标识这是一份类型定义资源 |
idField | 否 | 数据项中用作唯一标识的字段名,默认值为name |
jsonSchema | 否 | 一段 JSON Schema 规范,若提供,则该 Kind 下所有数据项在写入前都会按此规则校验 |
在源码 pkg/cluster/customdata/customdata.go 中,Kind结构体的定义与此一一对应:
type Kind struct { Name string `json:"name" jsonschema:"required"` IDField string `json:"idField,omitempty"` JSONSchema dynamicobject.DynamicObject `json:"jsonSchema,omitempty"` }idField 的默认与解析逻辑
idField的取值逻辑非常直接:为空时回退为name。对应源码 GetIDField 与 dataID:
func (k *Kind) GetIDField() string { if k.IDField == "" { return "name" } return k.IDField } func (k *Kind) dataID(data Data) string { var id string if k.IDField == "" { id, _ = data["name"].(string) } else { id, _ = data[k.IDField].(string) } return id }即:数据项的 ID 就是其idField字段的值,同一 Kind 内 ID 必须唯一。测试用例 TestDataID 验证了默认name与自定义key两种场景下的 ID 提取结果。
JSON Schema 的两层校验
JSON Schema 校验发生在两个时机,且都使用gojsonschema库:
- 定义 Kind 时:
PutKind会先加载kind.JSONSchema并调用gojsonschema.NewSchema校验 Schema 本身是否合法,非法 Schema 直接拒绝创建(customdata.go)。 - 写入数据时:
PutData与BatchUpdateData都会将数据项与 Schema 比对,校验失败返回validation failed错误(customdata.go)。
对应的测试 TestPutKind 覆盖了非法 Schema、重复创建、更新不存在 Kind 等失败路径,TestPutData 则覆盖了空 ID、校验失败、校验通过等场景。
创建 Kind 的命令
官方文档给出的两种方式等价:
egctl create -f kind1.yaml egctl apply -f kind1.yaml两者都支持从文件或 stdin 读取 YAML:create对应 HTTPPOST,apply同样通过POST路径落库(详见下文"批量更新"说明)。
CustomData:数据项
CustomData本质上是一个 map:键必须是字符串,值可以是任意合法的 JSON 值;嵌套 map 的键同样必须是字符串。
在源码中,数据项被定义为dynamicobject.DynamicObject,即map[string]interface{}(customdata.go、dynamicobject.go)。dynamicobject.go 中的UnmarshalYAML还特别处理了一个细节:从 YAML 反序列化时嵌套 map 可能变成map[interface{}]interface{},标准json包无法处理,因此会递归转换为map[string]interface{}——这正是文档要求"嵌套 map 的键必须是字符串"的根本原因。
数据项必须包含 ID 字段
每个CustomData数据项必须包含其所属CustomDataKind定义的idField。以上文kind1为例,其idField为name,因此数据项必须携带name字段作为标识。
写入时若 ID 为空,PutData会直接报错data id is empty(customdata.go)。
数据项示例
以下是一个kind1类型的数据项:
name: data1 field1: 12 field2: abc field3: [1, 2, 3, 4]name: data1是 ID 字段,取值为data1;field1、field2、field3是任意业务字段,值分别为数字、字符串和数组,均为合法 JSON 值。
数据项的键对应 etcd 存储键:同一 Kind 的数据存储在以/custom-data/{kind}/为前缀的键空间下,每条数据以idField的值作为键名(见下文"存储布局")。
REST API 速查
Custom Data 的 REST API 位于 Easegress API Server 的/apis/v2前缀下(见 pkg/api/api.go 中的APIPrefixV2常量,默认端口2381,即http://{ip}:{port}/apis/v2)。路由注册在 pkg/api/customdata.go,完整端点如下。
CustomDataKind 相关 API
| 操作 | 方法 | URL | Body |
|---|---|---|---|
| 创建 CustomDataKind | POST | /apis/v2/customdatakinds | Kind 定义(YAML) |
| 更新 CustomDataKind | PUT | /apis/v2/customdatakinds | Kind 定义(YAML) |
| 查询单个 Kind 定义 | GET | /apis/v2/customdatakinds/{kind name} | - |
| 列出所有 Kind 定义 | GET | /apis/v2/customdatakinds | - |
| 删除一个 Kind | DELETE | /apis/v2/customdatakinds/{kind name} | - |
值得注意:GET /apis/v2/customdatakinds与GET /apis/v2/customdatakinds/{kind name}返回的每个 Kind 都附带len字段(该 Kind 当前的数据条数),由DataLen计算得出(customdata.go、customdata.go)。这也是egctl get customdatakind能打印出 DATA-NUM 列的来源。
CustomData 相关 API
| 操作 | 方法 | URL | Body |
|---|---|---|---|
| 创建 CustomData | POST | /apis/v2/customdata/{kind name} | 数据项定义(YAML) |
| 更新 CustomData | PUT | /apis/v2/customdata/{kind name} | 数据项定义(YAML) |
| 查询单个数据项 | GET | /apis/v2/customdata/{kind name}/{data id} | - |
| 列出某 Kind 的所有数据 | GET | /apis/v2/customdata/{kind name} | - |
| 删除单个数据项 | DELETE | /apis/v2/customdata/{kind name}/{data id} | - |
| 删除某 Kind 的所有数据 | DELETE | /apis/v2/customdata/{kind name} | - |
| 批量更新(Change Request) | POST | /apis/v2/customdata/{kind name}/items | 变更请求(YAML) |
| 批量删除(某 Kind 全部数据) | DELETE | /apis/v2/customdata/{kind name}/items | - |
创建/更新的语义细节
从 pkg/api/customdata.go 与 pkg/api/customdata.go 可以看到:
create(POST)走PutData(kind, data, false):数据已存在则报错existed;update(PUT)走PutData(kind, data, true):数据不存在则报错not found;- 创建成功后响应头
Location会携带新资源的完整路径,响应码为201 Created。
同理,Kind 的创建/更新也遵循这一"幂等区分"约定:PutKind(kind, false)不允许覆盖已存在的 Kind,PutKind(kind, true)不允许更新不存在的 Kind(customdata.go)。
批量更新:Change Request
对于需要一次性增删多条数据的场景,Custom Data 提供了批量更新接口:POST /apis/v2/customdata/{kind name}/items,请求体是一个 Change Request(YAML),结构如下:
name: kind1 kind: CustomData rebuild: false delete: [data1, data2] list: - name: data3 field1: 12 - name: data4 field1: foo语义说明:
| 字段 | 默认值 | 说明 |
|---|---|---|
name | - | 目标CustomDataKind的名称 |
kind | - | 固定为CustomData |
rebuild | false | 为true时,先删除该 Kind 下的所有既有数据项,再处理list中的数据 |
delete | - | 要删除的数据项 ID 数组;当rebuild为true时该字段被忽略 |
list | - | 要创建或更新的数据项数组(写入采用 upsert 语义,同一 ID 直接覆盖) |
底层实现中,delete与list的写操作被放进**一个 etcd 事务(STM)**内原子执行,要么全部成功、要么全部失败(customdata.go),这保证了批量操作的一致性。rebuild的全量清理则由 API 层先调用DeleteAllData完成(pkg/api/customdata.go)。
客户端同样支持以 Change Request 形式创建或应用:
egctl create -f customdata-change-request.yaml egctl apply -f customdata-change-request.yaml其中name是CustomDataKind名称,kind为CustomData。
egctl 实战操作
egctl 同时保留了新旧两套命令:老式命令(v1 风格)在源码中已标记为(Deprecated),新式命令(v2 风格,与egctl get/describe/create/apply/delete/edit统一资源模型)为推荐用法。
新版命令(推荐)
新式命令在 cmd/client/commandv2 中注册,资源定义见 cmd/client/resources/customdata.go 与 cmd/client/resources/customdatakind.go:
# Kind 操作 egctl get customdatakind # 列出所有 Kind egctl get customdatakind kind1 # 查询单个 Kind egctl describe customdatakind kind1 # 描述单个 Kind egctl delete customdatakind kind1 # 删除一个 Kind # 数据操作(customdata 支持两级参数:<kind> [<id>]) egctl get customdata kind1 # 列出 kind1 的所有数据 egctl get customdata kind1 data1 # 查询 kind1 下的 data1 egctl describe customdata kind1 data1 egctl delete customdata kind1 data1 # 删除单条数据 egctl delete customdata kind1 --all # 删除 kind1 的全部数据 # 创建 / 应用 / 编辑 egctl create -f kind1.yaml # 创建 Kind(或通过 Change Request 写数据) egctl apply -f kind1.yaml egctl edit customdata kind1 # 以批量形式编辑 kind1 的所有数据 egctl edit customdata kind1 data1 # 编辑单条数据几个值得注意的行为(均有源码依据):
get customdata <kind>默认以表格输出,每行仅展示该 Kind 的 ID 字段值;describe则打印完整字段(customdata.go)。edit不允许修改 ID 字段:编辑保存时会比较新旧数据的idField值,不一致则报错edit cannot change the <idField> of custom data(customdata.go)。get/describe customdatakind会以表格展示NAME / ID-FIELD / JSON-SCHEMA / DATA-NUM四列(customdatakind.go)。- 所有 v2 命令的 URL 均由 cmd/client/general/urls.go 统一定义,前缀为
/apis/v2。
旧版命令(已弃用)
老式命令定义在 cmd/client/command/customdata.go,Short描述中明确标注(Deprecated),但功能仍完整可用:
egctl custom-data-kind list # 列出所有 Kind egctl custom-data-kind get <kind> # 查询单个 Kind egctl custom-data-kind create -f <kind file> # 创建 Kind egctl custom-data-kind update -f <kind file> # 更新 Kind egctl custom-data-kind delete <kind> # 删除 Kind egctl custom-data list <kind> # 列出某 Kind 的数据 egctl custom-data get <kind> <id> # 查询单条数据 egctl custom-data create <kind> -f <data file> # 创建数据 egctl custom-data update <kind> -f <data file> # 更新数据 egctl custom-data batch-update <kind> -f <change-request file> # 批量更新 egctl custom-data delete <kind> <id> # 删除单条数据底层实现:存储布局、并发与监听
etcd 存储布局
Custom Data 最终落在集群的 etcd 中,键空间由 pkg/cluster/layout.go 定义:
customDataKindPrefix = "/custom-data-kinds/" customDataPrefix = "/custom-data/"- Kind 定义存储键:
/custom-data-kinds/{kind name}; - 数据项存储键:
/custom-data/{kind name}/{data id}。
API Server 初始化时通过Layout()获取这两个前缀并构造 Store(pkg/api/server.go),后续所有读写都基于cluster.Cluster接口完成(GetRaw / Put / Delete / STM 等)。
并发安全与一致性
- 单条写:
PutData的"已存在/不存在"检查基于先读后写;批量写则通过cluster.STM(etcd 事务)包裹,确保delete与list原子生效。 - 删除 Kind 的级联行为:
DeleteKind会先调用DeleteAllData清空该 Kind 下所有数据,再删除 Kind 定义本身,避免产生悬挂数据(customdata.go)。 DELETE /apis/v2/customdatakinds会一次删除全部 Kind 及其数据(DeleteAllKinds),属于高危操作。
数据变更监听:Watch
除增删改查外,Store 还提供了Watch(ctx, kind, onChange)方法,用于持续监听某个 Kind 的数据变更:它通过集群 Syncer 拉取该 Kind 前缀下的全部键值,每次变化都会以完整数据切片回调onChange(customdata.go)。这为"数据变更驱动业务逻辑"(例如配置下发、规则同步)提供了标准机制。
实践建议与限制
- 先定义 Kind,再写数据:
PutData会先检查 Kind 是否存在,不存在直接返回kind %s not found; - 善用
jsonSchema做数据守门:它在 Kind 创建与数据写入两个阶段都会校验,是保证数据质量的第一道防线; - 大批量更新优先走 Change Request:借助 etcd STM 保证原子性,避免逐条
POST的非事务风险; rebuild: true会清空整个 Kind:适用于"整体替换"场景,但请确认不会误删存量数据;- ID 一经写入不可通过
edit修改:如需变更 ID,请走"删除 + 新建"流程。
说明:本文 API 与命令均基于当前仓库源码(pkg/api/customdata.go、pkg/cluster/customdata/customdata.go、cmd/client),默认 API Server 地址为
http://{ip}:2381,实际以你的 Easegress 集群配置为准。
- 云原生
- API网关
- 微服务
- 服务网格
【免费下载链接】easegress
A Cloud Native traffic orchestration system. (CNCF Project)
相关推荐
iOS Expanding Collection与Core Data集成:数据持久化的终极实践指南
iOS Expanding Collection与Core Data集成:数据持久化的终极实践指南 Expanding Collection是一个强大的iOS动
移动开发UI组件MDS 2.1 快速入门:如何在10分钟内理解这个革命性的移动出行数据规范
MDS 2.1 快速入门:如何在10分钟内理解这个革命性的移动出行数据规范 想要快速掌握全球移动出行数据标准MDS 2.1吗?🚀 这篇终极指南将带你了解这个改
如何永久保存微信聊天记忆:WeChatMsg开源工具终极指南
如何永久保存微信聊天记忆:WeChatMsg开源工具终极指南 你是否曾担心珍贵的微信聊天记录会随着手机更换而永久消失?在数字时代,微信对话不仅是简单的文字交流,
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考