brpc线程模型学习(转)

转自:https://blog.csdn.net/okiwilldoit/article/details/82755526

 

bthread是brpc使用的M:N线程库,目的是在提高程序的并发度的同时,降低编码难度,并在核数日益增多的CPU上提供更好的scalability和cache locality。

”M:N“是指M个bthread会映射至N个pthread,一般M远大于N。

由于linux当下的pthread实现(NPTL)是1:1的,M个bthread也相当于映射至N个LWP。bthread的前身是Distributed Process(DP)中的fiber,一个N:1的合作式线程库,等价于event-loop库,但写的是同步代码。

一.常见线程模型
1. 单线程reactor
N:1线程库,其处理流程如下所示:

 

 

缺点:
(1)各个回调之间会相互影响,如上图的callback2和callback3必须要等到callback1完成后才能进行。
(2)无法利用多核。
优点:无需加锁。

2. 多线程reactor
M:N线程库,其处理流程如下所示:

 

 

可以利用多核,可以共享地址空间,具有较好的性能,但并不能获得线性于CPU核数的性能扩展;

Removable by ET:使用边缘触发能减少就绪fd的数量,来降低开销;
Highly contended:reactor线程既要处理连接请求,又要处理线程池中线程的读写请求;
Cache bouncing:一个请求的处理会跨越不同的线程,cpu cache同步是微秒级的操作。

二.协程(coroutine)介绍
我们常说的协程特指N:1线程库,即所有的协程运行于一个系统线程中,计算能力和各类eventloop库等价。

由于不跨线程,协程之间的切换不需要系统调用,可以非常快(100ns-200ns),受cache一致性的影响也小。但代价是协程无法高效地利用多核,代码必须非阻塞,否则所有的协程都被卡住,对开发者要求苛刻。

协程的这个特点使其适合写运行时间确定的IO服务器,典型如http server,在一些精心调试的场景中,可以达到非常高的吞吐。

但百度内大部分在线服务的运行时间并不确定,且很多检索由几十人合作完成,一个缓慢的函数会卡住所有的协程。在这点上eventloop是类似的,一个回调卡住整个loop就卡住了,比如ubaserver(注意那个a,不是ubserver)是百度对异步框架的尝试,由多个并行的eventloop组成,真实表现糟糕:回调里打日志慢一些,访问redis卡顿,计算重一点,等待中的其他请求就会大量超时。

三. bthread介绍
bthread是一个M:N线程库,一个bthread被卡住不会影响其他bthread。
关键技术两点: work stealing调度和butex。前者让bthread更快地被调度到更多的核心上。后者让bthread和pthread可以相互等待和唤醒。
因为单线程原因,这两点协程都不需要。
(1)优点:
• 用户可以延续同步的编程模式,能在数百纳秒内建立bthread,可以用多种原语同步。
• bthread所有接口可在pthread中被调用并有合理的行为,使用bthread的代码可以在pthread中正常执行。
• 能充分利用多核。
(2)特征:

1. bthread和pthread worker如何对应?
pthread worker在任何时间只会运行一个bthread,当前bthread挂起时,pthread worker先尝试从本地runqueue弹出一个待运行的bthread,若没有,则随机偷另一个worker的待运行bthread,仍然没有才睡眠并会在有新的待运行bthread时被唤醒。

 

 


2. bthread中能调用阻塞的pthread或系统函数吗?
可以,只阻塞当前pthread worker。其他pthread worker不受影响。

3. 一个bthread阻塞会影响其他bthread吗?
不影响。若bthread因bthread API而阻塞,它会把当前pthread worker让给其他bthread。若bthread因pthread API或系统函数而阻塞,当前pthread worker上待运行的bthread会被其他空闲的pthread worker偷过去运行。

4. pthread中可以调用bthread API吗?
可以。bthread API在bthread中被调用时影响的是当前bthread,在pthread中被调用时影响的是当前pthread。使用bthread API的代码可以直接运行在pthread中。

5. 若有大量的bthread调用了阻塞的pthread或系统函数,会影响RPC运行么?
会。比如有8个pthread worker,当有8个bthread都调用了系统usleep()后,处理网络收发的RPC代码就暂时无法运行了。只要阻塞时间不太长, 这一般没什么影响, 毕竟worker都用完了, 除了排队也没有什么好方法. 在brpc中用户可以选择调大worker数来缓解问题, 在server端可设置ServerOptions.num_threads或-bthread_concurrency, 在client端可设置-bthread_concurrency.

那有没有完全规避的方法呢?

一个实际的解决方法是限制最大并发, 只要同时被处理的请求数低于worker数, 自然可以规避掉"所有worker被阻塞"的情况。
另一个解决方法当被阻塞的worker超过阈值时(比如8个中的6个), 就不在原地调用用户代码了, 而是扔到一个独立的线程池中运行. 这样即使用户代码全部阻塞, 也总能保留几个worker处理rpc的收发。
四. 判断使用同步或异步
计算qps * latency(in seconds),如果和cpu核数是同一数量级,就用同步,否则用异步。
比如:
• qps = 2000,latency = 10ms,计算结果 = 2000 * 0.01s = 20。和常见的32核在同一个数量级,用同步。
• qps = 100, latency = 5s, 计算结果 = 100 * 5s = 500。和核数不在同一个数量级,用异步。
• qps = 500, latency = 100ms,计算结果 = 500 * 0.1s = 50。基本在同一个数量级,可用同步。如果未来延时继续增长,考虑异步。

这个公式计算的是并发数,和线程数,cpu核数是可比的。

当这个值远大于cpu核数时,说明大部分操作并不耗费cpu,而是让大量线程阻塞着,使用异步可以明显节省线程资源(栈占用的内存)。
当这个值小于或和cpu核数差不多时,异步能节省的线程资源就很有限了,这时候简单易懂的同步代码更重要。
五. bthread应用场景,bthread或异步?
协程适合的场景:适合写运行时间确定的IO服务器,典型如http server,在一些精心调试的场景中,可以达到非常高的吞吐。
异步即用回调代替阻塞,有阻塞的地方就有回调。由于在协程在io阻塞时,会进行切换,io完成后,又回调,它是一种异步处理模式。
brpc中的异步和单线程的异步是完全不同的,异步回调会运行在与调用处不同的线程中,你会获得多核扩展性,但代价是你得意识到多线程问题。
有了bthread这个工具,用户甚至可以自己实现异步。以“半同步”为例,在brpc中用户可以这样使用:发起多个异步RPC后挨个Join,这个函数会阻塞直到RPC结束。

结论:
延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用bthread。

延时不长,qps不高时,我们更建议使用同步接口,这也是创建bthread的动机:维持同步代码也能提升交互性能。
如果仅仅是为了并发RPC,别用bthread。
————————————————
版权声明:本文为CSDN博主「okiwilldoit」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/okiwilldoit/article/details/82755526

posted @ 2021-08-12 17:17  鸭子船长  阅读(691)  评论(0编辑  收藏  举报