gRPC 源码分析(四): gRPC server 中 frame 的处理
在前面的两篇文章中, 我们大致了解了 gRPC server 是如何工作的. 从这篇开始, 我们一起来看看 gRPC server 的一些设计细节.
无论 gRPC server 是如何设计的, 它最终都会将 RPC 的执行结果通过 HTTP2 frame 的形式发送给 client. 本篇就来研究一下 server 是如何发送 frame 的.
gRPC Server 的 Framer
gRPC server 在每一次接收到一个新的来自 client 的连接后, 会创建一个 Framer
, 这个 Framer
就是实际上负责发送和接收 HTTP2 frame 的接口. 每一个 client 都会对应一个 Framer
来处理来自该 client 的所有 frame, 不管这些 frame 是不是属于一个 stream.
type framer struct {
// 一个包含了 buffer 的 net.Conn 的 writer
writer *bufWriter
// 原生的 http2.Framer, 负责数据读写
fr *http2.Framer
}
framer
其实就是对 golang 原生的 http2.Framer
的封装.
type bufWriter struct {
buf []byte
offset int
batchSize int
conn net.Conn
err error
onFlush func()
}
func newBufWriter(conn net.Conn, batchSize int) *bufWriter {
return &bufWriter{
buf: make([]byte, batchSize*2),
batchSize: batchSize,
conn: conn,
}
}
func (w *bufWriter) Write(b []byte) (n int, err error) {
// 在 write 之间检查上一次 write 是否发生了错误
if w.err != nil {
return 0, w.err
}
// 如果 batchSize 为 0, 说明不需要写缓存, 那么直接向 net.Conn 写数据
if w.batchSize == 0 { // Buffer has been disabled.
return w.conn.Write(b)
}
// 有可能出现需要写入的数据长度大于 buffer 中剩余空间的情况,
// 因此在 for 循环中持续地向 buf 中写入数据.
for len(b) > 0 {
nn := copy(w.buf[w.offset:], b)
b = b[nn:]
w.offset += nn
n += nn
// 当 buffer 中的数据超过了 batchSize, 就进行一次刷新
if w.offset >= w.batchSize {
err = w.Flush()
}
}
return n, err
}
// 刷新, 即向 net.Conn 写入数据
func (w *bufWriter) Flush() error {
if w.err != nil {
return w.err
}
if w.offset == 0 {
return nil
}
if w.onFlush != nil {
w.onFlush()
}
_, w.err = w.conn.Write(w.buf[:w.offset])
w.offset = 0
return w.err
}
gRPC server 在 http_util.go
中实现了一个简单的写缓存给 http2.framer
来作为 io.Writer
.
func newFramer(conn net.Conn, writeBufferSize, readBufferSize int, maxHeaderListSize uint32) *framer {
if writeBufferSize < 0 {
writeBufferSize = 0
}
var r io.Reader = conn
if readBufferSize > 0 {
r = bufio.NewReaderSize(r, readBufferSize)
}
w := newBufWriter(conn, writeBufferSize)
f := &framer{
writer: w,
fr: http2.NewFramer(w, r),
}
......
return f
}
而传递给 http2.framer
的 io.Reader
使用了 bufio
package.
gRPC Server 如何发送数据
gRPC server 为每一个 client 创建一个 loopyWriter
, 由这个 loopyWriter
负责发送数据.
type loopyWriter struct {
......
cbuf *controlBuffer
......
// estdStreams 中存储着所有还没有收到 headers frame 的 stream
estdStreams map[uint32]*outStream // Established streams.
// activeStreams 是一个 stream 的链表, 而每个 stream 内部也维护者一个链表, 用来存储需要被发送的 data item
activeStreams *outStreamList
// 负责数据发送的 framer
framer *framer
......
}
loopyWriter
在运行时协调内部的 controlBuffer
和 activeStreams
来有序地发送数据, 流程大致是这样的:
loopyWriter
从和 client 一一对应的controlBuffer
中读取下一个要发送给 client 的 message.controlBuffer
中可能保存各种各样的 message, 包括但不限于 dataFrame.controlBuffer
还承担着 client level 的流量控制.- 以 dataFrame 为例, 一个 dataFrame 中可能包含着多个 HTTP2 data frame. 这个 dataFrame 会首先被放入到对应 stream 的
itemList
的末尾. 每个 stream 中都维护着一个itemList
, 用来存放需要发送的数据. loopyWriter
中维护者一个 stream 的链表, 即activeStreams
.loopyWriter
每次取出链表头部的 stream, 将其itemList
中最多 16KB 的数据发送给 client, 然后将该 stream 放到activeStreams
链表的末尾.
gRPC server 在和 client 的通信中, 所有 stream (即所有 RPC call) 共享一个 loopyWriter.
下面我们来看看代码中是怎么做到的:
// run should be run in a separate goroutine.
// It reads control frames from controlBuf and processes them by:
// 1. Updating loopy's internal state, or/and
// 2. Writing out HTTP2 frames on the wire.
//
// Loopy keeps all active streams with data to send in a linked-list.
// All streams in the activeStreams linked-list must have both:
// 1. Data to send, and
// 2. Stream level flow control quota available.
//
// In each iteration of run loop, other than processing the incoming control
// frame, loopy calls processData, which processes one node from the activeStreams linked-list.
// This results in writing of HTTP2 frames into an underlying write buffer.
// When there's no more control frames to read from controlBuf, loopy flushes the write buffer.
// As an optimization, to increase the batch size for each flush, loopy yields the processor, once
// if the batch size is too low to give stream goroutines a chance to fill it up.
func (l *loopyWriter) run() (err error) {
defer func() {
if err == ErrConnClosing {
// Don't log ErrConnClosing as error since it happens
// 1. When the connection is closed by some other known issue.
// 2. User closed the connection.
// 3. A graceful close of connection.
if logger.V(logLevel) {
logger.Infof("transport: loopyWriter.run returning. %v", err)
}
err = nil
}
}()
for {
// block 当前 goroutine
it, err := l.cbuf.get(true)
if err != nil {
return err
}
// 处理 controlBuffer 中取出的数据, 对于 dataFrame 来说, 会讲 dataFrame 放到 对应的 stream 的 itemList 的末尾
if err = l.handle(it); err != nil {
return err
}
// 发送 activeStreams 中第一个 stream 中最多 16KB 的数据
if _, err = l.processData(); err != nil {
return err
}
gosched := true
hasdata:
for {
// 不 block 当前 goroutine
it, err := l.cbuf.get(false)
if err != nil {
return err
}
if it != nil {
if err = l.handle(it); err != nil {
return err
}
if _, err = l.processData(); err != nil {
return err
}
continue hasdata
}
isEmpty, err := l.processData()
if err != nil {
return err
}
if !isEmpty {
continue hasdata
}
if gosched {
gosched = false
// 当 framer 的 writer buffer 中数据过少时, yield processer 来让其他 goroutine 向 controlBuffer 中填充数据
if l.framer.writer.offset < minBatchSize {
runtime.Gosched()
continue hasdata
}
}
l.framer.writer.Flush()
break hasdata
}
}
}
run
方法中有几点值得注意:
l.cbuf.get
被调用了两次, 第一次调用会阻塞当前 goroutine, 而第二次不会. 第一次采取阻塞式调用, 是因为当前所有的activeStreams
中已经没有待发送的数据了, 因此可以阻塞, 避免无意义的轮询. 而第二次采用非阻塞式调用, 是因为此时activeStreams
可能还有待发送的数据, 无论controlBuffer
中是否有新的数据出现, 都应该继续向 client 发送数据.- 当 framer 中写缓存中的数据少于
minBatchSize
时, loopyWriter 会主动放弃 CPU. 当 loopyWriter 发送数据的速度快于 gRPC server 产生数据的速度时, loopyWriter 放弃 CPU, 从而让其他的 goroutine (stream 的 goroutine) 去填充数据. 这么做是为了能够尽量多地向net.Conn
传递数据, 即减少 TCP packet 的数量.
loopyWriter 如何发送 stream 中的数据
func (l *loopyWriter) processData() (bool, error) {
// connection level 流量控制
if l.sendQuota == 0 {
return true, nil
}
// 取出 activeStreams 中的第一个 stream
str := l.activeStreams.dequeue()
if str == nil {
return true, nil
}
// 第一个 item 一定是 dataFrame, dataFrame 是 gRPC 中定义的数据结构, 其中可能包含多个 HTTP2 的 data frame
dataItem := str.itl.peek().(*dataFrame)
if len(dataItem.h) == 0 && len(dataItem.d) == 0 { // Empty data frame
......
}
// 确定需要发送的数据大小, 并存放在 buf 中
var (
buf []byte
)
......
// 通过 framer 向 client 发送数据
if err := l.framer.fr.WriteData(dataItem.streamID, endStream, buf[:size]); err != nil {
return false, err
}
......
if str.itl.isEmpty() {
......
} else {
// 如果 stream 中还有数据待发送, 那么将这个 stream enqueue 回 activeStreams
l.activeStreams.enqueue(str)
}
return false, nil
}
在 loopyWriter.run()
中, 会不断地执行 processData
函数, 在函数中:
- 将
activeStreams
中的第一个 stream 取出. - 将 stream 的
itemList
中的第一个元素取出, 该元素一定是一个 dataFrame. dataFrame 是 gRPC 中定义的一种数据结构, 包含了需要向 client 发送的数据. 一个 dataFrame 的大小可能会超过http2MaxFrameLen
的限制, 最终会被拆分成多个 HTTP2 data frame 发送出去. - 经过流量控制, 确定本次可以发送的数据的大小, 并将要发送的数据存放在
buf
中. - 通过 framer 将数据发送给 client.
- 如果 stream 中还有待发送的数据, 就把 stream 重新放回到
activeStreams
中.
stream 中的数据是怎么存储的
stream 中的数据被存放在一个 itemList
单链表中, 我们来看看 gRPC 是如何实现这个单链表的.
// 链表中的节点
type itemNode struct {
it interface{}
next *itemNode
}
// 一个链表包含了指向链表的头尾指针
type itemList struct {
head *itemNode
tail *itemNode
}
// enqueue 函数
func (il *itemList) enqueue(i interface{}) {
// 构造节点
n := &itemNode{it: i}
// 链表尾指针为空, 说明链表中不存在节点
if il.tail == nil {
// 链表头尾指针都应该指向这个新节点
il.head, il.tail = n, n
return
}
// 链表已经存在节点, 让尾节点的 next 指针指向新节点
il.tail.next = n
// 更新链表的尾指针
il.tail = n
}
// 返回链表中的第一个节点中的数据, 但不移除该节点
func (il *itemList) peek() interface{} {
return il.head.it
}
// 将链表中的第一个节点移除
func (il *itemList) dequeue() interface{} {
// 头指针为空, 返回 nil
if il.head == nil {
return nil
}
// i 指向头节点
i := il.head.it
// 链表头指针指向下一个节点
il.head = il.head.next
// 如果发现链表中没有节点了, 那么更新尾指针为空
if il.head == nil {
il.tail = nil
}
return i
}
// 清空链表
func (il *itemList) dequeueAll() *itemNode {
// 直接让头尾指针为空
h := il.head
il.head, il.tail = nil, nil
return h
}
// 根据头指针判断链表是否为空
func (il *itemList) isEmpty() bool {
return il.head == nil
}
总结
本篇中我们了解了 gRPC server 是如何向 client 发送 HTTP2 frame 的. 可以看到 gRPC server 会循环发送所有 stream 中的数据, 相当于采用了一种 round robin 的策略, 来确保各个 stream 中的数据能够被均等地传输给 client.