Frugal
Github 项目主页: https://github.com/cloudwego/frugal
使用方法
Kitex 集成
说明:
- Server 端和 Client 端 可以独立使用 frugal;
- 传输的数据都是按标准 thrift 协议进行编码的;
- 如 Server 端开启 Frugal,需确保 Client 端指定了 Framed 或 TTHeaderFramed 协议;
- 如使用 slim 模板,必须指定 PayloadCodec 为开启 frugal;
生成脚手架
Kitex 命令行工具内建了集成 frugal 的能力。
命令行参数
Frugal Tag: -thrift frugal_tag
生成带有 frugal tag 的 Go struct,例如:
type Request struct {
Message string `thrift:"message,1" frugal:"1,default,string" json:"message"`
}
说明:
- frugal 强依赖该 tag,例如 set 和 list 在 golang 对应的类型都是 slice,需通过 tag 区分;
- 如无 frugal tag,kitex 会自动 fallback 到默认的 Go 编解码代码(前提是没使用 slim 模板);
- 如果不希望生成 frugal tag,可使用 -thrift frugal_tag = false。
Kitex >= v0.5.0 默认指定了该参数;旧版本需重手动指定该参数、执行 kitex 命令,例:
kitex -thrift frugal_tag -service service_name idl/api.thrift
Pretouch: -frugal-pretouch
在 init()
里生成调用 frugal.Pretouch
方法的代码,预处理(JIT 编译)所有请求/响应类型,减少首次请求耗时。
frugal 默认在首次编解码时调用 JIT Compiler,这会导致首次请求耗时较长。
例:
kitex -frugal-pretouch -service service_name idl/api.thrift
Slim 模板: -thrift template=slim
请升级 thriftgo 到 v0.3.0(或以上);旧版本生成的 struct 在 optional 字段的编解码上存在问题。
不生成 Thrift 编解码的 Go 源码(该代码实现了 thrift.TProtocol 接口),以减少代码量,提高 IDE 加载及编译速度。附:使用 slim 模板生成的样例代码。
frugal 用 JIT 生成编解码代码,不依赖生成的 Go 编解码代码。
例:
kitex -thrift frugal_tag,template=slim -service service_name idl/api.thrift
注:开启 Slim 会导致在不支持 frugal 的情况下无法 fallback、只能报错(例如 arm 架构,或无法从请求头中获取 thrift payload 的长度)。
示例用法
建议使用最新版 Kitex(>= v0.5.0)和 thriftgo(>= v0.3.0)。
保守版
kitex -thrift frugal_tag -service service_name idl/api.thrift
说明:
- 新版 Kitex (>=0.5.0)默认会生成 frugal tag;
- 不使用 pretouch:在单个项目里不一定所有类型都会被引用;可尝试打开后观察是否影响启动速度;
- 不使用 slim 模板:在不支持 frugal 的场景可以 fallback 到生成的 Thrift 编解码代码;
激进版
kitex -thrift frugal_tag,template=slim -frugal-pretouch -service service_name idl/api.thrift
说明:
- 开启 pretouch:可能会导致进程启动变慢
- 启用 slim 模板:在不支持 frugal 的场景无法 fallback 到生成的 Thrift 编解码代码,只能报错;
Kitex Server
注意事项
请确保 Client 端指定了 Framed 模式(或 TTHeaderFramed)
- 使用 Framed 模式可以保证请求头包含 payload size
- 如果无法获取到 Payload Size,目前 Kitex Server 只能 fallback 到 Go 编解码代码
- 如开启 slim 模板,则无法 fallback,会报错 “decode failed, codec msg type not match”
server.Option
Server 初始化时的相关参数。
server.WithPayloadCodec
用于启用 Frugal 编解码器:
server.WithPayloadCodec(
thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
)
注:如报错(找不到符号),说明当前 kitex 版本 + go 版本的组合不支持 frugal,例如 Go 1.21 + Kitex v0.7.1(Kitex 通过条件编译屏蔽不支持的版本)。
示例代码
package main
import (
"context"
"github.com/cloudwego/kitex-examples/kitex_gen/api"
"github.com/cloudwego/kitex-examples/kitex_gen/api/echo"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
"github.com/cloudwego/kitex/server"
)
type EchoImpl struct{}
func (e EchoImpl) Echo(ctx context.Context, req *api.Request) (r *api.Response, err error) {
return &api.Response{Message: req.Message}, nil
}
func main() {
code := thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
svr := echo.NewServer(new(EchoImpl), server.WithPayloadCodec(code))
err := svr.Run()
if err != nil {
panic(err)
}
}
Kitex Client
Client 初始化时的相关参数。
client.Option
client.WithPayloadCodec
用于启用 Frugal 编解码器:
client.WithPayloadCodec(
thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
)
注:如报错(找不到符号),说明当前 kitex 版本 + go 版本的组合不支持 frugal,例如 Go 1.21 + Kitex v0.7.1(Kitex 通过条件编译屏蔽不支持的版本)。
client.WithTransportProtocol
用于开启 Framed 模式,在 thrift pure payload 前增加 4 个字节(int32)用于告诉对端 payload size
client.WithTransportProtocol(transport.Framed)
注:
- 如不指定 Framed,可能存在如下问题:
- Server 端可能无法用 frugal 解码(因为读不到 Payload Size,详见 “Kitex Server -> 注意事项”);
- Server 端不会返回 Framed Payload,Client 可能无法用 frugal 解码(因为读不到 Payload Size);
- 如果目标 Server 不支持 Framed,则不应指定。不影响 Client 侧使用 frugal 编码;但 Server 回包如不是 Framed,Client 可能无法用 frugal 解码(这种情况慎用 slim 模板);
- 也可使用 TTHeaderFramed(即
TTHeader | Framed
位与结果)。
示例代码
package main
import (
"context"
"github.com/cloudwego/kitex-examples/kitex_gen/api"
"github.com/cloudwego/kitex-examples/kitex_gen/api/echo"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/klog"
"github.com/cloudwego/kitex/pkg/remote/codec/thrift"
"github.com/cloudwego/kitex/transport"
)
func main() {
codec := thrift.NewThriftCodecWithConfig(thrift.FrugalRead | thrift.FrugalWrite)
framed := client.WithTransportProtocol(transport.Framed)
server := client.WithHostPorts("127.0.0.1:8888")
cli := echo.MustNewClient("a.b.c", server, client.WithPayloadCodec(codec), framed)
rsp, err := cli.Echo(context.Background(), &api.Request{Message: "Hello"})
klog.Infof("resp: %v, err: %v", rsp, err)
}
直接使用 Frugal
某些场景(例如录制流量)可直接使用 frugal 编解码。
构造 Go Struct
frugal 的 JIT 编译器依赖带 frugal tag 的 Go struct。
注意:
- 由于一个方法的请求可能有多个参数,需要构造一个将这些参数按顺序封装起来的 struct,例如 Kitex 生成的结构体 EchoEchoArgs 封装了 Request;
- 请求的响应虽然只有一个参数,但也要封装成一个 struct,例如 EchoEchoResult 封装了 Response;
- 具体可参考示例代码。
使用 kitex 命令行工具
参见上文 “Kitex 集成 -> 代码生成”。
注:kitex 通过调用 thriftgo 生成代码。
使用 thriftgo (>= v0.3.0)
安装 thriftgo ( >= v0.3.0):
go install -v github.com/cloudwego/thriftgo@latest
基于 Thrift IDL 生成 Go struct:
thriftgo -r -o thrift -g go:frugal_tag,template=slim,package_prefix=github.com/example echo.thrift
手动编写(不建议)
请参考 thriftgo 生成的 struct (例:Request、Response)。
注意:
- 每个字段都应有 frugal tag;
- 对于 optional 字段,需在
InitDefault()
方法里写入默认值; - 需要构造封装请求/响应参数的结构体(例:EchoEchoArgs、EchoEchoResult)
编码
如只想用 thrift 编码(例如替代 json),可直接调用 frugal.EncodeObject(..)
方法。
如想生成符合 Thrift Binary protocol encoding 的 Thrift Payload(可发送给 Thrift Server),编码结果应含:
- Thrift Magic Number:int16,固定值 0x8001
- MessageType:int16,枚举值 CALL=1, REPLY=2, Exception=3, Oneway=4
- MethodName:长度(int32) + 名称([]byte)
- Sequence ID:int32
- 序列化后的请求/响应数据
其中 1~4 可以直接引用 Kitex 的实现, 5 可以调用 frugal.EncodeObject(buf, nil, data)
生成。
注:data
应是一个将所有请求/响应参数按顺序封装起来的 struct(例如 Kitex 生成的结构体 EchoEchoArgs、EchoEchoResult),具体可参考示例代码。
解码
根据 Thrift Binary protocol encoding,解码结果应包括:
- MethodName
- MessageType
- Sequence ID
- 请求/响应数据
其中 1~3 的解码可以引用 Kitex 的实现,4 的解码可调用 frugal.DecodeObject(buf, data)
完成。
注:data
应是一个将所有请求/响应参数按顺序封装起来的 struct(例如 Kitex 生成的结构体 EchoEchoArgs、EchoEchoResult),具体可参考示例代码。
示例代码
请参见:kitex-examples: frugal/codec/frugal.go
注意事项
Slim 模板
解码需 Payload Size:建议指定 Framed
Kitex 解码时,需从 Header 中获取 Payload Size,以截取完整 Thrift PurePayload,供 frugal 解码。
如启用了 Slim 模板,但无法从请求或相应的 Header 里获取到 Payload Size,Kitex 无法 fallback,只能报错:
codec msg type not match with thriftCodec
因此如果目标 Server 兼容 Framed,建议默认指定 Framed(或 TTHeaderFramed)。
ARM 架构支持:暂未实现
由于 frugal 目前不支持 ARM 架构,有 ARM 架构需求的项目请勿使用 slim 模板
- 在 Mac M1/M2 上开发时,可暂用 Rosetta 兼容 frugal
- slim 模板不生成 Go 编解码代码(仅 JIT 编解码),因此无法 fallback 到默认的编解码方案
性能测试数据
传统的 Thrift 编解码方式,要求用户必须要先生成编解码代码,Frugal 通过 JIT 编译技术在运行时动态生成编解码机器代码,避免了这一过程。
基于 JIT 技术 Frugal 可以生成比 Go 语言编译器性能更好的机器代码,在多核场景下,Frugal 的性能最高可达传统编解码方式的 5 倍左右。
性能测试数据如下:
name old time/op new time/op delta
MarshalAllSize_Parallel/small-16 78.8ns ± 0% 14.9ns ± 0% -81.10%
MarshalAllSize_Parallel/medium-16 1.34µs ± 0% 0.32µs ± 0% -76.32%
MarshalAllSize_Parallel/large-16 37.7µs ± 0% 9.4µs ± 0% -75.02%
UnmarshalAllSize_Parallel/small-16 368ns ± 0% 30ns ± 0% -91.90%
UnmarshalAllSize_Parallel/medium-16 11.9µs ± 0% 0.8µs ± 0% -92.98%
UnmarshalAllSize_Parallel/large-16 233µs ± 0% 21µs ± 0% -90.99%
name old speed new speed delta
MarshalAllSize_Parallel/small-16 7.31GB/s ± 0% 38.65GB/s ± 0% +428.84%
MarshalAllSize_Parallel/medium-16 12.9GB/s ± 0% 54.7GB/s ± 0% +322.10%
MarshalAllSize_Parallel/large-16 11.7GB/s ± 0% 46.8GB/s ± 0% +300.26%
UnmarshalAllSize_Parallel/small-16 1.56GB/s ± 0% 19.31GB/s ± 0% +1134.41%
UnmarshalAllSize_Parallel/medium-16 1.46GB/s ± 0% 20.80GB/s ± 0% +1324.55%
UnmarshalAllSize_Parallel/large-16 1.89GB/s ± 0% 20.98GB/s ± 0% +1009.73%
name old alloc/op new alloc/op delta
MarshalAllSize_Parallel/small-16 112B ± 0% 0B -100.00%
MarshalAllSize_Parallel/medium-16 112B ± 0% 0B -100.00%
MarshalAllSize_Parallel/large-16 779B ± 0% 57B ± 0% -92.68%
UnmarshalAllSize_Parallel/small-16 1.31kB ± 0% 0.10kB ± 0% -92.76%
UnmarshalAllSize_Parallel/medium-16 448B ± 0% 3022B ± 0% +574.55%
UnmarshalAllSize_Parallel/large-16 1.13MB ± 0% 0.07MB ± 0% -93.54%
name old allocs/op new allocs/op delta
MarshalAllSize_Parallel/small-16 1.00 ± 0% 0.00 -100.00%
MarshalAllSize_Parallel/medium-16 1.00 ± 0% 0.00 -100.00%
MarshalAllSize_Parallel/large-16 1.00 ± 0% 0.00 -100.00%
UnmarshalAllSize_Parallel/small-16 6.00 ± 0% 1.00 ± 0% -83.33%
UnmarshalAllSize_Parallel/medium-16 6.00 ± 0% 30.00 ± 0% +400.00%
UnmarshalAllSize_Parallel/large-16 4.80k ± 0% 0.76k ± 0% -84.10%
FAQ
如何不生成 frugal tag?
默认生成的 frugal tag 不影响性能,建议保留。
执行 kitex 命令行工具时加上参数 -thrift frugal_tag=false
。
注意:
- 如果不生成 frugal_tag,会导致无法启用 frugal
- Thrift 的 set 和 list 在 golang 生成的类型一样,编码无法区分,所以需要 tag;
- kitex 检测到请求/响应类型不包含 tag,无法使用 frugal,则会 fallback 到标准的 thrift 编解码方式。
- 如果开启 slim 模式,必须生成 frugal tag
Kitex Client 报错 encode failed: codec msg type not match with thriftCodec
Client 端报错信息如下:
failed with error: remote or network error[remote]: encode failed, codec msg type not match with thriftCodec
可能原因:
- 使用了 slim 模板,但没有指定 client.PayloadCodec 开启 frugal 编解码器
- 使用了 slim 模板,但没有生成带 frugal tag 的代码
Kitex Server 报错 decode failed, codec msg type not match with thriftCodec
日志信息如下(或在 client 端收到的响应里包含该错误信息):
decode failed, codec msg type not match with thriftCodec
可能原因:
- 使用了 slim 模板,但没有指定 server.PayloadCodec 开启 frugal 编解码器
- 使用了 slim 模板,但没有生成带 frugal tag 的代码
- Client 端没有指定 Transporting Protocol 为 Framed 或 TTHeaderFramed
frugal: type mismatch: 11 expected, got 10
根据 Thrift binary protocol, 11 是 BINARY(或string),10 是 I64 类型(不是 IDL 里的字段编号)
当前 IDL 定义的是 string 类型,但是报文里是 I64,导致无法正确解码。
请检查上下游的 IDL 是否一致、生成的编解码代码是否与 IDL 一致(可能没有用新的 IDL 重新生成代码,或没有正确提交到 git)。
从 decode 结果获取的字符串内容乱码
frugal <= v0.1.3 解码 string 类型时,默认使用 NOCOPY 模式(直接引用解码入参的 []byte);而在 Kitex 里该入参会被回收后复用,导致 string 的「值」被修改。
新版本默认禁用了 NOCOPY 模式,升级后即可修复。
编译 Kitex 项目时报错 undefined: thrift.FrugalRead
可能原因:
- 使用了不支持的版本 go 编译:使用 go1.16 ~ go1.21 进行编译
- 使用了不支持当前 Go 版本的 Kitex 版本:请升级到最新版 Kitex
- 例如:Kitex v0.7.1 在用 go1.21 编译时禁用了 frugal(发布该 Kitex 版本时 frugal 尚未支持 go1.21),需要升级到 Kitex >= v0.7.2
slim 模板下,Optional 字段解码时未填充默认值
已知问题:旧版本 thriftgo 在 slim 模板下未生成 InitDefault()
方法。
需升级到 thriftgo >= v0.3.0, 并重新生成 slim 模板代码。
frugal EncodeObject 报错 unexpected EOF: 38 bytes short
由于 frugal.EncodeObject
需要传入一个 buf,Kitex 的实现是先调用 frugal.EncodeSize(data)
计算所需 buf 的长度,分配 buf,再编码 data。
在「计算完 buf 大小」之后、「编码data」之前,业务代码可能会并发读写 data,导致实际编码数据超过预期(可能会越界报错,甚至panic)。
这种情况不属于 frugal 的 bug,需要业务代码自查,避免修改已传给 Kitex 的 Request/Response (包括其中的字段,特别是 string、slice 等非固定长度类型)。
frugal EncodeObject panic
可能是旧版本的问题,建议升级到最新版( >= v0.1.8)
go get github.com/cloudwego/frugal@latest
如果问题依然存在,请确认被 encode 的对象没有被并发读写(包括间接引用的其他对象)。
例如读取一个正被设置为空串的字符串,可能会读到无效的 string(StringHeader.Data = nil && StringHeader.Len > 0),导致在编码时 出现 “nil pointer error” panic 。