forked from apache/brpc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
gejun
committed
Aug 24, 2017
1 parent
6d96e46
commit af0e449
Showing
4 changed files
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
bthread([代码](https://svn.baidu.com/public/trunk/bthread))是baidu-rpc使用的M:N线程库,目的是在提高程序的并发度的同时,降低编码难度,并在核数日益增多的CPU上提供更好的scalability, | ||
cache | ||
locality。”M:N“是指M个bthread会映射至N个pthread,一般M远大于N。由于linux当下的pthread实现([NPTL](http://en.wikipedia.org/wiki/Native_POSIX_Thread_Library))是1:1的,M个bthread也相当于映射至N个[LWP](http://en.wikipedia.org/wiki/Light-weight_process)。bthread的前身是[DP](http://wiki.babel.baidu.com/twiki/bin/view/Com/Ecom/DistributedProcess)中的[fiber](https://svn.baidu.com/app/ecom/nova/trunk/public/streamfold/fiber/),一个N:1的合作式线程库,等价于event | ||
loop库,但写的是同步代码。 | ||
|
||
# Goals | ||
|
||
- 用户可以延续同步的编程模式,能很快地建立bthread,可以用多种原语同步。 | ||
- bthread所有接口可以在pthread中被调用并有合理的行为,使用bthread的代码可以在pthread中正常执行。 | ||
- 能充分利用多核。 | ||
- 更好的cache locality,更低的延时。 | ||
|
||
# NonGoals | ||
|
||
- 提供pthread的兼容接口,只需链接即可使用。拒绝理由:bthread没有优先级,不适用于所有的场景,链接的方式容易使用户在不知情的情况下误用bthread,造成bug。 | ||
- 修改内核让pthread支持同核快速切换。拒绝理由:拥有大量pthread后,每个线程对资源的需求被稀释了,基于thread-local | ||
cache的代码效果都会很差,比如tcmalloc。而独立的bthread不会有这个问题,因为它最终还是被映射到了少量的pthread。 | ||
|
||
# FAQ | ||
|
||
##### Q:bthread是协程(coroutine)吗? | ||
|
||
不是。我们常说的协程是N:1线程库,即所有的协程都运行于一个系统线程中,计算能力和各种eventloop等价。由于不跨线程,协程之间的切换不需要系统调用,可以非常快(100ns-200ns),受cache | ||
miss的影响也小。但相应的代价是协程无法高效地利用多核,代码必须非阻塞,否则所有的协程都被卡住,对开发者要求苛刻。协程的这个特点使其特别适合写运行时间确定的IO服务器,典型如http | ||
server,在一些精心调试的场景中,可以达到非常高的吞吐。但百度内大部分在线服务的运行时间可不确定,一个缓慢的函数很容易卡住所有的协程。在这点上eventloop是类似的,一个回调卡住整个loop就卡住了,比如ub**a**server(注意那个a,不是ubserver)是公司内对异步框架的尝试,由多个并行的eventloop组成,真实表现糟糕:回调里打日志慢一些,访问下redis,计算重一点,等待中的其他请求就会大量超时。所以这个框架从未流行起来。 | ||
|
||
bthread是一个M:N线程库,一个bthread被卡住不会影响其他bthread。关键技术两点:work | ||
stealing调度和butex,前者让bthread更快地被调度到更多的核心上,后者让bthread和pthread可以相互等待和唤醒。这两点协程都不需要。更多线程的知识查看[这里](http://wiki.baidu.com/display/RPC/Threading+Overview)。 | ||
|
||
##### Q: 我应该在程序中多使用bthread吗? | ||
|
||
不应该。除非你需要在一次RPC过程中[让一些代码并发运行](http://wiki.baidu.com/pages/viewpage.action?pageId=158717037),你不应该直接调用bthread函数,把这些留给baidu-rpc做更好。 | ||
|
||
##### Q:bthread和pthread worker如何对应? | ||
|
||
pthread worker在任何时间只会运行一个bthread,当前bthread挂起时,pthread | ||
worker先尝试从本地(runqueue)弹出一个待运行的bthread,若没有,则随机偷另一个worker的待运行bthread,仍然没有就睡眠在一个futex上,该futex会在任一worker有新的待运行bthread时被唤醒。 | ||
|
||
##### Q:bthread中能调用阻塞的pthread或系统函数吗? | ||
|
||
可以,只阻塞当前pthread worker。其他pthread worker不受影响。 | ||
|
||
##### Q:一个bthread阻塞会影响其他bthread吗? | ||
|
||
不影响。若bthread因bthread API而阻塞,它会把当前pthread worker让给其他bthread。若bthread因pthread | ||
API或系统函数而阻塞,当前pthread worker上待运行的bthread会被其他空闲的pthread worker偷过去运行。 | ||
|
||
##### Q:pthread中可以调用bthread API吗? | ||
|
||
可以。bthread | ||
API在bthread中被调用时影响的是当前bthread,在pthread中被调用时影响的是当前pthread。使用bthread | ||
API的代码可以直接运行在pthread中。 | ||
|
||
##### Q:若有大量的bthread调用了阻塞的pthread或系统函数,会影响RPC运行么? | ||
|
||
会。比如有8个pthread | ||
worker,当有8个bthread都调用了系统usleep()后,处理网络收发的RPC代码就暂时无法运行了。只要阻塞时间不太长, | ||
这一般没什么影响, 毕竟worker都用完了, 除了排队也没有什么好方法. | ||
在baidu-rpc中用户可以选择调大worker数来缓解问题, | ||
在server端可设置[ServerOptions.num_threads](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-worker线程数)或[-bthread_concurrency](http://brpc.baidu.com:8765/flags/bthread_concurrency), | ||
在client端可设置[-bthread_concurrency](http://brpc.baidu.com:8765/flags/bthread_concurrency). | ||
|
||
那有没有完全规避的方法呢? | ||
|
||
- 一个容易想到的方法是动态增加worker数. 但实际可能很糟糕, 当大量的worker同时被阻塞时, | ||
它们很可能在等待同一个资源(比如同一把锁), 增加worker可能只是增加了更多的等待者. | ||
- 大部分RPC框架采取的方法是区分io线程和worker线程, io线程专门处理收发, worker线程调用用户逻辑, | ||
即使worker线程全部阻塞也不会影响io线程. 但这个方法使得每个请求都要从io线程跳转至worker线程, | ||
增加了一次上下文切换, 在机器繁忙时, 切换都有一定概率无法被及时调度, 会导致更多的延时长尾. | ||
另一个问题是增加一层处理环节(io线程)并不能缓解拥塞, 如果worker线程全部卡住, 程序仍然会卡住, | ||
只是卡的地方从socket缓冲转移到了io线程和worker线程之间的消息队列. 换句话说, 在worker卡住时, | ||
还在运行的io线程做的可能是无用功. 事实上, 这正是上面提到的"没什么影响"真正的含义. | ||
- 一个实际的解决方法是[限制最大并发](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-限制最大并发), | ||
只要同时被处理的请求数低于worker数, 自然可以规避掉"所有worker被阻塞"的情况. | ||
- 另一个解决方法当被阻塞的worker超过阈值时(比如8个中的6个), 就不在原地调用用户代码了, | ||
而是扔到一个独立的线程池中运行. 这样即使用户代码全部阻塞, 也总能保留几个worker处理rpc的收发. | ||
不过目前bthread模式并没有这个机制, | ||
但类似的机制在[打开pthread模式](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-pthread模式)时已经被实现了. | ||
那像上面提到的, 这个机制是不是在用户代码都阻塞时也在做"无用功"呢? 可能是的. | ||
但这个机制更多是为了规避在一些极端情况下的死锁, 比如所有的用户代码都lock在一个pthread mutex上, | ||
并且这个mutex需要在某个RPC回调中unlock, 如果所有的worker都被阻塞, 那么就没有线程来处理RPC回调了, | ||
整个程序就死锁了. 虽然绝大部分的RPC实现都有这个潜在问题, 但实际出现频率似乎很低, | ||
只要养成不在锁内做RPC的好习惯, 这是完全可以规避的. | ||
|
||
##### Q:bthread会有[Channel](https://gobyexample.com/channels)吗? | ||
|
||
不会。channel代表的是两点间的关系,而很多现实问题是多点的,这个时候使用channel最自然的解决方案就是:有一个角色负责操作某件事情或某个资源,其他线程都通过channel向这个角色发号施令。如果我们在程序中设置N个角色,让它们各司其职,那么程序就能分类有序地运转下去。所以使用channel的潜台词就是把程序划分为不同的角色。channel固然直观,但是有代价:额外的上下文切换。做成任何事情都得等到被调用处被调度,处理,回复,调用处才能继续。这个再怎么优化,再怎么尊重cache | ||
locality,也是有明显开销的。另外一个现实是:用channel的代码也不好写。由于业务一致性的限制,一些资源往往被绑定在一起,所以一个角色很可能身兼数职,但它做一件事情时便无法做另一件事情,而事情又有优先级。各种打断、跳出、继续形成的最终代码异常复杂。 | ||
|
||
我们需要的往往是buffered | ||
channel,扮演的是队列和有序执行的作用,bthread在r31345之后提供[ExecutionQueue](http://wiki.baidu.com/pages/viewpage.action?pageId=160291992),可以完成这个目的。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
baidu-rpc提供了[异步接口](http://wiki.baidu.com/pages/viewpage.action?pageId=213828685#id-创建和访问Client-异步访问),所以一个常见的问题是:我应该用异步接口还是bthread? | ||
|
||
短回答:延时不高时你应该先用简单易懂的同步接口,不行的话用异步接口,只有在需要多核并行计算时才用bthread。 | ||
|
||
# 同步或异步 | ||
|
||
异步即用回调代替阻塞,有阻塞的地方就有回调。虽然在javascript这种语言中回调工作的很好,接受度也非常高,但只要你用过,就会发现这和我们需要的回调是两码事,这个区别不是[lambda](https://en.wikipedia.org/wiki/Anonymous_function),也不是[future](https://en.wikipedia.org/wiki/Futures_and_promises),而是javascript是单线程的。javascript的回调放到多线程下可能没有一个能跑过,race | ||
condition太多了,单线程的同步方法和多线程的同步方法是两个世界。那是不是服务能搞成类似的形式呢?多个线程,每个都是独立的eventloop。可以,ub**a**server就是(注意带a),但实际效果糟糕,因为阻塞改回调可不简单,当阻塞发生在循环,条件分支,深层子函数中时,改造特别困难,况且很多老代码、第三方代码你根本不可能去改造。结果是代码中会出现不可避免的阻塞,导致那个线程中其他回调都被延迟,流量超时,server性能不符合预期。如果你说,”我想把现在的同步代码改造为大量的回调,除了我其他人都看不太懂,并且性能可能更差了”,我猜大部分人不会同意。别被那些鼓吹异步的人迷惑了,他们写的是从头到尾从下到上全异步且不考虑多线程的代码,和你要写的完全是两码事。 | ||
|
||
baidu-rpc中的异步和单线程的异步是完全不同的,异步回调会运行在与调用处不同的线程中,你会获得多核扩展性,但代价是你得意识到多线程问题。你可以在回调中阻塞,只要线程够用,对server整体的性能并不会有什么影响。不过异步代码还是很难写的,所以我们提供了[组合访问](http://wiki.baidu.com/pages/viewpage.action?pageId=213828709)来简化问题,通过组合不同的channel,你可以声明式地执行复杂的访问,而不用太关心其中的细节。 | ||
|
||
当然,延时不长,qps不高时,我们更建议使用同步接口,这也是我们创建bthread的动机:维持同步代码也能提升交互性能。 | ||
|
||
Icon | ||
|
||
判断使用同步或异步:计算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这个工具,用户甚至可以自己实现异步。以“半同步”为例,在baidu-rpc中用户有多种选择: | ||
|
||
- 发起多个异步RPC后挨个Join,这个函数会阻塞直到RPC结束。(这儿是为了和bthread对比,实现中我们建议你使用[ParallelChannel](http://wiki.baidu.com/pages/viewpage.action?pageId=213828709#id-组合访问-ParallelChannel),而不是自己Join) | ||
- 启动多个bthread各自执行同步RPC后挨个join bthreads。 | ||
|
||
哪种效率更高呢?显然是前者。后者不仅要付出创建bthread的代价,在RPC过程中bthread还被阻塞着,不能用于其他用途。 | ||
|
||
Icon | ||
|
||
如果仅仅是为了并发RPC,别用bthread。 | ||
|
||
|
||
|
||
不过当你需要并行计算时,问题就不同了。使用bthread可以简单地构建树形的并行计算,充分利用多核资源。比如检索过程中有三个环节可以并行处理,你可以建立两个bthread运行两个环节,在原地运行剩下的环节,最后join那两个bthread。过程大致如下: | ||
|
||
这么实现的point: | ||
|
||
- 你当然可以建立三个bthread分别执行三个部分,最后join它们,但相比这个方法要多耗费一个线程资源。 | ||
- bthread从建立到执行是有延时的(调度延时),在不是很忙的机器上,这个延时的中位数在3微秒左右,90%在10微秒内,99.99%在30微秒内。这说明两点: | ||
- 计算时间超过1ms时收益比较明显。如果计算非常简单,几微秒就结束了,用bthread是没有意义的。 | ||
- 尽量让原地运行的部分最慢,那样bthread中的部分即使被延迟了几微秒,最后可能还是会先结束,而消除掉延迟的影响。并且join一个已结束的bthread时会立刻返回,不会有上下文切换开销。 | ||
|
||
另外当你有类似线程池的需求时,像执行一类job的线程池时,也可以用bthread代替。如果对job的执行顺序有要求,你可以使用基于bthread的[ExecutionQueue](http://wiki.baidu.com/pages/viewpage.action?pageId=160291992)。 |
Oops, something went wrong.