如何使用brpc实现run-to-completion模型?
按我的理解,bthread是brpc设计的核心,而正如bthread or not中提到的,由于bthread本身的开销大概在10us左右,如果计算任务只要不足10us那么使用bthread就是一件不太值得的事情。
目前存储IO在闪存介质上已经进入到了10-100us的性能水平,这还是底层IO的耗时,如果使用SPDK等异步IO框架,实际上在io submit这一阶段的耗时已经远远低于10us了,所以业界普遍采用了在一个线程内使用run-to-completion的模型来处理如下某个用户请求所涉及的各个步骤:
- 查询RDMA接收/发送队列是否有报文要处理,如果有则在当前线程直接处理
- 查询SPDK的completion queue是否有任务处理, 如果有则在当前线程直接处理
- 任务如果有IO需求,则提交给SPDK队列后返回
- 任务结束后提交回应给RDMA队列后返回
整个的操作是在一个线程上运行的,虽然不能利用多核的处理能力,但单核的处理能力可以跑满,在保证上述多个步骤都以极低延迟进行时,整个请求的处理时间将非常短。
我们在使用brpc的过程中确实遇到了一些问题,在将某个任务从接入层传递给存储层时至少消耗10-20us的bthread开销,由于我们追求极低的存储延迟,这是不可接受的。而这一情况在我们使用-usercode_in_pthread后也没有好转。
所以如果我们希望在brpc框架上使用run-to-completion模型的话,官方是否有建议的方式呢?我们计划的方式是在接入层收到请求后(此时还是运行在bthread中),将创建的状态机扔进一个高性能的消息队列TaskQueue,我们会为每一个core创建一个pthread来looping这个TaskQueue,至少除了数据接收/发送(RDMA处理)部分以外的其他部分都可以运行在一个thread内采用run-to-completion的方式完成。
还有一种方式,我们想基于brpc实现一种poller的注册机制,比如braft中,各个apply的task可以注册到我们的poller thread上,当事件触发后在poller thread上进行原地调用,但是目前来看对源码的改动会比较大。
很想倾听下百度的朋友在这个问题上的解决方案,谢谢。
同样的需求。希望能够实现这样一种模型:brpc的每个io thread上实现run-to-completion模型,rpc call 不推到worker thread,把灵活性留个用户,用户可以自己控制什么时候切线程,控制线程的负载均衡,自己保证调用都是非阻塞的,这样用户能获得极致的性能,还可以在线程上集成N:1的协程。对于追求us级别极致延迟的服务很有用。 不知道这个模型在brpc中如何实现?brpc在线程模型的策略上是可以定制扩展的吗?有兴趣的朋友可以讨论一下。
同样的需求。希望能够实现这样一种模型:brpc的每个io thread上实现run-to-completion模型,rpc call 不推到worker thread,把灵活性留个用户,用户可以自己控制什么时候切线程,控制线程的负载均衡,自己保证调用都是非阻塞的,这样用户能获得极致的性能,还可以在线程上集成N:1的协程。对于追求us级别极致延迟的服务很有用。 不知道这个模型在brpc中如何实现?brpc在线程模型的策略上是可以定制扩展的吗?有兴趣的朋友可以讨论一下。
这个也许可以在启动的那个bthread上扩展一下bthread_attr_t,通过特殊flag让bthread原地运行。不过brpc整体上是为偏应用层面的场景设计的,如果是底层存储(如虚机云磁盘)或是高频交易等特别在意延时的场景,要冲击硬件极限估计有点困难,从epoll/kqueue直接开始写可能上限更高。不过,我也想说明一下,如果实际执行的worker代码不能做到完全异步(linux的IO就不行),其实在一个线程中从头到尾运行未必是最优的,也许在测试环境中不卡不拥塞时各种指标很好,但实际运行时不一定完全是这样。
同样的需求。希望能够实现这样一种模型:brpc的每个io thread上实现run-to-completion模型,rpc call 不推到worker thread,把灵活性留个用户,用户可以自己控制什么时候切线程,控制线程的负载均衡,自己保证调用都是非阻塞的,这样用户能获得极致的性能,还可以在线程上集成N:1的协程。对于追求us级别极致延迟的服务很有用。 不知道这个模型在brpc中如何实现?brpc在线程模型的策略上是可以定制扩展的吗?有兴趣的朋友可以讨论一下。
这个也许可以在启动的那个bthread上扩展一下bthread_attr_t,通过特殊flag让bthread原地运行。不过brpc整体上是为偏应用层面的场景设计的,如果是底层存储(如虚机云磁盘)或是高频交易等特别在意延时的场景,要冲击硬件极限估计有点困难,从epoll/kqueue直接开始写可能上限更高。不过,我也想说明一下,如果实际执行的worker代码不能做到完全异步(linux的IO就不行),其实在一个线程中从头到尾运行未必是最优的,也许在测试环境中不卡不拥塞时各种指标很好,但实际运行时不一定完全是这样。
谢谢回复,如果可以让bthread在指定的线程原地运行,不跨线程,是不是就相当于N:1的协程了?这的确和业务场景有关。RPC的Interface用起来还是很舒服省心的,从epoll开始写做到生产级别稳定好用也并非易事。业内有一些实践已经证明,用户态写裸盘可以做到完全无阻塞调用,在每个IO线程中集成run-to-completion线程模型,one loop per thread,很适合高性能低延迟的场景,毕竟一次nvme io已经不足10 us了,这个尺度下线程调度加上cache miss的综合开销显得就很大了。
我理解在现有的M:N模型上,每个worker线程都运行一个非阻塞的epoll_wait或者其他的reactor,保证这个worker始终是运行着的,然后把signal_task这个功能禁用掉,其实就变成了bthread在一个worker上调度执行了。这时候steal_task就变成了一个被动的过程,一些worker确实空闲了就尝试去偷取一些其它worker上的任务。