Go猜想录
大道至简,悟者天成
gRPC-Go -- #7 链路级别流控 sendQuota

在向 HTTP/2 链路发送数据帧时,发送方不能无限制地发送数据。本文将介绍 gRPC-Go 在链路级别的流量控制机制,即何时允许发送数据,何时需要暂停。

loopyWriter

loopyWriter 的主要功能是向 HTTP/2 链路发送数据帧,其中的 sendQuota 字段负责链路级别的流量控制,表示链路的发送窗口大小。初始化时,sendQuota 使用 defaultWindowSize,默认值为 65535,且没有配置项可以修改。

// The default value of flow control window size in HTTP2 spec.
const defaultWindowSize = 65535

type loopyWriter struct {
	...
	sendQuota uint32
	...
}

唯一能够修改 sendQuota 默认值的是 BDP。当 BDP 发生变化时,会发送窗口大小更新帧,streamID 为 0 表示这是链路级别的发送窗口更新。

func (t *http2Server) updateFlowControl(n uint32) {
	...
	t.controlBuf.put(&outgoingWindowUpdate{
		streamID:  0,
		increment: t.fc.newLimit(n),
	})
	...
}

接收到该帧后,sendQuota 会相应调整,注意 increment 可以是正数或负数。

func (l *loopyWriter) incomingWindowUpdateHandler(w *incomingWindowUpdate) {
	// Otherwise update the quota.
	if w.streamID == 0 {
		l.sendQuota += w.increment
		return
	}
	...
}

消耗 sendQuota

在发送数据前,会检查 sendQuota 是否充足,若发送成功,sendQuota 会按发送的数据大小减少相应的配额。

func (l *loopyWriter) processData() (bool, error) {
	// sendQuota 不足,直接返回
	if l.sendQuota == 0 {
		return true, nil
	}
	...
	l.sendQuota -= uint32(size)
	...
}

补充 sendQuota

接收方在 handleData 方法中处理收到的 http2 数据帧时,可能会发送窗口更新帧以补充 sendQuota 的配额。具体满足什么条件会发送呢,请听下回分解。

func (t *http2Server) handleData(f *http2.DataFrame) {
	...
	// 接收数据时可能会发送窗口更新帧
	if w := t.fc.onData(size); w > 0 {
		t.controlBuf.put(&outgoingWindowUpdate{
			streamID:  0,
			increment: w,
		})
	}
	// 发送 bdpPing 前可能会发送窗口更新帧
	if sendBDPPing {
		// Avoid excessive ping detection (e.g. in an L7 proxy)
		// by sending a window update prior to the BDP ping.
		if w := t.fc.reset(); w > 0 {
			t.controlBuf.put(&outgoingWindowUpdate{
				streamID:  0,
				increment: w,
			})
		}
		t.controlBuf.put(bdpPing)
	}
	...
}

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。