konakona
Dream Afar.
konakona

Hertz : 探究 dst 在 Get()/Post() 中的作用方式

Hertz 是 CloudWeGo 旗下由字节跳动开源的高性能 Go HTTP 框架,专注微服务、网关与高并发场景,内置连接池、限流、熔断等能力。

Get()Post() 其实就是对外部 HTTP 接口最常用的调用方式,比如:

  • 调用第三方 API(天气、支付)
  • 下载大文件(镜像、视频)
  • 批量爬取(采集数据)

由于 Hertz 默认对响应体使用了对象池+缓冲区优化,懂得如何传入 dst,就能把内存分配压到最低。但是 dst 到底是什么?作用如何?

本着追求卓越不断探索的精神,我深入研究了 Hertz 的底层代码,让我带你一步步理解 dst 的作用。


首先看看 Get() 的签名长啥样(Post() 类似):

func (c *Client) Get(
    ctx context.Context,
    url string,
    dst []byte,      // 传 nil 或 预分配的缓冲区
    opts ...Option,
) (statusCode int, body []byte, err error
  • dst 默认传 nil:内部会用一个池中对象,再分配新的切片。
  • 自定义 dst:例如 dst := make([]byte, 0, 1<<20),传入后响应直接写到你预分配的缓冲区上。
https://blog.img.crazyphper.com/2024/08/image.png
官方文档的说明,这是常人无法看懂的地步啊!🤣

深入分析 client.doRequestFollowRedirectsBuffer()

让我们直接分析doRequestFollowRedirectsBuffer()方法,Get()Post() 的形参 dst 最终会流经这个方法。

func doRequestFollowRedirectsBuffer(ctx context.Context, req *protocol.Request, dst []byte, url string, c Doer) (statusCode int, body []byte, err error) {
	resp := protocol.AcquireResponse()
	bodyBuf := resp.BodyBuffer()
	oldBody := bodyBuf.B
	bodyBuf.B = dst

	statusCode, _, err = DoRequestFollowRedirects(ctx, req, resp, url, defaultMaxRedirectsCount, c)

	// In HTTP2 scenario, client use stream mode to create a request and its body is in body stream.
	// In HTTP1, only client recv body exceed max body size and client is in stream mode can trig it.
	body = resp.Body()
	bodyBuf.B = oldBody
	protocol.ReleaseResponse(resp)

	return statusCode, body, err
}

是不是有点儿懵?乍看之下你可能会有以下疑问:

  • dst 传入后似乎没有被显性使用
  • bodyBuf.B 获得 dst 的值传递后,但在哪里使用了bodyBuf 呢?

关键点分析

关键在于 bodyBuf := resp.BodyBuffer() 会返回一个 *bytebufferpool.ByteBuffer。在这段代码中,dst 被传入并赋值给 bodyBuf.B。这是为了让响应的数据直接写入到外部供给的 dst 缓冲区,而不是使用默认的缓冲区。这种做法实际上是一种优化内存使用的策略 —— 知晓这一特性就好,通常来说在小型应用中我们也就用个 nil 🤣

赋值完成后,DoRequestFollowRedirects() 函数被调用,它负责处理请求和可能的重定向。尽管代码中没有直接操作 bodyBuf.B,但所有的响应数据都会被写入 bodyBuf.B,正是因为此时 bodyBuf.B 引用了传入的 dstdst 赋给 bodyBuf.B,等于把外部缓冲区接入这一池化系统,拿来就写,不用再分配。

最后,代码将 bodyBuf.B 恢复为原来的 oldBody,这是为了确保函数调用结束后 resp.BodyBuffer() 的状态保持不变,以防后续操作依赖于其初始状态。

流程揭秘

下面是一张简化调用链,直观标注数据写入点:

https://blog.img.crazyphper.com/2024/08/WX20250529-185918@2x.png
流程图

重点DoRequestFollowRedirects 内部所有写操作都会落到你传的 dst 上。

总结一下!

📖 dst 和 bodyBuf.B 没有被显式调用,但实际上,它们在 DoRequestFollowRedirects 函数内部起到了关键作用。

📖 通过将dst 赋值给 bodyBuf.B 来让数据写入外部缓冲区。这是一种内存优化策略。

什么情况/场景下有必要传dst 呢?

通常来说,小型程序或者不考虑复用缓冲区的程序 dst 只需要设为 nil,但在下面这些场景下传递外部缓冲区可以带来明显的优势:

  • 缓冲区复用:在循环或批量处理 HTTP 请求的场景中,复用一个缓冲区来存储响应数据可以避免每次请求都创建新的 []byte,从而显著降低内存使用和分配时间。
  • 高并发和大数据场景:在这些场景中,减少内存分配操作可以提高性能。
  • 性能优化需求:在性能要求极高的应用中,开发者可以通过显式管理内存分配和缓冲区来优化性能。通过传入 dst,可以更精细地控制内存的使用,避免不必要的内存分配和复制。

场景:并发 1000 个请求、每次响应约 200KB。

用法平均分配次数GC 触发平均耗时
nil1000多次120ms
预分配 1MiB00 次85ms

可以看到:分配次数为 0 时,延迟和 GC 开销都大幅下降。

赞赏

团哥

文章作者

继续玩我的CODE,让别人说去。 低调,就是这么自信。

konakona

Hertz : 探究 dst 在 Get()/Post() 中的作用方式
为什么hertz的技术文档不能写好一点儿呢?
扫描二维码继续阅读
2024-08-11