gRPC 源码分析(五): gRPC server 的流量控制 - 采样流量控制

1,362 阅读5分钟

gRPC 源码分析(五): gRPC server 的流量控制 - 采样流量控制

我们都知道 TCP 是有流量控制机制的. gRPC server 在应用层实现了自己的流量控制, 并将流量控制分为三个层级:

  1. sample level 流量控制
  2. connection level 流量控制
  3. stream level 流量控制

流量控制可以说是 gRPC 高性能的关键, 通过动态地控制数据发送和接收的速率, gRPC 保证来任何网络情况下都能发挥最大的性能, 尽量提高传输带宽并降低传输延迟. 本篇我们先来看一看 gRPC server 是如何实现 sample level 的流量控制的.

BDP 估算和动态流量控制窗口

以下内容翻译自 gRPC 的一篇官方博客, 介绍了 sample level 流量控制的意义和原理.

BDP 估算和动态流量控制这个 feature 缩小了 gRPC 和 HTTP/1.1 在高延迟网络环境下的性能差距.

Bandwidth Delay Product (BDP), 即带宽延迟积, 是网络连接的带宽和数据往返延迟的乘积. BDP 能够有效地告诉我们, 如果充分利用了网络连接, 那么在某一刻在网络连接上可以存在多少字节的数据.

计算 BDP 并进行相应调整的算法最开始是由 @ejona 提出的, 后来由 gRPC-C Core 和 gRPC-Java 实现. BDP 的想法简单而实用: 每次接收者得到一个 data frame, 它就会发出一个 BDP ping frame (一个只有 BDP 估算器使用的 ping frame). 之后, 接收者会统计指导收到 ACK 之前收到的字节数. 这个大约在 1.5RTT (往返时间) 中收到的所有字节的总和是有效 BDP1.5 的近似值. 如果该值接近当前流量窗口的大小 (例如超过 2/3), 接收者就需要增加窗口的大小. 窗口的大小被设定为 BDP (所有采样期间接受到的字节总和) 的两倍.

BDP 采样目前在 gRPC-go 的 server 端是默认开启的.

接下来我们看看 gRPC server 在代码中是何如实现基于 BDP 估算的流量控制的.

gRPC sample level 流量控制的核心: bdpEstimator

在 gRPC server 端定义了一个bdpEstimator , 是用来计算 BDP 的核心:

type bdpEstimator struct {
	// sentAt is the time when the ping was sent.
	sentAt time.Time

	mu sync.Mutex
	// bdp is the current bdp estimate.
	bdp uint32
	// sample is the number of bytes received in one measurement cycle.
	sample uint32
	// bwMax is the maximum bandwidth noted so far (bytes/sec).
	bwMax float64
	// bool to keep track of the beginning of a new measurement cycle.
	isSent bool
	// Callback to update the window sizes.
	updateFlowControl func(n uint32)
	// sampleCount is the number of samples taken so far.
	sampleCount uint64
	// round trip time (seconds)
	rtt float64
}

bdpEstimator 有两个主要的方法 addcalculate :

// add 的返回值指示 loopyWriter 是否发送 BDP ping frame 给 client
func (b *bdpEstimator) add(n uint32) bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	// 如果 bdp 已经达到上限, 就不再发送 BDP ping 进行采样
	if b.bdp == bdpLimit {
		return false
	}
	// 如果在当前时间点没有 BDP ping frame 发送出去, 就应该发送, 来进行采样
	if !b.isSent {
		b.isSent = true
		b.sample = n
		b.sentAt = time.Time{}
		b.sampleCount++
		return true
	}
	// 已经有 BDP ping frame 发送出去了, 但是还没有收到 ACK
	b.sample += n
	return false
}

add 函数有两个左右:

  1. 告知 loopyWriter 是否开始采样.
  2. 记录采样开始的时间和初始数据量.
func (t *http2Server) handleData(f *http2.DataFrame) {
	size := f.Header().Length
	var sendBDPPing bool
	if t.bdpEst != nil {
		sendBDPPing = t.bdpEst.add(size)
	}
	......
	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)
	}
}

var bdpPing = &ping{data: [8]byte{2, 4, 16, 16, 9, 14, 7, 7}}

handleData 函数是 gRPC server 收到来自 client 的 HTTP/2 data frame 之后执行的函数, 从中我们可以看出:

  1. gRPC server 和每一个 client 之间都维护者一个 bdpEstimator .
  2. 每次收到一个 data frame, gRPC server 都会判断是否需要进行采样. 同一时刻, 同一个 client 只会进行一次采样.
  3. 如果需要进行采样, 就向 client 发送一个 bdpPing frame.

Client 端在收到一个 bdpPing frame 之后, 会立刻返回一个 ACK, server 会捕捉到这个 ACK:

func (t *http2Server) handlePing(f *http2.PingFrame) {
	if f.IsAck() {
		......
		// Maybe it's a BDP ping.
		if t.bdpEst != nil {
			t.bdpEst.calculate(f.Data)
		}
		return
	}
	......
}

handlePing 是 server 在收到一个 HTTP/2 ping frame 之后调用的函数, 可以看到当 ping frame 是一个 ack 时, 会调用 calculate 这个函数.

func (b *bdpEstimator) calculate(d [8]byte) {
	// Check if the ping acked for was the bdp ping.
	if bdpPing.data != d {
		return
	}
	b.mu.Lock()
	rttSample := time.Since(b.sentAt).Seconds()
	if b.sampleCount < 10 {
		// Bootstrap rtt with an average of first 10 rtt samples.
		b.rtt += (rttSample - b.rtt) / float64(b.sampleCount)
	} else {
		// Heed to the recent past more.
		b.rtt += (rttSample - b.rtt) * float64(alpha)
	}
	b.isSent = false
	// The number of bytes accumulated so far in the sample is smaller
	// than or equal to 1.5 times the real BDP on a saturated connection.
	bwCurrent := float64(b.sample) / (b.rtt * float64(1.5))
	if bwCurrent > b.bwMax {
		b.bwMax = bwCurrent
	}
	// If the current sample (which is smaller than or equal to the 1.5 times the real BDP) is
	// greater than or equal to 2/3rd our perceived bdp AND this is the maximum bandwidth seen so far, we
	// should update our perception of the network BDP.
	if float64(b.sample) >= beta*float64(b.bdp) && bwCurrent == b.bwMax && b.bdp != bdpLimit {
		sampleFloat := float64(b.sample)
		b.bdp = uint32(gamma * sampleFloat)
		if b.bdp > bdpLimit {
			b.bdp = bdpLimit
		}
		bdp := b.bdp
		b.mu.Unlock()
		b.updateFlowControl(bdp)
		return
	}
	b.mu.Unlock()
}

calculate 中, 经过一系列的计算得到了当前的 bdp 的值, 如果需要更新流量控制的话, 会调用之前注册在 bdpEstimator 中的 updateFlowControl 函数, 并将新的窗口大小传递进去.

那么 updateFlowControl 中是怎么处理新的窗口大小的呢?

// updateFlowControl updates the incoming flow control windows
// for the transport and the stream based on the current bdp
// estimation.
func (t *http2Server) updateFlowControl(n uint32) {
	t.mu.Lock()
	for _, s := range t.activeStreams {
		s.fc.newLimit(n)
	}
	t.initialWindowSize = int32(n)
	t.mu.Unlock()
	t.controlBuf.put(&outgoingWindowUpdate{
		streamID:  0,
		increment: t.fc.newLimit(n),
	})
	t.controlBuf.put(&outgoingSettings{
		ss: []http2.Setting{
			{
				ID:  http2.SettingInitialWindowSize,
				Val: n,
			},
		},
	})

}

有几点值得注意的是:

  1. 对于 server 来说, BDP 影响的是 incoming traffic. 也就是说影响的是 client 发送数据的速率和 server 接收数据的速率, 而并不会影响 server 发送数据的速率.
  2. BDP 采样结果会影响 HTTP/2 的窗口大小, connection level 的窗口大小 以及 stream level 的窗口大小. BDP 对 gRPC 的影响是全面的.

总结

本篇中我们了解了 gRPC server 的 BDP 采样流量控制, 下一篇我们来看看 gRPC server 在 connection level 的流量控制是怎么实现的.