grpc-go脑爆-流控

流控是不是导致单连接中stream处理耗时不稳定的原因。

在http1.1中,连接是请求独占,没有流量控制,请求分包发送直接写入tcp连接中,原理简单些。

http2,随着连接建立持续时间的推移,流控导致sender发送速度和receiver处理速度相匹配,可以想象一下假设h2没有流控,所有stream争相写入tcp连接。

注意在该场景下个别stream在receiver的处理速度较慢会不会干扰到同一连接的其他stream的处理,

首先,对receiver处理速度进行细致的描述,receiver的处理要具体分为框架内和框架外两种不同的场景:

框架内,单goroutine从framer中读取Frame,写入相应stream的recvBuffer,从headerFrame收到开始点建立的Stream一直block在recvBuffer的Read上,一旦发现io.EOF,迅速往下走,交给app层处理。

面对这种场景,首先是懵逼的,那么stream的流控到底在处理app层处理慢的stream上有什么措施,这里就需要对流控中WINDOW_UPDATE在grpc-go中receiver端的比较细致把控才可以。

receiver触发窗口调节的第一个为止是stream的Read,这是app层在要数据,所以这个地方的窗口增大数值和初始化grpc server时设置的最大接收字节数有直接关系,具体是怎么算的呢?连接建立两个level的流控初始窗口都是64k,stream的这个数值可以通过SETTINGS在建立连接的时候进行设置,conn的这个数值必须通过WINDOW_UPDATE进行调节,上面这两个设定还没有参透设计的缘由,暂时不理。初始stream窗口是64k,这也是grpc-go endpoints之间的一个共识,就相当于程序的默认值,不过不能低于这个大小,现在假设我们的sender可能发送比较大的数据类似1G,那么我们就将server的最大接收字节数设为1G。注意这块有个容易犯的主观臆断,increment是对window的增加,实际上这个increment是在初始window大小的框框内归还sender的quota

stream建立后,第一个影响窗口的因素出现了,用户设置的receiver最大可接收的字节数,新stream会block在Read方法,等待所有frame的到达,Read之前会根据这个设定调节一次sender的窗口。这个调节和初始stream窗口都是属于receiver内部做的,根据配置仅仅能做这些。

还有一出调节是在io.ReadFull内部触发的,推测下层tcp连接的Read每次只读部分数据,ReadFull就是要一直读到EOF才能满足,每次Read操作代表数据从tcp连接buffer到app层,之前对于pendingData和pendingUpdate的界定一直比较模糊,现在清楚了。那么接下来的问题是,为什么每次操作都需要更新窗口?应该是为了探测读取到应用层的字节数是否超过limit的1/4,当被读取到app层的字节多了之后,接收数据的buffer已经空出来了,不在占用pendingData的空间,那么就找一个触发点即不太频繁,又能比较保守的防止不及时告诉sender归还quota带来的延时,只能说1/4是个神奇的数字,应该是通过实践得出的。

影响receiver定义sender发送窗口的因素不只一个,之前提到过很多次BDP,但没有深入讨论,BDP是当前endpoints之间的带宽能力的一个评估数值,想象一下,endpoints之间的链路被字节占满,但每个字节又在链路上不停留的一直干到应用层,当所有自己都处于理想状态的时候,BDP的值是最理想的准确值,BDP的计算从每次收到dataFrame就发出ping开始,收到ping的ack结束,期间收集所有收到的dataFrame的大小,除以1.5rtt,如果计算得到的bdp满足一些条件,就更新所有stream和连接本身的limit为当前dbp。也就是说bdp是一种更先进的,根据endpoints之间网络传输状况动态调整window的机制。该机制是否能迅速让sender充分利用网络与receiver交互,会干扰到stream的frame的传递速度。这块也是需要探测的点,但是这是receiver在grpc内部对sender做出的干扰。不能算是框架内处理。

话题扯远了。

框架外的情况比较简单,就是用户实现的业务逻辑是否最优,cpu/内存的使用等等。不做赘述。

所以stream在框架外是单独的goroutine在处理,cpu竞争条件和负载良好的情况下,可以想象不存在资源竞争,不会互相干扰。但是框架内部同一连接读取frame是共享单goroutine,endpoints之间的bdp是有上限的,那么bdp中如果某个stream占用过多,可能间接干扰其他stream能占用bdp的概率,可能会存在竞争tcp连接资源的情况。那么流控到底为这种情况带来了什么?

上面已经描述了,流控初始连接建立阶段会有个初始窗口,每次receiver读取数据会根据当前已经传递给app层多少,把这些quota还给sender,但不会调节整体的窗口大小。窗口大小是在每次计算bdp时看当前连接是否能承载更高数据传输量进行调节的,所以bdp导致的窗口调节,是针对连接,但是stream和连接的窗口是保持一致的。

说了半天废话,发现流控的本质是维持连接的可用性和效率。如果没有流控,sender大量产生数据到tcp连接,让连接出现拥堵,拥堵的概念是一旦注入过多数据到tcp连接,性能开始变差(超出app耗时容忍范围)就算是拥塞,这种情况是持续的,导致endpoints在稳定的超出容忍范围的流量下,长时间不可用。处理上面的情况就是流控的意义。

grpc-go脑爆-流控
Share this