From 1d4d16ca1881fd87d9a3a73b72f7f5b982ebfdc0 Mon Sep 17 00:00:00 2001 From: alice <90381261+alice-yyds@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:18:48 +0800 Subject: [PATCH 1/4] =?UTF-8?q?doc:=20upload=20file=20=E6=8C=89=E9=9C=80?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E4=BD=BF=E7=94=A8=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tutorials/advanced-feature/fieldmask.md | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md diff --git a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md new file mode 100644 index 0000000000..098fc84147 --- /dev/null +++ b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md @@ -0,0 +1,350 @@ +--- +title: "按需序列化使用指南" +date: 2024-09-20 +weight: 1 +keywords: ["按需序列化使用指南"] +description: "" +--- + +## 什么是 Thrift FieldMask? + +FieldMask 是受到 [Protobuf](https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask) 的启发,用于在 RPC 调用时**指示用户关心的数据并过滤掉无用的数据**的一种手段。该技术不但可以在 RPC 服务中实现**特定字段的屏蔽**,同时还可以**减少消息传输开销**以提升服务性能,目前已广泛应用于 Protobuf[服务](https://netflixtechblog.com/practical-api-design-at-netflix-part-1-using-protobuf-fieldmask-35cfdc606518)中。 + +对于 thrift RPC 服务来说,有如下潜在使用场景: + +- 下发字段管控。如 隐私合规,请求打包 等业务 +- 减少公共结构体中带来的冗余字段传输。如多个业务方调用同一个 A 服务由 统一公共仓库 生成的 kitex client + +## 使用方式 + +### 更新生成代码 + +首先,您必须使用两个选项生成此功能的代码 `with_fieldmask` 和 `with_reflection` + +``` +$ kitex -thrift with_field_mask -thrift with_reflection ${your_idl} +``` + +### **构建 FieldMask** + +要构建字段掩码,您需要两件事: + +- ThriftPath - 用于描述您想要(或屏蔽)的字段 +- 类型描述符 TypeDescriptor - 用于验证您传递的 Thrift 路径是否与消息定义(IDL)兼容 + +#### ThriftPath + +表示 Thrift 消息的任意**一个端点**位置的路径字符串。它用于从 Thrift 根消息出发中定位数据,**自顶向下**定义。例如,有如下的 Thrift 消息: + +```go +struct Example { + 1: string Foo, + 2: i64 Bar: Example Self +} +``` + +ThriftPath `$.Foo` 表示 Example.Foo 的字符串值,`$.Self.Bar` 表示第二层 i64 值 Example.Self.Bar。Thrift 路径还支持在所有四种嵌套类型(LIST/SET/MAP/STRUCT)类型的对象中定位元素(见下文),而不仅仅是 STRUCT。实际构建 `NewFieldmask` 需要**多个 thrift path 进行组合**。 + +##### 详细**语法** + +这是基本假设: + +- `fieldname` 是结构体中字段的字段名,它**必须只**包含'[a-zA-Z]'字母,整数和字符'_'。 +- `index` 是列表或集合中元素的索引,它**必须只**包含整数。 +- `key` 是映射中元素的字符串类型键。它可以包含任何字母,但**必须**是带引号的字符串。 +- `id` 是映射中元素的整型键,它**必须只**包含整数。 +- 除 `key` 中之外,ThriftPath 任何位置都不能含有空白(\n\r\b\t)字符 + +这是详细的语法: + +#### **类型描述符** + +类型描述符是消息定义的运行时表示,定位与[Protobuf 描述符](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto)一致。详见 [Thriftgo 反射使用文档](https://www.cloudwego.io/zh/docs/kitex/tutorials/code-gen/thrift-reflection/) + +要获得类型描述符,您必须先启用 thrift 反射功能,该功能是在 thriftgo [v0.3.0](https://github.com/cloudwego/thriftgo/pull/83) 中引入的。您可以使用选项 `with_reflection` 为此功能生成相关代码。生成成功后,每个 STRUCT 类型都将带上 GetTypeDescriptor**() **这个函数。 + +#### 示例 + +下面,我们以[一个 IDL](https://github.com/cloudwego/kitex-tests/blob/feat/fieldmask_test/idl/fieldmask.thrift) 为例,展示 fieldmask 的使用方式。具体代码详见 [main_test. go](https://github.com/cloudwego/kitex-tests/blob/feat/fieldmask_test/thriftrpc/fieldmask/main_test.go)。 + +```thrift +namespace go fieldmask + +struct BizRequest { + 1: string A + 2: required string B + 3: optional binary RespMask //这里用于传递fieldmask +} + +struct BizResponse { + 1: string A + 2: required string B + 3: string C +} + +service BizService { + BizResponse BizMethod1(1: BizRequest req) +} +``` + +您可以在应用程序的**初始化**阶段创建一个字段掩码(建议),或者**每次在 返回响应/发出请求 之前**在 bizhandler 中创建一个字段掩码。 + +```go +import ( + "sync" + "github.com/cloudwego/thriftgo/fieldmask" +) + +var fieldmaskCache sync.Map + +// initialize request and response fieldmasks and cache them +func init() { + // construct a fieldmask with TypeDescriptor and thrift pathes + respMask, err := fieldmask.NewFieldMask( + (*fieldmask0.BizResponse)(nil).GetTypeDescriptor(), + "$.A") + if err != nil { + panic(err) + } + fmCache.Store("BizResponse", respMask) + + reqMask, err := fieldmask.NewFieldMask( + (*fieldmask0.BizRequest)(nil).GetTypeDescriptor(), + "$.B", "$.RespMask") + if err != nil { + panic(err) + } + fmCache.Store("BizRequest", reqMask) +} +``` + +(OPTION)如果你想将你的 fieldmask 设置为黑名单模式(什么是黑名单见【可见性】),可以在创建时开启选项 `[Options.BlackListMode](https://github.com/cloudwego/thriftgo/blob/main/fieldmask/mask.go#L111)`: + +```go +// black list mod + respMaskBlack, err := fieldmask.Options{BlackListMode: true}.NewFieldMask((*fieldmask0.BizResponse)(nil).GetTypeDescriptor(), "$.A", "$.B") + if err != nil { + panic(err) + } + fmCache.Store("BizResponse-Black", respMaskBlack) + reqMaskBlack, err := fieldmask.Options{BlackListMode: true}.NewFieldMask((*fieldmask0.BizRequest)(nil).GetTypeDescriptor(), "$.A") + if err != nil { + panic(err) + } + fmCache.Store("BizRequest-Black", reqMaskBlack) +``` + +### 设置 FieldMask 到序列化对象(当前进程使用) + +现在你可以在你的**请求或响应对象**上使用生成的 API `Set_FieldMask` 设置字段掩码。然后 kitex 本身会注意到字段掩码,并在请求/响应的序列化过程中使用它,无论是客户端还是服务器端。 + +- 服务端 + +```go +type BizResponse struct { + A string `thrift:"A,1" frugal:"1,default,string" json:"A"` + B string `thrift:"B,2,required" frugal:"2,required,string" json:"B"` + C string `thrift:"C,3" frugal:"3,default,string" json:"C"` + _fieldmask *fieldmask.FieldMask +} + +func (s *BizServiceImpl) BizMethod1(ctx context.Context, req *biz.BizRequest) (resp *biz.BizResponse, err error) { + resp := biz.NewBizResponse() // resp = biz.NewBizResponse + resp.A = "A" + resp.B = "B" + resp.C = "C" + // try set resp's fieldmask + respMask, ok := fmCache.Load("BizResponse") + if ok { + resp.Set_FieldMask(respMask.(*fieldmask.FieldMask)) + } + return resp, nil +} +``` + +- 客户端 + +```go +req := biz.NewBizRequest() +req.A = "A" +req.B = "B" +// try set request's fieldmask +reqMask, ok := fmCache.Load("BizRequest") +if ok { + req.Set_FieldMask(reqMask.(*fieldmask.FieldMask)) +} +resp, err := cli.BizMethod1(context.Background(), req) +``` + +### **传递 FieldMask(对端使用)** + +如果你想要通知对端服务屏蔽特定字段,目前您可以在你的请求定义中**添加一个二进制字段**来携带字段掩码,并将您正在使用的字段掩码**显式序列化**到该字段中。我们提供了两个封装的 API 用于序列化/反序列化: + +- [thriftgo/fieldmask. Marshal/Unmarshal](https://github.com/cloudwego/thriftgo/blob/9e8d1cafba62a37789c431270a816ad35a6c46e0/fieldmask/serdes.go):包函数,将 Fieldmask 序列化/反序列化二进制字节。**我们建议您使用这个 API 而不是下面一个,因为由于使用缓存,它要快得多——除非您的应用程序缺少内存或者需要频繁更新 fieldmask**。 +- [字段掩码. MarshalJSON/UnmarshalJSON](https://github.com/cloudwego/thriftgo/blob/9e8d1cafba62a37789c431270a816ad35a6c46e0/fieldmask/serdes.go):FieldMask 对象方法,将字段掩码序列化/反序列化为/从 JSON 字节(不推荐) + +例如,我们可以将响应的字段掩码作为请求的**一个 binary 类型字段**进行传递,例如: + +- 客户端设置&传递 + +```go +type BizRequest struct { + A string `thrift:"A,1" frugal:"1,default,string" json:"A"` + B string `thrift:"B,2,required" frugal:"2,required,string" json:"B"` + RespMask []byte `thrift:"RespMask,3,optional" frugal:"3,optional,binary" json:"RespMask,omitempty"` +} + +func TestClient() { + req := fieldmask0.NewBizRequest() + req.A = "A" + req.B = "B" + + // try get reponse's fieldmask + respMask, ok := fmCache.Load("BizResponse") + if ok { + // serialize the respMask + fm, err := fieldmask.Marshal(respMask.(*fieldmask.FieldMask)) + if err != nil { + t.Fatal(err) + } + // let request carry fm + req.RespMask = fm + } + + resp, err := cli.BizMethod1(context.Background(), req) +} +``` + +- 服务器端接受&处理 + +```go +// BizMethod1 implements the BizServiceImpl interface. +func (s *BizServiceImpl) BizMethod1(ctx context.Context, req *fieldmask0.BizRequest) (resp *fieldmask0.BizResponse, err error) { + resp = fieldmask0.NewBizResponse() + resp.A = "A" + resp.B = "B" + + // check if request carries a fieldmask + if req.RespMask != nil { + println("got fm", string(req.RespMask)) + fm, err := fieldmask.Unmarshal(req.RespMask) + if err != nil { + return nil, err + } + // set fieldmask for response + resp.Set_FieldMask(fm) + } + + return +} +``` + +### 最终效果 + +一旦您为请求/响应设置了 fieldmask,另一方将仅接收到 fieldmask 设置的非必需字段的真实值,或者 fieldmask 未屏蔽的必需字段的零值。业务预期效果如下 + +- 客户端 + +```go +if resp.A == "" { // resp.A in mask + t.Fatal() +} +if resp.B == "" { // resp.B not in mask, but it's required, so still written + t.Fail() +} +if resp.C != "" { // resp.C not in mask + t.Fail() +} +``` + +- 服务器端 + +```go +if req.A != "" { // req.A not in mask + return nil, errors.New("request must mask BizRequest.A!") +} +if req.B == "" { // req.B in mask + return nil, errors.New("request must not mask BizRequest.B!") +} +``` + +### 查看 FieldMask(Reflection of FieldMask) + +如果业务有需要检查对端传入的 FieldMask 并实现特定业务逻辑,我们提供了一下 API: + +- 类型:使用 [fieldmask.Type()](https://pkg.go.dev/github.com/cloudwego/thriftgo@v0.3.12/fieldmask#FieldMask.Type) 获取当前 fieldmask 类型 +- 定位:使用 [fieldmask.GetPath()](https://pkg.go.dev/github.com/cloudwego/thriftgo@v0.3.12/fieldmask#FieldMask.GetPath) 定位到**某个 thrift path 下的**节点 +- 遍历:使用 [fiedmask.ForEachChild()](https://pkg.go.dev/github.com/cloudwego/thriftgo@v0.3.12/fieldmask#FieldMask.ForEachChild) 遍历**当前 fieldmask 的** **下一层** 节点 + +详细使用见[ mask_test.go](https://github.com/cloudwego/thriftgo/blob/main/fieldmask/mask_test.go#L23) + +## 注意事项 + +### 可见性(黑名单 or 白名单) + +**FieldMask 默认为白名单 :**掩码中的一个字段表示“**通过**”(**将被** 序列化/反序列化),不在掩码中的字段表示“**过滤**”(**不会被**序列化/反序列化)。 + +但是也允许用户将 FieldMask **设置为黑名单,**需要在创建时开启选项 `Options.BlackListMode`。此时掩码中的一个字段表示“**过滤**”(**不将被** 序列化/反序列化),不在掩码中的字段表示“**通过**”(**会被**序列化/反序列化)。具体如何使用详见 [main_test. go](https://github.com/cloudwego/kitex-tests/blob/feat/fieldmask_test/thriftrpc/fieldmask/main_test.go)。 + +### **实现约定** + +- **空的 fieldmask 表示“全部通过”**(无论黑名单还是白名单) +- 对于既不是 string 也不是 int 类型建的 map,**只允许'*'标记作为键** +- 安全起见,Required 字段可以不在掩码中(过滤),但是它们序列化时**仍将被写入当前值**。 + + - Tips:如果想不在掩码中的 required 字段写成零值,可以开启选项 -`thrift field_mask_zero_required` 并重新生成代码。需要注意的是,STRUCT 类型也写零值(写入一个 FieldStop(0))——这意味着, 如果该 STRUCT 中含有 required 字段会可能会引起对端报错 +- FieldMask 设置**必须从请求/响应根对象开始(IDL 中定义的 method 的 respXX/requstXX 结构体),并对整个对象生效** + + - Tips:如果您想从某一非根对象设置 FieldMask 并生效,需要**添加** -`thrift field_mask_halfway` **选项并重新生成代码**。但是这样会有一个潜在风险:如果不同父对象引用了同一个子对象,并且这两个父对象分别设置了不同的 fieldmask,那只有其中一个父对象相对于这个子对象的 fieldmask 会生效。 + +### 性能 + +- 构建 fieldmask 的开销很高。但是考虑到大部分业务需求中不会频繁更新 fieldmask,建议用户采用上述示例中的 `init()+sync.Map` 方式构建和获取。同样,传输 fieldmask 也有一定开销,因此建议使用 thriftgo 提供的 fieldmask.Marshal()/Unmarshal() 进行(见【**传递 FieldMask**】) +- 序列化性能:主要取决于**过滤数据的比例,过滤比例越大最终服务序列化性能越好**。参见[baseline_test. go](https://github.com/cloudwego/kitex-tests/blob/feat/fieldmask_test/codegen/fieldmask/baseline_test.go) + +``` +goos: darwin +goarch: amd64 +cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz +BenchmarkFastWriteNesting/full-16 4505 ns/op 0 B/op 0 allocs/op +BenchmarkFastWriteNesting/half-16 2121 ns/op 0 B/op 0 allocs/op +BenchmarkFastReadNesting/full-16 13864 ns/op 11874 B/op 173 allocs/op +BenchmarkFastReadNesting/half-16 7938 ns/op 5273 B/op 77 allocs/op +``` + +案例解释: + +- Nesting:两层字段结构体,数据大小 6455B +- FastWrite:序列化测试 +- FastRead:反序列化测试 +- full:开启 with_field_mask 选项生成,但不使用字段掩码 +- half:开启 with_field_mask 选项生成,并使用字段掩码来过滤一半的数据 + +## 获取代码 + +这个功能目前正在开发中,如果你想尝试,可以**获取分支代码,**并**安装相应的二进制工具**。 + +- ThriftGo:版本应 >= v0.3.12 + + - 命令行工具:用 thriftgo -version 确认;如版本较低,可手动安装最新版: + + ```go + go install github.com/cloudwego/thriftgo@latest + ``` + - go.mod: 使用 go get 更新项目依赖 + + ```bash + go get github.com/cloudwego/thriftgo@latest + ``` +- Kitex 命令行:版本应 >= v0.10.0 + + - 命令行工具 + + ```go + go install github.com/cloudwego/kitex/tool/cmd/kitex@latest + ``` + - 框架 + + ```go + go get github.com/cloudwego/kitex@latest + ``` From 1f25c14c069d471ab2d880560ab290b42edc8713 Mon Sep 17 00:00:00 2001 From: alice <90381261+alice-yyds@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:22:33 +0800 Subject: [PATCH 2/4] =?UTF-8?q?doc:=20upload=20file=20=E6=8C=89=E9=9C=80?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E4=BD=BF=E7=94=A8=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md index 098fc84147..4145d5a45b 100644 --- a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md +++ b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md @@ -99,7 +99,7 @@ var fieldmaskCache sync.Map // initialize request and response fieldmasks and cache them func init() { - // construct a fieldmask with TypeDescriptor and thrift pathes + // construct a fieldmask with TypeDescriptor and thrift paths respMask, err := fieldmask.NewFieldMask( (*fieldmask0.BizResponse)(nil).GetTypeDescriptor(), "$.A") @@ -199,7 +199,7 @@ func TestClient() { req.A = "A" req.B = "B" - // try get reponse's fieldmask + // try get response's fieldmask respMask, ok := fmCache.Load("BizResponse") if ok { // serialize the respMask From 7d2ffa63920f79fcc92e3bcdc13af6d933523ecc Mon Sep 17 00:00:00 2001 From: Yi Duan Date: Fri, 20 Sep 2024 16:09:19 +0800 Subject: [PATCH 3/4] Update fieldmask.md --- .../docs/kitex/Tutorials/advanced-feature/fieldmask.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md index 4145d5a45b..9bc1542614 100644 --- a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md +++ b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md @@ -56,6 +56,16 @@ ThriftPath `$.Foo` 表示 Example.Foo 的字符串值,`$.Self.Bar` 表示第 - 除 `key` 中之外,ThriftPath 任何位置都不能含有空白(\n\r\b\t)字符 这是详细的语法: + +ThriftPath | Description +-- | -- +$ | 根对象,每个路径都必须以它开头。 +.`fieldname` | 获取结构体中的 fieldname 子字段。例如,$. Fielda.ChildrenB。如果 fieldname 是路径终点且改字段为复杂类型(map/list/struct),则表示获取该字段下的所有子字段。 +[`index`,`index`...] | 获取 List 中的特定 index 的元素。索引必须是整数。例如:$. FieldList[1,3,4]。注意:可以编写超出实际列表大小的索引,但是实际无用。 +{"`key`","`key`"...} | 获取字符串类型键的映射中的特定 key 的值。例如:$. StrMap{""abcd"",""1234""} +{`id`,`id`...} | 获取整数类型键的映射中具有特定 id 的子字段。例如,$.IntMap{1,2} +\* | 获取所有字段/元素,即:$. StrMap{*} 表示获取映射 Root.StrMap 所有元素;$.List[*] 表示或者 List 中的所有元素。 + #### **类型描述符** From aa27ea02d263895c1fc0575bc5937d99575b1dbe Mon Sep 17 00:00:00 2001 From: Yi Duan Date: Fri, 20 Sep 2024 16:12:37 +0800 Subject: [PATCH 4/4] Update fieldmask.md --- content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md index 9bc1542614..c0840b1de2 100644 --- a/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md +++ b/content/zh/docs/kitex/Tutorials/advanced-feature/fieldmask.md @@ -56,7 +56,7 @@ ThriftPath `$.Foo` 表示 Example.Foo 的字符串值,`$.Self.Bar` 表示第 - 除 `key` 中之外,ThriftPath 任何位置都不能含有空白(\n\r\b\t)字符 这是详细的语法: - + ThriftPath | Description -- | -- $ | 根对象,每个路径都必须以它开头。 @@ -65,7 +65,6 @@ $ | 根对象,每个路径都必须以它开头。 {"`key`","`key`"...} | 获取字符串类型键的映射中的特定 key 的值。例如:$. StrMap{""abcd"",""1234""} {`id`,`id`...} | 获取整数类型键的映射中具有特定 id 的子字段。例如,$.IntMap{1,2} \* | 获取所有字段/元素,即:$. StrMap{*} 表示获取映射 Root.StrMap 所有元素;$.List[*] 表示或者 List 中的所有元素。 - #### **类型描述符**