Golang中x/net/http2是否应该将头信息和数据包合并为一个?

Golang中x/net/http2是否应该将头信息和数据包合并为一个? 当发送大量小请求时,这里会出现性能损失。 那么是否应该合并头部和正文数据包,并且在写入头部后不立即刷新?

				StreamID:      streamID,
				BlockFragment: chunk,
				EndStream:     endStream,
				EndHeaders:    endHeaders,
			})
			first = false
		} else {
			cc.fr.WriteContinuation(streamID, endHeaders, chunk)
		}
	}
	cc.bw.Flush()
	return cc.werr
}

// internal error values; they don't escape to callers
var (
	// abort request body write; don't send cancel
	errStopReqBodyWrite = errors.New("http2: aborting request body write")

	// abort request body write, but send stream reset of cancel.
	errStopReqBodyWriteAndCancel = errors.New("http2: canceling request")

更多关于Golang中x/net/http2是否应该将头信息和数据包合并为一个?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

非常感谢! 我为此做了一个测试。 我为此开了一个issue

更多关于Golang中x/net/http2是否应该将头信息和数据包合并为一个?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的。HeaderBody 具有不同的帧类型,并且我不想合并这些帧。我希望在写入头部后不立即刷新,这样,Linux 网络协议就不会将它们分成两个 TCP 数据包发送。

谢谢! 我认为这可能不是为了多路复用。

这里提到,多路复用是通过流实现的。每个HTTP请求/响应都与它自己的流相关联,彼此独立。

https://httpwg.org/specs/rfc7540.html#Overview

原文如下:

Multiplexing of requests is achieved by having each HTTP request/response exchange associated with its own stream (Section 5). Streams are largely independent of each other, so a blocked or stalled request or response does not prevent progress on other streams.

好的,也许它并不适用于多路复用。正如之前所说,我对HTTP2规范几乎一无所知。

但我(诚然基于猜测的)观点仍然成立。HTTP2严格区分不同的帧类型,这可能就是为什么 http2 包不合并头部和正文的原因。

// 代码示例:展示HTTP2帧处理
func handleFrame(frame http2.Frame) {
    switch f := frame.(type) {
    case *http2.HeadersFrame:
        // 处理头部帧
    case *http2.DataFrame:
        // 处理数据帧
    }
}

我对HTTP/2的内部机制完全不了解,但通过浏览网络,我得到的印象是HTTP/2总是将HEADERS帧与DATA帧分开,这可能是为了获得更好的多路复用能力。

RFC 7540 - 超文本传输协议版本2 (HTTP/2)

HTTP/2中的基本协议单元是帧(第4.1节)。每种帧类型都有不同的用途。例如,HEADERSDATA帧构成了HTTP请求和响应的基础(第8.1节

HTTP/2及其工作原理。TLS有助于提高安全性。现在它… | 作者 Carson | Medium

HTTP/2简介

这样,Linux网络协议就不会将数据分开发送到两个TCP数据包中。

好奇:你测试过这个吗? 如果测试过,你看到了显著的性能提升吗?

根据我的基本理解,HTTP2帧被多路复用到TCP连接上,但对于网络栈如何管理TCP数据包并没有直接影响。

我的假设基于这个SO回答,特别是第一点和第二条评论:

  • 要点在于,单个TCP连接可能包含来自许多不同HTTP/2流的帧,这些帧是交错排列的。与TCP数据包的关系在这里并不重要——TCP数据包由你的TCP栈重新组装成TCP流,不应影响你对HTTP/2的理解。

@laike9m:每个TCP“数据包”只包含TCP流的一部分。这些数据包与任何类型的消息都没有关系——因为TCP是单个流,而不是一系列消息。因此,一个应用层帧可能被打包在多个TCP数据包中,一个数据包可能包含多个应用层帧,等等。应用层帧与TCP数据包之间根本没有定义好的映射关系。

再次声明,我不是网络专家,但这些回答符合我对OSI模型及其网络栈各层之间关注点分离的理解。

所以,也许你不需要太担心刷新头部带来的性能损失。

在HTTP/2协议实现中,头部帧和数据帧的合并需要谨慎处理。HTTP/2规范要求头部帧必须在数据帧之前发送,但可以通过WriteHeaders方法的EndStream参数来控制是否立即结束流。对于大量小请求的场景,可以考虑以下优化方案:

// 示例:优化小请求的发送性能
func writeRequest(cc *http2ClientConn, req *http.Request, endStream bool) error {
    streamID := cc.nextStreamID()
    
    // 准备头部帧
    hf := hpack.NewEncoder(&headerBuf)
    // ... 编码头部 ...
    
    // 如果请求体很小,可以尝试合并
    var bodyData []byte
    if req.Body != nil && endStream {
        // 读取小请求体
        bodyData, _ = io.ReadAll(req.Body)
        req.Body.Close()
    }
    
    // 发送头部帧,根据情况设置EndStream
    cc.fr.WriteHeaders(http2.HeadersFrameParam{
        StreamID:      streamID,
        BlockFragment: headerBuf.Bytes(),
        EndStream:     len(bodyData) == 0 && endStream,
        EndHeaders:    true,
    })
    
    // 如果有小请求体,立即发送数据帧
    if len(bodyData) > 0 {
        cc.fr.WriteData(streamID, endStream, bodyData)
    }
    
    // 延迟刷新以提高批处理效果
    if cc.bw.Available() < 1024 {
        cc.bw.Flush()
    }
    
    return nil
}

对于x/net/http2包的实现,当前的设计选择是合理的:

  1. 协议合规性:HTTP/2规范要求头部帧必须首先发送,数据帧随后
  2. 流控制:分离头部和数据允许更好的流控制管理
  3. 优先级处理:头部帧可以携带优先级信息,需要优先处理

性能优化建议在应用层实现:

// 应用层批处理示例
type BatchedRequest struct {
    Header http.Header
    Body   []byte
}

func sendBatchedRequests(cc *http2ClientConn, requests []BatchedRequest) error {
    for _, req := range requests {
        // 发送头部帧
        cc.fr.WriteHeaders(http2.HeadersFrameParam{
            StreamID:      cc.nextStreamID(),
            BlockFragment: encodeHeaders(req.Header),
            EndStream:     len(req.Body) == 0,
            EndHeaders:    true,
        })
        
        // 发送数据帧(如果有)
        if len(req.Body) > 0 {
            cc.fr.WriteData(streamID, true, req.Body)
        }
    }
    
    // 批量刷新
    cc.bw.Flush()
    return cc.werr
}

当前的x/net/http2实现已经考虑了性能优化,通过缓冲和适当的刷新策略来减少系统调用。强制合并头部和数据包可能违反协议规范,并导致以下问题:

  1. 流重置处理复杂化
  2. 优先级信号延迟
  3. 流量控制窗口更新延迟
  4. 与中间件的兼容性问题

对于性能敏感的应用,建议:

  • 使用连接池复用连接
  • 实施请求批处理
  • 调整Flush阈值
  • 监控实际性能指标进行调优
回到顶部