grpc-go单连接的设计问题

beego是基于http1.1实现的框架,每个请求会启动单独的goroutine处理,这个goroutine会独占tcp连接,所以无论write请求还是read回复都在单独的goroutine中,所以beego的在请求级别是并发的,而brpc请求的read和process也进行了并发,只是brpc的并发能力考虑到了cpu级别的cache,体现形式是bthread。

这里着重讨论的是grpc-go,前面对beego和brpc进行简单的描述,目的是在并发能力这块grpc-go因为基于http2,有着协议层面的先天的难度,http2为了在web端提升连接的利用率,设计了多路复用,tcp连接的复用导致在框架层面引入流控和请求分frame,这样对于框架层面的并发能力处理也引入了不小的复杂度。

grpc-go当前在sender端做的事主要有几个方面:

  • 新建stream,并向controlBuffer注册这个stream,之后和receiver就通过这个stream交互
  • 发送请求的序列化和压缩,并分frame发送给receiver
  • 监听stream中的chan,等待收到所有receiver的response内容

grpc-go在以上3方面的处理选择了简单的方案,每个连接配置一个controlBuffer(以下称为cb)和一个该buffer的consumer(loopyWriter),一个stream上的frame要求是按序发出,这里的按序就是按照进行write的系统调用,其他交给tcp。grpc-go采用这种简单的模式,天然的让所有frame的进出都是有序的,正确性得到了保证,一句话:什么事都交给cb做。

简单在grpc-go中的弊端就是,在对接tcp连接这块提前利用fifo queue进行了排序,没有把竞争推迟到更下层go标准库Read或者Write这两个方法,linux上历来的竞争越贴近core越好,为什么这么说,linux的调度程序是高度优化过的,你要做的就是把任务拆解为合适的大小(这里要考虑并发粒度和上下文切换的代价,是需要实际压测的),然后把任务交给linux让他在多core的cpu上调度获得并发能力。想象一下,你在上层就把可能并发的任务合并了,肯定会导致并发能力下降,导致整个请求的处理延时上升。

那么,grpc-go中应该怎么把并发能力释放出来呢,设想几个场景:

  • 如果参考beego或者net/http这种基于http1.1的框架(连接独占肯定是不能参考,要不就退化了,h2就没有任何意义了),一个stream的请求发出以及回复等待由单独的goroutine处理,这个goroutine当前已经存在,但只提供并发能力给用户的业务逻辑。请求发出部分是能做到的,共享tcp连接,先发headerFrame,再发dataFrame;回复接受部分,只能单起goroutine从tcp连接读并积累。但这样做有什么问题,grpc-go分frame入cb的目的就是为了要进行流控以及防止连接独占。如果每个stream都尽自己可能去竞争tcp连接的Write,造成的问题是个别stream存在请求波动较大的情况,那stream内部是否可以有节制的使用tcp连接呢?那就要看什么是有节制,单纯看stream-level的流控,是可以进行调整的,但是流控disable的情况下,使用共享的tcp连接资源需要了解其他stream的状态才能更有节制。
  • 通过前一点的描述,有没有更好的方案让stream的处理更加的并发呢?所有请求的发出当前是在cb中汇集,这块是否能让consume queue这个操作并发起来,更快的消耗掉请求,在高并发下,在消耗速度上能通过增加goroutine的数量进行扩展。在使用共享资源的情况下,为了更好的(更均衡/更快,根据系统诉求不一样,设计方式不一样)利用共享资源办事,是需要独立出调度程序的,这里可以把tcp连接想象成共享资源,将frame按照stream分开,stream领取goroutine资源并处理自己的请求发出,并发能力就出来了。

以上讨论的grpc-go性能问题,单纯在说一个场景,交互只涉及到2个点,2点之间的单连接模式是存在设计问题的。

grpc-go单连接的设计问题
Share this