grpc-go性能优化(一)

HandleStreams是所有frame的入口,grpc-go当前处理的frame类型分为6种,下面分类型讨论下怎么让frame中的互相干扰逻辑并发起来。

MetaHeadersFrame

请求的第一个frame,第一步是解析header中的内容,解析过程耗时固定,不能分离到goroutine中,但是当解析出错,就用到controlbuf,这里是第一个点,这里给到controlbuf的是一个cleanupStream,loopyWriter这个对外接口人负责处理,就是向外发一个RST_STREAM,receiver端还要从transport全局的map中摘除这个frame,这一个put操作还是会收到loopyWriter的干扰,但本身两者可以尽量没有关系,这个点一定是不被允许的,虽然出现的几率很小,但是大型高并发线上服务一旦出现波动是很难重现和调试的。cleanupStream是异步被处理,虽然尽快处理是理想的,这时候就要在几种方法下tradeoff:

  • rst操作保留在read流程,rst操作是要通过conn的Write写出,这里存在竞争,这块依赖于response的写出竞争程度,不可靠
  • rst操作通过lock-free queue分离,最终在loopyWriter中发出
  • rst操作通过wait-free queue分离,最终也在loopyWriter发出(queue实现难度大,要求操作系统环境苛刻)
  • rst操作是因为header内容的解析,header的解析单独分离到stream goroutine中,但要承担goroutine生成的 & 调度的代价,不好评估

这里选择一个固定的服务器环境,对同样的压力请求下,进行压测&打点,看框架的稳定程度,这块怎么判断框架的稳定程度可能才是核心,不能光在理论上探索,不过理论肯定是第一步,否则就是瞎测试了,大部分压测都是如此,只要不是大型网络应用的大公司,都存在这个问题。

inTapHandle报错或者连接并发stream的id超过最大有符号int,也会触发rst,和上面同样处理。

一个conn上的stream都会放到activeStreams中,http1.1框架一般有新req直接在goroutine全部处理,但是http2因为有流控,流控有两个级别:

  • connection-level
  • stream-level

看grpc-go的处理方式就是在loopyWriter在向外写数据(写前减 & 写后增)的时候进行增减,stream也是在这里增加控制,这里有个常见的设计但是大家都会选择忽略或者认为这么设计又合乎常理,但却从未考虑过为什么:http client通过连接数限制与服务器的连接数,这个连接数分为整体连接总量和跟每个host保持的连接数。

  • 上限,client和server单conn交互所需的时间,在网络稳定的情况下,通常所描述的网络波动,是因为网络被共用,部分使用方没有按照规矩发送过多数据,还有可能是中间硬件有物理问题,原因很复杂,所以网络在小范围内波动就没问题。根据上述的交互时间估计出需要的保持的conn总数,这是上限,可以适当调大,防止网络偶尔的波动。
  • 双向保护,使用这么多的conn,client和server是否能有效操作这么多conn,会在上限基础上减去一部分,这个通常是实践测试所得,这数据c和s的conn处理能力。c和s的承受能力分开来看,一般肯定是不一样的,或者c的设置是以s一定能满足需求的背景下得出的。

所以连接总量是双向保护和上限中的最小值,跟每个host保持的连接数只有第二点就是要保护单个服务器,因为服务器一般是多个进程组成。

那么http2流控是否和上面的设计有对照关系呢?

connection-level是上限,但不是conn总数,而是两端发送数据的某时间段内(下次更改是在pingAck之后,根据计算得到的bdp调整)上限,所有stream要在这个框架内调整发送量。

stream-level是双向保护,但保护的对象不是server,而是保护其他stream的frame传输。

grpc-go在这块始终保持两个level的数字是一样的,应该是没有更好的实现方案,在https://httpwg.org/specs/rfc7540.html 对于流控没有明确两个level的关系是怎么样的。

header的最后环节,因为grpc-go使用loopyWriter统一处理一个conn上的所有response写出,所以在这里也要有每个stream的容器&上下文,stream处理的首尾都依赖单goroutine,这块个人觉得grpc-go的处理过于倾向于实现的简单性,由于单conn写出的高效性,所以一般的服务也没大问题。stream用户逻辑的部分是提供了并发性的,但除此之外都是单goroutine在与conn对接。==所以stream的处理逻辑肯定是要收敛到自己内部的。

DataFrame

data的处理主要是汇聚数据给相应的stream goroutine,识别frame的EOF,data的流转都要通知给bdp预估模块(这个功能也搞到主read流程,很奇怪,bdp中也有lock),整个的请求处理流程到处是lock。

这块针对bdp模块的数据收集,要改下实现方案,请求处理才是框架的核心,通过lock-free的queue把消息发出去这里预估的数据是要应用到整个transport的。

数据处理frame,只要想办法把bdp和流控异步出去就没问题了。

RSTStreamFrame

这个命令通过控制goroutine传入stream,stream内部只处理请求

SettingsFrame

同上

PingFrame

同上

WindowUpdateFrame

同上

貌似唯一需要特别关注的就是header和data,只在header进行了一些讨论。下面就要来实践了。

grpc-go性能优化(一)
Share this