字节跳动开源 Go HTTP 框架 Hertz 设计实践( 三 )


流式处理Hertz 提供 Server 和 Client 的流式处理能力 。HTTP 的文件场景是十分常见的场景,除了 Server 侧的上传场景之外,Client 的下载场景也十分常见 。为此,Hertz 支持了 Server 和 Client 的流式处理 。在内部网关场景中,从 Gin 迁移到 Hertz 后,cpu 使用量随流量大小不同可节省 30%-60% 不等,服务压力越大,收益越大 。Hertz 开启流式功能的方式也很容易,只需要在 Server 上或 Client 上添加一个配置即可,可参考 CloudWeGo 官网 Hertz 文档的流式处理部分 。
由于 Netpoll 采用 LT 的触发模式,由网络库主动将将数据从 TCP 缓冲区读到用户态,并存储到 buffer 中,否则 epoll 事件会持续触发 。因此 Server 在超大请求的场景下,由于 Netpoll 持续将数据读到用户态内存中,可能会有 OOM 的风险 。HTTP 文件上传场景就是一个典型的场景,但 HTTP 上传服务又是很常见的场景,因此我们支持标准网络库 go net,并针对 Hertz 做了特殊优化,暴露出 Read() 接口,防止 OOM 发生 。
对于 Client,情况并不相同 。流式场景下会将连接封装成 Reader 暴露给用户,而 Client 有连接池管理,那这样连接就多了一种状态,何时关连接,何时复用连接成了一个问题 。由于框架侧并不知道该连接何时会用完,框架侧复用该连接不现实,会导致串包问题 。由于 GC 会关闭连接,因此我们起初设想流式场景下的连接交由用户后,由 GC 负责关闭,这样也不会导致资源泄漏 。但是在测试后发现,由于 GC 存在一定时间间隔,另外 TCP 中主动关闭连接的一方需要等待 2RTT,在高并发场景下会导致 fd 被打满的情况 。最终我们提供了复用连接的接口,对于性能有场要求用户,在使用完连接后可以将连接重新放入连接池中复用 。
性能表现Hertz 使用字节跳动自研高性能网络库 Netpoll,在提高网络库效率方面有诸多实践,参考已发布文章字节跳动在 Go 网络库上的实践 。除此之外,Netpoll 还针对 HTTP 场景进行优化,通过减少拷贝和系统调用次数提高吞吐以及降低时延 。为了衡量 Hertz 性能指标,我们选取了社区中有代表性的框架 Gin(net/http)和 Fasthttp 作为对比,如图 3 所示 。可以看到,Hertz 的极限吞吐、TP99 等指标均处于业界领先水平 。未来,Hertz 还将继续和 Netpoll 深度配合,探索 HTTP 框架性能的极限 。

字节跳动开源 Go HTTP 框架 Hertz 设计实践

文章插图
 
图 3:Hertz 和其他框架性能对比
一个 Demo下面简单演示一下 Hertz 是如何开发一个服务的 。
  1. 首先,定义 IDL,这里使用 Thrift 作为 IDL 的定义(也支持使用 Protobuf 定义的 IDL),编写一个名为 Demo 的 service 。这个服务有一个 API: Hello,它的请求参数是一个 query,响应是一个包含一个 RespBody 字段的 Json 。
// idl/hello.thriftnamespace go hello.examplestruct HelloReq {1: string Name (api.query="name");}struct HelloResp {1: string RespBody;}service HelloService {HelloResp Hello(1: HelloReq request) (api.get="/hello");}
  1. 接下来我们使用 hz 生成代码,并整理和拉取依赖
$ hz new -idl idl/hello.thrift -mod Demo$ go mod tidy && go mod verify
  1. 填充业务逻辑,比如我们返回 hello,${Name},那我们在biz/handler/example/hello_service.go 中添加以下代码即可
// Hello .// @router /hello [GET]func Hello(ctx context.Context, c *app.RequestContext) {var err errorvar req example.HelloReqerr = c.BindAndValidate(&req)if err != nil {c.String(400, err.Error())return}resp := new(example.HelloResp)resp.RespBody = "hello, " + req.Namec.JSON(200, resp)}
  1. 编译并运行项目
$ go build$ ./Demo到现在一个简单的 Hertz 项目已经生成,下面我们来测试一下
$ curl http://localhost:8888/hello?name=Xiaoming// 如果看到以下返回说明服务已经正常启动起来啦$ {"RespBody":"hello, Xiaoming"}(以上 demo 可以在 hertz-examples 中查看) 之后就可以愉快的构建自己的项目了 。
后记希望以上的分享能够让大家对 Hertz 有一个整体上的认识 。同时,我们也在不断地迭代 Hertz、完善 CloudWeGo 整体生态 。欢迎各位感兴趣的同学们加入我们,共同建设 CloudWeGo 。
参考资料