Commit dc706073 authored by chenzhangyi01's avatar chenzhangyi01

Add docs for server and builtin services

Change-Id: I98123c4b7ac814c2248fe7e85e393aaecd83e2d9
parent d5b90801
“雪崩”指的是访问服务集群时绝大部分请求都超时,且在流量减少时仍无法恢复的现象。下面解释这个现象的来源。
当流量超出服务的最大qps时,服务将无法正常服务;当流量恢复正常时(小于服务的处理能力),积压的请求会被处理,虽然其中很大一部分可能会因为处理的不及时而超时,但服务本身一般还是会恢复正常的。这就相当于一个水池有一个入水口和一个出水口,如果入水量大于出水量,水池子终将盛满,多出的水会溢出来。但如果入水量降到出水量之下,一段时间后水池总会排空。雪崩并不是单一服务能产生的。
如果一个请求经过两个服务,情况就有所不同了。比如请求访问A服务,A服务又访问了B服务。当B被打满时,A处的client会大量超时,如果A处的client在等待B返回时也阻塞了A的服务线程(常见),且使用了固定个数的线程池(常见),那么A处的最大qps就从**线程数 / 平均延时**,降到了**线程数 / 超时**。由于超时往往是平均延时的3~4倍,A处的最大qps会相应地下降3~4倍,从而产生比B处更激烈的拥塞。如果A还有类似的上游,拥塞会继续传递上去。但这个过程还是可恢复的。B处的流量终究由最前端的流量触发,只要最前端的流量回归正常,B处的流量总会慢慢降下来直到能正常回复大多数请求,从而让A恢复正常。
但有两个例外:
1. A可能对B发起了过于频繁的基于超时的重试。这不仅会让A的最大qps降到**线程数 / 超时**,还会让B处的qps翻**重试次数**倍。这就可能陷入恶性循环了:只要**线程数 / 超时 \* 重试次数**大于B的最大qps**,**B就无法恢复 -> A处的client会继续超时 -> A继续重试 -> B继续无法恢复。
2. A或B没有限制某个缓冲或队列的长度,或限制过于宽松。拥塞请求会大量地积压在那里,要恢复就得全部处理完,时间可能长得无法接受。由于有限长的缓冲或队列需要在填满时解决等待、唤醒等问题,有时为了简单,代码可能会假定缓冲或队列不会满,这就埋下了种子。即使队列是有限长的,恢复时间也可能很长,因为清空队列的过程是个追赶问题,排空的时间取决于**积压的请求数 / (最大qps - 当前qps)**,如果当前qps和最大qps差的不多,积压的请求又比较多,那排空时间就遥遥无期了。
了解这些因素后可以更好的理解baidu-rpc中相关的设计。
1. 拥塞时A服务最大qps的跳变是因为线程个数是**硬限**,单个请求的处理时间很大程度上决定了最大qps。而baidu-rpc server端默认在bthread中处理请求,个数是软限,单个请求超时只是阻塞所在的bthread,并不会影响为新请求建立新的bthread。baidu-rpc也提供了完整的异步接口,让用户可以进一步提高io-bound服务的并发度,降低服务被打满的可能性。
2. baidu-rpc中[重试](http://wiki.baidu.com/pages/viewpage.action?pageId=213828685#id-创建和访问Client-重试)默认只在连接出错时发起,避免了流量放大,这是比较有效率的重试方式。如果需要基于超时重试,可以设置[backup request](http://wiki.baidu.com/pages/viewpage.action?pageId=213828685#id-创建和访问Client-backuprequest),这类重试最多只有一次,放大程度降到了最低。baidu-rpc中的RPC超时是deadline,超过后RPC一定会结束,这让用户对服务的行为有更好的预判。在之前的一些实现中,RPC超时是单次超时*重试次数,在实践中容易误判。
3. baidu-rpc server端的[max_concurrency选项](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-限制最大并发)控制了server的最大并发:当同时处理的请求数超过max_concurrency时,server会回复client错误,而不是继续积压。这一方面在服务开始的源头控制住了积压的请求数,尽量避免延生到用户缓冲或队列中,另一方面也让client尽快地去重试其他server,对集群来说是个更好的策略。
对于baidu-rpc的用户来说,要防止雪崩,主要注意两点:
1. 评估server的最大并发,设置合理的max_concurrency值。这个默认是不设的,也就是不限制。无论程序是同步还是异步,用户都可以通过 **最大qps \* 非拥塞时的延时**(秒)来评估最大并发,原理见[little's law](https://en.wikipedia.org/wiki/Little),这两个量都可以在baidu-rpc中的内置服务中看到。max_concurrency与最大并发相等或大一些就行了。
2. 注意考察重试发生时的行为,特别是在定制RetryPolicy时。如果你只是用默认的baidu-rpc重试,一般是安全的。但用户程序也常会自己做重试,比如通过一个Channel访问失败后,去访问另外一个Channel,这种情况下要想清楚重试发生时最差情况下请求量会放大几倍,服务是否可承受。
\ No newline at end of file
# 什么是内置服务?
内置服务以多种形式展现服务器内部状态,提高你开发和调试服务的效率。baidu-rpc通过HTTP协议提供内置服务,可通过浏览器或curl访问,服务器会根据User-Agent返回纯文本或html,你也可以添加?console=1要求返回纯文本。我们在自己的开发机上启动了[一个长期运行的例子](http://brpc.baidu.com:8765/),你可以点击后随便看看。服务端口只有在8000-8999内才能被笔记本访问到,对于范围之外的端口可以使用[rpc_view](http://wiki.baidu.com/display/RPC/rpc_view)或在命令行中使用curl <SERVER-URL>
从浏览器访问:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-9-27%2023%3A45%3A26.png?version=1&modificationDate=1443368728000&api=v2)
从命令行访问:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-9-27%2023%3A46%3A48.png?version=1&modificationDate=1443368810000&api=v2)
# 安全模式
出于安全考虑,直接对外服务需要关闭内置服务(包括经过nginx或其他http server转发流量的),具体方法请阅读[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-安全模式)
# 主要服务
-
# 其他服务
[version服务](http://brpc.baidu.com:8765/version)可以查看服务器的版本。用户可通过Server::set_version()设置Server的版本,如果用户没有设置,框架会自动为用户生成,规则:`baidu_rpc_server_<service-name1>_<service-name2> ...`
[health服务](http://brpc.baidu.com:8765/health)可以探测服务的存活情况。
[protobufs服务](http://brpc.baidu.com:8765/protobufs)可以查看程序中所有的protobuf结构体。
[vlog服务](http://brpc.baidu.com:8765/vlog)可以查看程序中当前可开启的[VLOG](http://wiki.baidu.com/display/RPC/streaming_log#streaming_log-VLOG)
dir服务可以浏览服务器上的所有文件,这个服务比较敏感,默认关闭。
threads服务可以查看进程内所有线程的运行状况,调用时对程序性能较大,默认关闭。
其他还有一些调试服务,如有需求请联系我们。
\ No newline at end of file
[connections服务](http://brpc.baidu.com:8765/connections)可以查看所有的连接。一个典型的页面如下:
server_socket_count: 5
| CreatedTime | RemoteSide | SSL | Protocol | fd | BytesIn/s | In/s | BytesOut/s | Out/s | BytesIn/m | In/m | BytesOut/m | Out/m | SocketId |
| -------------------------- | ------------------- | ---- | --------- | ---- | --------- | ---- | ---------- | ----- | --------- | ---- | ---------- | ----- | -------- |
| 2015/09/21-21:32:09.630840 | 172.22.38.217:51379 | No | http | 19 | 1300 | 1 | 269 | 1 | 68844 | 53 | 115860 | 53 | 257 |
| 2015/09/21-21:32:09.630857 | 172.22.38.217:51380 | No | http | 20 | 1308 | 1 | 5766 | 1 | 68884 | 53 | 129978 | 53 | 258 |
| 2015/09/21-21:32:09.630880 | 172.22.38.217:51381 | No | http | 21 | 1292 | 1 | 1447 | 1 | 67672 | 52 | 143414 | 52 | 259 |
| 2015/09/21-21:32:01.324587 | 127.0.0.1:55385 | No | baidu_std | 15 | 1480 | 20 | 880 | 20 | 88020 | 1192 | 52260 | 1192 | 512 |
| 2015/09/21-21:32:01.325969 | 127.0.0.1:55387 | No | baidu_std | 17 | 4016 | 40 | 1554 | 40 | 238879 | 2384 | 92660 | 2384 | 1024 |
channel_socket_count: 1
| CreatedTime | RemoteSide | SSL | Protocol | fd | BytesIn/s | In/s | BytesOut/s | Out/s | BytesIn/m | In/m | BytesOut/m | Out/m | SocketId |
| -------------------------- | -------------- | ---- | --------- | ---- | --------- | ---- | ---------- | ----- | --------- | ---- | ---------- | ----- | -------- |
| 2015/09/21-21:32:01.325870 | 127.0.0.1:8765 | No | baidu_std | 16 | 1554 | 40 | 4016 | 40 | 92660 | 2384 | 238879 | 2384 | 0 |
channel_short_socket_count: 0
` `
上述信息分为三段:
- 第一段是server接受(accept)的连接。
- 第二段是server与下游的单连接(使用baidu::rpc::Channel建立),fd为-1的是虚拟连接,对应第三段中所有相同RemoteSide的连接。
- 第三段是server与下游的短连接或连接池(pooled connections),这些连接从属于第二段中的相同RemoteSide的虚拟连接。
表格标题的含义:
- RemoteSide : 远端的ip和端口。
- SSL:是否使用SSL加密,若为Yes的话,一般是HTTPS连接。
- Protocol : 使用的协议,可能为baidu_std hulu_pbrpc sofa_pbrpc memcache http public_pbrpc nova_pbrpc nshead_server等。
- fd : file descriptor(文件描述符),可能为-1。
- BytesIn/s : 上一秒读入的字节数
- In/s : 上一秒读入的消息数(消息是对request和response的统称)
- BytesOut/s : 上一秒写出的字节数
- Out/s : 上一秒写出的消息数
- BytesIn/m: 上一分钟读入的字节数
- In/m: 上一分钟读入的消息数
- BytesOut/m: 上一分钟写出的字节数
- Out/m: 上一分钟写出的消息数
- SocketId :内部id,用于debug,用户不用关心。
典型截图分别如下所示:
单连接:![img](http://wiki.baidu.com/download/attachments/165876298/image2016-7-15%2016%3A7%3A41.png?version=1&modificationDate=1468570062000&api=v2)
连接池:![img](http://wiki.baidu.com/download/attachments/165876298/image2016-7-15%2016%3A8%3A40.png?version=1&modificationDate=1468570121000&api=v2)
短连接:![img](http://wiki.baidu.com/download/attachments/165876298/image2016-7-15%2016%3A9%3A1.png?version=1&modificationDate=1468570141000&api=v2)
baidu-rpc可以分析花在等待锁上的时间及发生等待的函数。
# 开启方法
按需开启。无需配置,不依赖tcmalloc,不需要链接frame pointer或libunwind。如果只是baidu-rpc client或没有使用baidu-rpc,看[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)
# 图示
当很多线程争抢同一把锁时,一些线程无法立刻获得锁,而必须睡眠直到某个线程退出临界区。这个争抢过程我们称之为**contention**。在多核机器上,当多个线程需要操作同一个资源却被一把锁挡住时,便无法充分发挥多个核心的并发能力。现代OS通过提供比锁更底层的同步原语,使得无竞争锁完全不需要系统调用,只是一两条wait-free,耗时10-20ns的原子操作,非常快。而锁一旦发生竞争,一些线程就要陷入睡眠,再次醒来触发了OS的调度代码,代价至少为3-5us。所以让锁尽量无竞争,让所有线程“一起飞”是需要高性能的server的永恒话题。
r31906后baidu-rpc支持contention profiler,可以分析在等待锁上花费了多少时间。等待过程中线程是睡着的不会占用CPU,所以contention profiler中的时间并不是cpu时间,也不会出现在[cpu profiler](http://wiki.baidu.com/display/RPC/cpu+profiler)中。cpu profiler可以抓到特别频繁的锁(以至于花费了很多cpu),但耗时真正巨大的临界区往往不是那么频繁,而无法被cpu profiler发现。**contention profiler和cpu profiler好似互补关系,前者分析等待时间(被动),后者分析忙碌时间。**还有一类由用户基于condition或sleep发起的主动等待时间,无需分析。
目前contention profiler支持pthread_mutex_t(非递归)和bthread_mutex_t,开启后每秒最多采集1000个竞争锁,这个数字由[参数bvar_collector_expected_per_second](http://brpc.baidu.com:8765/flags/bvar_collector_expected_per_second)控制(同时影响rpc_dump)。如果一秒内竞争锁的次数N超过了1000,那么每把锁会有1000/N的概率被采集。在我们的各类测试场景中(qps在10万-60万不等)没有观察到被采集程序的性能有明显变化。
我们通过实际例子来看下如何使用contention profiler,点击“contention”按钮(more左侧)后就会开启默认10秒的分析过程。下图是[libraft](http://wiki.baidu.com/display/RAFT/RAFT)中的一个示例程序的锁状况,这个程序是3个节点复制组的leader,qps在10-12万左右。左上角的**Total seconds: 2.449**是采集时间内(10秒)在锁上花费的所有等待时间。注意是“等待”,无竞争的锁不会被采集也不会出现在下图中。顺着箭头往下走能看到每份时间来自哪些函数。
![img](http://wiki.baidu.com/download/attachments/165876314/screencapture-10-81-3-185-8101-hotspots-contention-1453209162877.png?version=1&modificationDate=1453212153000&api=v2)
上图有点大,让我们放大一个局部看看。下图红框中的0.768是这个局部中最大的数字,它代表raft::LogManager::get_entry在等待涉及到bvar::detail::UniqueLockBase的函数上共等待了0.768秒(10秒内)。我们如果觉得这个时间不符合预期,就可以去排查代码。
![img](http://wiki.baidu.com/download/attachments/165876314/image2016-1-19%2022%3A13%3A26.png?version=1&modificationDate=1453212828000&api=v2)
点击上方的count选择框,可以查看锁的竞争次数。选择后左上角变为了**Total samples: 439026**,代表采集时间内总共的锁竞争次数(估算)。图中箭头上的数字也相应地变为了次数,而不是时间。对比同一份结果的时间和次数,可以更深入地理解竞争状况。
![img](http://wiki.baidu.com/download/attachments/165876314/image2016-1-19%2022%3A17%3A40.png?version=1&modificationDate=1453213082000&api=v2)
\ No newline at end of file
baidu-rpc可以分析程序中的热点函数。
# 开启方法
1. 在COMAKE中增加`CONFIGS('``thirdsrc/tcmalloc@2.5.0.5977``', Libraries('libtcmalloc_and_profiler.a'))`
1. 这么写也开启了tcmalloc,不建议单独链接cpu profiler而不链接tcmalloc,可能越界访问导致[crash](https://code.google.com/p/gperftools/source/browse/README#sl_svn1035d5c18f64d114ac790b92a96f3b3a1a301eb9_207)**。**可能由于tcmalloc不及时归还内存,越界访问不会crash。
2. 这个版本的tcmalloc使用frame pointer而不是libunwind回溯栈,请确保在CXXFLAGS或CFLAGS中加上`-fno-omit-frame-pointer`,否则函数间的调用关系会丢失,最后产生的图片中都是彼此独立的函数方框。
2. 定义宏BAIDU_RPC_ENABLE_CPU_PROFILER。在COMAKE中加入`CXXFLAGS('-DBAIDU_RPC_ENABLE_CPU_PROFILER')`
3. 如果只是baidu-rpc client或没有使用baidu-rpc,看[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)
注意要关闭Server端的认证,否则可能会看到这个:
```
$ tools/pprof --text localhost:9002/pprof/profile
Use of uninitialized value in substitution (s///) at tools/pprof line 2703.
http://localhost:9002/profile/symbol doesn't exist
```
server端可能会有这样的日志:
```
FATAL: 12-26 10:01:25: * 0 [src/baidu/rpc/policy/giano_authenticator.cpp:65][4294969345] Giano fails to verify credentical, 70003
WARNING: 12-26 10:01:25: * 0 [src/baidu/rpc/input_messenger.cpp:132][4294969345] Authentication failed, remote side(127.0.0.1:22989) of sockfd=5, close it
```
# 图示
下图是一次运行cpu profiler后的结果:
- 左上角是总体信息,包括时间,程序名,总采样数等等。
- View框中可以选择查看之前运行过的profile结果,Diff框中可选择查看和之前的结果的变化量,重启后清空。
- 代表函数调用的方框中的字段从上到下依次为:函数名,这个函数本身(除去所有子函数)占的采样数和比例,这个函数及调用的所有子函数累计的采样数和比例。采样数越大框越大。
- 方框之间连线上的数字表示被采样到的上层函数对下层函数的调用数,数字越大线越粗。
热点分析一般开始于找到最大的框最粗的线考察其来源及去向。
cpu profiler的原理是在定期被调用的SIGPROF handler中采样所在线程的栈,由于handler(在linux 2.6后)会被随机地摆放于活跃线程的栈上运行,cpu profiler在运行一段时间后能以很大的概率采集到所有活跃线程中的活跃函数,最后根据栈代表的函数调用关系汇总为调用图,并把地址转换成符号,这就是我们看到的结果图了。采集频率由环境变量CPUPROFILE_FREQUENCY控制,默认100,即每秒钟100次或每10ms一次。。在实践中cpu profiler对原程序的影响不明显。
![img](http://wiki.baidu.com/download/attachments/165876310/image2016-1-19%2023%3A28%3A21.png?version=1&modificationDate=1453217323000&api=v2)
你也可以使用[public/baidu-rpc/tools/pprof](https://svn.baidu.com/public/trunk/baidu-rpc/tools/pprof)或gperftools中的pprof进行profiling。
比如`pprof --text localhost:9002 --seconds=5`的意思是统计运行在本机9002端口的server的cpu情况,时长5秒。一次运行的例子如下:
```
$ tools/pprof --text 0.0.0.0:9002 --seconds=5
Gathering CPU profile from http://0.0.0.0:9002/pprof/profile?seconds=5 for 5 seconds to
/home/gejun/pprof/echo_server.1419501210.0.0.0.0
Be patient...
Wrote profile to /home/gejun/pprof/echo_server.1419501210.0.0.0.0
Removing funlockfile from all stack traces.
Total: 2946 samples
1161 39.4% 39.4% 1161 39.4% syscall
248 8.4% 47.8% 248 8.4% baidu::bthread::TaskControl::steal_task
227 7.7% 55.5% 227 7.7% writev
87 3.0% 58.5% 88 3.0% ::cpp_alloc
74 2.5% 61.0% 74 2.5% __read_nocancel
46 1.6% 62.6% 48 1.6% tc_delete
42 1.4% 64.0% 42 1.4% baidu::rpc::Socket::Address
41 1.4% 65.4% 41 1.4% epoll_wait
35 1.2% 66.6% 35 1.2% memcpy
33 1.1% 67.7% 33 1.1% __pthread_getspecific
33 1.1% 68.8% 33 1.1% baidu::rpc::Socket::Write
33 1.1% 69.9% 33 1.1% epoll_ctl
28 1.0% 70.9% 42 1.4% baidu::rpc::policy::ProcessRpcRequest
27 0.9% 71.8% 27 0.9% baidu::IOBuf::_push_back_ref
27 0.9% 72.7% 27 0.9% baidu::bthread::TaskGroup::ending_sched
```
省略–text进入交互模式,如下图所示:
```
$ tools/pprof localhost:9002 --seconds=5
Gathering CPU profile from http://0.0.0.0:9002/pprof/profile?seconds=5 for 5 seconds to
/home/gejun/pprof/echo_server.1419501236.0.0.0.0
Be patient...
Wrote profile to /home/gejun/pprof/echo_server.1419501236.0.0.0.0
Removing funlockfile from all stack traces.
Welcome to pprof! For help, type 'help'.
(pprof) top
Total: 2954 samples
1099 37.2% 37.2% 1099 37.2% syscall
253 8.6% 45.8% 253 8.6% baidu::bthread::TaskControl::steal_task
240 8.1% 53.9% 240 8.1% writev
90 3.0% 56.9% 90 3.0% ::cpp_alloc
67 2.3% 59.2% 67 2.3% __read_nocancel
47 1.6% 60.8% 47 1.6% baidu::IOBuf::_push_back_ref
42 1.4% 62.2% 56 1.9% baidu::rpc::policy::ProcessRpcRequest
41 1.4% 63.6% 41 1.4% epoll_wait
38 1.3% 64.9% 38 1.3% epoll_ctl
37 1.3% 66.1% 37 1.3% memcpy
35 1.2% 67.3% 35 1.2% baidu::rpc::Socket::Address
```
\ No newline at end of file
baidu-rpc使用gflags管理配置。如果你的程序也使用gflags,那么你应该已经可以修改和baidu-rpc相关的flags,你可以浏览[flags服务](http://brpc.baidu.com:8765/flags)了解每个flag的具体功能。如果你的程序还没有使用gflags,我们建议你使用,原因如下:
- 命令行和文件均可传入,前者方便做测试,后者适合线上运维。放在文件中的gflags可以reload。而configure只支持从文件读取配置。
- 你可以在浏览器中查看baidu-rpc服务器中所有gflags,并对其动态修改(如果允许的话)。configure不可能做到这点。
- gflags分散在和其作用紧密关联的文件中,更好管理。而使用configure需要聚集到一个庞大的读取函数中。
如果你依赖了baidu-rpc,那么你已经依赖了third-64/gflags,如果你需要依赖某个特定版本的话,在COMAKE中加入CONFIGS('third-64/gflags@<specific-version>')。
# Usage of gflags
gflags一般定义在需要它的源文件中。#include <gflags/gflags.h>后在全局scope加入DEFINE_*<type>*(*<name>*, *<default-value>*, *<description>*); 比如:
```c++
#include <gflags/gflags.h>
...
DEFINE_bool(hex_log_id, false, "Show log_id in hexadecimal");
DEFINE_int32(health_check_interval, 3, "seconds between consecutive health-checkings");
```
一般在main函数开头用ParseCommandLineFlags处理程序参数:
```c++
#include <gflags/gflags.h>
...
int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true/*表示把识别的参数从argc/argv中删除*/);
...
}
```
如果要从conf/gflags.conf中加载gflags,则可以加上参数-flagfile=conf/gflags.conf。如果希望默认(什么参数都不加)就从文件中读取,则可以在程序中直接给flagfile赋值,一般这么写
```c++
google::SetCommandLineOption("flagfile", "conf/gflags.conf");
```
程序启动时会检查conf/gflags.conf是否存在,如果不存在则会报错:
```
$ ./my_program
conf/gflags.conf: No such file or directory
```
更具体的使用指南请阅读[官方文档](http://gflags.github.io/gflags/)
# flagfile
在命令行中参数和值之间可不加等号,而在flagfile中一定要加。比如`./myapp -param 7`是ok的,但在`./myapp -flagfile=./gflags.conf`对应的gflags.conf中一定要写成**-param=7****--param=7**,否则就不正确且不会报错。
在命令行中字符串可用单引号或双引号包围,而在flagfile中不能加。比如`./myapp -name="tom"``./myapp -name='tom'`都是ok的,但在`./myapp -flagfile=./gflags.conf`对应的gflags.conf中一定要写成**-name=tom****--name=tom**,如果写成-name="tom"的话,引号也会作为值的一部分。配置文件中的值可以有空格,比如gflags.conf中写成-name=value with spaces是ok的,参数name的值就是value with spaces,而在命令行中要用引号括起来。
flagfile中参数可由单横线(如-foo)或双横线(如--foo)打头,但不能以三横线或更多横线打头,否则的话是无效参数且不会报错!
flagfile中以`#开头的行被认为是注释。开头的空格和空白行都会被忽略。`
flagfile中可以使用`--flagfile包含另一个flagfile。`
# Change gflag on-the-fly
[flags服务](http://brpc.baidu.com:8765/flags)可以查看服务器进程中所有的gflags。修改过的flags会以红色高亮。“修改过”指的是修改这一行为,即使再改回默认值,仍然会显示为红色。
/flags:列出所有的gflags
/flags/NAME:查询名字为NAME的gflag
/flags/NAME1,NAME2,NAME3:查询名字为NAME1或NAME2或NAME3的gflag
/flags/foo*,b$r:查询名字与某一统配符匹配的gflag,注意用$代替?匹配单个字符,因为?在url中有特殊含义。
访问/flags/NAME?setvalue=VALUE即可动态修改一个gflag的值,validator会被调用。
Icon
为了防止误修改,需要动态修改的gflag必须有validator,显示此类gflag名字时有(R)后缀。
![img](http://wiki.baidu.com/download/thumbnails/71337189/image2015-9-21%2021%3A49%3A48.png?version=1&modificationDate=1442843389000&api=v2)
*修改成功后会显示如下信息*
![img](http://wiki.baidu.com/download/attachments/37774685/image2015-2-22%2012%3A30%3A54.png?version=1&modificationDate=1424579454000&api=v2)
*尝试修改不允许修改的gflag会显示如下错误信息*
![img](http://wiki.baidu.com/download/attachments/37774685/image2015-2-22%2012%3A30%3A1.png?version=1&modificationDate=1424579401000&api=v2)
*设置一个不允许的值会显示如下错误(flag值不会变化)*
![img](http://wiki.baidu.com/download/attachments/37774685/image2015-2-22%2012%3A35%3A9.png?version=1&modificationDate=1424579709000&api=v2)
r31658之后支持可视化地修改,在浏览器上访问时将看到(R)下多了下划线:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2022%3A55%3A29.png?version=1&modificationDate=1450968931000&api=v2)
点击后在一个独立页面可视化地修改对应的flag:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2022%3A56%3A3.png?version=1&modificationDate=1450968965000&api=v2)
填入true后确定:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2022%3A57%3A13.png?version=1&modificationDate=1450969035000&api=v2)
返回/flags可以看到对应的flag已经被修改了:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2022%3A57%3A57.png?version=1&modificationDate=1450969079000&api=v2)
关于重载gflags,重点关注:
- 避免在一段代码中多次调用同一个gflag,应把该gflag的值保存下来并调用该值。因为gflag的值随时可能变化,而产生意想不到的结果。
- 使用google::GetCommandLineOption()访问string类型的gflag,直接访问是线程不安全的。
- 处理逻辑和副作用应放到validator里去。比如修改FLAGS_foo后得更新另一处的值,如果只是写在程序初始化的地方,而不是validator里,那么重载时这段逻辑就运行不到了。
如果你确认某个gflag不需要额外的线程同步和处理逻辑就可以重载,那么可以用如下方式为其注册一个总是返回true的validator:
```c++
DEFINE_bool(hex_log_id, false, "Show log_id in hexadecimal");
BAIDU_RPC_VALIDATE_GFLAG(hex_log_id, baidu::rpc::PassValidate/*always true*/);
```
这个flag是单纯的开关,修改后不需要更新其他数据(没有处理逻辑),代码中前面看到true后面看到false也不会产生什么后果(不需要线程同步),所以我们让其默认可重载。
对于int32和int64类型,有一个判断是否为正数的常用validator:
```c++
DEFINE_int32(health_check_interval, 3, "seconds between consecutive health-checkings");
BAIDU_RPC_VALIDATE_GFLAG(health_check_interval, baidu::rpc::PositiveInteger);
```
以上操作都可以在命令行中进行:
```shell
$ curl brpc.baidu.com:8765/flags/health_check_interval
Name | Value | Description | Defined At
---------------------------------------
health_check_interval (R) | 3 | seconds between consecutive health-checkings | src/baidu/rpc/socket_map.cpp
```
1.0.251.32399后增加了-immutable_flags,打开后所有的gflags将不能被动态修改。当一个服务对某个gflag值比较敏感且不希望在线上被误改,可打开这个开关。打开这个开关的同时也意味着你无法动态修改线上的配置,每次修改都要重启程序,对于还在调试阶段或待收敛阶段的程序不建议打开。
\ No newline at end of file
baidu-rpc可以分析内存是被哪些函数占据的。heap profiler的原理是每分配满一些内存就采样调用处的栈,“一些”由环境变量TCMALLOC_SAMPLE_PARAMETER控制,默认524288,即512K字节。根据栈表现出的函数调用关系汇总为我们看到的结果图。在实践中heap profiler对原程序的影响不明显。
# 开启方法
1. 在COMAKE中增加`CONFIGS('thirdsrc/tcmalloc@2.5.0.5977')`。如果要同时使用cpu profiler,加上`Libraries('libtcmalloc_and_profiler.a')`
1. 这个版本的tcmalloc使用frame pointer而不是libunwind回溯栈,请确保在CXXFLAGS或CFLAGS中加上`-fno-omit-frame-pointer`,否则函数间的调用关系会丢失,最后产生的图片中都是彼此独立的函数方框。
2. 在COMAKE的CPPFLAGS中增加`-DBAIDU_RPC_ENABLE_HEAP_PROFILER`
3. 在shell中`export TCMALLOC_SAMPLE_PARAMETER=524288`。该变量指每分配这么多字节内存时做一次统计,默认为0,代表不开启内存统计。[官方文档](http://gperftools.googlecode.com/svn/trunk/doc/tcmalloc.html)建议设置为524288。这个变量也可在运行前临时设置,如`TCMALLOC_SAMPLE_PARAMETER=524288 ./server`。如果没有这个环境变量,可能会看到这样的结果:
```
$ tools/pprof --text localhost:9002/pprof/heap
Fetching /pprof/heap profile from http://localhost:9002/pprof/heap to
/home/gejun/pprof/echo_server.1419559063.localhost.pprof.heap
Wrote profile to /home/gejun/pprof/echo_server.1419559063.localhost.pprof.heap
/home/gejun/pprof/echo_server.1419559063.localhost.pprof.heap: header size >= 2**16
```
4. 如果只是baidu-rpc client或没有使用baidu-rpc,看[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)。
注意要关闭Server端的认证,否则可能会看到这个:
```
$ tools/pprof --text localhost:9002/pprof/heap
Use of uninitialized value in substitution (s///) at tools/pprof line 2703.
http://localhost:9002/pprof/symbol doesn't exist
```
server端可能会有这样的日志:
```
FATAL: 12-26 10:01:25: * 0 [src/baidu/rpc/policy/giano_authenticator.cpp:65][4294969345] Giano fails to verify credentical, 70003
WARNING: 12-26 10:01:25: * 0 [src/baidu/rpc/input_messenger.cpp:132][4294969345] Authentication failed, remote side(127.0.0.1:22989) of sockfd=5, close it
```
# 图示
![img](http://wiki.baidu.com/download/attachments/165876312/image2016-1-19%2023%3A8%3A50.png?version=1&modificationDate=1453216152000&api=v2)
左上角是当前程序通过malloc分配的内存总量,顺着箭头上的数字可以看到内存来自哪些函数。
点击左上角的text选择框可以查看文本格式的结果,有时候这种按分配量排序的形式更方便。
![img](http://wiki.baidu.com/download/attachments/165876312/image2016-1-19%2023%3A12%3A44.png?version=1&modificationDate=1453216386000&api=v2)
左上角的两个选择框作用分别是:
- View:当前正在看的profile。选择<new profile>表示新建一个。新建完毕后,View选择框中会出现新profile,URL也会被修改为对应的地址。这意味着你可以通过粘贴URL分享结果,点击链接的人将看到和你一模一样的结果,而不是重做profiling的结果。你可以在框中选择之前的profile查看。历史profiie保留最近的32个,可通过[--max_profiles_kept](http://brpc.baidu.com:8765/flags/max_profiles_kept)调整。
- Diff:和选择的profile做对比。<none>表示什么都不选。如果你选择了之前的某个profile,那么将看到View框中的profile相比Diff框中profile的变化量。
下图演示了勾选Diff和Text的效果。
![img](http://wiki.baidu.com/download/attachments/37774685/prof.gif?version=1&modificationDate=1494403248000&api=v2)
你也可以使用pprof脚本(public/baidu-rpc/tools/pprof)在命令行中查看文本格式结果:
```
$ tools/pprof --text db-rpc-dev00.db01:8765/pprof/heap
Fetching /pprof/heap profile from http://db-rpc-dev00.db01:8765/pprof/heap to
/home/gejun/pprof/play_server.1453216025.db-rpc-dev00.db01.pprof.heap
Wrote profile to /home/gejun/pprof/play_server.1453216025.db-rpc-dev00.db01.pprof.heap
Adjusting heap profiles for 1-in-524288 sampling rate
Heap version 2
Total: 38.9 MB
35.8 92.0% 92.0% 35.8 92.0% ::cpp_alloc
2.1 5.4% 97.4% 2.1 5.4% base::FlatMap
0.5 1.3% 98.7% 0.5 1.3% base::IOBuf::append
0.5 1.3% 100.0% 0.5 1.3% base::IOBufAsZeroCopyOutputStream::Next
0.0 0.0% 100.0% 0.6 1.5% MallocExtension::GetHeapSample
0.0 0.0% 100.0% 0.5 1.3% ProfileHandler::Init
0.0 0.0% 100.0% 0.5 1.3% ProfileHandlerRegisterCallback
0.0 0.0% 100.0% 0.5 1.3% __do_global_ctors_aux
0.0 0.0% 100.0% 1.6 4.2% _end
0.0 0.0% 100.0% 0.5 1.3% _init
0.0 0.0% 100.0% 0.6 1.5% baidu::rpc::CloseIdleConnections
0.0 0.0% 100.0% 1.1 2.9% baidu::rpc::GlobalUpdate
0.0 0.0% 100.0% 0.6 1.5% baidu::rpc::PProfService::heap
0.0 0.0% 100.0% 1.9 4.9% baidu::rpc::Socket::Create
0.0 0.0% 100.0% 2.9 7.4% baidu::rpc::Socket::Write
0.0 0.0% 100.0% 3.8 9.7% baidu::rpc::Span::CreateServerSpan
0.0 0.0% 100.0% 1.4 3.5% baidu::rpc::SpanQueue::Push
0.0 0.0% 100.0% 1.9 4.8% base::ObjectPool
0.0 0.0% 100.0% 0.8 2.0% base::ResourcePool
0.0 0.0% 100.0% 1.0 2.6% base::iobuf::tls_block
0.0 0.0% 100.0% 1.0 2.6% bthread::TimerThread::Bucket::schedule
0.0 0.0% 100.0% 1.6 4.1% bthread::get_stack
0.0 0.0% 100.0% 4.2 10.8% bthread_id_create
0.0 0.0% 100.0% 1.1 2.9% bvar::Variable::describe_series_exposed
0.0 0.0% 100.0% 1.0 2.6% bvar::detail::AgentGroup
0.0 0.0% 100.0% 0.5 1.3% bvar::detail::Percentile::operator
0.0 0.0% 100.0% 0.5 1.3% bvar::detail::PercentileSamples
0.0 0.0% 100.0% 0.5 1.3% bvar::detail::Sampler::schedule
0.0 0.0% 100.0% 6.5 16.8% leveldb::Arena::AllocateNewBlock
0.0 0.0% 100.0% 0.5 1.3% leveldb::VersionSet::LogAndApply
0.0 0.0% 100.0% 4.2 10.8% pthread_mutex_unlock
0.0 0.0% 100.0% 0.5 1.3% pthread_once
0.0 0.0% 100.0% 0.5 1.3% std::_Rb_tree
0.0 0.0% 100.0% 1.5 3.9% std::basic_string
0.0 0.0% 100.0% 3.5 9.0% std::string::_Rep::_S_create
```
baidu-rpc还提供一个类似的growth profiler分析内存的分配去向(不考虑释放)。
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-10-1%209%3A55%3A9.png?version=1&modificationDate=1443664514000&api=v2)
这里特指“纯粹"的HTTP service,而不是可通过HTTP访问的pb服务。虽然用不到pb消息,但“纯粹”的HTTP Service也必须定义在.proto文件中,只是request和response都是空的结构体。这么做是确保所有的服务声明集中在proto文件中,而不是散列在.proto、程序、配置等多个地方。示例代码见[http_server.cpp](https://svn.baidu.com/public/trunk/baidu-rpc/example/http_c++/http_server.cpp)
# URL前缀为/ServiceName/MethodName
所有pb service默认都能通过/ServiceName/MethodName来访问,其中ServiceName不包括package。对于公司内的纯HTTP服务,一般来说这种形式的URL也够用了。实现步骤如下:
\1. 填写proto文件。
下面代码里的HttpRequest和HttpResponse都是空的,因为http数据在Controller中。http request的头在Controller.http_request()中,body在Controller.request_attachment()中。类似的,http response的头在Controller.http_response(),body在Controller.response_attachment()。
```protobuf
option cc_generic_services = true;
 
message HttpRequest { };
message HttpResponse { };
 
service HttpService {
      rpc Echo(HttpRequest) returns (HttpResponse);
};
```
\2.实现Service。和其他pb service一样,也是继承定义在.pb.h中的service基类。
```c++
class HttpServiceImpl : public HttpService {
public:
    ...
    virtual void Echo(google::protobuf::RpcController* cntl_base,
                      const HttpRequest* /*request*/,
                      HttpResponse* /*response*/,
                      google::protobuf::Closure* done) {
        baidu::rpc::ClosureGuard done_guard(done);
        baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
 
        // 这里返回纯文本。
        cntl->http_response().set_content_type("text/plain");
       
        // 把请求的query-string和body打印出来,作为回复内容。
        base::IOBufBuilder os;
        os << "queries:";
        for (baidu::rpc::URI::QueryIterator it = cntl->http_request().uri().QueryBegin();
                it != cntl->http_request().uri().QueryEnd(); ++it) {
            os << ' ' << it->first << '=' << it->second;
        }
        os << "\nbody: " << cntl->request_attachment() << '\n';
        os.move_to(cntl->response_attachment());
    }
};
```
实现完毕插入Server后可通过如下URL访问,/HttpService/Echo后的部分在cntl->http_request().unresolved_path()中,unresolved_path总是normalized。
| URL | 访问方法 | cntl->http_request().uri().path() | cntl->http_request().unresolved_path() |
| -------------------------- | ---------------- | --------------------------------- | -------------------------------------- |
| /HttpService/Echo | HttpService.Echo | "/HttpService/Echo" | "" |
| /HttpService/Echo/Foo | HttpService.Echo | "/HttpService/Echo/Foo" | "Foo" |
| /HttpService/Echo/Foo/Bar | HttpService.Echo | "/HttpService/Echo/Foo/Bar" | "Foo/Bar" |
| /HttpService//Echo///Foo// | HttpService.Echo | "/HttpService//Echo///Foo//" | "Foo" |
| /HttpService | 访问错误 | | |
# URL前缀为/ServiceName
一些资源类的HTTP服务可能会需要这种类型的URL,比如FileService提供对文件的访问,/FileService/foobar.txt代表访问运行目录下的foobar.txt文件,而/FileService/app/data/boot.cfg代表app/data目录下的boot.cfg文件。
实现方法:
\1. proto文件中应以FileService为服务名,以default_method为方法名。
```protbuf
option cc_generic_services = true;
message HttpRequest { };
message HttpResponse { };
service FileService {
rpc default_method(HttpRequest) returns (HttpResponse);
}
```
\2.实现Service。
```c++
class FileServiceImpl: public FileService {
public:
...
virtual void default_method(google::protobuf::RpcController* cntl_base,
const HttpRequest* /*request*/,
HttpResponse* /*response*/,
google::protobuf::Closure* done) {
baidu::rpc::ClosureGuard done_guard(done);
baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
cntl->response_attachment().append("Getting file: ");
cntl->response_attachment().append(cntl->http_request().unresolved_path());
}
};
```
实现完毕插入Server后可通过如下URL访问,/FileService之后的路径在cntl->http_request().unresolved_path()中 (r32097前被称为method_path),unresolved_path总是normalized。
| URL | 访问方法 | cntl->http_request().uri().path() | cntl->http_request().unresolved_path() |
| ------------------------------- | -------------------------- | --------------------------------- | -------------------------------------- |
| /FileService | FileService.default_method | "/FileService" | "" |
| /FileService/123.txt | FileService.default_method | "/FileService/123.txt" | "123.txt" |
| /FileService/mydir/123.txt | FileService.default_method | "/FileService/mydir/123.txt" | "mydir/123.txt" |
| /FileService//mydir///123.txt// | FileService.default_method | "/FileService//mydir///123.txt//" | "mydir/123.txt" |
# Restful URL
r32097后,baidu-rpc支持为service中的每个方法指定一个URL。接口如下:
```c++
// 如果restful_mappings不为空, service中的方法可通过指定的URL被HTTP协议访问,而不是/ServiceName/MethodName.
// 映射格式:"PATH1 => NAME1, PATH2 => NAME2 ..."
// PATHs是有效的HTTP路径, NAMEs是service中的方法名.
int AddService(google::protobuf::Service* service,
ServiceOwnership ownership,
base::StringPiece restful_mappings);
```
比如下面的QueueService包含多个http方法。
```protobuf
service QueueService {
    rpc start(HttpRequest) returns (HttpResponse);
    rpc stop(HttpRequest) returns (HttpResponse);
    rpc get_stats(HttpRequest) returns (HttpResponse);
    rpc download_data(HttpRequest) returns (HttpResponse);
};
```
如果我们像之前那样把它插入server,那么只能通过`/QueueService/start, /QueueService/stop等url来访问`
而在调用AddService时指定第三个参数(restful_mappings)就能定制URL了,如下所示:
```c++
// r33521前星号只能出现在最后
if (server.AddService(&queue_svc,
                      baidu::rpc::SERVER_DOESNT_OWN_SERVICE,
                      "/v1/queue/start   => start,"
                      "/v1/queue/stop    => stop,"
                      "/v1/queue/stats/* => get_stats") != 0) {
    LOG(ERROR) << "Fail to add queue_svc";
    return -1;
}
 
// r33521后星号可出现在中间
if (server.AddService(&queue_svc,
                      baidu::rpc::SERVER_DOESNT_OWN_SERVICE,
                      "/v1/*/start   => start,"
                      "/v1/*/stop    => stop,"
                      "*.data        => download_data") != 0) {
    LOG(ERROR) << "Fail to add queue_svc";
    return -1;
}
```
上面代码中AddService的第三个参数分了三行,但实际上是一个字符串。这个字符串包含以逗号(,)分隔的三个映射关系,每个映射告诉baidu-rpc:在遇到箭头左侧的URL时调用右侧的方法。"/v1/queue/stats/*"中的星号可匹配任意字串。在r33521前星号只能加在URL最后。
关于映射规则:
- 多个路径可映射至同一个方法。
- service不要求是纯HTTP,pb service也支持。
- 没有出现在映射中的方法仍旧通过/ServiceName/MethodName访问。出现在映射中的方法不再能通过/ServiceName/MethodName访问。
- ==> ===> ...都是可以的。开头结尾的空格,额外的斜杠(/),最后多余的逗号,都不要紧。
- r33521前PATH和PATH/* 是冲突的,不能同时出现在一个字符串中。r33521后两者可以共存。
- r33521前星号后不能有更多字符,r33521后可以,即支持后缀匹配。
- 一个路径中只能出现一个星号。
`cntl.http_request().unresolved_path()` 对应星号(*)匹配的部分,保证normalized:开头结尾都不包含斜杠(/),中间斜杠不重复。比如:
![img](http://wiki.baidu.com/download/attachments/71337204/image2016-3-1%2012%3A26%3A51.png?version=1&modificationDate=1456806412000&api=v2)
或:
![img](http://wiki.baidu.com/download/attachments/71337204/image2016-3-1%2012%3A27%3A47.png?version=1&modificationDate=1456806467000&api=v2)
unresolved_path都是`"foo/bar"`,左右、中间多余的斜杠被移除了。
注意:`cntl.http_request().uri().path()`不保证normalized,这两个例子中分别为`"//v1//queue//stats//foo///bar//////"``"//vars///foo////bar/////"`
/status页面上的方法名后会加上所有相关的URL,形式是:@URL1 @URL2 ...
![img](http://wiki.baidu.com/download/attachments/71337204/image2016-3-1%200%3A12%3A36.png?version=1&modificationDate=1456762356000&api=v2)
# HTTP参数
## HTTP headers
http header是一系列key/value对,有些由HTTP协议规定有特殊含义,其余则由用户自由设定。
http headers易与query string混淆,后者是URL的一部分,常见形式是key1=value1&key2=value2&...,也可以表达key/value关系,且更容易在界面上操作。但用query string表达key/value并不是HTTP规范的一部分,更多是大家约定成俗的方式。就我的感受而言,由于http headers是协议的一部分,被所有http server认知,所以常用于机器接口,传递框架或协议层面的参数;而query string作为URL的一部分,很方便被人修改和阅读,常用于传递用户层面的参数。
```c++
// 获得header中"User-Agent"的值,大小写不敏感。
const std::string* user_agent_str = cntl->http_request().GetHeader("User-Agent");
if (user_agent_str != NULL) {  // has the header
    LOG(TRACE) << "User-Agent is " << *user_agent_str;
}
...
 
// 在header中增加"Accept-encoding: gzip",大小写不敏感。
cntl->http_response().SetHeader("Accept-encoding", "gzip");
// 覆盖为"Accept-encoding: deflate"
cntl->http_response().SetHeader("Accept-encoding", "deflate");
// 增加一个value,逗号分隔,变为"Accept-encoding: deflate,gzip"
cntl->http_response().AppendHeader("Accept-encoding", "gzip");
```
## Content-Type
Content-type记录body的类型,是一个使用频率较高的header,单独抽取出来方便使用,相应地,GetHeader()获取不到Content-Type。
```c++
// Get Content-Type
if (cntl->http_request().content_type() == "application/json") {
    ...
}
...
// Set Content-Type
cntl->http_response().set_content_type("text/html");
```
如果RPC失败(Controller被SetFailed), Content-Type会框架强制设为text/plain,而response body设为Controller::ErrorText()。
## Status Code
status code是http response特有的字段,标记http请求的完成情况。请使用定义在[http_status_code.h](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/http_status_code.h)中的enum,遵守HTTP协议。
```c++
// Get Status Code
if (cntl->http_response().status_code() == baidu::rpc::HTTP_STATUS_NOT_FOUND) {
    LOG(FATAL) << "FAILED: " << controller.http_response().reason_phrase();
}
...
// Set Status code
cntl->http_response().set_status_code(baidu::rpc::HTTP_STATUS_INTERNAL_SERVER_ERROR);
cntl->http_response().set_status_code(baidu::rpc::HTTP_STATUS_INTERNAL_SERVER_ERROR, "My explanation of the error...");
```
以下代码在302错误时重定向:
```c++
cntl->http_response().set_status_code(baidu::rpc::HTTP_STATUS_FOUND);
cntl->http_response().SetHeader("Location", "http://bj.bs.bae.baidu.com/family/image001(4979).jpg");
```
![img](http://wiki.baidu.com/download/attachments/71337204/image2015-12-9%2015%3A1%3A47.png?version=1&modificationDate=1449644507000&api=v2)
## Query String
如上面的[HTTP headers](http://wiki.baidu.com/pages/viewpage.action?pageId=213828736#id-实现HTTPService-HTTPheaders)中提到的那样,我们按约定成俗的方式来理解query string,即key1=value1&key2=value2&...。只有key而没有value也是可以的,仍然会被GetQuery查询到,只是值为空字符串,这常被用做bool型的开关。接口定义在[uri.h](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/uri.h)
```c++
const std::string* time_value = cntl->http_request().uri().GetQuery("time");
if (time_value != NULL) { // the query string is present
LOG(TRACE) << "time = " << *time_value;
}
...
cntl->http_request().uri().SetQuery("time", "2015/1/2");
```
# 查看server收到的请求和发出的回复
打开[-http_verbose](http://brpc.baidu.com:8765/flags/http_verbose)即可在stderr看到所有的http request和response,注意这应该只用于线下调试,而不是线上程序。
# 压缩response body
http服务常对http body进行压缩,对于文本网页可以有效减少传输时间,加快页面的展现速度。
在r33093后,调用Controller::set_response_compress_type(baidu::rpc::COMPRESS_TYPE_GZIP)将尝试用gzip压缩http body,并设置"Content-Encoding"为"gzip"。“尝试”指的是如果请求中没有设置Accept-encoding或不包含gzip,压缩不会进行。比如curl不加--compressed时是不支持压缩的,这时server端总是会返回不压缩的结果。
# 解压request body
出于通用性考虑且解压代码不复杂,baidu-rpc不会自动解压request body,用户可以自己做,方法如下:
```c++
#include <baidu/rpc/policy/gzip_compress.h>
...
const std::string* encoding = cntl->http_request().GetHeader("Content-Encoding");
if (encoding != NULL && *encoding == "gzip") {
    base::IOBuf uncompressed;
    if (!baidu::rpc::policy::GzipDecompress(cntl->request_attachment(), &uncompressed)) {
        LOG(ERROR) << "Fail to un-gzip request body";
        return;
    }
    cntl->request_attachment().swap(uncompressed);
}
// cntl->request_attachment()中已经是解压后的数据了
```
# 开启HTTPS
要开启HTTPS,首先确保你的COMAKE/BCLOUD中依赖有最新的openssl库(openssl-1.0.2h)
```python
CONFIGS('third-64/openssl@1.0.2.6123')
```
因为大部分机器自带的openssl版本很旧,有严重的安全漏洞,支持的加密算法也少,违背了开启SSL的初衷
然后在设置ServerOptions中的SSLOptions
```c++
// 证书结构
struct CertInfo {
    // PEM格式证书文件
    // 当存在证书链时, 将所有证书链上的证书append为一个文件
    std::string certificate_file;
  
    // PEM格式的密钥文件
    std::string private_key_file;
  
    // 指定该证书绑定的域名,首字符支持通配符(类似*.abc.com)
    // 访问这些域名的请求,会使用该证书进行SSL握手,在client最终显示该证书的信息
    // 如果没指定此字段,程序会自动尝试从证书文件中提取域名信息
    std::vector<std::string> sni_filters;
};
 
struct SSLOptions {
    // 要加载的所有证书
    std::vector<CertInfo> certs;
 
    // 当HTTPS请求到来时,会自动根据访问域名找相应的证书
    // 如果没有找到相匹配的证书,默认情况使用certs中的第一张证书
    // 除非开启strict_sni,则此时会拒绝该请求
    bool strict_sni;
 
    // ... 其他选项
};
```
其余选项还包括:密钥套件选择(推荐密钥ECDHE-RSA-AES256-GCM-SHA384,chrome默认第一优先密钥,安全性很高,但比较耗性能)、session复用等,具体见server.h
另外,开启HTTPS后,原先的HTTP请求也可以通过同一个端口来访问,Server会自动判断哪些是HTTP,哪些是HTTPS;用户也可以在callback中通过Controller接口来判断:
```c++
bool Controller::is_ssl() const;
```
# 性能
没有极端性能要求的产品线都有使用HTTP协议的倾向,特别是移动端产品线,所以我们很重视HTTP的实现质量,具体来说:
- 使用了node.js的[http parser](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/http_parser.h)(部分来自nginx)解析http消息,这是一个轻量、优秀的实现。
- 使用[rapidjson](https://github.com/miloyip/rapidjson)解析json,这是一个主打性能的json库,由一位腾讯专家开发。
- 在最差情况下解析http请求的时间复杂度也是O(N),其中N是请求的字节数。反过来说,如果解析代码要求http请求是完整的,那么它可能会花费O(N^2)的时间。HTTP请求普遍较大,这一点意义还是比较大的。
- 来自不同client的http消息是高度并发的,即使相当复杂的http消息也不会影响对其他客户端的响应。其他rpc和[基于单线程reactor](http://wiki.baidu.com/display/RPC/Threading+Overview#ThreadingOverview-单线程reactor)的各类http server往往难以做到这一点。
# 持续发送
r33796前baidu-rpc server不适合发送超大或无限长的body。r33796后baidu-rpc server支持。方法如下:
\1. 调用Controller::CreateProgressiveAttachment()创建可持续发送的body。
`boost::intrusive_ptr<baidu::rpc::ProgressiveAttachment> pa(cntl->CreateProgressiveAttachment());`
返回的ProgressiveAttachment对象需要用boost::intrusive_ptr<>管理,定义在<baidu/rpc/progressive_attachment.h>中。
\2. 调用ProgressiveAttachment::Write()发送数据。如果写入发生在server回调结束前,发送的数据将会被缓存直到回调结束发送了header部分后才会开始发送数据。如果写入发生在server回调结束后,发送的数据将立刻以chunked mode写出。
\3. 发送完毕后确保所有的boost::intrusive_ptr<baidu::rpc::ProgressiveAttachment>都析构了。
# 持续接收
目前baidu-rpc server不支持在接受完http请求的header部分就调用用户的服务回调,即baidu-rpc server不适合接收超长或无限长的body。
# FAQ
### Q: baidu-rpc前的nginx报了final fail (ff)
baidu-rpc server同端口支持多种协议,当它遇到非法HTTP请求并解析失败后,无法说这个请求一定是HTTP。在r31355之后,server会对query-string及之后出现解析错误的请求返回HTTP 400错误并关闭连接(因为有很大概率是HTTP请求),但如果是HTTP method错误,诸如出现GET、POST、HEAD等标准方法之外的东西或严重的格式错误(可能由HTTP client有bug导致),server仍会直接断开连接,导致nginx的ff。
解决方案: 在使用Nginx转发流量时,可以对$HTTP_method做一下过滤,只放行允许的方法。或者干脆在proxy时设置proxy_method为指定方法,来避免ff。
### Q: baidu-rpc支持http chunked方式传输吗
支持。
### Q: HTTP请求的query string中含有BASE64编码过的value,为什么有时候无法正常解析
根据[HTTP协议](http://tools.ietf.org/html/rfc3986#section-2.2)中的要求,以下字符应该使用%编码
> ```
> reserved = gen-delims / sub-delims
>
> gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
>
> sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
> / "*" / "+" / "," / ";" / "="
> ```
Base64 编码后的字符串中,会以"="或者"=="作为结尾(比如: ?wi=NDgwMDB8dGVzdA==&anothorkey=anothervalue), 这个字段可能会被正确解析,也可能不会,取决于具体实现,用户不应该做任何假设.
一个解决方法是删除末尾的"=", 不影响Base64的[正常解码](http://en.wikipedia.org/wiki/Base64#Padding); 第二个方法是在这个URI在base64之后在使用%编码,使用的地方先进行%解码,然后再用base64解码.
\ No newline at end of file
# 1.检查工作线程的数量
查看 /vars/bthread_worker_**count** 和 /vars/bthread_worker_**usage**。分别是工作线程的个数,和正在被使用的工作线程个数。
Icon
如果usage和count接近,说明线程不够用了。
比如,下图中有24个工作线程,正在使用的是23.93个,说明所有的工作线程都被打满了,不够用了。
**![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A14%3A8.png?version=1&modificationDate=1452244448000&api=v2)**
下图中正在使用的只有2.36个,工作线程明显是足够的。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A16%3A34.png?version=1&modificationDate=1452244594000&api=v2)
把 /vars/bthread_worker_count;bthread_worker_usage?expand 拼在服务url后直接看到这两幅图,就像[这样](http://brpc.baidu.com:8765/vars/bthread_worker_count;bthread_worker_usage?expand)
# 2.检查CPU的使用程度
查看 /vars/process_core_**count** 和 /vars/process_cpu_**usage**。分别是cpu核心的个数,和正在使用的cpu核数。
Icon
如果usage和count接近,说明CPU不够用了。
下图中cpu核数为24,正在使用的核心数是20.9个,CPU是瓶颈了。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A28%3A37.png?version=1&modificationDate=1452245317000&api=v2)
下图中正在使用的核心数是2.06,CPU是够用的。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A26%3A25.png?version=1&modificationDate=1452245185000&api=v2)
# 3.定位问题
如果process_cpu_usage和bthread_worker_usage接近,说明是cpu-bound,工作线程大部分时间在做计算。
如果process_cpu_usage明显小于bthread_worker_usage,说明是io-bound,工作线程大部分时间在阻塞。
1 - process_cpu_usage / bthread_worker_usage就是大约在阻塞上花费的时间比例,比如process_cpu_usage = 2.4,bthread_worker_usage = 18.5,那么工作线程大约花费了87.1% 的时间在阻塞上。
## 3.1 定位cpu-bound问题
原因可能是单机性能不足,或上游分流不均。
### 排除上游分流不均的嫌疑
在不同服务的[vars界面](http://brpc.baidu.com:8765/vars)输入qps,查看不同的qps是否符合预期,就像这样:
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A5%3A40.png?version=1&modificationDate=1452247540000&api=v2)
或者在命令行中用curl直接访问,像这样:
```shell
$ curl brpc.baidu.com:8765/vars/*qps*
bthread_creation_qps : 95
rpc_server_8765_example_echo_service_echo_qps : 57
```
如果不同机器的分流确实不均,且难以解决,可以考虑[限制最大并发](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-限制最大并发)
### 优化单机性能
请使用[CPU profiler](http://wiki.baidu.com/display/RPC/cpu+profiler)分析程序的热点,用数据驱动优化。一般来说一个卡顿的cpu-bound程序一般能看到显著的热点。
## 3.2 定位io-bound问题
原因可能有:
- 线程确实配少了
- 访问下游服务的client不支持bthread,且延时过长
- 阻塞来自程序内部的锁,IO等等。
如果阻塞无法避免,考虑用异步。
### 排除工作线程数不够的嫌疑
如果线程数不够,你可以尝试动态调大工作线程数,切换到/flags页面,点击bthread_concurrency右边的(R):
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A35%3A54.png?version=1&modificationDate=1452245754000&api=v2)
进入后填入新的线程数确认即可:
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A36%3A48.png?version=1&modificationDate=1452245808000&api=v2)
回到flags界面可以看到bthread_concurrency已变成了新值。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A37%3A33.png?version=1&modificationDate=1452245853000&api=v2)
不过,调大线程数未必有用。如果工作线程是由于访问下游而大量阻塞,调大工作线程数是没有用的。因为真正的瓶颈在于后端的,调大线程后只是让每个线程的阻塞时间变得更长。
比如在我们这的例子中,调大线程后新增的工作线程仍然被打满了。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2017%3A39%3A37.png?version=1&modificationDate=1452245977000&api=v2)
### 排除锁的嫌疑
如果程序被某把锁挡住了,也可能呈现出“io-bound”的特征。先用[contention profiler](http://wiki.baidu.com/display/RPC/contention+profiler)排查锁的竞争状况。
### 使用rpcz
rpcz可以帮助你看到最近的所有请求,和处理它们时在每个阶段花费的时间(单位都是微秒)。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A14%3A8.png?version=1&modificationDate=1452248048000&api=v2)
点击一个span链接后看到该次RPC何时开始,每个阶段花费的时间,何时结束。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A15%3A7.png?version=1&modificationDate=1452248107000&api=v2)
这是一个典型的server在严重阻塞的例子。从接收到请求到开始运行花费了20ms,说明server已经没有足够的工作线程来及时完成工作了。
现在这个span的信息比较少,我们去程序里加一些。你可以使用TRACEPRINTF向rpcz打印日志。打印内容会嵌入在rpcz的时间流中。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A26%3A19.png?version=1&modificationDate=1452248779000&api=v2)
重新运行后,查看一个span,里面的打印内容果然包含了我们增加的TRACEPRINTF。
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A28%3A27.png?version=1&modificationDate=1452248908000&api=v2)
在运行到第一条TRACEPRINTF前,用户回调已运行了2051微秒(假设这符合我们的预期),紧接着foobar()却花费了8036微秒,我们本来以为这个函数会很快返回的。范围进一步缩小了。
重复这个过程,直到找到那个造成问题的函数。
### 使用bvar
TRACEPRINTF主要适合若干次的函数调用,如果一个函数调用了很多次,或者函数本身开销很小,每次都往rpcz打印日志是不合适的。这时候你可以使用bvar。
[bvar](http://wiki.baidu.com/display/RPC/bvar)是一个多线程下的计数库,可以以极低的开销统计用户递来的数值,相比“打日志大法”几乎不影响程序行为。你不用完全了解bvar的完整用法,只要使用bvar::LatencyRecorder即可。
仿照如下代码对foobar的运行时间进行监控。
```c++
#include <base/time.h>
#include <bvar/bvar.h>
bvar::LatencyRecorder g_foobar_latency("foobar");
...
void search() {
...
base::Timer tm;
tm.start();
foobar();
tm.stop();
g_foobar_latency << tm.u_elapsed();
...
}
```
重新运行程序后,在vars的搜索框中键入foobar,显示如下:
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A41%3A11.png?version=1&modificationDate=1452249671000&api=v2)
点击一个bvar可以看到动态图,比如点击cdf后看到
![img](http://wiki.baidu.com/download/attachments/161461013/image2016-1-8%2018%3A42%3A46.png?version=1&modificationDate=1452249766000&api=v2)
根据延时的分布,你可以推测出这个函数的整体行为,对大多数请求表现如何,对长尾表现如何。
你可以在子函数中继续这个过程,增加更多bvar,并比对不同的分布,最后定位来源。
### 只使用了baidu-rpc client
得打开dummy server提供内置服务,方法见[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)
\ No newline at end of file
ub是百度内广泛使用的老RPC框架,在迁移ub服务时不可避免地需要[访问ub-server](http://wiki.baidu.com/pages/viewpage.action?pageId=213828700)或被ub-client访问。ub使用的协议种类很多,但都以nshead作为二进制包的头部,这类服务在baidu-rpc中统称为**“nshead service”**
nshead后大都使用mcpack/compack作为序列化格式,注意这不是“协议”。"协议"除了序列化格式,还涉及到各种特殊字段的定义,一种序列化格式可能会衍生出很多协议。ub没有定义标准协议,所以即使都使用mcpack/compack,产品线的通信协议也是五花八门,无法互通。鉴于此,我们提供了一套接口,让用户能够灵活的处理自己产品线的协议,同时享受baidu-rpc提供的builtin services等一系列框架福利。
# 使用ubrpc的服务
ubrpc协议的基本形式是nshead+compack或mcpack2,但compack或mcpack2中包含一些RPC过程需要的特殊字段。
在baidu-rpc r31687之后,用protobuf写的服务可以通过mcpack2pb被ubrpc client访问,步骤如下:
## 把idl文件转化为proto文件
使用脚本[public/mcpack2pb/idl2proto](https://svn.baidu.com/public/trunk/mcpack2pb/idl2proto)把idl文件自动转化为proto文件,下面是转化后的proto文件。
```protobuf
// Converted from echo.idl by public/mcpack2pb/idl2proto
import "idl_options.proto";
option (idl_support) = true;
option cc_generic_services = true;
message EchoRequest {
required string message = 1;
}
message EchoResponse {
required string message = 1;
}
// 对于有多个参数的idl方法,需要定义一个包含所有request或response的消息,作为对应方法的参数。
message MultiRequests {
required EchoRequest req1 = 1;
required EchoRequest req2 = 2;
}
message MultiResponses {
required EchoRequest res1 = 1;
required EchoRequest res2 = 2;
}
service EchoService {
// 对应idl中的void Echo(EchoRequest req, out EchoResponse res);
rpc Echo(EchoRequest) returns (EchoResponse);
// 对应idl中的EchoWithMultiArgs(EchoRequest req1, EchoRequest req2, out EchoResponse res1, out EchoResponse res2);
rpc EchoWithMultiArgs(MultiRequests) returns (MultiResponses);
}
```
原先的echo.idl文件如下:
```protobuf
struct EchoRequest {
string message;
};
struct EchoResponse {
string message;
};
service EchoService {
void Echo(EchoRequest req, out EchoResponse res);
uint32_t EchoWithMultiArgs(EchoRequest req1, EchoRequest req2, out EchoResponse res1, out EchoResponse res2);
};
```
## 在COMAKE中设置protoc和mcpack2pb的参数
注意--mcpack_out要和--cpp_out一致,你可以先设成--mcpack_out=.,执行comake2或bcloud后看错误信息中的--cpp_out的值,再把--mcpack_out设成一样的。
BCLOUD中要把`/public/mcpack2pb/protoc-gen-mcpack`替换成`/public/mcpack2pb/protoc-gen-mcpack**.forbcloud**,并把ENV.WorkRoot()替换为WORKROOT的实际值。`
```pyton
PROTOC(ENV.WorkRoot()+"/third-64/protobuf/bin/protoc")
PROTOFLAGS("--plugin=protoc-gen-mcpack=" + ENV.WorkRoot() + "/public/mcpack2pb/protoc-gen-mcpack --mcpack_out=.")
PROTOFLAGS('--proto_path=' + ENV.WorkRoot() + '/public/mcpack2pb/')
PROTOFLAGS('--proto_path=' + ENV.WorkRoot() + '/third-64/protobuf/include/')
```
## 实现生成的Service基类
```c++
class EchoServiceImpl : public EchoService {
public:
...
// 对应idl中的void Echo(EchoRequest req, out EchoResponse res);
virtual void Echo(google::protobuf::RpcController* cntl_base,
const EchoRequest* request,
EchoResponse* response,
google::protobuf::Closure* done) {
baidu::rpc::ClosureGuard done_guard(done);
baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
// 填充response。
response->set_message(request->message());
// 对应的idl方法没有返回值,不需要像下面方法中那样set_idl_result()。
// 可以看到这个方法和其他protobuf服务没有差别,所以这个服务也可以被ubrpc之外的协议访问。
}
virtual void EchoWithMultiArgs(google::protobuf::RpcController* cntl_base,
const MultiRequests* request,
MultiResponses* response,
google::protobuf::Closure* done) {
baidu::rpc::ClosureGuard done_guard(done);
baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
// 填充response。response是我们定义的包含所有idl response的消息。
response->mutable_res1()->set_message(request->req1().message());
response->mutable_res2()->set_message(request->req2().message());
// 告诉RPC有多个request和response。
cntl->set_idl_names(baidu::rpc::idl_multi_req_multi_res);
// 对应idl方法的返回值。
cntl->set_idl_result(17);
}
};
```
## 设置ServerOptions.nshead_service
```c++
#include <baidu/rpc/ubrpc2pb_protocol.h>
...
baidu::rpc::ServerOptions option;
option.nshead_service = new baidu::rpc::policy::UbrpcCompackAdaptor; // mcpack2用UbrpcMcpack2Adaptor
```
例子见[example/echo_c++_ubrpc_compack](https://svn.baidu.com/public/trunk/baidu-rpc/example/echo_c++_ubrpc_compack/)
# 使用nshead+blob的服务
[NsheadService](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/nshead_service.h)是baidu-rpc中所有处理nshead打头协议的基类,实现好的NsheadService实例得赋值给ServerOptions.nshead_service才能发挥作用。不赋值的话,默认是NULL,代表不支持任何nshead开头的协议,这个server被nshead开头的数据包访问时会报错。明显地,**一个Server只能处理一种以nshead开头的协议。**
NsheadService的接口如下,基本上用户只需要实现`ProcessNsheadRequest`这个函数。
```c++
// 代表一个nshead请求或回复。
struct NsheadMessage {
nshead_t head;
base::IOBuf body;
};
// 实现这个类并复制给ServerOptions.nshead_service来让baidu-rpc处理nshead请求。
class NsheadService : public Describable {
public:
NsheadService();
NsheadService(const NsheadServiceOptions&);
virtual ~NsheadService();
// 实现这个方法来处理nshead请求。注意这个方法可能在调用时controller->Failed()已经为true了。
// 原因可能是Server.Stop()被调用正在退出(错误码是baidu::rpc::ELOGOFF)
// 或触发了ServerOptions.max_concurrency(错误码是baidu::rpc::ELIMIT)
// 在这种情况下,这个方法应该通过返回一个代表错误的response让客户端知道这些错误。
// Parameters:
// server The server receiving the request.
// controller Contexts of the request.
// request The nshead request received.
// response The nshead response that you should fill in.
// done You must call done->Run() to end the processing, baidu::rpc::ClosureGuard is preferred.
virtual void ProcessNsheadRequest(const Server& server,
Controller* controller,
const NsheadMessage& request,
NsheadMessage* response,
NsheadClosure* done) = 0;
};
```
完整的example在[example/nshead_extension_c++](https://svn.baidu.com/public/trunk/baidu-rpc/example/nshead_extension_c++/)
# 使用nshead+mcpack/compack/idl的服务
idl是mcpack/compack的前端,用户只要在idl文件中描述schema,就可以生成一些C++结构体,这些结构体可以打包为mcpack/compack。如果你的服务仍在大量地使用idl生成的结构体,且短期内难以修改,同时想要使用baidu-rpc提升性能和开发效率的话,可以实现[NsheadService](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/nshead_service.h),其接口接受nshead + 二进制包为request,用户填写自己的处理逻辑,最后的response也是nshead+二进制包。流程与protobuf方法保持一致,但过程中不涉及任何protobuf的序列化和反序列化,用户可以自由地理解nshead后的二进制包,包括用idl加载mcpack/compack数据包。
不过,你应当充分意识到这么改造的坏处:
> **这个服务在继续使用mcpack/compack作为序列化格式,相比protobuf占用成倍的带宽和打包时间。**
为了解决这个问题,我们提供了[public/mcpack2pb](http://wiki.baidu.com/display/RPC/idl+%3C%3D%3E+protobuf),允许把protobuf作为mcpack/compack的前端。你只要写一份proto文件,就可以同时解析mcpack/compack和protobuf格式的请求。使用这个方法,使用idl描述的服务的可以平滑地改造为使用proto文件描述,而不用修改上游client(仍然使用mcpack/compack)。你产品线的服务可以逐个地从mcpack/compack/idl切换为protobuf,从而享受到性能提升,带宽节省,全新开发体验等好处。你可以自行在NsheadService使用public/mcpack2pb,也可以联系我们,提供更高质量的协议支持。
# 使用nshead+protobuf的服务
如果你的协议已经使用了nshead + protobuf,或者你想把你的协议适配为protobuf格式,那可以使用另一种模式:实现[NsheadPbServiceAdaptor](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/nshead_pb_service_adaptor.h)(NsheadService的子类)。
工作步骤:
- Call ParseNsheadMeta() to understand the nshead header, user must tell RPC which pb method to call in the callback.
- Call ParseRequestFromIOBuf() to convert the body after nshead header to pb request, then call the pb method.
- When user calls server's done to end the RPC, SerializeResponseToIOBuf() is called to convert pb response to binary data that will be appended after nshead header and sent back to client.
这样做的好处是,这个服务还可以被其他使用protobuf的协议访问,比如标准协议,hulu协议,sofa协议等等。NsheadPbServiceAdaptor的主要接口如下。完整的example在[这里](https://svn.baidu.com/public/trunk/baidu-rpc/example/nshead_pb_extension_c++/)
```c++
class NsheadPbServiceAdaptor : public NsheadService {
public:
    NsheadPbServiceAdaptor() : NsheadService(
        NsheadServiceOptions(false, SendNsheadPbResponseSize)) {}
    virtual ~NsheadPbServiceAdaptor() {}
 
    // Fetch meta from `nshead_req' into `meta'.
    // Params:
    //   server: where the RPC runs.
    //   nshead_req: the nshead request that server received.
    //   controller: If something goes wrong, call controller->SetFailed()
    //   meta: Set meta information into this structure. `full_method_name'
    //         must be set if controller is not SetFailed()-ed
    // FIXME: server is not needed anymore, controller->server() is same
    virtual void ParseNsheadMeta(const Server& server,
                                 const NsheadMessage& nshead_req,
                                 Controller* controller,
                                 NsheadMeta* meta) const = 0;
    // Transform `nshead_req' to `pb_req'.
    // Params:
    //   meta: was set by ParseNsheadMeta()
    //   nshead_req: the nshead request that server received.
    //   controller: you can set attachment into the controller. If something
    //               goes wrong, call controller->SetFailed()
    //   pb_req: the pb request should be set by your implementation.
    virtual void ParseRequestFromIOBuf(const NsheadMeta& meta,
                                       const NsheadMessage& nshead_req,
                                       Controller* controller,
                                       google::protobuf::Message* pb_req) const = 0;
    // Transform `pb_res' (and controller) to `nshead_res'.
    // Params:
    //   meta: was set by ParseNsheadMeta()
    //   controller: If something goes wrong, call controller->SetFailed()
    //   pb_res: the pb response that returned by pb method. [NOTE] `pb_res'
    //           can be NULL or uninitialized when RPC failed (indicated by
    //           Controller::Failed()), in which case you may put error
    //           information into `nshead_res'.
    //   nshead_res: the nshead response that will be sent back to client.
    virtual void SerializeResponseToIOBuf(const NsheadMeta& meta,
                                          Controller* controller,
                                          const google::protobuf::Message* pb_res,
                                          NsheadMessage* nshead_res) const = 0;
};
```
用户能通过/rpcz看到最近请求的详细信息,并可以插入注释(annotation),不同于tracing system(如[dapper](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/36356.pdf))以全局视角看到整体系统的延时分布,rpcz更多是一个调试工具,虽然角色有所不同,但在baidu-rpc中rpcz和tracing的数据来源是一样的。当每秒请求数小于1万时,rpcz会记录所有的请求,超过1万时,rpcz会随机忽略一些请求把采样数控制在1万左右。rpcz可以淘汰时间窗口之前的数据,通过-span_keeping_seconds选项设置,默认1小时。[一个长期运行的例子](http://brpc.baidu.com:8765/rpcz)
关于开销:我们的实现完全规避了线程竞争,开销极小,在qps 30万的测试场景中,观察不到明显的性能变化,对大部分应用而言应该是“free”的。即使采集了几千万条请求,rpcz也不会增加很多内存,一般在50兆以内。rpcz会占用一些磁盘空间(就像日志一样),如果设定为存一个小时的数据,一般在几百兆左右。
## 开关方法
默认不开启,加入[-enable_rpcz](http://brpc.baidu.com:8765/flags/*rpcz*)选项会在启动后开启。
若启动时未加-enable_rpcz,则可在启动后访问SERVER_URL/rpcz/enable动态开启rpcz,访问SERVER_URL/rpcz/disable则关闭,这两个链接等价于访问SERVER_URL/flags/enable_rpcz?setvalue=true和SERVER_URL/flags/enable_rpcz?setvalue=false。在r31010之后,rpc在html版本中增加了一个按钮可视化地开启和关闭。
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2013%3A20%3A38.png?version=1&modificationDate=1450934438000&api=v2)
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-12-24%2013%3A21%3A34.png?version=1&modificationDate=1450934494000&api=v2)
如果只是baidu-rpc client或没有使用baidu-rpc,看[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)
## 数据展现
/rpcz展现的数据分为两层。
### 第一层
看到最新请求的概况,点击链接进入第二层。
![img](http://wiki.baidu.com/download/attachments/37774685/image2015-1-21%2020%3A20%3A53.png?version=1&modificationDate=1421842854000&api=v2)
### 第二层
看到某系列(trace)或某个请求(span)的详细信息。一般通过点击链接进入,也可以把trace=和span=作为query-string拼出链接
![img](http://wiki.baidu.com/download/attachments/37774685/image2015-1-21%2020%3A23%3A35.png?version=1&modificationDate=1421843015000&api=v2)
内容说明:
- 时间分为了绝对时间(如2015/01/21-20:20:30.817392,小数点后精确到微秒)和前一个时间的差值(如. 19,代表19微秒)。
- trace=ID有点像“session id”,对应一个系统中完成一次对外服务牵涉到的所有服务,即上下游server都共用一个trace-id。span=ID对应一个server或client中一个请求的处理过程。trace-id和span-id在概率上唯一。
- 第一层页面中的request=和response=后的是数据包的字节数,包括附件但不包括协议meta。第二层中request和response的字节数一般在括号里,比如"Responded(13)"中的13。
- 点击链接可能会访问其他server上的rpcz,点浏览器后退一般会返回到之前的页面位置。
- I'm the last call, I'm about to ...都是用户的annotation。
## Annotation
只要你使用了baidu-rpc,就可以使用[TRACEPRINTF](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/traceprintf.h)打印内容到事件流中,比如:
```c++
TRACEPRINTF("Hello rpcz %d", 123);
```
这条annotation会按其发生时间插入到对应请求的rpcz中。从这个角度看,rpcz是请求级的日志。如果你用TRACEPRINTF打印了沿路的上下文,便可看到请求在每个阶段停留的时间,牵涉到的数据集和参数。这是个很有用的功能。
\ No newline at end of file
[RPC接口规范](http://gollum.baidu.com/RPCSpec)规定百度内使用protobuf作为通用的描述语言,我们谈论的Service默认指google::protobuf::Service。
# 填写proto文件
```C++
# 告诉protoc要生成C++ Service基类,如果是java或python,则应分别修改为java_generic_services和py_generic_services
option cc_generic_services = true;
message EchoRequest {
required string message = 1;
};
message EchoResponse {
required string message = 1;
};
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse);
};
```
protobuf的更多用法请阅读[protobuf官方文档](https://developers.google.com/protocol-buffers/docs/proto#options)
# 实现生成的Service基类
protoc运行后会生成[echo.pb.cc](http://echo.pb.cc/)和echo.pb.h文件,你得include echo.pb.h,实现其中的EchoService基类:
**my_echo_service.cpp**
```c++
#include "echo.pb.h"
...
class MyEchoService : public EchoService  {
public:
    void Echo(::google::protobuf::RpcController* cntl_base,
              const ::example::EchoRequest* request,
              ::example::EchoResponse* response,
              ::google::protobuf::Closure* done) {
        // 这个对象确保在return时自动调用done->Run()
        baidu::rpc::ClosureGuard done_guard(done);
         
        baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
 
        // 填写response
        response->set_message(request->message());
    }
};
```
当客户端发来请求时,Echo会被调用。4个参数的含义分别是:
**controller**
在baidu-rpc中可以静态转为baidu::rpc::Controller(前提是这运行baidu-rpc的Server中),包含了所有request和response之外的参数集合,具体接口查阅[controller.h](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/controller.h)
**request**
请求,只读的,解析自client端发来的二进制数据包。
**response**
回复。需要你来填充,如果你没有填充所有的**required**字段,框架会发现并令该次调用失败。
**done**
done包含了调用服务回调后的下一步动作,包括检查response正确性,序列化,打包,发送等逻辑。
> 不管成功失败,done->Run()必须在请求处理完成后被调用。
为什么框架不自己调用done?这是为了允许用户把done保存下来,在服务回调之后调用,即实现异步Service。
我们强烈建议使用**ClosureGuard**确保done->Run()被调用,即在服务回调开头的那句:
```c++
baidu::rpc::ClosureGuard done_guard(done);
```
不管在中间还是末尾脱离服务回调,都会使done_guard析构,其中会调用done->Run()。这个机制称为[RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization)。没有这个的话你得在每次return前都加上done->Run(),**极易忘记**
在异步Service中,退出服务回调时请求未处理完成,done->Run()不应被调用,done应被保存下来供以后调用,乍看起来,这里并不需要用ClosureGuard。但在实践中,异步Service照样会因各种原因跳出回调,如果不使用ClosureGuard,一些分支很可能会忘记done->Run()。所以我们同样建议使用done_guard,与同步Service不同的是,为了避免脱离函数时done->Run()被调用,你可以调用done_guard.release()来释放其中的done。
一般来说,同步Service和异步Service分别按如下代码处理done:
```c++
class MyFooService: public FooService  {
public:
    // 同步服务
    void SyncFoo(::google::protobuf::RpcController* cntl_base,
                 const ::example::EchoRequest* request,
                 ::example::EchoResponse* response,
                 ::google::protobuf::Closure* done) {
         baidu::rpc::ClosureGuard done_guard(done);
         ...
    }
 
    // 异步服务
    void AsyncFoo(::google::protobuf::RpcController* cntl_base,
                  const ::example::EchoRequest* request,
                  ::example::EchoResponse* response,
                  ::google::protobuf::Closure* done) {
         baidu::rpc::ClosureGuard done_guard(done);
         ...
         done_guard.release();
    }
};
```
ClosureGuard的接口如下:
```c++
// RAII: Call Run() of the closure on destruction.
class ClosureGuard {
public:
    ClosureGuard();
    // Constructed with a closure which will be Run() inside dtor.
    explicit ClosureGuard(google::protobuf::Closure* done);
    
    // Call Run() of internal closure if it's not NULL.
    ~ClosureGuard();
 
    // Call Run() of internal closure if it's not NULL and set it to `done'.
    void reset(google::protobuf::Closure* done);
 
    // Set internal closure to NULL and return the one before set.
    google::protobuf::Closure* release();
};
```
> Service在插入[baidu::rpc::Server](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/server.h)后才可能提供服务。
## 标记当前调用为失败
调用Controller.SetFailed()可以把当前调用设置为失败,当发送过程出现错误时,框架也会调用这个函数。用户一般是在服务的CallMethod里调用这个函数,比如某个处理环节出错,SetFailed()后便可调用done->Run()并跳出函数了(如果使用了ClosureGuard的话在跳出函数时会自动调用done,不用手动)。Server端的done的逻辑主要是发送response回client,当其发现用户调用了SetFailed()后,会把错误信息送回client。client收到后,它的Controller::Failed()会为true(成功时为false),Controller::ErrorCode()和Controller::ErrorText()则分别是错误码和错误信息。
对于http访问,用户还可以设置[status-code](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html),在server端一般是调用controller.http_response().set_status_code(),标准的status-code定义在[http_status_code.h](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/http_status_code.h)中。如果SetFailed了但没有设置status-code,默认设为baidu::rpc::HTTP_STATUS_INTERNAL_SERVER_ERROR(500错误)
## 获取Client的地址和端口
controller->remote_side()可获得发送该请求的client地址和端口,类型是base::EndPoint。如果client是nginx,remote_side()是nginx的ip。要获取真实client的ip,可以在nginx里设置proxy_header ClientIp $remote_addr; 在rpc中通过controller->http_request().GetHeader("ClientIp")获得对应的值。
打印方式:
```c++
LOG(INFO) << "remote_side=" << cntl->remote_side();
printf("remote_side=%s\n", base::endpoint2str(cntl->remote_side()).c_str());
```
## 获取Server的地址和端口
r31384前server在接受连接时会把server端的端口打印到日志中,但没有接口获得。r31384后调用controller->local_side()获得,类型是base::EndPoint。
打印方式:
```c++
LOG(INFO) << "local_side=" << cntl->local_side();
printf("local_side=%s\n", base::endpoint2str(cntl->local_side()).c_str());
```
## 异步Service
即done->Run()在Service回调之外被调用。
大多数Server在进入回调函数后使用baidu::rpc::ClosureGuard以确保在退出回调时done->Run()会被自动调用(向client发回response),这种是**同步service**
有些server以等待后端服务返回结果为主,且处理时间特别长,为了及时地释放出线程资源,更好的办法是把done注册到被等待事件的回调中,等到事件发生后再调用done->Run(),这种是**异步service**
异步service的最后一行一般是done_guard.release()以确保done_guard在析构时不会调用done->Run(),而是在事件回调中调用。例子请看[example/session_data_and_thread_local](https://svn.baidu.com/public/trunk/baidu-rpc/example/session_data_and_thread_local/)
>Service和Channel都可以使用done来表达后续的操作,但它们是完全不同的,请勿混淆:
>
>- Service的done由框架创建,用户处理请求后调用done把response发回给client。
>- Channel的done由用户创建,递给框架,待RPC结束后被框架调用以执行用户的后续代码。
# 插入Service
默认构造后的Server不包含任何服务,也不会对外提供服务,仅仅是一个对象。
通过AddService插入你的Service实例。
**server.h**
```c++
int AddService(google::protobuf::Service* service, ServiceOwnership ownership);
```
若ownership参数为SERVER_OWNS_SERVICE,Server在析构时会一并删除Service,否则应设为SERVER_DOESNT_OWN_SERVICE。插入上节中的MyEchoService代码如下:
```c++
baidu::rpc::Server server;
MyEchoService my_echo_service;
if (server.AddService(&my_echo_service, baidu::rpc::SERVER_DOESNT_OWN_SERVICE) != 0) {
    LOG(FATAL) << "Fail to add my_echo_service";
    return -1;
}
```
>Server启动后你无法再修改其中的Service。
# 启动
**server.h**
```c++
int Start(const char* ip_and_port_str, const ServerOptions* opt);
int Start(EndPoint ip_and_port, const ServerOptions* opt);
int Start(int port, const ServerOptions* opt);
int Start(const char *ip_str, PortRange port_range, const ServerOptions *opt);  // r32009后增加
```
"localhost:9000", "cq01-cos-dev00.cq01:8000", “127.0.0.1:7000"都是合法的"ip_and_port_str"。其他重载形式请阅读[server.h](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/server.h)
options为NULL时所有参数取默认值,如果你要使用非默认值,这么做就行了:
```c++
baidu::rpc::ServerOptions options;  // 包含了默认值
options.xxx = yyy;
...
server.Start(..., &options);
```
## 监听多个端口
一个server只能监听一个端口(不考虑ServerOptions.internal_port),需要监听N个端口就起N个Server。
# 停止
```
server.Stop(closewait_ms); // r28921后closewait_ms无效
server.Join();
```
Stop()不会阻塞,Join()会。分成两个函数的原因在于当多个Server需要退出时,可以先全部Stop再一起Join,如果一个个Stop/Join,可能得花费Server个数倍的等待时间。
r28921之前:如果closewait_ms不会0,且还有请求在被处理,那么Server不会立刻关闭与client的连接,而是继续处理请求,同时对client发来的新请求立刻回复ELOGOFF错误以防止新请求加入,这个过程最多持续closewait_ms毫秒,之后Server会关闭所有连接并退出。
r28921之后:closewait_ms失效,不管它是什么值,server在退出时总会等待所有正在被处理的请求完成,同时对新请求立刻回复ELOGOFF错误。这么做的原因在于只要server退出时仍有处理线程运行,就有访问到已释放内存的风险。如果你的server“退不掉”,很有可能是由于某个检索线程hang住了,
当client看到ELOGOFF时,会跳过对应的server,并在其他server上重试对应的请求。所以在一般情况下baidu-rpc总是“优雅退出”的,重启或上线时几乎不会或只会丢失很少量的流量。
r31371后增加了RunUntilAskedToQuit()函数,简化了大部分情况下server的运转和停止代码。在server.Start后,只需如下代码即会让server运行直到按到Ctrl-C。
```c++
// Wait until Ctrl-C is pressed, then Stop() and Join() the server.
server.RunUntilAskedToQuit();
 
// server已经停止了,这里可以写释放资源的代码。
```
Join()完成后可以修改其中的Service,并重新Start。
# 被HTTP client访问
每个Protobuf Service默认可以通过HTTP访问,body被视为json串可通过名字与pb消息相互转化。以[echo server](http://brpc.baidu.com:8765/)为例,你可以通过http协议访问这个服务。
```shell
# before r31987
$ curl -H 'Content-Type: application/json' -d '{"message":"hello"}' http://brpc.baidu.com:8765/EchoService/Echo
{"message":"hello"}
# after r31987
$ curl -d '{"message":"hello"}' http://brpc.baidu.com:8765/EchoService/Echo
{"message":"hello"}
```
注意:
- 在r31987前必须把Content-Type指定为applicaiton/json(-H选项),否则服务器端不会做转化。r31987后不需要。
- r34740之后可以指定Content-Type为application/proto (-H选项) 来传递protobuf二进制格式.
## json<=>pb
json通过名字与pb字段一一对应,结构层次也应匹配。json中一定要包含pb的required字段,否则转化会失败,对应请求会被拒绝。json中可以包含pb中没有定义的字段,但不会作为pb的unknown字段被继续传递。转化规则详见[json <=> protobuf](http://wiki.baidu.com/display/RPC/json+%3C%3D%3E+protobuf)
r34532后增加选项-pb_enum_as_number,开启后pb中的enum会转化为它的数值而不是名字,比如在`enum MyEnum { Foo = 1; Bar = 2; };`中不开启此选项时MyEnum类型的字段会转化为"Foo"或"Bar",开启后为1或2。此选项同时影响client发出的请求和server返回的回复。由于转化为名字相比数值有更好的前后兼容性,此选项只应用于兼容无法处理enum为名字的场景。
## 兼容(很)老版本client
15年时,baidu-rpc允许一个pb service被http协议访问时,不设置pb请求,即使里面有required字段。一般来说这种service会自行解析http请求和设置http回复,并不会访问pb请求。但这也是非常危险的行为,毕竟这是pb service,但pb请求却是未定义的。这种服务在升级到新版本rpc时会遇到障碍,因为baidu-rpc早不允许这种行为。为了帮助这种服务升级,r34953后baidu-rpc允许用户经过一些设置后不把http body自动转化为pb(从而可自行处理),方法如下:
```c++
baidu::rpc::ServiceOptions svc_opt;
svc_opt.ownership = ...;
svc_opt.restful_mappings = ...;
svc_opt.allow_http_body_to_pb = false; //关闭http body至pb的自动转化
server.AddService(service, svc_opt);
```
如此设置后service收到http请求后不会尝试把body转化为pb请求,所以pb请求总是未定义状态,用户得根据`cntl->request_protocol() == baidu::rpc::PROTOCOL_HTTP`来判断请求是否是http,并自行对http body进行解析。
相应地,r34953中当cntl->response_attachment()不为空时(且pb回复不为空),框架不再报错,而是直接把cntl->response_attachment()作为回复的body。这个功能和设置allow_http_body_to_pb与否无关,如果放开自由度导致过多的用户犯错,可能会有进一步的调整。
# 协议支持
server端会自动尝试其支持的协议,无需用户指定。`cntl->protocol()`可获得当前协议。server能从一个listen端口建立不同协议的连接,不需要为不同的协议使用不同的listen端口,一个连接上也可以传输多种协议的数据包(但一般不会这么做),支持的协议有:
- [标准协议](http://wiki.baidu.com/display/pp/Protobuf+RPC),显示为"baidu_std",默认启用。
- hulu协议,显示为"hulu",默认启动。
- http协议,显示为”http“,默认启用。
- sofa协议,显示为”sofa“,默认启用。
- nova协议,显示为”nova“ (r32206前显示为"nshead_server"),默认不启用,开启方式:
```c++
#include <baidu/rpc/policy/nova_pbrpc_protocol.h>
...
ServerOptions options;
...
options.nshead_service = new baidu::rpc::policy::NovaServiceAdaptor;
```
- public/pbrpc协议,显示为"public_pbrpc" (r32206前显示为"nshead_server"),默认不启用,开启方式:
```c++
#include <baidu/rpc/policy/public_pbrpc_protocol.h>
...
ServerOptions options;
...
options.nshead_service = new baidu::rpc::policy::PublicPbrpcServiceAdaptor;
```
- nshead_mcpack协议,显示为"nshead_mcpack",默认不启用,开启方式:
```c++
#include <baidu/rpc/policy/nshead_mcpack_protocol.h>
...
ServerOptions options;
...
options.nshead_service = new baidu::rpc::policy::NsheadMcpackAdaptor;
```
顾名思义,这个协议的数据包由nshead+mcpack构成,mcpack中不包含特殊字段。不同于用户基于NsheadService的实现,这个协议使用了mcpack2pb:任何protobuf service都可以接受这个协议的请求。由于没有传递ErrorText的字段,当发生错误时server只能关闭连接。
- ITP协议,显示为"itp",默认不启用,使用方式见[ITP](http://wiki.baidu.com/display/RPC/ITP)。
- UB相关的协议请阅读[实现NsheadService](http://wiki.baidu.com/pages/viewpage.action?pageId=213828733)。
如果你有更多的协议需求,可以联系我们。
# 设置
## 版本
Server.set_version(...)可以为server设置一个名称+版本,可通过/version内置服务访问到。名字中请包含服务名,而不是仅仅是一个版本号。
## 关闭闲置连接
如果一个连接在ServerOptions.idle_timeout_sec对应的时间内没有读取或写出数据,则被视为”闲置”而被server主动关闭,打开[-log_idle_connection_close](http://brpc.baidu.com:8765/flags/log_idle_connection_close)后关闭前会打印一条日志。默认值为-1,代表不开启。
## pid_file
```
默认为空。如果设置了此字段,Server启动时会创建一个同名文件,内容为进程号。
```
## 在每条日志后打印hostname
此功能只对[base/logging.h](https://svn.baidu.com/public/trunk/common/base/logging.h)中的日志宏有效。打开[-log_hostname](http://brpc.baidu.com:8765/flags/log_hostname)后每条日志后都会带本机名称,如果所有的日志需要汇总到一起进行分析,这个功能可以帮助你了解某条日志来自哪台机器。
## 打印FATAL日志后退出程序
打开[-crash_on_fatal_log](http://brpc.baidu.com:8765/flags/crash_on_fatal_log)后如果程序使用LOG(FATAL)打印了异常日志或违反了CHECK宏中的断言,那么程序会在打印日志后abort,这一般也会产生coredump文件。这个开关可在对程序的压力测试中打开,以确认程序没有进入过严重错误的分支。
> 虽然LOG(ERROR)在打印至comlog时也显示为FATAL,但那只是因为comlog没有ERROR这个级别,ERROR并不受这个选项影响,LOG(ERROR)总不会导致程序退出。一般的惯例是,ERROR表示可容忍的错误,FATAL代表不可逆转的错误。
## 最低日志级别
此功能只对[base/logging.h](https://svn.baidu.com/public/trunk/common/base/logging.h)中的日志宏有效。设置[-min_log_level](http://brpc.baidu.com:8765/flags/min_log_level)后只有**不低于**被设置日志级别的日志才会被打印,这个选项可以动态修改。设置值和日志级别的对应关系:0=INFO 1=NOTICE 2=WARNING 3=ERROR 4=FATAL
被拦住的日志产生的开销只是一次if判断,也不会评估参数(比如某个参数调用了函数,日志不打,这个函数就不会被调用),这和comlog是完全不同的。如果日志最终打印到comlog,那么还要经过comlog中的日志级别的过滤。
## 打印发送给client的错误
server的框架部分在出现错误时一般是不打日志的,因为当大量client出现错误时,可能会导致server高频打印日志,雪上加霜。但有时为了调试问题,或就是需要让server打印错误,打开参数[-log_error_text](http://brpc.baidu.com:8765/flags/log_error_text)即可。
## 限制最大消息
为了保护server和client,当server收到的request或client收到的response过大时,server或client会拒收并关闭连接。此最大尺寸由[-max_body_size](http://brpc.baidu.com:8765/flags/max_body_size)控制,单位为字节。
超过最大消息时会打印如下错误日志:
> FATAL: 05-10 14:40:05: * 0 src/baidu/rpc/input_messenger.cpp:89] A message from 127.0.0.1:35217(protocol=baidu_std) is bigger than 67108864 bytes, the connection will be closed. Set max_body_size to allow bigger messages
protobuf中有[类似的限制](https://github.com/google/protobuf/blob/master/src/google/protobuf/io/coded_stream.h#L364),在r34677之前,即使用户设置了足够大的-max_body_size,仍然有可能因为protobuf中的限制而被拒收,出错时会打印如下日志:
> FATAL: 05-10 13:35:02: * 0 google/protobuf/io/coded_stream.cc:156] A protocol message was rejected because it was too big (more than 67108864 bytes). To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.
在r34677后,baidu-rpc移除了protobuf中的限制,只要-max_body_size足够大,protobuf不会再打印限制错误。此功能对protobuf的版本没有要求。
## 压缩
set_response_compress_type()设置response的压缩方式,默认不压缩。注意附件不会被压缩。HTTP body的压缩方法见[server压缩response body](http://wiki.baidu.com/pages/viewpage.action?pageId=213828736#id-实现HTTPService-压缩responsebody)。
支持的压缩方法有:
- baidu::rpc::CompressTypeSnappy : [snanpy压缩](http://google.github.io/snappy/),压缩和解压显著快于其他压缩方法,但压缩率最低。
- baidu::rpc::CompressTypeGzip : [gzip压缩](http://en.wikipedia.org/wiki/Gzip),显著慢于snappy,但压缩率高
- baidu::rpc::CompressTypeZlib : [zlib压缩](http://en.wikipedia.org/wiki/Zlib),比gzip快10%~20%,压缩率略好于gzip,但速度仍明显慢于snappy。
更具体的性能对比见[Client-压缩](http://wiki.baidu.com/pages/viewpage.action?pageId=213828685#id-创建和访问Client-压缩).
## 附件
标准协议和hulu协议支持附件,这段数据由用户自定义,不经过protobuf的序列化。站在server的角度,设置在Controller::response_attachment()的附件会被client端收到,request_attachment()则包含了client端送来的附件。附件不受压缩选项影响。
在http协议中,附件对应[message body](http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html),比如要返回的数据就设置在response_attachment()中。
## 验证client身份
如果server端要开启验证功能,需要继承实现`Authenticator`中的`VerifyCredential`接口
```c++
class Authenticator {                                                                                                                                                              
public:                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
    // Implement this method to verify credential information                                                                                                                      
    // `auth_str' from `client_addr'. You can fill credential                                                                                                                      
    // context (result) into `*out_ctx' and later fetch this                                                                                                                       
    // pointer from `Controller'.                                                                                                                                                  
    // Returns 0 on success, error code otherwise                                                                                                                                  
    virtual int VerifyCredential(const std::string& auth_str,                                                                                                                      
                                 const base::EndPoint& client_addr,                                                                                                                
                                 AuthContext* out_ctx) const = 0;                                                                                                                                                                                                                                                                                                     
}; 
 
class AuthContext {
public:
    const std::string& user() const;
    const std::string& group() const;
    const std::string& roles() const;
    const std::string& starter() const;
    bool is_service() const;
};
```
当server收到连接上的第一个包时,会尝试解析出其中的身份信息部分(如标准协议里的auth字段、HTTP协议里的Authorization头),然后附带client地址信息一起调用`VerifyCredential`。
若返回0,表示验证成功,用户可以把验证后的信息填入`AuthContext`,后续可通过`controller->auth_context()获取,用户不需要关心controller->auth_context()的分配和释放`
否则,表示验证失败,连接会被直接关闭,client访问失败。
由于server的验证是基于连接的,`VerifyCredential`只会在每个连接建立之初调用,后续请求默认通过验证。
最后,把实现的`Authenticator`实例赋值到`ServerOptions.auth`,即开启验证功能,需要保证该实例在整个server运行周期内都有效,不能被析构。
我们为公司统一的Giano验证方案提供了默认的Authenticator实现,配合Giano的具体用法参看[Giano快速上手手册.pdf](http://wiki.baidu.com/download/attachments/37774685/Giano%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E6%89%8B%E5%86%8C.pdf?version=1&modificationDate=1421990746000&api=v2)中的鉴权部分。
server端开启giano认证的方式:
```c++
// Create a baas::CredentialVerifier using Giano's API
baas::CredentialVerifier verifier = CREATE_MOCK_VERIFIER(baas::sdk::BAAS_OK);
 
// Create a baidu::rpc::policy::GianoAuthenticator using the verifier we just created 
// and then pass it into baidu::rpc::ServerOptions
baidu::rpc::policy::GianoAuthenticator auth(NULL, &verifier);
baidu::rpc::ServerOptions option;
option.auth = &auth;
```
## worker线程数
设置ServerOptions.num_threads即可,默认是cpu core的个数(包含超线程的)。
> ServerOptions.num_threads仅仅是提示值。
你不能认为Server就用了这么多线程,因为进程内的所有Server和Channel会共享线程资源,线程总数是所有ServerOptions.num_threads和bthread_concurrency中的最大值。Channel没有相应的选项,但可以通过--bthread_concurrency调整。比如一个程序内有两个Server,num_threads分别为24和36,bthread_concurrency为16。那么worker线程数为max(24, 36, 16) = 36。这不同于其他RPC实现中往往是加起来。
> 另外,baidu-rpc不区分io线程和worker线程。baidu-rpc知道如何编排IO和处理代码,以获得更高的并发度和线程利用率。
## 限制最大并发
“并发”在中文背景下有两种含义,一种是连接数,一种是同时在处理的请求数。有了[epoll](https://linux.die.net/man/4/epoll)之后我们就不太关心连接数了,baidu-rpc在所有上下文中提到的并发(concurrency)均指同时在处理的请求数,不是连接数。
在传统的同步rpc server中,最大并发不会超过worker线程数(上面的num_threads选项),设定worker数量一般也限制了并发。但baidu-rpc的请求运行于bthread中,M个bthread会映射至N个worker中(一般M大于N),所以同步server的并发度可能超过worker数量。另一方面,异步server虽然占用的线程较少,但有时也需要控制并发量。
baidu-rpc支持设置server级和method级的最大并发,当server或method同时处理的请求数超过并发度限制时,它会立刻给client回复ELIMIT错误,而不会调用服务回调。看到ELIMIT错误的client应尝试另一个server。在一些情况下,这个选项可以防止server出现过度排队,或用于限制server占用的资源,但在大部分情况下并无必要。
### 为什么超过最大并发要立刻给client返回错误而不是排队?
当前server达到最大并发并不意味着集群中的其他server也达到最大并发了,立刻让client获知错误,并去尝试另一台server在全局角度是更好的策略。
### 选择最大并发数
最大并发度=极限qps*平均延时([little's law](https://en.wikipedia.org/wiki/Little%27s_law)),平均延时指的是server在正常服务状态(无积压)时的延时,设置为计算结果或略大的值即可。当server的延时较为稳定时,限制最大并发的效果和限制qps是等价的。但限制最大并发实现起来比限制qps容易多了,只需要一个计数器加加减减即可,这也是大部分流控都限制并发而不是qps的原因,比如tcp中的“窗口"即是一种并发度。
### 限制server级别并发度
设置ServerOptions.max_concurrency,默认值0代表不限制。访问内置服务不受此选项限制。
r34101后调用Server.ResetMaxConcurrency()可在server启动后动态修改server级别的max_concurrency。
### 限制method级别并发度
r34591后调用server.MaxConcurrencyOf("...") = ...可设置method级别的max_concurrency。可能的设置方法有:
```c++
server.MaxConcurrencyOf("example.EchoService.Echo") = 10;
server.MaxConcurrencyOf("example.EchoService", "Echo") = 10;
server.MaxConcurrencyOf(&service, "Echo") = 10;
```
此设置一般**发生在AddService后,server启动前**。当设置失败时(比如对应的method不存在),server会启动失败同时提示用户修正MaxConcurrencyOf设置错误。
当method级别和server级别的max_concurrency都被设置时,先检查server级别的,再检查method级别的。
注意:没有service级别的max_concurrency。
## pthread模式
用户代码(客户端的done,服务器端的CallMethod)默认在栈为1M的bthread中运行。但有些用户代码无法在bthread中运行,比如:
- JNI会检查stack layout而无法在bthread中运行。
- 代码中广泛地使用pthread local传递session数据(跨越了某次RPC),短时间内无法修改。请注意,如果代码中完全不使用baidu-rpc的客户端,在bthread中运行是没有问题的,只要代码没有明确地支持bthread就会阻塞pthread,并不会产生问题。
对于这些情况,baidu-rpc提供了pthread模式,开启**-usercode_in_pthread**后,用户代码均会在pthread中运行,原先阻塞bthread的函数转而阻塞pthread。
> **r33447前请勿在开启-usercode_in_pthread的代码中发起同步RPC,只要同时进行的同步RPC个数超过工作线程数就会死锁。**
打开pthread模式在性能上的注意点:
- 开启这个开关后,RPC操作都会阻塞pthread,server端一般需要设置更多的工作线程(ServerOptions.num_threads),调度效率会略微降低。
- pthread模式下运行用户代码的仍然是bthread,只是很特殊,会直接使用pthread worker的栈。这些特殊bthread的调度方式和其他bthread是一致的,这方面性能差异很小。
- bthread端支持一个独特的功能:把当前使用的pthread worker 让给另一个bthread运行,以消除一次上下文切换。client端的实现利用了这点,从而使一次RPC过程中3次上下文切换变为了2次。在高QPS系统中,消除上下文切换可以明显改善性能和延时分布。但pthread模式不具备这个能力,在高QPS系统中性能会有一定下降。
- pthread模式中线程资源是硬限,一旦线程被打满,请求就会迅速拥塞而造成大量超时。比如下游服务大量超时后,上游服务可能由于线程大都在等待下游也被打满从而影响性能。开启pthread模式后请考虑设置ServerOptions.max_concurrency以控制server的最大并发。而在bthread模式中bthread个数是软限,对此类问题的反应会更加平滑。
打开pthread模式可以让一些产品快速尝试baidu-rpc,但我们仍然建议产品线逐渐地把代码改造为使用bthread local从而最终能关闭这个开关。
## 安全模式
如果你的服务流量来自外部(包括经过nginx等转发),你需要注意一些安全因素:
### 对外禁用内置服务
内置服务很有用,但包含了大量内部信息,不应对外暴露。有多种方式可以对外禁用内置服务:
- 设置内部端口。把ServerOptions.internal_port设为一个**仅允许百度内网访问**的端口。你可通过internal_port访问到内置服务,但通过对外端口(Server.Start时传入的那个)访问内置服务时将看到如下错误:
> [a27eda84bcdeef529a76f22872b78305] Not allowed to access builtin services, try ServerOptions.internal_port=... instead if you're inside Baidu's network
- 前端server指定转发路径。nginx等http server可配置URL的映射关系,比如下面的配置把访问/MyAPI的外部流量映射到`target-server的/ServiceName/MethodName`。当外部流量尝试访问内置服务,比如说/status时,将直接被nginx拒绝。
```nginx
location /MyAPI {
...
proxy_pass http://<target-server>/ServiceName/MethodName$query_string # $query_string是nginx变量,更多变量请查询http://nginx.org/en/docs/http/ngx_http_core_module.html
...
}
```
> **另外请勿开启**-enable_dir_service**和**-enable_threads_service**两个选项,它们虽然很方便,但可能会暴露服务器上的其他信息,有安全隐患。早于r30869 (1.0.106.30846)的rpc版本没有这两个选项而是默认打开了这两个服务,请升级rpc确保它们关闭。
>
> 检查现有rpc服务是否打开了这两个开关:
>
> `curl -s -m 1 <HOSTNAME>:<PORT>/flags/enable_dir_service,enable_threads_service | awk '{if($3=="false"){++falsecnt}else if($3=="Value"){isrpc=1}}END{if(isrpc!=1||falsecnt==2){print "SAFE"}else{print "NOT SAFE"}}'`**
### 对返回的URL进行转义
可调用baidu::rpc::WebEscape()对url进行转义,防止恶意URI注入攻击。
### 不返回内部server地址
可以考虑对server地址做签名。比如在设置internal_port后,server返回的错误信息中的IP信息是其MD5签名,而不是明文。
## 定制/health页面
/health页面默认返回"OK",r32162后可以定制/health页面的内容:先继承[HealthReporter](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/health_reporter.h),在其中实现生成页面的逻辑(就像实现其他http service那样),然后把实例赋给ServerOptions.health_reporter,这个实例不被server拥有,必须保证在server运行期间有效。用户在定制逻辑中可以根据业务的运行状态返回更多样的状态信息。
## 私有变量
百度内的检索程序大量地使用了[thread-local storage](https://en.wikipedia.org/wiki/Thread-local_storage) (缩写tls),有些是为了缓存频繁访问的对象以避免反复创建,有些则是为了在全局函数间隐式地传递状态。你应当尽量避免后者,这样的函数难以测试,不设置thread-local变量甚至无法运行。baidu-rpc中有三套机制解决和thread-local相关的问题。
### session-local
session-local data与一次检索绑定,从进service回调开始,到done被调用结束。 所有的session-local data在server停止时删除。
session-local data得从server端的Controller获得, server-thread-local可以在任意函数中获得,只要这个函数直接或间接地运行在server线程中。当service是同步时,session-local和server-thread-local基本没有差别,除了前者需要Controller创建。当service是异步时,且你需要在done中访问到数据,这时只能用session-local,出了service回调后server-thread-local已经失效。
**示例用法:**
```c++
struct MySessionLocalData {
MySessionLocalData() : x(123) {}
int x;
};
class EchoServiceImpl : public example::EchoService {
public:
...
void Echo(google::protobuf::RpcController* cntl_base,
const example::EchoRequest* request,
example::EchoResponse* response,
google::protobuf::Closure* done) {
...
baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
// Get the session-local data which is created by ServerOptions.session_local_data_factory
// and reused between different RPC.
MySessionLocalData* sd = static_cast<MySessionLocalData*>(cntl->session_local_data());
if (sd == NULL) {
cntl->SetFailed("Require ServerOptions.session_local_data_factory to be set with a correctly implemented instance");
return;
}
...
```
**使用方法:**
设置ServerOptions.session_local_data_factory后访问Controller.session_local_data()即可获得session-local数据。若没有设置,Controller.session_local_data()总是返回NULL。若ServerOptions.reserved_session_local_data大于0,Server会在启动前就创建这么多个数据。
```c++
struct ServerOptions {
...
// The factory to create/destroy data attached to each RPC session.
// If this field is NULL, Controller::session_local_data() is always NULL.
// NOT owned by Server and must be valid when Server is running.
// Default: NULL
const DataFactory* session_local_data_factory;
// Prepare so many session-local data before server starts, so that calls
// to Controller::session_local_data() get data directly rather than
// calling session_local_data_factory->Create() at first time. Useful when
// Create() is slow, otherwise the RPC session may be blocked by the
// creation of data and not served within timeout.
// Default: 0
size_t reserved_session_local_data;
};
```
**实现session_local_data_factory**
session_local_data_factory的类型为[DataFactory](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/data_factory.h),你需要实现其中的CreateData和DestroyData。
注意:CreateData和DestroyData会被多个线程同时调用,必须线程安全。
```c++
class MySessionLocalDataFactory : public baidu::rpc::DataFactory {
public:
void* CreateData() const {
return new MySessionLocalData;
}
void DestroyData(void* d) const {
delete static_cast<MySessionLocalData*>(d);
}
};
int main(int argc, char* argv[]) {
...
MySessionLocalDataFactory session_local_data_factory;
baidu::rpc::Server server;
baidu::rpc::ServerOptions options;
...
options.session_local_data_factory = &session_local_data_factory;
...
```
### server-thread-local
server-thread-local与一个检索线程绑定,从进service回调开始,到出service回调结束。所有的server-thread-local data在server停止时删除。在实现上server-thread-local是一个特殊的bthread-local。
**示例用法:**
```c++
struct MyThreadLocalData {
MyThreadLocalData() : y(0) {}
int y;
};
class EchoServiceImpl : public example::EchoService {
public:
...
void Echo(google::protobuf::RpcController* cntl_base,
const example::EchoRequest* request,
example::EchoResponse* response,
google::protobuf::Closure* done) {
...
baidu::rpc::Controller* cntl = static_cast<baidu::rpc::Controller*>(cntl_base);
// Get the thread-local data which is created by ServerOptions.thread_local_data_factory
// and reused between different threads.
// "tls" is short for "thread local storage".
MyThreadLocalData* tls = static_cast<MyThreadLocalData*>(baidu::rpc::thread_local_data());
if (tls == NULL) {
cntl->SetFailed("Require ServerOptions.thread_local_data_factory "
"to be set with a correctly implemented instance");
return;
}
...
```
**使用方法:**
设置ServerOptions.thread_local_data_factory后访问Controller.thread_local_data()即可获得thread-local数据。若没有设置,Controller.thread_local_data()总是返回NULL。若ServerOptions.reserved_thread_local_data大于0,Server会在启动前就创建这么多个数据。
```c++
struct ServerOptions {
...
// The factory to create/destroy data attached to each searching thread
// in server.
// If this field is NULL, baidu::rpc::thread_local_data() is always NULL.
// NOT owned by Server and must be valid when Server is running.
// Default: NULL
const DataFactory* thread_local_data_factory;
// Prepare so many thread-local data before server starts, so that calls
// to baidu::rpc::thread_local_data() get data directly rather than calling
// thread_local_data_factory->Create() at first time. Useful when Create()
// is slow, otherwise the RPC session may be blocked by the creation
// of data and not served within timeout.
// Default: 0
size_t reserved_thread_local_data;
};
```
**实现thread_local_data_factory:**
thread_local_data_factory的类型为[DataFactory](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/data_factory.h),你需要实现其中的CreateData和DestroyData。
注意:CreateData和DestroyData会被多个线程同时调用,必须线程安全。
```c++
class MyThreadLocalDataFactory : public baidu::rpc::DataFactory {
public:
void* CreateData() const {
return new MyThreadLocalData;
}
void DestroyData(void* d) const {
delete static_cast<MyThreadLocalData*>(d);
}
};
int main(int argc, char* argv[]) {
...
MyThreadLocalDataFactory thread_local_data_factory;
baidu::rpc::Server server;
baidu::rpc::ServerOptions options;
...
options.thread_local_data_factory = &thread_local_data_factory;
...
```
### bthread-local
Session-local和server-thread-local对大部分server已经够用。不过在一些情况下,我们可能需要更通用的thread-local方案。在这种情况下,你可以使用bthread_key_create, bthread_key_destroy, bthread_getspecific, bthread_setspecific等函数,它们的用法完全等同于[pthread中的函数](http://linux.die.net/man/3/pthread_key_create)。
这些函数同时支持bthread和pthread,当它们在bthread中被调用时,获得的是bthread私有变量,而当它们在pthread中被调用时,获得的是pthread私有变量。但注意,这里的“pthread私有变量”不是pthread_key_create创建的pthread-local,使用pthread_key_create创建的pthread-local是无法被bthread_getspecific访问到的,这是两个独立的体系。由于pthread与LWP是1:1的关系,由gcc的__thread,c++11的thread_local等声明的变量也可视作pthread-local,同样无法被bthread_getspecific访问到。
由于baidu-rpc会为每个请求建立一个bthread,server中的bthread-local行为特殊:当一个检索bthread退出时,它并不删除bthread-local,而是还回server的一个pool中,以被其他bthread复用。这可以避免bthread-local随着bthread的创建和退出而不停地构造和析构。这对于用户是透明的。
> **在使用bthread-local前确保baidu-rpc的版本 >= 1.0.130.31109**
>
> 在那个版本之前的bthread-local没有在不同bthread间重用线程私有的存储(keytable)。由于baidu-rpc server会为每个请求创建一个bthread, bthread-local函数会频繁地创建和删除thread-local数据,性能表现不佳。之前的实现也无法在pthread中使用。
**主要接口:**
```c++
// Create a key value identifying a slot in a thread-specific data area.
// Each thread maintains a distinct thread-specific data area.
// `destructor', if non-NULL, is called with the value associated to that key
// when the key is destroyed. `destructor' is not called if the value
// associated is NULL when the key is destroyed.
// Returns 0 on success, error code otherwise.
extern int bthread_key_create(bthread_key_t* key, void (*destructor)(void* data)) __THROW;
 
// Delete a key previously returned by bthread_key_create().
// It is the responsibility of the application to free the data related to
// the deleted key in any running thread. No destructor is invoked by
// this function. Any destructor that may have been associated with key
// will no longer be called upon thread exit.
// Returns 0 on success, error code otherwise.
extern int bthread_key_delete(bthread_key_t key) __THROW;
 
// Store `data' in the thread-specific slot identified by `key'.
// bthread_setspecific() is callable from within destructor. If the application
// does so, destructors will be repeatedly called for at most
// PTHREAD_DESTRUCTOR_ITERATIONS times to clear the slots.
// NOTE: If the thread is not created by baidu-rpc server and lifetime is
// very short(doing a little thing and exit), avoid using bthread-local. The
// reason is that bthread-local always allocate keytable on first call to
// bthread_setspecific, the overhead is negligible in long-lived threads,
// but noticeable in shortly-lived threads. Threads in baidu-rpc server
// are special since they reuse keytables from a bthread_keytable_pool_t
// in the server.
// Returns 0 on success, error code otherwise.
// If the key is invalid or deleted, return EINVAL.
extern int bthread_setspecific(bthread_key_t key, void* data) __THROW;
 
// Return current value of the thread-specific slot identified by `key'.
// If bthread_setspecific() had not been called in the thread, return NULL.
// If the key is invalid or deleted, return NULL.
extern void* bthread_getspecific(bthread_key_t key) __THROW;
```
**使用步骤:**
- 创建一个bthread_key_t,它代表一个bthread私有变量。
```c++
static void my_data_destructor(void* data) {
...
}
bthread_key_t tls_key;
if (bthread_key_create(&tls_key, my_data_destructor) != 0) {
LOG(ERROR) << "Fail to create tls_key";
return -1;
}
```
- get/set bthread私有变量。一个线程中第一次访问某个私有变量返回NULL。
```c++
// in some thread ...
MyThreadLocalData* tls = static_cast<MyThreadLocalData*>(bthread_getspecific(tls_key));
if (tls == NULL) { // First call to bthread_getspecific (and before any bthread_setspecific) returns NULL
tls = new MyThreadLocalData; // Create thread-local data on demand.
CHECK_EQ(0, bthread_setspecific(tls_key, tls)); // set the data so that next time bthread_getspecific in the thread returns the data.
}
```
- 在所有线程都不使用某个bthread_key_t后删除它。如果删除了一个仍在被使用的bthread_key_t,相关的私有变量就泄露了。
**示例代码:**
```c++
static void my_thread_local_data_deleter(void* d) {
delete static_cast<MyThreadLocalData*>(d);
}
class EchoServiceImpl : public example::EchoService {
public:
EchoServiceImpl() {
CHECK_EQ(0, bthread_key_create(&_tls2_key, my_thread_local_data_deleter));
}
~EchoServiceImpl() {
CHECK_EQ(0, bthread_key_delete(_tls2_key));
};
...
private:
bthread_key_t _tls2_key;
}
class EchoServiceImpl : public example::EchoService {
public:
...
void Echo(google::protobuf::RpcController* cntl_base,
const example::EchoRequest* request,
example::EchoResponse* response,
google::protobuf::Closure* done) {
...
// You can create bthread-local data for your own.
// The interfaces are similar with pthread equivalence:
// pthread_key_create -> bthread_key_create
// pthread_key_delete -> bthread_key_delete
// pthread_getspecific -> bthread_getspecific
// pthread_setspecific -> bthread_setspecific
MyThreadLocalData* tls2 = static_cast<MyThreadLocalData*>(bthread_getspecific(_tls2_key));
if (tls2 == NULL) {
tls2 = new MyThreadLocalData;
CHECK_EQ(0, bthread_setspecific(_tls2_key, tls2));
}
...
```
# FAQ
### Q: Fail to write into fd=1865 SocketId=8905@10.208.245.43:54742@8230: Got EOF是什么意思
A: 一般是client端使用了连接池或短连接模式,在RPC超时后会关闭连接,server写回response时发现连接已经关了就报这个错。Got EOF就是指之前已经收到了EOF(对端正常关闭了连接)。client端使用单连接模式server端一般不会报这个。
### Q: Remote side of fd=9 SocketId=2@10.94.66.55:8000 was closed是什么意思
这不是错误,是常见的warning日志,表示对端关掉连接了(EOF)。这个日志有时对排查问题有帮助。r31210之后,这个日志默认被关闭了。如果需要打开,可以把参数-log_connection_close设置为true(支持[动态修改](http://wiki.baidu.com/display/RPC/flags#flags-Changegflagon-the-fly)
### Q: 为什么server端线程数设了没用
baidu-rpc同一个进程中所有的server[共用线程](http://wiki.baidu.com/pages/viewpage.action?pageId=213828715#id-创建和设置Server-worker线程数),如果创建了多个server,最终的工作线程数是最大的那个。
### Q: 为什么client端的延时远大于server端的延时
可能是server端的工作线程不够用了,出现了排队现象。排查方法请查看[高效率排查服务卡顿](http://wiki.baidu.com/pages/viewpage.action?pageId=161461013)
### Q: 程序切换到rpc之后,会出现莫名其妙的core,像堆栈被写坏
baidu-rpc的Server是运行在bthread之上,默认栈大小为1M,而pthread默认栈大小为10M,所以在pthread上正常运行的程序,在bthread上可能遇到栈不足。
解决方案:添加以下gflag,调整栈大小。第一个表示调整栈大小为10M左右,如有必要,可以更大。第二个表示每个工作线程cache的栈个数
**--stack_size_normal=10000000 --tc_stack_normal=1**
注意:不是说程序core了就意味着”栈不够大“了...只是因为这个试起来最容易,所以优先排除掉可能性。
### Q: Fail to open /proc/self/io
有些内核没这个文件,不影响服务正确性,但如下几个bvar会无法更新:
process_io_read_bytes_second
process_io_write_bytes_second
process_io_read_second
process_io_write_second
### Q: json串="[1,2,3]"没法直接转为protobuf message
不行,最外层必须是json object(大括号包围的)
\ No newline at end of file
[/status](http://brpc.baidu.com:8765/status)可以访问服务的主要统计信息。这些信息和/vars是同源的,但按服务重新组织方便查看。
![img](http://wiki.baidu.com/download/attachments/165876293/image2016-12-22%2011%3A19%3A56.png?version=1&modificationDate=1482376797000&api=v2)
上图中字段的含义分别是:
- **non_service_error**: "non"修饰的是“service_error",后者即是分列在各个服务下的error,此外的error都计入non_service_error。服务处理过程中client断开连接导致无法成功写回response就算non_service_error。而服务内部对后端的连接断开属于服务内部逻辑,只要最终服务成功地返回了response,即使错误也是计入该服务的error,而不是non_service_error。
- **connection_count**: 向该server发起请求的连接个数,不包含[对外连接](http://brpc.baidu.com:8765/vars/rpc_channel_connection_count)的个数。
- **example.EchoService**: 服务的完整名称,包含名字空间。
- **Echo (EchoRequest****) returns (EchoResponse****)**: 方法签名,一个服务可包含多个方法,点击request/response上的链接可查看对应的protobuf结构体。
- **count**`:` 成功处理的请求总个数。
- `**error**`: 失败的请求总个数。
- `**latency**`: 在web界面下从右到左分别是过去60秒,60分钟,24小时,30天的平均延时。在文本界面下是10秒内([-bvar_dump_interval](http://brpc.baidu.com:8765/flags/bvar_dump_interval)控制)的平均延时。
- `**latency_percentiles**`: 是延时的50%, 90%, 99%, 99.9%分位值,统计窗口默认10秒([-bvar_dump_interval](http://brpc.baidu.com:8765/flags/bvar_dump_interval)控制),web界面下有曲线。
- `**latency_cdf**`: 是分位值的另一种展现形式,类似histogram,只能在web界面下查看。
- `**max_latency**`: 在web界面下从右到左分别是过去60秒,60分钟,24小时,30天的最大延时。在文本界面下是10秒内([-bvar_dump_interval](http://brpc.baidu.com:8765/flags/bvar_dump_interval)控制)的最大延时。
- `**qps**`: 在web界面下从右到左分别是过去60秒,60分钟,24小时,30天的平均qps。在文本界面下是10秒内([-bvar_dump_interval](http://brpc.baidu.com:8765/flags/bvar_dump_interval)控制)的平均qps。
- `**processing**`: 正在处理的请求个数。如果持续不为0(特别是在压力归0后),应考虑程序是否有bug。
用户可通过让对应Service实现[baidu::rpc::Describable](https://svn.baidu.com/public/trunk/baidu-rpc/src/baidu/rpc/describable.h)自定义在/status页面上的描述.
比如:
![img](http://wiki.baidu.com/download/attachments/165876293/image2017-1-14%2023%3A58%3A20.png?version=1&modificationDate=1484409504000&api=v2)
[public/bvar](https://svn.baidu.com/public/trunk/bvar/)是多线程环境下的计数器类库,方便记录和查看用户程序中的各类数值,它利用了thread local存储避免了cache bouncing,相比UbMonitor几乎不会给程序增加性能开销,也快于竞争频繁的原子操作。baidu-rpc集成了bvar,[/vars](http://brpc.baidu.com:8765/vars)可查看所有曝光的bvar,[/vars/VARNAME](http://brpc.baidu.com:8765/vars/rpc_socket_count)可查阅某个bvar,增加计数器的方法请查看[bvar](http://wiki.baidu.com/display/RPC/bvar)。baidu-rpc大量使用了bvar提供统计数值,当你需要在多线程环境中计数并展现时,应该第一时间想到bvar。但bvar不能代替所有的计数器,它的本质是把写时的竞争转移到了读:读得合并所有写过的线程中的数据,而不可避免地变慢了。当你读写都很频繁并得基于数值做一些逻辑判断时,你不应该用bvar。
## 查询方法
[/vars](http://brpc.baidu.com:8765/vars) : 列出所有的bvar
[/vars/NAME](http://brpc.baidu.com:8765/vars/rpc_socket_count):查询名字为NAME的bvar
[/vars/NAME1,NAME2,NAME3](http://brpc.baidu.com:8765/vars/pid;process_cpu_usage;rpc_controller_count):查询名字为NAME1或NAME2或NAME3的bvar
[/vars/foo*,b$r](http://brpc.baidu.com:8765/vars/rpc_server*_count;iobuf_blo$k_*):查询名字与某一统配符匹配的bvar,注意用$代替?匹配单个字符,因为?在url中有特殊含义。
以下动画演示了如何使用过滤功能。你可以把包含过滤表达式的url复制粘贴给他人,他们点开后将看到你看到的内容。
![img](http://wiki.baidu.com/download/attachments/37774685/filter_bvar.gif?version=1&modificationDate=1494403262000&api=v2)
1.0.123.31011及之后的版本加入了一个搜索框加快了寻找特定bvar的速度,在这个搜索框你只需键入bvar名称的一部分,框架将补上*进行模糊查找。不同的名称间可以逗号、分号或空格分隔。
![img](http://wiki.baidu.com/download/attachments/37774685/search_var.gif?version=1&modificationDate=1494403229000&api=v2)
你也可以在命令行中访问vars:
```
$ curl brpc.baidu.com:8765/vars/bthread*
bthread_creation_count : 125134
bthread_creation_latency : 3
bthread_creation_latency_50 : 3
bthread_creation_latency_90 : 5
bthread_creation_latency_99 : 7
bthread_creation_latency_999 : 12
bthread_creation_latency_9999 : 12
bthread_creation_latency_cdf : "click to view"
bthread_creation_latency_percentiles : "[3,5,7,12]"
bthread_creation_max_latency : 7
bthread_creation_qps : 100
bthread_group_status : "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "
bthread_num_workers : 24
bthread_worker_usage : 1.01056
```
## 查看历史趋势
点击大部分数值型的bvar会显示其历史趋势。每个可点击的bvar记录了过去60秒,60分钟,24小时,30天总计174个值。当有1000个可点击bvar时大约会占1M内存。
![img](http://wiki.baidu.com/download/attachments/37774685/plot_bvar.gif?version=1&modificationDate=1494403276000&api=v2)
## 统计和查看分位值
x%分位值(percentile)是指把一段时间内的N个统计值排序,排在第N * x%位的值就是x%分位值。比如一段时间内有1000个值,排在第500位(1000 * 50%)的值是50%分位值(即中位数),排在第990位的是99%分位值(1000 * 99%),排在第999位的是99.9%分位值。分位值能比平均值更准确的刻画数值分布,对衡量系统SLA有重要意义。对于最常见的延时统计,平均值很难反映出实质性的内容,99.9%分位值往往更加关键,它决定了系统能做什么。
分位值可以绘制为CDF曲线和按时间变化时间。
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-9-21%2022%3A34%3A14.png?version=1&modificationDate=1442846055000&api=v2)
上图是CDF曲线。纵轴是延时。横轴是小于纵轴数值的数据比例。很明显地,这个图就是由从10%到99.99%的所有分位值组成。比如横轴=50%处对应的纵轴值便是50%分位值。那为什么要叫它CDF?CDF是[Cumulative Distribution Function](https://en.wikipedia.org/wiki/Cumulative_distribution_function)的缩写。当我们选定一个纵轴值x时,对应横轴的含义是"数值 <= x的比例”,如果数值是来自随机采样,那么含义即为“数值 <= x的概率”,这不就是概率的定义么?CDF的导数是[概率密度函数](https://en.wikipedia.org/wiki/Probability_density_function),换句话说如果我们把CDF的纵轴分为很多小段,对每个小段计算两端对应的横轴值之差,并把这个差作为新的横轴,那么我们便绘制了PDF曲线,就像(横着的)正态分布,泊松分布那样。但密度会放大差距,中位数的密度往往很高,在PDF中很醒目,这使得边上的长尾相对很扁而不易查看,所以大部分系统测试结果选择CDF曲线而不是PDF曲线。
可以用一些简单规则衡量CDF曲线好坏:
- 越平越好。一条水平线是最理想的,这意味着所有的数值都相等,没有任何等待,拥塞,停顿。当然这是不可能的。
- 99%之后越窄越好:99%之后是长尾的聚集地,对大部分系统的SLA有重要影响,越少越好。如果存储系统给出的性能指标是"99.9%的读请求在xx毫秒内完成“,那么你就得看下99.9%那儿的值;如果检索系统给出的性能指标是”99.99%的请求在xx毫秒内返回“,那么你得关注99.99%分位值。
一条真实的好CDF曲线的特征是”斜率很小,尾部很窄“。
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-9-21%2022%3A57%3A1.png?version=1&modificationDate=1442847422000&api=v2)
上图是按时间变化曲线。包含了4条曲线,横轴是时间,纵轴从上到下分别对应99.9%,99%,90%,50%分位值。颜色从上到下也越来越浅(从橘红到土黄)。滑动鼠标可以阅读对应数据点的值,上图中显示是”39秒种前的99%分位值是330微秒”。这幅图中不包含99.99%的曲线,因为99.99%分位值常明显大于99.9%及以下的分位值,画在一起的话会使得其他曲线变得很”矮“,难以辨认。你可以点击以"_latency_9999"结尾的bvar独立查看99.99%曲线,当然,你也可以独立查看50%,90%,99%,99.9%等曲线。按时间变化曲线可以看到分位值的变化趋势,对分析系统的性能变化很实用。
baidu-rpc的服务都会自动统计延时分布,用户不用自己加了。如下图所示:
![img](http://wiki.baidu.com/download/attachments/71337189/image2015-9-21%2023%3A5%3A41.png?version=1&modificationDate=1442847942000&api=v2)
你可以用bvar::LatencyRecorder统计非baidu-rpc服务的延时,这么做(更具体的使用方法请查看[bvar-c++](http://wiki.baidu.com/pages/viewpage.action?pageId=133624370)):
```c++
#include <bvar/bvar.h>
...
bvar::LatencyRecorder g_latency_recorder("client"); // expose this recorder
...
void foo() {
...
g_latency_recorder << my_latency;
...
}
```
如果这个程序使用了baidu-rpc server,那么你应该已经可以在/vars看到client_latency, client_latency_cdf等变量,点击便可查看动态曲线。如下图所示:
![img](http://wiki.baidu.com/download/thumbnails/71337189/image2015-9-21%2023%3A33%3A16.png?version=1&modificationDate=1442849597000&api=v2)
## 非baidu-rpc server
如果这个程序只是一个baidu-rpc client或根本没有使用baidu-rpc,并且你也想看到动态曲线,看[这里](http://wiki.baidu.com/pages/viewpage.action?pageId=213843633)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment