泛化调用接入 dynamicgo 指南

泛化调用接入 dynamicgo 指南;高性能泛化调用实现

背景

Dynamicgo 提供了高效的 thrift 数据处理。Kitex 通过集成 Dynamicgo 实现了更高性能的 JSON/HTTP 泛化调用。

注意:当前仅支持用于 JSON 泛化调用和 JSON 格式的 HTTP 泛化调用(数据格式:json)

新旧方式的使用差异

用法与原始 Kitex 泛化调用基本相同,但有以下区别。

Descriptor

  • Descriptor provider

    • Thrift idl provider 在 Kitex 的原始方式中,我们有一些函数(例如 NewThriftFileProvider)来解析 idl 并返回 DescriptorProvider,其中包括 ServiceDescriptor 的通道。 在新的方式中,由于 dynamicgo 使用的 DescriptorKitex/generic 不同,我们提供了 3 个新函数来解析 idl 并返回 dynamicgo 的 ServiceDescriptor
      • NewThriftFileProviderWithDynamicGo(path string, includeDirs ...string):创建 thriftFileProvider,它从给定的路径实现 DescriptorProvider 并包含目录
      • NewThriftContentProviderWithDynamicGo(main string, includes map[string]string):创建 ThriftContentProvider 它实现了 DescriptorProvider 与动态从内容
        • 您可以 IDL 与旧方法相同的方法 UpdateIDL
      • NewThriftContentWithAbsIncludePathProviderWithDynamicGo(mainIDLPath字符串,包括map[string]string):创建 ThriftContentWithAbsIncludePathProvider 实现 DescriptorProvider(absinclude path)和从 content
        • 您可以与旧方法相同的方法更新 IDL
  • Provider option

    • GetProviderOption 是一个接口,其中包含一个 func Option() 来获取 ProviderOption。ProviderOption 有一个 bool 字段 DynamicGoEnable,它指示是否启用了 Dynamicgo。
    • 上面提到的三个 idl parse 函数返回的 provider 实现了 GetProviderOption。基本上,当用户调用三个 idl parse 函数中的一个时,DynamicGoExpected 将为 true,但如果在函数内部获取 Dynamicgo 的 provider 失败,则为 false。

Call options

  • Dynamicgo conv 选项 Dynamicgo 需要设置自己的转换选项conv. Options。json/超文本传输协议泛化调用的默认 conv.Options 如下:
DefaultJSONDynamicgoConvOpts = conv.Options{
   WriteRequireField: true,
   WriteDefaultField: true,
}

DefaultHTTPDynamicgoConvOpts = conv.Options{
   EnableHttpMapping:     true,
   EnableValueMapping:    true,
   WriteRequireField:     true,
   WriteDefaultField:     true,
   OmitHttpMappingErrors: true,
   NoBase64Binary:        true,
   UseKitexHttpEncoding:  true,
}

  如果您想使用自定义的 conv. Options,可以通过下面的选项进行设置:

  - WithCustomDynamicgoConvOpts(optsconv. Options):自定义的 json/超文本传输协议的 conv 选项

  • 仅用于 HTTP 泛化调用的选项 在 Kitex 原始超文本传输协议泛化调用(数据格式:json)中,resp body 的类型为 map[string]interface{},存储在 HTTPResponse.Body 中。然而,在带有 Dynamicgo 的超文本传输协议泛化调用(数据格式:json)中,resp body 的类型将是 json 字符串,它存储在 HTTPResponse.RawBody 。 我们提供了一个函数 UseRawBodyForHTTPResp(enablebool),以便您可以根据自己的偏好选择响应类型。使用 rawbody 将大幅提升性能,推荐使用。

Break Change

  • Thrift Exception 信息

    • JSON 泛化调用 原始泛化调用和使用 Dynamicgo 的泛化调用在 thrift 异常字段的错误信息上存在差异。原始泛化调用返回一个 map 字符串作为 thrift 异常字段的错误信息,但是使用 Dynamicgo 的泛化调用返回一个 json 字符串。例如:
      • 先前:remote or network error[remote]: map[string]interface {}{"code":400, "msg":"this is an exception"}
      • 使用 Dynamicgo:remote or network error[remote]: {"code":400,"msg":"this is an exception"}
    • HTTP generic call (TODO) HTTP 的泛化调用不支持 thrift 异常字段处理。
  • 类型转换

    • Bool <> string:在 Kitex 的原始方式中,即使 IDL 声明为 bool 类型的字段值为字符串(例如"true"),它也可以被编码,但是 Dynamicgo 在编码过程中会产生错误。

Fallback

由于当前 dynamicgo 仅支持 x86-64 环境,因此仅在以下条件下才会激活使用 Dynamicgo 的泛化调用。

  • CPU 架构:amd64 && go 版本 >=go1.16

  • JSON 泛化

    • ProviderOption.DynamicGoEnable 值为 true
    • 在服务器端:客户端使用 json 泛化调用,或者客户端不使用 json 泛化调用但传输协议不是 PurePayload。
  • HTTP 泛化

    • ProviderOption.DynamicGoEnable 值为 true
    • UseRawBodyForHTTPResp(enablebool) 已启用

如果不满足这些条件,将 fallback 到原来的泛化调用实现。

开启条件宿主机环境选项
jsonCPU 架构:amd64 && go>=1.16- ProviderOptionDynamicGoEnable 为 true
- 客户端使用 泛化调用,或者其传输协议使用的是 TTHeader、Framed、TTHeaderFramed 中的一种
httpCPU 架构:amd64 && go>=1.16- ProviderOptionDynamicGoEnable 为 true
- UseRawBodyForHTTPResp(true) 启用(可选)

JSON 泛化调用示例

完整代码

客户端使用

  • 请求

类型:JSON 字符串

  • 回应

类型:JSON 字符串

如果使用默认的 Dynamicog 选项,则不需要修改 Kitex JSON 泛化调用代码。

package main

import (
    "github.com/cloudwego/Kitex/pkg/generic"
     bgeneric "github.com/cloudwego/Kitex/client/genericclient"
)

func main() {
    // Local file idl parsing
    // YOUR_IDL_PATH: thrift file path ex.) ./idl/example.thrift
    // includeDirs: Specify the include paths, by default the relative path of the current file is used to find the include
    p, err := generic.NewThriftFileProviderWithDynamicGo("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // Constructing generic call of JSON request and response types
    g, err := generic.JSONThriftGeneric(p)
    // Set generic option for dynamicgo if needed
    //  var dopts []generic.Option
    //  dopts = append(dopts, generic.WithCustomDynamicgoConvOpts(conv.Options{your conv options}))
    //  g, err := generic.JSONThriftGeneric(p, dopts...)
    if err != nil {
        panic(err)
    }
    cli, err := bgeneric.NewClient("psm", g, opts...)
    if err != nil {
        panic(err)
    }
    // 'ExampleMethod' method name must be included in the idl definition
    resp, err := cli.GenericCall(ctx, "ExampleMethod", "{\"Msg\": \"hello\"}")
    // resp is a JSON string
}

服务端使用

  • 请求

类型:JSON 字符串

  • 回应

类型:JSON 字符串

如果使用默认的 Dynamicog 选项,则不需要修改 Kitex JSON 泛化调用代码。

package main

import (
    "github.com/cloudwego/Kitex/pkg/generic"
    bgeneric "github.com/cloudwego/Kitex/server/genericserver"
)

func main() {
    // Local file idl parsing
    // YOUR_IDL_PATH: thrift file path ex.) ./idl/example.thrift
    p, err := generic.NewThriftFileProviderWithDynamicGo("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // Constructing generic call of JSON request and response types
    g, err := generic.JSONThriftGeneric(p)
    // Set generic option for dynamicgo if needed
    //  var dopts []generic.Option
    //  dopts = append(dopts, generic.WithCustomDynamicgoConvOpts(conv.Options{your conv options}))
    //  g, err := generic.JSONThriftGeneric(p, dopts...)
    if err != nil {
        panic(err)
    }
    svc := bgeneric.NewServer(new(GenericServiceImpl), g, opts...)
    if err != nil {
        panic(err)
    }
    err := svr.Run()
    if err != nil {
        panic(err)
    }
    // resp is a JSON string
}

type GenericServiceImpl struct {
}

func (g *GenericServiceImpl) GenericCall(ctx context.Context, method string, request interface{}) (response interface{}, err error) {
        // use jsoniter or other json parse sdk to assert request
        m := request.(string)
        fmt.Printf("Recv: %v\n", m)
        return  "{\"Msg\": \"world\"}", nil
}

HTTP 通用调用(数据格式:json)示例

完整代码

客户端使用

HTTP 泛化调用仅支持客户端。

  • 请求

类型:*generic.HTTPRequest

  • 回应

类型:*generic.HTTPResponse

package main

import (
    bgeneric "github.com/cloudwego/Kitex/client/genericclient"
    "github.com/cloudwego/Kitex/pkg/generic"
)

func main() {
    // Local file idl parsing
    // YOUR_IDL_PATH: thrift file path ex.) ./idl/example.thrift
    // includeDirs: Specify the include paths, by default the relative path of the current file is used to find the include
    p, err := generic.NewThriftFileProviderWithDynamicGo("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // Set generic option for dynamicgo
    var dopts []generic.Option
    dopts = append(dopts, generic.UseRawBodyForHTTPResp(true))
    // Constructing generic call of http
    g, err := generic.HTTPThriftGeneric(p, dopts...)
    if err != nil {
        panic(err)
    }
    cli, err := bgeneric.NewClient("psm", g, opts...)
    if err != nil {
        panic(err)
    }

    // Construct the request, or get it from ginex
    body := map[string]interface{}{
                "text": "text",
                "some": map[string]interface{}{
                        "id":   1,
                        "text": "text",
                },
                "req_items_map": map[string]interface{}{
                        "1": map[string]interface{}{
                                "id":   1,
                                "text": "text",
                        },
                },
        }
    data, err := json.Marshal(body)
    if err != nil {
        panic(err)
    }
    url := "http://example.com/life/client/1/1?v_int64=1&req_items=item1,item2,itme3&cids=1,2,3&vids=1,2,3"
    req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(data))
    if err != nil {
        panic(err)
    }
    req.Header.Set("token", "1")
    customReq, err := generic.FromHTTPRequest(req) // Considering that the business may use third-party http request, you can construct your own conversion function
    // customReq *generic.HttpRequest
    // Since the method for http generic is obtained from the http request via the bam rule, just fill in the blanks

    resp, err := cli.GenericCall(ctx, "", customReq)
    realResp := resp.(*generic.HttpResponse)
    realResp.Write(w) // Write back to ResponseWriter for http gateway

     // The body will be stored in RawBody of HTTPResponse as []byte type.
    // Without using dynamicgo, the body will be stored in Body of HTTPResponse as map[string]interface{} type.
    node, err := sonic.Get(gr.RawBody, "msg")
    val, err := node.String() // get the value of "msg"
    println(val == base64.StdEncoding.EncodeToString([]byte(mockMyMsg))) // true
    _, ok := gr.Body["msg"]
    println(ok) // false
}

性能测试

以下测试结果使用多个嵌套复杂结构作为性能测试的 payload,并发控制在 100,请求总数 2000000,服务器分配 4 核 Intel(R)Xeon(R)Gold 5118CPU@2.30GHz。Go 版本为 go1.17.11,cpu 架构为 linux/amd64。

“original”是指传统的泛化调用,“dynamicgo”是指使用 dynamicgo 泛化调用,“fallback”是指不满足启用 dynamicgo 条件的泛化调用(=普通泛化调用)。

Source code

Type of generic callData sizeVersionTPSTP99TP999Server CPU AVGClient CPU AVGThroughput differences (compare to original)
json generic1Koriginal14305.0525.86ms61.17ms393.06517.370%
dynamicgo26282.0912.27ms48.45ms394.00521.83+84%
fallback14371.3125.67ms60.38ms392.70523.19+0.5%
5Koriginal4523.3276.38ms113.14ms393.06517.370%
dynamicgo13877.9222.48ms56.97ms395.30546.33+207%
fallback4528.6075.84ms111.34ms392.70523.19+0.1%
10Koriginal2595.68130.63ms190.90ms394.18523.920%
dynamicgo9180.5134.99ms83.01ms396.12555.33+254%
fallback2600.34130.81ms189.28ms393.98531.17+0.2%
http generic1Koriginal74563.405.75ms9.42ms281.821487.580%
dynamicgo113614.372.70ms5.64ms373.701015.46+52%
fallback74741.625.69ms9.45ms283.281493.95+0.2%
5Koriginal16442.0857.39ms91.49ms168.521508.090%
dynamicgo48715.665.90ms11.36ms391.271140.84+196%
fallback16457.0058.37ms90.48ms165.491509.61+0.1%
10Koriginal8002.7097.59ms149.83ms149.531524.450%
dynamicgo26857.579.47ms21.94ms394.421138.70+236%
fallback8019.3997.11ms149.50ms148.031527.77+0.2%