client.md 47.7 KB
Newer Older
1 2
[English version](../en/client.md)

3 4
# 示例程序

gejun's avatar
gejun committed
5
Echo的[client端代码](https://github.com/brpc/brpc/blob/master/example/echo_c++/client.cpp)
6 7 8 9 10 11 12 13 14 15

# 事实速查

- Channel.Init()是线程不安全的。
- Channel.CallMethod()是线程安全的,一个Channel可以被所有线程同时使用。
- Channel可以分配在栈上。
- Channel在发送异步请求后可以析构。
- 没有brpc::Client这个类。

# Channel
gejun's avatar
gejun committed
16
Client指发起请求的一端,在brpc中没有对应的实体,取而代之的是[brpc::Channel](https://github.com/brpc/brpc/blob/master/src/brpc/channel.h),它代表和一台或一组服务器的交互通道,Client和Channel在角色上的差别在实践中并不重要,你可以把Channel视作Client。
gejun's avatar
gejun committed
17

18
Channel可以**被所有线程共用**,你不需要为每个线程创建独立的Channel,也不需要用锁互斥。不过Channel的创建和Init并不是线程安全的,请确保在Init成功后再被多线程访问,在没有线程访问后再析构。
gejun's avatar
gejun committed
19

gejun's avatar
gejun committed
20
一些RPC实现中有ClientManager的概念,包含了Client端的配置信息和资源管理。brpc不需要这些,以往在ClientManager中配置的线程数、长短连接等等要么被加入了brpc::ChannelOptions,要么可以通过gflags全局配置,这么做的好处:
gejun's avatar
gejun committed
21

gejun's avatar
gejun committed
22 23 24
1. 方便。你不需要在创建Channel时传入ClientManager,也不需要存储ClientManager。否则不少代码需要一层层地传递ClientManager,很麻烦。gflags使一些全局行为的配置更加简单。
2. 共用资源。比如server和channel可以共用后台线程。(bthread的工作线程)
3. 生命周期。析构ClientManager的过程很容易出错,现在由框架负责则不会有问题。
gejun's avatar
gejun committed
25 26

就像大部分类那样,Channel必须在**Init**之后才能使用,options为NULL时所有参数取默认值,如果你要使用非默认值,这么做就行了:
27
```c++
28
brpc::ChannelOptions options;  // 包含了默认值
gejun's avatar
gejun committed
29 30 31 32
options.xxx = yyy;
...
channel.Init(..., &options);
```
gejun's avatar
gejun committed
33
注意Channel不会修改options,Init结束后不会再访问options。所以options一般就像上面代码中那样放栈上。Channel.options()可以获得channel在使用的所有选项。
gejun's avatar
gejun committed
34

gejun's avatar
gejun committed
35 36 37 38
Init函数分为连接一台服务器和连接服务集群。

# 连接一台服务器

39
```c++
gejun's avatar
gejun committed
40
// options为NULL时取默认值
gejun's avatar
gejun committed
41 42 43 44
int Init(EndPoint server_addr_and_port, const ChannelOptions* options);
int Init(const char* server_addr_and_port, const ChannelOptions* options);
int Init(const char* server_addr, int port, const ChannelOptions* options);
```
gejun's avatar
gejun committed
45
这类Init连接的服务器往往有固定的ip地址,不需要名字服务和负载均衡,创建起来相对轻量。但是**请勿频繁创建使用域名的Channel**。这需要查询dns,可能最多耗时10秒(查询DNS的默认超时)。重用它们。
gejun's avatar
gejun committed
46 47 48

合法的“server_addr_and_port”:
- 127.0.0.1:80
gejun's avatar
gejun committed
49
- www.foo.com:8765
gejun's avatar
gejun committed
50 51 52 53 54 55 56 57
- localhost:9000

不合法的"server_addr_and_port":
- 127.0.0.1:90000     # 端口过大
- 10.39.2.300:8000   # 非法的ip

# 连接服务集群

58 59 60 61
```c++
int Init(const char* naming_service_url,
         const char* load_balancer_name,
         const ChannelOptions* options);
gejun's avatar
gejun committed
62
```
gejun's avatar
gejun committed
63
这类Channel需要定期从`naming_service_url`指定的名字服务中获得服务器列表,并通过`load_balancer_name`指定的负载均衡算法选择出一台机器发送请求。
gejun's avatar
gejun committed
64

gejun's avatar
gejun committed
65
**不应该**在每次请求前动态地创建此类(连接服务集群的)Channel。因为创建和析构此类Channel牵涉到较多的资源,比如在创建时得访问一次名字服务,否则便不知道有哪些服务器可选。由于Channel可被多个线程共用,一般也没有必要动态创建。
gejun's avatar
gejun committed
66

gejun's avatar
gejun committed
67
`load_balancer_name`为NULL或空时,此Init等同于连接单台server的Init,`naming_service_url`应该是"ip:port"或"域名:port"。你可以通过这个Init函数统一Channel的初始化方式。比如你可以把`naming_service_url``load_balancer_name`放在配置文件中,要连接单台server时把`load_balancer_name`置空,要连接服务集群时则设置一个有效的算法名称。
gejun's avatar
gejun committed
68 69 70 71 72

## 名字服务

名字服务把一个名字映射为可修改的机器列表,在client端的位置如下:

73
![img](../images/ns.png)
gejun's avatar
gejun committed
74

gejun's avatar
gejun committed
75 76 77
有了名字服务后client记录的是一个名字,而不是每一台下游机器。而当下游机器变化时,就只需要修改名字服务中的列表,而不需要逐台修改每个上游。这个过程也常被称为“解耦上下游”。当然在具体实现上,上游会记录每一台下游机器,并定期向名字服务请求或被推送最新的列表,以避免在RPC请求时才去访问名字服务。使用名字服务一般不会对访问性能造成影响,对名字服务的压力也很小。

`naming_service_url`的一般形式是"**protocol://service_name**"
gejun's avatar
gejun committed
78

gejun's avatar
gejun committed
79
### bns://\<bns-name\>
gejun's avatar
gejun committed
80

81
BNS是百度内常用的名字服务,比如bns://rdev.matrix.all,其中"bns"是protocol,"rdev.matrix.all"是service-name。相关一个gflag是-ns_access_interval: ![img](../images/ns_access_interval.png)
gejun's avatar
gejun committed
82

gejun's avatar
gejun committed
83
如果BNS中显示不为空,但Channel却说找不到服务器,那么有可能BNS列表中的机器状态位(status)为非0,含义为机器不可用,所以不会被加入到server候选集中.状态位可通过命令行查看:
gejun's avatar
gejun committed
84 85 86

`get_instance_by_service [bns_node_name] -s`

gejun's avatar
gejun committed
87
### file://\<path\>
gejun's avatar
gejun committed
88

gejun's avatar
gejun committed
89
服务器列表放在`path`所在的文件里,比如"file://conf/local_machine_list"中的“conf/local_machine_list”对应一个文件,其中每行应是一台服务器的地址。当文件更新时, brpc会重新加载。
gejun's avatar
gejun committed
90

gejun's avatar
gejun committed
91
### list://\<addr1\>,\<addr2\>...
gejun's avatar
gejun committed
92

gejun's avatar
gejun committed
93
服务器列表直接跟在list://之后,以逗号分隔,比如"list://db-bce-81-3-186.db01:7000,m1-bce-44-67-72.m1:7000,cp01-rd-cos-006.cp01:7000" 中有三个地址。
gejun's avatar
gejun committed
94

gejun's avatar
gejun committed
95
### http://\<url\>
gejun's avatar
gejun committed
96

gejun's avatar
gejun committed
97
连接一个域名下所有的机器, 例如http://www.baidu.com:80 ,注意连接单点的Init(两个参数)虽然也可传入域名,但只会连接域名下的一台机器。
gejun's avatar
gejun committed
98 99 100 101 102

### 名字服务过滤器

当名字服务获得机器列表后,可以自定义一个过滤器进行筛选,最后把结果传递给负载均衡:

103
![img](../images/ns_filter.jpg)
gejun's avatar
gejun committed
104 105

过滤器的接口如下:
gejun's avatar
gejun committed
106
```c++
gejun's avatar
gejun committed
107 108 109 110 111 112 113 114 115 116
// naming_service_filter.h
class NamingServiceFilter {
public:
    // Return true to take this `server' as a candidate to issue RPC
    // Return false to filter it out
    virtual bool Accept(const ServerNode& server) const = 0;
};
 
// naming_service.h
struct ServerNode {
gejun's avatar
gejun committed
117
    butil::EndPoint addr;
gejun's avatar
gejun committed
118 119 120
    std::string tag;
};
```
gejun's avatar
gejun committed
121 122 123 124 125
常见的业务策略如根据server的tag进行过滤。

自定义的过滤器配置在ChannelOptions中,默认为NULL(不过滤)。

```c++
126
class MyNamingServiceFilter : public brpc::NamingServiceFilter {
gejun's avatar
gejun committed
127
public:
128
    bool Accept(const brpc::ServerNode& server) const {
gejun's avatar
gejun committed
129 130 131 132 133 134 135 136
        return server.tag == "main";
    }
};
 
int main() {
    ...
    MyNamingServiceFilter my_filter;
    ...
137
    brpc::ChannelOptions options;
gejun's avatar
gejun committed
138 139 140 141 142 143 144 145 146
    options.ns_filter = &my_filter;
    ...
}
```

## 负载均衡

当下游机器超过一台时,我们需要分割流量,此过程一般称为负载均衡,在client端的位置如下图所示:

147
![img](../images/lb.png)
gejun's avatar
gejun committed
148

gejun's avatar
gejun committed
149
理想的算法是每个请求都得到及时的处理,且任意机器crash对全局影响较小。但由于client端无法及时获得server端的延迟或拥塞,而且负载均衡算法不能耗费太多的cpu,一般来说用户得根据具体的场景选择合适的算法,目前rpc提供的算法有(通过load_balancer_name指定):
gejun's avatar
gejun committed
150 151 152

### rr

gejun's avatar
gejun committed
153
即round robin,总是选择列表中的下一台服务器,结尾的下一台是开头,无需其他设置。比如有3台机器a,b,c,那么brpc会依次向a, b, c, a, b, c, ...发送请求。注意这个算法的前提是服务器的配置,网络条件,负载都是类似的。
gejun's avatar
gejun committed
154 155 156 157 158 159 160

### random

随机从列表中选择一台服务器,无需其他设置。和round robin类似,这个算法的前提也是服务器都是类似的。

### la

gejun's avatar
gejun committed
161
locality-aware,优先选择延时低的下游,直到其延时高于其他机器,无需其他设置。实现原理请查看[Locality-aware load balancing](lalb.md)
gejun's avatar
gejun committed
162 163 164 165 166 167 168

### c_murmurhash or c_md5

一致性哈希,与简单hash的不同之处在于增加或删除机器时不会使分桶结果剧烈变化,特别适合cache类服务。

发起RPC前需要设置Controller.set_request_code(),否则RPC会失败。request_code一般是请求中主键部分的32位哈希值,**不需要和负载均衡使用的哈希算法一致**。比如用c_murmurhash算法也可以用md5计算哈希值。

gejun's avatar
gejun committed
169
[src/brpc/policy/hasher.h](https://github.com/brpc/brpc/blob/master/src/brpc/policy/hasher.h)中包含了常用的hash函数。如果用std::string key代表请求的主键,controller.set_request_code(brpc::policy::MurmurHash32(key.data(), key.size()))就正确地设置了request_code。
gejun's avatar
gejun committed
170 171 172

注意甄别请求中的“主键”部分和“属性”部分,不要为了偷懒或通用,就把请求的所有内容一股脑儿计算出哈希值,属性的变化会使请求的目的地发生剧烈的变化。另外也要注意padding问题,比如struct Foo { int32_t a; int64_t b; }在64位机器上a和b之间有4个字节的空隙,内容未定义,如果像hash(&foo, sizeof(foo))这样计算哈希值,结果就是未定义的,得把内容紧密排列或序列化后再算。

gejun's avatar
gejun committed
173
实现原理请查看[Consistent Hashing](consistent_hashing.md)
gejun's avatar
gejun committed
174 175 176

## 健康检查

gejun's avatar
gejun committed
177
连接断开的server会被暂时隔离而不会被负载均衡算法选中,brpc会定期连接被隔离的server,以检查他们是否恢复正常,间隔由参数-health_check_interval控制:
gejun's avatar
gejun committed
178 179 180 181 182

| Name                      | Value | Description                              | Defined At              |
| ------------------------- | ----- | ---------------------------------------- | ----------------------- |
| health_check_interval (R) | 3     | seconds between consecutive health-checkings | src/brpc/socket_map.cpp |

gejun's avatar
gejun committed
183
一旦server被连接上,它会恢复为可用状态。如果在隔离过程中,server从名字服务中删除了,brpc也会停止连接尝试。
gejun's avatar
gejun committed
184 185 186

# 发起访问

187
一般来说,我们不直接调用Channel.CallMethod,而是通过protobuf生成的桩XXX_Stub,过程更像是“调用函数”。stub内没什么成员变量,建议在栈上创建和使用,而不必new,当然你也可以把stub存下来复用。Channel::CallMethod和stub访问都是**线程安全**的,可以被所有线程同时访问。比如:
gejun's avatar
gejun committed
188
```c++
gejun's avatar
gejun committed
189 190 191 192
XXX_Stub stub(&channel);
stub.some_method(controller, request, response, done);
```
甚至
gejun's avatar
gejun committed
193
```c++
gejun's avatar
gejun committed
194 195
XXX_Stub(&channel).some_method(controller, request, response, done);
```
gejun's avatar
gejun committed
196
一个例外是http client。访问http服务和protobuf没什么关系,直接调用CallMethod即可,除了Controller和done均为NULL,详见[访问HTTP服务](http_client.md)
gejun's avatar
gejun committed
197 198 199

## 同步访问

gejun's avatar
gejun committed
200
指的是:CallMethod会阻塞到收到server端返回response或发生错误(包括超时)。
gejun's avatar
gejun committed
201

gejun's avatar
gejun committed
202
同步访问中的response/controller不会在CallMethod后被框架使用,它们都可以分配在栈上。注意,如果request/response字段特别多字节数特别大的话,还是更适合分配在堆上。
gejun's avatar
gejun committed
203
```c++
gejun's avatar
gejun committed
204 205
MyRequest request;
MyResponse response;
206
brpc::Controller cntl;
gejun's avatar
gejun committed
207 208 209 210 211 212
XXX_Stub stub(&channel);
 
request.set_foo(...);
cntl.set_timeout_ms(...);
stub.some_method(&cntl, &request, &response, NULL);
if (cntl->Failed()) {
gejun's avatar
gejun committed
213
    // RPC失败了. response里的值是未定义的,勿用。
gejun's avatar
gejun committed
214 215 216 217 218 219 220
} else {
    // RPC成功了,response里有我们想要的回复数据。
}
```

## 异步访问

gejun's avatar
gejun committed
221
指的是:给CallMethod传递一个额外的回调对象done,CallMethod在发出request后就结束了,而不是在RPC结束后。当server端返回response或发生错误(包括超时)时,done->Run()会被调用。对RPC的后续处理应该写在done->Run()里,而不是CallMethod后。
gejun's avatar
gejun committed
222 223 224

由于CallMethod结束不意味着RPC结束,response/controller仍可能被框架及done->Run()使用,它们一般得创建在堆上,并在done->Run()中删除。如果提前删除了它们,那当done->Run()被调用时,将访问到无效内存。

gejun's avatar
gejun committed
225
你可以独立地创建这些对象,并使用[NewCallback](#使用NewCallback)生成done,也可以把Response和Controller作为done的成员变量,[一起new出来](#继承google::protobuf::Closure),一般使用前一种方法。
gejun's avatar
gejun committed
226

gejun's avatar
gejun committed
227
**发起异步请求后Request和Channel也可以立刻析构**。这两样和response/controller是不同的。注意:这是说Channel的析构可以立刻发生在CallMethod**之后**,并不是说析构可以和CallMethod同时发生,删除正被另一个线程使用的Channel是未定义行为(很可能crash)。
gejun's avatar
gejun committed
228 229

### 使用NewCallback
gejun's avatar
gejun committed
230
```c++
231
static void OnRPCDone(MyResponse* response, brpc::Controller* cntl) {
gejun's avatar
gejun committed
232
    // unique_ptr会帮助我们在return时自动删掉response/cntl,防止忘记。gcc 3.4下的unique_ptr是模拟版本。
gejun's avatar
gejun committed
233
    std::unique_ptr<MyResponse> response_guard(response);
234
    std::unique_ptr<brpc::Controller> cntl_guard(cntl);
gejun's avatar
gejun committed
235
    if (cntl->Failed()) {
gejun's avatar
gejun committed
236
        // RPC失败了. response里的值是未定义的,勿用。
gejun's avatar
gejun committed
237 238 239 240 241 242 243
    } else {
        // RPC成功了,response里有我们想要的数据。开始RPC的后续处理。    
    }
    // NewCallback产生的Closure会在Run结束后删除自己,不用我们做。
}
 
MyResponse* response = new MyResponse;
244
brpc::Controller* cntl = new brpc::Controller;
gejun's avatar
gejun committed
245 246
MyService_Stub stub(&channel);
 
gejun's avatar
gejun committed
247
MyRequest request;  // 你不用new request,即使在异步访问中.
gejun's avatar
gejun committed
248 249 250 251
request.set_foo(...);
cntl->set_timeout_ms(...);
stub.some_method(cntl, &request, response, google::protobuf::NewCallback(OnRPCDone, response, cntl));
```
gejun's avatar
gejun committed
252
由于protobuf 3把NewCallback设置为私有,r32035后brpc把NewCallback独立于[src/brpc/callback.h](https://github.com/brpc/brpc/blob/master/src/brpc/callback.h)(并增加了一些重载)。如果你的程序出现NewCallback相关的编译错误,把google::protobuf::NewCallback替换为brpc::NewCallback就行了。
gejun's avatar
gejun committed
253 254 255

### 继承google::protobuf::Closure

gejun's avatar
gejun committed
256
使用NewCallback的缺点是要分配三次内存:response, controller, done。如果profiler证明这儿的内存分配有瓶颈,可以考虑自己继承Closure,把response/controller作为成员变量,这样可以把三次new合并为一次。但缺点就是代码不够美观,如果内存分配不是瓶颈,别用这种方法。
gejun's avatar
gejun committed
257
```c++
gejun's avatar
gejun committed
258 259 260
class OnRPCDone: public google::protobuf::Closure {
public:
    void Run() {
gejun's avatar
gejun committed
261
        // unique_ptr会帮助我们在return时自动delete this,防止忘记。gcc 3.4下的unique_ptr是模拟版本。
gejun's avatar
gejun committed
262 263 264
        std::unique_ptr<OnRPCDone> self_guard(this);
          
        if (cntl->Failed()) {
gejun's avatar
gejun committed
265
            // RPC失败了. response里的值是未定义的,勿用。
gejun's avatar
gejun committed
266 267 268 269 270 271
        } else {
            // RPC成功了,response里有我们想要的数据。开始RPC的后续处理。
        }
    }
 
    MyResponse response;
272
    brpc::Controller cntl;
gejun's avatar
gejun committed
273 274 275 276 277
}
 
OnRPCDone* done = new OnRPCDone;
MyService_Stub stub(&channel);
 
gejun's avatar
gejun committed
278
MyRequest request;  // 你不用new request,即使在异步访问中.
gejun's avatar
gejun committed
279 280 281 282 283
request.set_foo(...);
done->cntl.set_timeout_ms(...);
stub.some_method(&done->cntl, &request, &done->response, done);
```

gejun's avatar
gejun committed
284
### 如果异步访问中的回调函数特别复杂会有什么影响吗?
gejun's avatar
gejun committed
285 286 287

没有特别的影响,回调会运行在独立的bthread中,不会阻塞其他的逻辑。你可以在回调中做各种阻塞操作。

gejun's avatar
gejun committed
288
### rpc发送处的代码和回调函数是在同一个线程里执行吗?
gejun's avatar
gejun committed
289 290 291 292

一定不在同一个线程里运行,即使该次rpc调用刚进去就失败了,回调也会在另一个bthread中运行。这可以在加锁进行rpc(不推荐)的代码中避免死锁。

## 等待RPC完成
gejun's avatar
gejun committed
293
注意:当你需要发起多个并发操作时,可能[ParallelChannel](combo_channel.md#parallelchannel)更方便。
gejun's avatar
gejun committed
294 295

如下代码发起两个异步RPC后等待它们完成。
gejun's avatar
gejun committed
296
```c++
297 298
const brpc::CallId cid1 = controller1->call_id();
const brpc::CallId cid2 = controller2->call_id();
gejun's avatar
gejun committed
299 300 301 302
...
stub.method1(controller1, request1, response1, done1);
stub.method2(controller2, request2, response2, done2);
...
303 304
brpc::Join(cid1);
brpc::Join(cid2);
gejun's avatar
gejun committed
305 306 307
```
**在发起RPC前**调用Controller.call_id()获得一个id,发起RPC调用后Join那个id。

gejun's avatar
gejun committed
308
Join()的行为是等到RPC结束**且done->Run()运行后**,一些Join的性质如下:
gejun's avatar
gejun committed
309 310

- 如果对应的RPC已经结束,Join将立刻返回。
gejun's avatar
gejun committed
311
- 多个线程可以Join同一个id,它们都会醒来。
gejun's avatar
gejun committed
312 313
- 同步RPC也可以在另一个线程中被Join,但一般不会这么做。 

gejun's avatar
gejun committed
314
Join()在之前的版本叫做JoinResponse(),如果你在编译时被提示deprecated之类的,修改为Join()。
gejun's avatar
gejun committed
315

gejun's avatar
gejun committed
316
在RPC调用后`Join(controller->call_id())`**错误**的行为,一定要先把call_id保存下来。因为RPC调用后controller可能被随时开始运行的done删除。下面代码的Join方式是**错误**的。
gejun's avatar
gejun committed
317

gejun's avatar
gejun committed
318
```c++
gejun's avatar
gejun committed
319 320 321 322 323 324 325 326 327 328 329 330 331 332
static void on_rpc_done(Controller* controller, MyResponse* response) {
    ... Handle response ...
    delete controller;
    delete response;
}
 
Controller* controller1 = new Controller;
Controller* controller2 = new Controller;
MyResponse* response1 = new MyResponse;
MyResponse* response2 = new MyResponse;
...
stub.method1(controller1, &request1, response1, google::protobuf::NewCallback(on_rpc_done, controller1, response1));
stub.method2(controller2, &request2, response2, google::protobuf::NewCallback(on_rpc_done, controller2, response2));
...
333 334
brpc::Join(controller1->call_id());   // 错误,controller1可能被on_rpc_done删除了
brpc::Join(controller2->call_id());   // 错误,controller2可能被on_rpc_done删除了
gejun's avatar
gejun committed
335 336 337 338
```

## 半同步

gejun's avatar
gejun committed
339
Join可用来实现“半同步”访问:即等待多个异步访问完成。由于调用处的代码会等到所有RPC都结束后再醒来,所以controller和response都可以放栈上。
gejun's avatar
gejun committed
340
```c++
341 342
brpc::Controller cntl1;
brpc::Controller cntl2;
gejun's avatar
gejun committed
343 344 345
MyResponse response1;
MyResponse response2;
...
346 347
stub1.method1(&cntl1, &request1, &response1, brpc::DoNothing());
stub2.method2(&cntl2, &request2, &response2, brpc::DoNothing());
gejun's avatar
gejun committed
348
...
349 350
brpc::Join(cntl1.call_id());
brpc::Join(cntl2.call_id());
gejun's avatar
gejun committed
351
```
352
brpc::DoNothing()可获得一个什么都不干的done,专门用于半同步访问。它的生命周期由框架管理,用户不用关心。
gejun's avatar
gejun committed
353

gejun's avatar
gejun committed
354
注意在上面的代码中,我们在RPC结束后又访问了controller.call_id(),这是没有问题的,因为DoNothing中并不会像上节中的on_rpc_done中那样删除Controller。
gejun's avatar
gejun committed
355 356 357

## 取消RPC

gejun's avatar
gejun committed
358
brpc::StartCancel(call_id)可取消对应的RPC,call_id必须**在发起RPC前**通过Controller.call_id()获得,其他时刻都可能有race condition。
gejun's avatar
gejun committed
359

gejun's avatar
gejun committed
360
注意:是brpc::StartCancel(call_id),不是controller->StartCancel(),后者被禁用,没有效果。后者是protobuf默认提供的接口,但是在controller对象的生命周期上有严重的竞争问题。
gejun's avatar
gejun committed
361 362 363 364 365 366 367 368

顾名思义,StartCancel调用完成后RPC并未立刻结束,你不应该碰触Controller的任何字段或删除任何资源,它们自然会在RPC结束时被done中对应逻辑处理。如果你一定要在原地等到RPC结束(一般不需要),则可通过Join(call_id)。

关于StartCancel的一些事实:

- call_id在发起RPC前就可以被取消,RPC会直接结束(done仍会被调用)。
- call_id可以在另一个线程中被取消。
- 取消一个已经取消的call_id不会有任何效果。推论:同一个call_id可以被多个线程同时取消,但最多一次有效果。
gejun's avatar
gejun committed
369
- 这里的取消是纯client端的功能,**server端未必会取消对应的操作**,server cancelation是另一个功能。
gejun's avatar
gejun committed
370 371 372

## 获取Server的地址和端口

gejun's avatar
gejun committed
373
remote_side()方法可知道request被送向了哪个server,返回值类型是[butil::EndPoint](https://github.com/brpc/brpc/blob/master/src/butil/endpoint.h),包含一个ip4地址和端口。在RPC结束前调用这个方法都是没有意义的。
gejun's avatar
gejun committed
374 375

打印方式:
gejun's avatar
gejun committed
376
```c++
gejun's avatar
gejun committed
377
LOG(INFO) << "remote_side=" << cntl->remote_side();
gejun's avatar
gejun committed
378
printf("remote_side=%s\n", butil::endpoint2str(cntl->remote_side()).c_str());
gejun's avatar
gejun committed
379 380 381 382 383 384
```
## 获取Client的地址和端口

r31384后通过local_side()方法可**在RPC结束后**获得发起RPC的地址和端口。

打印方式:
gejun's avatar
gejun committed
385
```c++
gejun's avatar
gejun committed
386
LOG(INFO) << "local_side=" << cntl->local_side(); 
gejun's avatar
gejun committed
387
printf("local_side=%s\n", butil::endpoint2str(cntl->local_side()).c_str());
gejun's avatar
gejun committed
388
```
gejun's avatar
gejun committed
389
## 应该重用brpc::Controller吗?
gejun's avatar
gejun committed
390

gejun's avatar
gejun committed
391
不用刻意地重用,但Controller是个大杂烩,可能会包含一些缓存,Reset()可以避免反复地创建这些缓存。
gejun's avatar
gejun committed
392

gejun's avatar
gejun committed
393
在大部分场景下,构造Controller(snippet1)和重置Controller(snippet2)的性能差异不大。
gejun's avatar
gejun committed
394
```c++
gejun's avatar
gejun committed
395 396
// snippet1
for (int i = 0; i < n; ++i) {
397
    brpc::Controller controller;
gejun's avatar
gejun committed
398 399 400 401 402
    ...
    stub.CallSomething(..., &controller);
}
 
// snippet2
403
brpc::Controller controller;
gejun's avatar
gejun committed
404 405 406 407 408 409 410 411 412 413 414 415
for (int i = 0; i < n; ++i) {
    controller.Reset();
    ...
    stub.CallSomething(..., &controller);
}
```
但如果snippet1中的Controller是new出来的,那么snippet1就会多出“内存分配”的开销,在一些情况下可能会慢一些。

# 设置

Client端的设置主要由三部分组成:

gejun's avatar
gejun committed
416 417
- brpc::ChannelOptions: 定义在[src/brpc/channel.h](https://github.com/brpc/brpc/blob/master/src/brpc/channel.h)中,用于初始化Channel,一旦初始化成功无法修改。
- brpc::Controller: 定义在[src/brpc/controller.h](https://github.com/brpc/brpc/blob/master/src/brpc/controller.h)中,用于在某次RPC中覆盖ChannelOptions中的选项,可根据上下文每次均不同。
gejun's avatar
gejun committed
418
- 全局gflags:常用于调节一些底层代码的行为,一般不用修改。请自行阅读服务[/flags页面](flags.md)中的说明。
gejun's avatar
gejun committed
419 420 421 422 423 424 425 426

Controller包含了request中没有的数据和选项。server端和client端的Controller结构体是一样的,但使用的字段可能是不同的,你需要仔细阅读Controller中的注释,明确哪些字段可以在server端使用,哪些可以在client端使用。

一个Controller对应一次RPC。一个Controller可以在Reset()后被另一个RPC复用,但一个Controller不能被多个RPC同时使用(不论是否在同一个线程发起)。

Controller的特点:
1. 一个Controller只能有一个使用者,没有特殊说明的话,Controller中的方法默认线程不安全。
2. 因为不能被共享,所以一般不会用共享指针管理Controller,如果你用共享指针了,很可能意味着出错了。
gejun's avatar
gejun committed
427
3. Controller创建于开始RPC前,析构于RPC结束后,常见几种模式:
gejun's avatar
gejun committed
428 429 430
   - 同步RPC前Controller放栈上,出作用域后自行析构。注意异步RPC的Controller绝对不能放栈上,否则其析构时异步调用很可能还在进行中,从而引发未定义行为。
   - 异步RPC前new Controller,done中删除。

431 432 433 434
## 线程数

和大部分的RPC框架不同,brpc中并没有独立的Client线程池。所有Channel和Server通过[bthread](http://wiki.baidu.com/display/RPC/bthread)共享相同的线程池. 如果你的程序同样使用了brpc的server, 仅仅需要设置Server的线程数。 或者可以通过[gflags](http://wiki.baidu.com/display/RPC/flags)设置[-bthread_concurrency](http://brpc.baidu.com:8765/flags/bthread_concurrency)来设置全局的线程数.

gejun's avatar
gejun committed
435 436
## 超时

gejun's avatar
gejun committed
437
**ChannelOptions.timeout_ms**是对应Channel上所有RPC的总超时,Controller.set_timeout_ms()可修改某次RPC的值。单位毫秒,默认值1秒,最大值2^31(约24天),-1表示一直等到回复或错误。
gejun's avatar
gejun committed
438

gejun's avatar
gejun committed
439
**ChannelOptions.connect_timeout_ms**是对应Channel上所有RPC的连接超时,单位毫秒,默认值1秒。-1表示等到连接建立或出错,此值被限制为不能超过timeout_ms。注意此超时独立于TCP的连接超时,一般来说前者小于后者,反之则可能在connect_timeout_ms未达到前由于TCP连接超时而出错。
gejun's avatar
gejun committed
440

gejun's avatar
gejun committed
441
注意1:brpc中的超时是deadline,超过就意味着RPC结束,超时后没有重试。其他实现可能既有单次访问的超时,也有代表deadline的超时。迁移到brpc时请仔细区分。
gejun's avatar
gejun committed
442

gejun's avatar
gejun committed
443
注意2:RPC超时的错误码为**ERPCTIMEDOUT (1008)**,ETIMEDOUT的意思是连接超时,且可重试。
gejun's avatar
gejun committed
444 445 446 447 448

## 重试

ChannelOptions.max_retry是该Channel上所有RPC的默认最大重试次数,Controller.set_max_retry()可修改某次RPC的值,默认值3,0表示不重试。

gejun's avatar
gejun committed
449 450 451
r32111后Controller.retried_count()返回重试次数。

r34717后Controller.has_backup_request()获知是否发送过backup_request。
gejun's avatar
gejun committed
452 453 454 455 456 457

**重试时框架会尽量避开之前尝试过的server。**

重试的触发条件有(条件之间是AND关系):
### 连接出错

gejun's avatar
gejun committed
458 459 460
如果server一直没有返回,但连接没有问题,这种情况下不会重试。如果你需要在一定时间后发送另一个请求,使用backup request。

工作机制如下:如果response没有在backup_request_ms内返回,则发送另外一个请求,哪个先回来就取哪个。新请求会被尽量送到不同的server。注意如果backup_request_ms大于超时,则backup request总不会被发送。backup request会消耗一次重试次数。backup request不意味着server端cancel。
gejun's avatar
gejun committed
461 462 463 464 465 466 467

ChannelOptions.backup_request_ms影响该Channel上所有RPC,单位毫秒,默认值-1(表示不开启),Controller.set_backup_request_ms()可修改某次RPC的值。

### 没到超时

超时后RPC会尽快结束。

gejun's avatar
gejun committed
468
### 有剩余重试次数
gejun's avatar
gejun committed
469

gejun's avatar
gejun committed
470
Controller.set_max_retry(0)或ChannelOptions.max_retry=0关闭重试。
gejun's avatar
gejun committed
471 472 473

### 错误值得重试

gejun's avatar
gejun committed
474
一些错误重试是没有意义的,就不会重试,比如请求有错时(EREQUEST)不会重试,因为server总不会接受,没有意义。
gejun's avatar
gejun committed
475

gejun's avatar
gejun committed
476
用户可以通过继承[brpc::RetryPolicy](https://github.com/brpc/brpc/blob/master/src/brpc/retry_policy.h)自定义重试条件。比如brpc默认不重试HTTP相关的错误,而你的程序中希望在碰到HTTP_STATUS_FORBIDDEN (403)时重试,可以这么做:
gejun's avatar
gejun committed
477

gejun's avatar
gejun committed
478
```c++
gejun's avatar
gejun committed
479
#include <brpc/retry_policy.h>
gejun's avatar
gejun committed
480
 
481
class MyRetryPolicy : public brpc::RetryPolicy {
gejun's avatar
gejun committed
482
public:
483 484 485
    bool DoRetry(const brpc::Controller* cntl) const {
        if (cntl->ErrorCode() == brpc::EHTTP && // HTTP错误
            cntl->http_response().status_code() == brpc::HTTP_STATUS_FORBIDDEN) {
gejun's avatar
gejun committed
486 487 488
            return true;
        }
        // 把其他情况丢给框架。
489
        return brpc::DefaultRetryPolicy()->DoRetry(cntl);
gejun's avatar
gejun committed
490 491 492 493 494 495
    }
};
...
 
// 给ChannelOptions.retry_policy赋值就行了。
// 注意:retry_policy必须在Channel使用期间保持有效,Channel也不会删除retry_policy,所以大部分情况下RetryPolicy都应以单例模式创建。
496
brpc::ChannelOptions options;
gejun's avatar
gejun committed
497 498 499 500 501
static MyRetryPolicy g_my_retry_policy;
options.retry_policy = &g_my_retry_policy;
...
```

gejun's avatar
gejun committed
502 503 504 505 506
一些提示:

* 通过cntl->response()可获得对应RPC的response。
* 对ERPCTIMEDOUT代表的RPC超时总是不重试,即使你继承的RetryPolicy中允许。

gejun's avatar
gejun committed
507 508
### 重试应当保守

gejun's avatar
gejun committed
509
由于成本的限制,大部分线上server的冗余度是有限的,主要是满足多机房互备的需求。而激进的重试逻辑很容易导致众多client对server集群造成2-3倍的压力,最终使集群雪崩:由于server来不及处理导致队列越积越长,使所有的请求得经过很长的排队才被处理而最终超时,相当于服务停摆。默认的重试是比较安全的: 只要连接不断RPC就不会重试,一般不会产生大量的重试请求。用户可以通过RetryPolicy定制重试策略,但也可能使重试变成一场“风暴”。当你定制RetryPolicy时,你需要仔细考虑client和server的协作关系,并设计对应的异常测试,以确保行为符合预期。
gejun's avatar
gejun committed
510 511 512

## 协议

gejun's avatar
gejun committed
513 514 515
Channel的默认协议是baidu_std,可通过设置ChannelOptions.protocol换为其他协议,这个字段既接受enum也接受字符串。

目前支持的有:
gejun's avatar
gejun committed
516

gejun's avatar
gejun committed
517
- PROTOCOL_BAIDU_STD 或 “baidu_std",即[百度标准协议](baidu_std.md),默认为单连接。
gejun's avatar
gejun committed
518 519
- PROTOCOL_HULU_PBRPC 或 "hulu_pbrpc",hulu的协议,默认为单连接。
- PROTOCOL_NOVA_PBRPC 或 ”nova_pbrpc“,网盟的协议,默认为连接池。
gejun's avatar
gejun committed
520
- PROTOCOL_HTTP 或 ”http", http 1.0或1.1协议,默认为连接池(Keep-Alive)。具体方法见[访问HTTP服务](http_client.md)
gejun's avatar
gejun committed
521
- PROTOCOL_SOFA_PBRPC 或 "sofa_pbrpc",sofa-pbrpc的协议,默认为单连接。
gejun's avatar
gejun committed
522
- PROTOCOL_PUBLIC_PBRPC 或 "public_pbrpc",public_pbrpc的协议,默认为连接池。
gejun's avatar
gejun committed
523
- PROTOCOL_UBRPC_COMPACK 或 "ubrpc_compack",public/ubrpc的协议,使用compack打包,默认为连接池。具体方法见[ubrpc (by protobuf)](ub_client.md)。相关的还有PROTOCOL_UBRPC_MCPACK2或ubrpc_mcpack2,使用mcpack2打包。
gejun's avatar
gejun committed
524 525
- PROTOCOL_NSHEAD_CLIENT 或 "nshead_client",这是发送baidu-rpc-ub中所有UBXXXRequest需要的协议,默认为连接池。具体方法见[访问UB](ub_client.md)
- PROTOCOL_NSHEAD 或 "nshead",这是发送NsheadMessage需要的协议,默认为连接池。具体方法见[nshead+blob](ub_client.md#nshead-blob)
gejun's avatar
gejun committed
526 527
- PROTOCOL_MEMCACHE 或 "memcache",memcached的二进制协议,默认为单连接。具体方法见[访问memcached](memcache_client.md)
- PROTOCOL_REDIS 或 "redis",redis 1.2后的协议(也是hiredis支持的协议),默认为单连接。具体方法见[访问Redis](redis_client.md)
gejun's avatar
gejun committed
528 529 530 531 532
- PROTOCOL_NSHEAD_MCPACK 或 "nshead_mcpack", 顾名思义,格式为nshead + mcpack,使用mcpack2pb适配,默认为连接池。
- PROTOCOL_ESP 或 "esp",访问使用esp协议的服务,默认为连接池。

## 连接方式

gejun's avatar
gejun committed
533
brpc支持以下连接方式:
gejun's avatar
gejun committed
534

gejun's avatar
gejun committed
535 536 537
- 短连接:每次RPC前建立连接,结束后关闭连接。由于每次调用得有建立连接的开销,这种方式一般用于偶尔发起的操作,而不是持续发起请求的场景。没有协议默认使用这种连接方式,http 1.0对连接的处理效果类似短链接。
- 连接池:每次RPC前取用空闲连接,结束后归还,一个连接上最多只有一个请求,一个client对一台server可能有多条连接。http 1.1和各类使用nshead的协议都是这个方式。
- 单连接:进程内所有client与一台server最多只有一个连接,一个连接上可能同时有多个请求,回复返回顺序和请求顺序不需要一致,这是baidu_std,hulu_pbrpc,sofa_pbrpc协议的默认选项。
gejun's avatar
gejun committed
538

539 540 541 542 543 544 545
|                     | 短连接                                      | 连接池                   | 单连接                 |
| ------------------- | ---------------------------------------- | --------------------- | ------------------- |
| 长连接                 | 否                                        | 是                     | 是                   |
| server端连接数(单client) | qps*latency (原理见[little's law](https://en.wikipedia.org/wiki/Little%27s_law)) | qps*latency           | 1                   |
| 极限qps               | 差,且受限于单机端口数                              | 中等                    | 高                   |
| latency             | 1.5RTT(connect) + 1RTT + 处理时间            | 1RTT + 处理时间           | 1RTT + 处理时间         |
| cpu占用               | 高, 每次都要tcp connect                       | 中等, 每个请求都要一次sys write | 低, 合并写出在大流量时减少cpu占用 |
gejun's avatar
gejun committed
546 547 548 549

框架会为协议选择默认的连接方式,用户**一般不用修改**。若需要,把ChannelOptions.connection_type设为:

- CONNECTION_TYPE_SINGLE 或 "single" 为单连接
gejun's avatar
gejun committed
550 551 552 553 554 555 556

- CONNECTION_TYPE_POOLED 或 "pooled" 为连接池, 与单个远端的最大连接数由-max_connection_pool_size控制:

  | Name                         | Value | Description                              | Defined At          |
  | ---------------------------- | ----- | ---------------------------------------- | ------------------- |
  | max_connection_pool_size (R) | 100   | maximum pooled connection count to a single endpoint | src/brpc/socket.cpp |

gejun's avatar
gejun committed
557
- CONNECTION_TYPE_SHORT 或 "short" 为短连接
gejun's avatar
gejun committed
558

gejun's avatar
gejun committed
559 560
- 设置为“”(空字符串)则让框架选择协议对应的默认连接方式。

gejun's avatar
gejun committed
561
brpc支持[Streaming RPC](streaming_rpc.md),这是一种应用层的连接,用于传递流式数据。
gejun's avatar
gejun committed
562 563 564

## 关闭连接池中的闲置连接

gejun's avatar
gejun committed
565
当连接池中的某个连接在-idle_timeout_second时间内没有读写,则被视作“闲置”,会被自动关闭。默认值为10秒。此功能只对连接池(pooled)有效。打开-log_idle_connection_close在关闭前会打印一条日志。
gejun's avatar
gejun committed
566 567 568 569 570

| Name                      | Value | Description                              | Defined At              |
| ------------------------- | ----- | ---------------------------------------- | ----------------------- |
| idle_timeout_second       | 10    | Pooled connections without data transmission for so many seconds will be closed. No effect for non-positive values | src/brpc/socket_map.cpp |
| log_idle_connection_close | false | Print log when an idle connection is closed | src/brpc/socket.cpp     |
gejun's avatar
gejun committed
571 572 573 574 575

## 延迟关闭连接

多个channel可能通过引用计数引用同一个连接,当引用某个连接的最后一个channel析构时,该连接将被关闭。但在一些场景中,channel在使用前才被创建,用完立刻析构,这时其中一些连接就会被无谓地关闭再被打开,效果类似短连接。

gejun's avatar
gejun committed
576
一个解决办法是用户把所有或常用的channel缓存下来,这样自然能避免channel频繁产生和析构,但目前brpc没有提供这样一个utility,用户自己(正确)实现有一些工作量。
gejun's avatar
gejun committed
577

gejun's avatar
gejun committed
578 579 580 581 582 583 584
另一个解决办法是设置全局选项-defer_close_second

| Name               | Value | Description                              | Defined At              |
| ------------------ | ----- | ---------------------------------------- | ----------------------- |
| defer_close_second | 0     | Defer close of connections for so many seconds even if the connection is not used by anyone. Close immediately for non-positive values | src/brpc/socket_map.cpp |

设置后引用计数清0时连接并不会立刻被关闭,而是会等待这么多秒再关闭,如果在这段时间内又有channel引用了这个连接,它会恢复正常被使用的状态。不管channel创建析构有多频率,这个选项使得关闭连接的频率有上限。这个选项的副作用是一些fd不会被及时关闭,如果延时被误设为一个大数值,程序占据的fd个数可能会很大。
gejun's avatar
gejun committed
585 586 587

## 连接的缓冲区大小

gejun's avatar
gejun committed
588
-socket_recv_buffer_size设置所有连接的接收缓冲区大小,默认-1(不修改)
gejun's avatar
gejun committed
589

gejun's avatar
gejun committed
590 591 592 593 594 595
-socket_send_buffer_size设置所有连接的发送缓冲区大小,默认-1(不修改)

| Name                    | Value | Description                              | Defined At          |
| ----------------------- | ----- | ---------------------------------------- | ------------------- |
| socket_recv_buffer_size | -1    | Set the recv buffer size of socket if this value is positive | src/brpc/socket.cpp |
| socket_send_buffer_size | -1    | Set send buffer size of sockets if this value is positive | src/brpc/socket.cpp |
gejun's avatar
gejun committed
596 597 598

## log_id

599
通过set_log_id()可设置64位整型log_id。这个id会和请求一起被送到服务器端,一般会被打在日志里,从而把一次检索经过的所有服务串联起来。字符串格式的需要转化为64位整形才能设入log_id。
gejun's avatar
gejun committed
600 601 602

## 附件

gejun's avatar
gejun committed
603
baidu_std和hulu_pbrpc协议支持附件,这段数据由用户自定义,不经过protobuf的序列化。站在client的角度,设置在Controller::request_attachment()的附件会被server端收到,response_attachment()则包含了server端送回的附件。附件不受压缩选项影响。
gejun's avatar
gejun committed
604 605 606

在http协议中,附件对应[message body](http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html),比如要POST的数据就设置在request_attachment()中。

607
## 认证
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
client端的认证一般分为2种:

1. 基于请求的认证:每次请求都会带上认证信息。这种方式比较灵活,认证信息中可以含有本次请求中的字段,但是缺点是每次请求都会需要认证,性能上有所损失
2. 基于连接的认证:当TCP连接建立后,client发送认证包,认证成功后,后续该连接上的请求不再需要认证。相比前者,这种方式灵活度不高(一般ren认证包里只能携带本机一些静态信息),但性能较好,一般用于单连接/连接池场景

针对第一种认证场景,在实现上非常简单,将认证的格式定义加到请求结构体中,每次当做正常RPC发送出去即可;针对第二种场景,brpc提供了一种机制,只要用户继承实现:

```c++
class Authenticator {
public:
    virtual ~Authenticator() {}

    // Implement this method to generate credential information
    // into `auth_str' which will be sent to `VerifyCredential'
    // at server side. This method will be called on client side.
    // Returns 0 on success, error code otherwise
    virtual int GenerateCredential(std::string* auth_str) const = 0;
};
```

那么当用户并发调用RPC接口用单连接往同一个server发请求时,框架会自动保证:建立TCP连接后,连接上的第一个请求中会带有上述`GenerateCredential`产生的认证包,其余剩下的并发请求不会带有认证信息,依次排在第一个请求之后。整个发送过程依旧是并发的,并不会等第一个请求先返回。若server端认证成功,那么所有请求都能成功返回;若认证失败,一般server端则会关闭连接,这些请求则会收到相应错误。
gejun's avatar
gejun committed
629

gejun's avatar
gejun committed
630
目前自带协议中支持客户端认证的有:[baidu_std](baidu_std.md)(默认协议), HTTP, hulu_pbrpc, ESP。对于自定义协议,一般可以在组装请求阶段,调用Authenticator接口生成认证串,来支持客户端认证。
631

gejun's avatar
gejun committed
632 633 634 635 636 637 638 639
## 重置

调用Reset方法可让Controller回到刚创建时的状态。

别在RPC结束前重置Controller,行为是未定义的。

## 压缩

640 641 642 643 644
set_request_compress_type()设置request的压缩方式,默认不压缩。

注意:附件不会被压缩。

HTTP body的压缩方法见[client压缩request body](http_client#压缩request-body)
gejun's avatar
gejun committed
645 646 647

支持的压缩方法有:

648 649 650
- brpc::CompressTypeSnappy : [snanpy压缩](http://google.github.io/snappy/),压缩和解压显著快于其他压缩方法,但压缩率最低。
- brpc::CompressTypeGzip : [gzip压缩](http://en.wikipedia.org/wiki/Gzip),显著慢于snappy,但压缩率高
- brpc::CompressTypeZlib : [zlib压缩](http://en.wikipedia.org/wiki/Zlib),比gzip快10%~20%,压缩率略好于gzip,但速度仍明显慢于snappy。
gejun's avatar
gejun committed
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687

下表是多种压缩算法应对重复率很高的数据时的性能,仅供参考。

| Compress method | Compress size(B) | Compress time(us) | Decompress time(us) | Compress throughput(MB/s) | Decompress throughput(MB/s) | Compress ratio |
| --------------- | ---------------- | ----------------- | ------------------- | ------------------------- | --------------------------- | -------------- |
| Snappy          | 128              | 0.753114          | 0.890815            | 162.0875                  | 137.0322                    | 37.50%         |
| Gzip            | 10.85185         | 1.849199          | 11.2488             | 66.01252                  | 47.66%                      |                |
| Zlib            | 10.71955         | 1.66522           | 11.38763            | 73.30581                  | 38.28%                      |                |
| Snappy          | 1024             | 1.404812          | 1.374915            | 695.1555                  | 710.2713                    | 8.79%          |
| Gzip            | 16.97748         | 3.950946          | 57.52106            | 247.1718                  | 6.64%                       |                |
| Zlib            | 15.98913         | 3.06195           | 61.07665            | 318.9348                  | 5.47%                       |                |
| Snappy          | 16384            | 8.822967          | 9.865008            | 1770.946                  | 1583.881                    | 4.96%          |
| Gzip            | 160.8642         | 43.85911          | 97.13162            | 356.2544                  | 0.78%                       |                |
| Zlib            | 147.6828         | 29.06039          | 105.8011            | 537.6734                  | 0.71%                       |                |
| Snappy          | 32768            | 16.16362          | 19.43596            | 1933.354                  | 1607.844                    | 4.82%          |
| Gzip            | 229.7803         | 82.71903          | 135.9995            | 377.7849                  | 0.54%                       |                |
| Zlib            | 240.7464         | 54.44099          | 129.8046            | 574.0161                  | 0.50%                       |                |

下表是多种压缩算法应对重复率很低的数据时的性能,仅供参考。

| Compress method | Compress size(B) | Compress time(us) | Decompress time(us) | Compress throughput(MB/s) | Decompress throughput(MB/s) | Compress ratio |
| --------------- | ---------------- | ----------------- | ------------------- | ------------------------- | --------------------------- | -------------- |
| Snappy          | 128              | 0.866002          | 0.718052            | 140.9584                  | 170.0021                    | 105.47%        |
| Gzip            | 15.89855         | 4.936242          | 7.678077            | 24.7294                   | 116.41%                     |                |
| Zlib            | 15.88757         | 4.793953          | 7.683384            | 25.46339                  | 107.03%                     |                |
| Snappy          | 1024             | 2.087972          | 1.06572             | 467.7087                  | 916.3403                    | 100.78%        |
| Gzip            | 32.54279         | 12.27744          | 30.00857            | 79.5412                   | 79.79%                      |                |
| Zlib            | 31.51397         | 11.2374           | 30.98824            | 86.90288                  | 78.61%                      |                |
| Snappy          | 16384            | 12.598            | 6.306592            | 1240.276                  | 2477.566                    | 100.06%        |
| Gzip            | 537.1803         | 129.7558          | 29.08707            | 120.4185                  | 75.32%                      |                |
| Zlib            | 519.5705         | 115.1463          | 30.07291            | 135.697                   | 75.24%                      |                |
| Snappy          | 32768            | 22.68531          | 12.39793            | 1377.543                  | 2520.582                    | 100.03%        |
| Gzip            | 1403.974         | 258.9239          | 22.25825            | 120.6919                  | 75.25%                      |                |
| Zlib            | 1370.201         | 230.3683          | 22.80687            | 135.6524                  | 75.21%                      |                |

# FAQ

gejun's avatar
gejun committed
688
### Q: brpc能用unix domain socket吗
gejun's avatar
gejun committed
689

690
不能。同机TCP socket并不走网络,相比unix domain socket性能只会略微下降。一些不能用TCP socket的特殊场景可能会需要,以后可能会扩展支持。
gejun's avatar
gejun committed
691

692
### Q: Fail to connect to xx.xx.xx.xx:xxxx, Connection refused
gejun's avatar
gejun committed
693 694 695

一般是对端server没打开端口(很可能挂了)。 

696
### Q: 经常遇到至另一个机房的Connection timedout
gejun's avatar
gejun committed
697

gejun's avatar
gejun committed
698
![img](../images/connection_timedout.png)
gejun's avatar
gejun committed
699

gejun's avatar
gejun committed
700
这个就是连接超时了,调大连接和RPC超时:
gejun's avatar
gejun committed
701

gejun's avatar
gejun committed
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
```c++
struct ChannelOptions {
    ...
    // Issue error when a connection is not established after so many
    // milliseconds. -1 means wait indefinitely.
    // Default: 200 (milliseconds)
    // Maximum: 0x7fffffff (roughly 30 days)
    int32_t connect_timeout_ms;
    
    // Max duration of RPC over this Channel. -1 means wait indefinitely.
    // Overridable by Controller.set_timeout_ms().
    // Default: 500 (milliseconds)
    // Maximum: 0x7fffffff (roughly 30 days)
    int32_t timeout_ms;
    ...
};
```
gejun's avatar
gejun committed
719

720
注意: 连接超时不是RPC超时,RPC超时打印的日志是"Reached timeout=..."。
gejun's avatar
gejun committed
721 722 723

### Q: 为什么同步方式是好的,异步就crash了

724
重点检查Controller,Response和done的生命周期。在异步访问中,RPC调用结束并不意味着RPC整个过程结束,而是在进入done->Run()时才会结束。所以这些对象不应在调用RPC后就释放,而是要在done->Run()里释放。你一般不能把这些对象分配在栈上,而应该分配在堆上。详见[异步访问](client.md#异步访问)
gejun's avatar
gejun committed
725

726
### Q: 怎么确保请求只被处理一次
gejun's avatar
gejun committed
727

728
这不是RPC层面的事情。当response返回且成功时,我们确认这个过程一定成功了。当response返回且失败时,我们确认这个过程一定失败了。但当response没有返回时,它可能失败,也可能成功。如果我们选择重试,那一个成功的过程也可能会被再执行一次。一般来说带副作用的RPC服务都应当考虑[幂等](http://en.wikipedia.org/wiki/Idempotence)问题,否则重试可能会导致多次叠加副作用而产生意向不到的结果。只有读的检索服务大都没有副作用而天然幂等,无需特殊处理。而带写的存储服务则要在设计时就加入版本号或序列号之类的机制以拒绝已经发生的过程,保证幂等。
gejun's avatar
gejun committed
729

730
### Q: Invalid address=`bns://group.user-persona.dumi.nj03'
gejun's avatar
gejun committed
731
```
gejun's avatar
gejun committed
732
FATAL 04-07 20:00:03 7778 src/brpc/channel.cpp:123] Invalid address=`bns://group.user-persona.dumi.nj03'. You should use Init(naming_service_name, load_balancer_name, options) to access multiple servers.
gejun's avatar
gejun committed
733
```
734
访问名字服务要使用三个参数的Init,其中第二个参数是load_balancer_name,而这里用的是两个参数的Init,框架认为是访问单点,就会报这个错。
735

736
### Q: 两端都用protobuf,为什么不能互相访问
737

738
**协议 !=protobuf**。protobuf负责一个包的序列化,协议中的一个消息可能会包含多个protobuf包,以及额外的长度、校验码、magic number等等。打包格式相同不意味着协议可以互通。在brpc中写一份代码就能服务多协议的能力是通过把不同协议的数据转化为统一的编程接口完成的,而不是在protobuf层面。
739 740 741

### Q: 为什么C++ client/server 能够互相通信, 和其他语言的client/server 通信会报序列化失败的错误

742
检查一下C++ 版本是否开启了压缩 (Controller::set_compress_type), 目前其他语言的rpc框架还没有实现压缩,互相返回会出现问题。 
743 744 745 746 747 748 749

# 附:Client端基本流程

![img](../images/client_side.png)

主要步骤:

gejun's avatar
gejun committed
750 751
1. 创建一个[bthread_id](https://github.com/brpc/brpc/blob/master/src/bthread/id.h)作为本次RPC的correlation_id。
2. 根据Channel的创建方式,从进程级的[SocketMap](https://github.com/brpc/brpc/blob/master/src/brpc/socket_map.h)中或从[LoadBalancer](https://github.com/brpc/brpc/blob/master/src/brpc/load_balancer.h)中选择一台下游server作为本次RPC发送的目的地。
gejun's avatar
gejun committed
752
3. 根据连接方式(单连接、连接池、短连接),选择一个[Socket](https://github.com/brpc/brpc/blob/master/src/brpc/socket.h)
753
4. 如果开启验证且当前Socket没有被验证过时,第一个请求进入验证分支,其余请求会阻塞直到第一个包含认证信息的请求写入Socket。server端只对第一个请求进行验证。
gejun's avatar
gejun committed
754
5. 根据Channel的协议,选择对应的序列化函数把request序列化至[IOBuf](https://github.com/brpc/brpc/blob/master/src/butil/iobuf.h)
755
6. 如果配置了超时,设置定时器。从这个点开始要避免使用Controller对象,因为在设定定时器后随时可能触发超时->调用到用户的超时回调->用户在回调中析构Controller。
756 757
7. 发送准备阶段结束,若上述任何步骤出错,会调用Channel::HandleSendFailed。
8. 将之前序列化好的IOBuf写出到Socket上,同时传入回调Channel::HandleSocketFailed,当连接断开、写失败等错误发生时会调用此回调。
758
9. 如果是同步发送,Join correlation_id;否则至此CallMethod结束。
759 760 761
10. 网络上发消息+收消息。
11. 收到response后,提取出其中的correlation_id,在O(1)时间内找到对应的Controller。这个过程中不需要查找全局哈希表,有良好的多核扩展性。
12. 根据协议格式反序列化response。
762
13. 调用Controller::OnRPCReturned,可能会根据错误码判断是否需要重试,或让RPC结束。如果是异步发送,调用用户回调。最后摧毁correlation_id唤醒Join着的线程。