Dynamicgo provides fast json⇆thrift conversion. Kitex has achieved higher performance generic calls by integrating dynamicgo.
Generic calls using dynamicgo are supported only for json generic calls and http generic calls (data format: json).
The usage is basically the same as the original Kitex generic call, with the following differences.
NewThriftFileProvider
) to parse idl and return DescriptorProvider
, which includes a channel of ServiceDescriptor
.
In the new way, we provide 3 functions to parse idl and return DescriptorProvider
including dynamicgo’s ServiceDescriptor
.NewThriftFileProviderWithDynamicGo(path string, includeDirs ...string)
: create thriftFileProvider which implements DescriptorProvider with dynamicgo from given path and include dirsNewThriftContentProviderWithDynamicGo(main string, includes map[string]string)
: create ThriftContentProvider which implements DescriptorProvider with dynamicgo from contentUpdateIDL
as the old wayNewThriftContentWithAbsIncludePathProviderWithDynamicGo(mainIDLPath string, includes map[string]string)
: create ThriftContentWithAbsIncludePathProvider which implements DescriptorProvider (abs include path) with dynamicgo from contentUpdateIDL
as the old wayGetProviderOption
is an interface which contains a func Option()
to get ProviderOption
. ProviderOption has a bool field DynamicGoEnabled
, which indicates whether dynamicgo is enabled or not.GetProviderOption
. Basically, when the user calls one of the three idl parse functions, DynamicGoExpected
will be true, but if getting dynamicgo’s provider fails inside the function, it will be false.conv.Options
. The default conv.Options
for json/http generic call are as follows:DefaultJSONDynamicgoConvOpts = conv.Options{
WriteRequireField: true,
WriteDefaultField: true,
}
DefaultHTTPDynamicgoConvOpts = conv.Options{
EnableHttpMapping: true,
EnableValueMapping: true,
WriteRequireField: true,
WriteDefaultField: true,
OmitHttpMappingErrors: true,
NoBase64Binary: true,
UseKitexHttpEncoding: true,
}
If you want to use your customized conv.Options
, you can set it through this option below:
- WithCustomDynamicgoConvOpts(opts conv.Options)
: customized dynamicgo conv options for json/http generic call
HTTPResponse.Body
. However, in the http generic call (data format: json) with dynamicgo, the type of resp body will be json byte slice, which is stored in HTTPResponse.RawBody
.
We provide a function UseRawBodyForHTTPResp(enable bool)
so that you can choose according to your preference of response type.Thrift exception field
remote or network error[remote]: map[string]interface {}{"code":400, "msg":"this is an exception"}
remote or network error[remote]: {"code":400,"msg":"this is an exception"}
Type conversion
String2Int64
of the conv.Options true
, but please note that an int64 value will be written as a string in decoding when String2Int64=true
.descriptor.Void{}
, but encoding with dynamicgo just accepts only descriptor.Void{}
.// GenericCall ...
func (g *GenericServiceImpl) GenericCall(ctx context.Context, method string, request interface{}) (response interface{}, err error) {
msg := request.(string)
fmt.Printf("Recv: %v\n", msg)
return resp, nil
}
[original Kitex way]
resp can be descriptor.Void{} or any string (like "Void")
[dynamicgo way]
resp can be only descriptor.Void{}
Note: Generic call using dynamicgo is only activated under the following conditions.
Encoding:
Decoding:
DynamicGoEnabled
of ProviderOption
is trueDynamicGoEnabled
of ProviderOption
is trueUseRawBodyForHTTPResp(enable bool)
is enabledIf these conditions are not met, the original generic call functions will be called.
Fallback condition | Encoding | Decoding |
---|---|---|
json | CPU architecture: !amd64 || go version < go1.16 | - DynamicGoEnabled of ProviderOption is false- Only happens on the server side. The client doesn’t use json generic call and the transport protocol is PurePayload |
http | CPU architecture: !amd64 || go version < go1.16 | DynamicGoEnabled of ProviderOption is false ||UseRawBodyForHTTPResp(enable bool) is not enabled |
Type: JSON string
Type: JSON string
You do not need to modify your Kitex json generic call code if you use the default dynamicog option.
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
}
Type: JSON string
Type: JSON string
You do not need to modify your Kitex json generic call code if you use the default dynamicog option.
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 generic call only supports client side.
Type: *generic.HTTPRequest
Type: *generic.HTTPResponse
YOUR_IDL.thrift
namespace go Kitex.example.server
struct BinaryWrapper {
1: binary msg (api.body = "msg")
2: bool got_base64 (api.body = "got_base64")
3: required i64 num (api.body = "num", api.js_conv="")
4: optional string str (api.query = "str", go.tag = "json:\"STR\"")
}
service ExampleService {
BinaryWrapper BinaryEcho(1: BinaryWrapper req) (api.get = '/BinaryEcho', api.baseurl = 'example.com')
}
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)
}
url := "http://example.com/BinaryEcho"
body := map[string]interface{}{
"msg": []byte(mockMyMsg),
"got_base64": true,
"num": "",
}
data, err := json.Marshal(body)
if err != nil {
panic(err)
}
// construct a http request
req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(data))
if err != nil {
panic(err)
}
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)
// 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
}
----------
// HTTPResponse ...
type HTTPResponse struct {
Header http.Header
StatusCode int32
RawBody []byte // this field is set only when generic.UseRawBodyForHTTPResp(true) is set
Body map[string]interface{}
GeneralBody interface{} // body of other representation, used with ContentType
ContentType MIMEType
Renderer Renderer
}
You can refer to the unit test code for more details.
The following test results use multiple nested complex structures as payloads for performance tests. The concurrency is controlled at 100, the total number of requests is 2000000, and the server is allocated 4 cores Intel (R) Xeon (R) Gold 5118 CPU @2.30GHz
. Go version is go1.17.11, and cpu architecture is linux/amd64.
As for ‘Version’, ‘original’ means the conventional generic call, ‘dynamicgo’ refers to a generic call with dynamicgo, and ‘fallback’ refers to a generic call that doesn’t meet the conditions to enable dynamicgo (= normal generic call).
Type of generic call | Data size | Version | TPS | TP99 | TP999 | Server CPU AVG | Client CPU AVG | Throughput differences (compare to original) |
---|---|---|---|---|---|---|---|---|
json generic | 1K | original | 14305.05 | 25.86ms | 61.17ms | 393.06 | 517.37 | 0% |
dynamicgo | 26282.09 | 12.27ms | 48.45ms | 394.00 | 521.83 | +84% | ||
fallback | 14371.31 | 25.67ms | 60.38ms | 392.70 | 523.19 | +0.5% | ||
5K | original | 4523.32 | 76.38ms | 113.14ms | 393.06 | 517.37 | 0% | |
dynamicgo | 13877.92 | 22.48ms | 56.97ms | 395.30 | 546.33 | +207% | ||
fallback | 4528.60 | 75.84ms | 111.34ms | 392.70 | 523.19 | +0.1% | ||
10K | original | 2595.68 | 130.63ms | 190.90ms | 394.18 | 523.92 | 0% | |
dynamicgo | 9180.51 | 34.99ms | 83.01ms | 396.12 | 555.33 | +254% | ||
fallback | 2600.34 | 130.81ms | 189.28ms | 393.98 | 531.17 | +0.2% | ||
http generic | 1K | original | 74563.40 | 5.75ms | 9.42ms | 281.82 | 1487.58 | 0% |
dynamicgo | 113614.37 | 2.70ms | 5.64ms | 373.70 | 1015.46 | +52% | ||
fallback | 74741.62 | 5.69ms | 9.45ms | 283.28 | 1493.95 | +0.2% | ||
5K | original | 16442.08 | 57.39ms | 91.49ms | 168.52 | 1508.09 | 0% | |
dynamicgo | 48715.66 | 5.90ms | 11.36ms | 391.27 | 1140.84 | +196% | ||
fallback | 16457.00 | 58.37ms | 90.48ms | 165.49 | 1509.61 | +0.1% | ||
10K | original | 8002.70 | 97.59ms | 149.83ms | 149.53 | 1524.45 | 0% | |
dynamicgo | 26857.57 | 9.47ms | 21.94ms | 394.42 | 1138.70 | +236% | ||
fallback | 8019.39 | 97.11ms | 149.50ms | 148.03 | 1527.77 | +0.2% |