手机端访问controller_BRPC详解(二)——Client端概述

该篇文章主要取自官方github文档,具体可参考:

https://github.com/apache/incubator-brpc/blob/master/docs/cn/client.md​github.com

整体架构

22325d15fea8f18dd5b99470730570f1.png
BRPC整体架构——client端

一、Channel

Client指发起请求的一端,在brpc中没有对应的实体,取而代之的是brpc::Channel,它代表和一台或一组服务器的交互通道,Client和Channel在角色上的差别在实践中并不重要,你可以把Channel视作Client。

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

一些RPC实现中有ClientManager的概念,包含了Client端的配置信息和资源管理。brpc不需要这些,以往在ClientManager中配置的线程数、长短连接等等要么被加入了brpc::ChannelOptions,要么可以通过gflags全局配置,这么做的好处:方便、共用资源、生命周期(析构ClientManager由框架完成不容易出错)。 就像大部分类那样,Channel必须在Init之后才能使用,options为NULL时所有参数取默认值。 Init函数分为连接一台服务器和连接服务集群。

1、连接一台服务器

// options为NULL时取默认值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); 

这类Init连接的服务器往往有固定的ip地址,不需要命名服务和负载均衡,创建起来相对轻量。但是请勿频繁创建使用域名的Channel。这需要查询dns,可能最多耗时10秒(查询DNS的默认超时)。重用它们。

2、连接服务集群

int Init(const char* naming_service_url, 
         const char* load_balancer_name, 
         const ChannelOptions* options); 

这类Channel需要定期从naming_service_url指定的命名服务中获得服务器列表,并通过load_balancer_name指定的负载均衡算法选择出一台机器发送请求。 你不应该在每次请求前动态地创建此类(连接服务集群的)Channel。因为创建和析构此类Channel牵涉到较多的资源,比如在创建时得访问一次命名服务,否则便不知道有哪些服务器可选。由于Channel可被多个线程共用,一般也没有必要动态创建。

二、命名服务

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

28804e70b6eea11824dace2a98b5d1f0.png
命名服务

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

用户可以通过实现brpc::NamingService来对接更多命名服务,具体可以参考命名服务

三、命名服务过滤器

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

19236367e9cbabfe1eea529a677c9c6e.png
命名服务过滤器

四、负载均衡

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

ca668b0e8c34a5e736636e267adbad62.png
负载均衡

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

1、rr

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

2、wrr

即weighted round robin, 根据服务器列表配置的权重值来选择服务器。服务器被选到的机会正比于其权重值,并且该算法能保证同一服务器被选到的结果较均衡的散开。

3、random

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

4、la

locality-aware,优先选择延时低的下游,直到其延时高于其他机器,无需其他设置。实现原理请查看。实现原理可以参考locality-aware

5、c_murmurhash or c_md5

一致性哈希,与简单hash的不同之处在于增加或删除机器时不会使分桶结果剧烈变化,特别适合cache类服务。发起RPC前需要设置Controller.set_request_code(),否则RPC会失败。request_code一般是请求中主键部分的32位哈希值,不需要和负载均衡使用的哈希算法一致。比如用c_murmurhash算法也可以用md5计算哈希值。

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

6、从集群宕机后恢复时的客户端限流

集群宕机指的是集群中所有server都处于不可用的状态。由于健康检查机制,当集群恢复正常后,server会间隔性地上线。当某一个server上线后,所有的流量会发送过去,可能导致服务再次过载。若熔断开启,则可能导致其它server上线前该server再次熔断,集群永远无法恢复。作为解决方案,brpc提供了在集群宕机后恢复时的限流机制:当集群中没有可用server时,集群进入恢复状态,假设正好能服务所有请求的server数量为min_working_instances,当前集群可用的server数量为q,则在恢复状态时,client接受请求的概率为q/min_working_instances,否则丢弃;若一段时间hold_seconds内q保持不变,则把流量重新发送全部可用的server上,并离开恢复状态。在恢复阶段时,可以通过判断controller.ErrorCode()是否等于brpc::ERJECT来判断该次请求是否被拒绝,被拒绝的请求不会被框架重试。

此恢复机制要求下游server的能力是类似的,所以目前只针对rr和random有效,开启方式是在load_balancer_name后面加上min_working_instances和hold_seconds参数的值,例如:

channel.Init("http://...", "random:min_working_instances=6 hold_seconds=10", &options);

7、健康检查

连接断开的server会被暂时隔离而不会被负载均衡算法选中,brpc会定期连接被隔离的server,以检查他们是否恢复正常,间隔由参数-health_check_interval控制

五、发起访问

一般来说,我们不直接调用Channel.CallMethod,而是通过protobuf生成的XXX_Stub,过程更像是“调用函数”。stub内没什么成员变量,建议在栈上创建和使用,而不必new,当然你也可以把stub存下来复用。Channel::CallMethod和stub访问都是线程安全的,可以被所有线程同时访问。比如:

XXX_Stub stub(&channel); 
stub.some_method(controller, request, response, done); 

或者

XXX_Stub(&channel).some_method(controller, request, response, done); 

一个例外是http/h2 client。访问http服务和protobuf没什么关系,直接调用CallMethod即可,除了Controller和done均为NULL。

1、同步访问

指的是:CallMethod会阻塞到收到server端返回response或发生错误(包括超时)。

同步访问中的response/controller不会在CallMethod后被框架使用,它们都可以分配在栈上。注意,如果request/response字段特别多字节数特别大的话,还是更适合分配在堆上。

MyRequest request; 
MyResponse response; 
brpc::Controller cntl; 
XXX_Stub stub(&channel); 
request.set_foo(...); 
cntl.set_timeout_ms(...); 
stub.some_method(&cntl, &request, &response, NULL); 
if (cntl->Failed()) { 
    // RPC失败了. response里的值是未定义的,勿用。 
} else { 
    // RPC成功了,response里有我们想要的回复数据。 
} 

2、异步访问

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

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

你可以独立地创建这些对象,并使用NewCallback生成done,也可以把Response和Controller作为done的成员变量,一起new出来,一般使用前一种方法。

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

(1)、使用NewCallback

static void OnRPCDone(MyResponse* response, brpc::Controller* cntl) { 
    // unique_ptr会帮助我们在return时自动删掉response/cntl,防止忘记。gcc 3.4下的unique_ptr是模拟版本。 
    std::unique_ptr<MyResponse> response_guard(response); 
    std::unique_ptr<brpc::Controller> cntl_guard(cntl); 
    if (cntl->Failed()) { 
        // RPC失败了. response里的值是未定义的,勿用。 
    } else { 
        // RPC成功了,response里有我们想要的数据。开始RPC的后续处理。     
    } 
    // NewCallback产生的Closure会在Run结束后删除自己,不用我们做。 
} 
MyResponse* response = new MyResponse; 
brpc::Controller* cntl = new brpc::Controller; 
MyService_Stub stub(&channel); 
MyRequest request;  // 你不用new request,即使在异步访问中. 
request.set_foo(...); 
cntl->set_timeout_ms(...); 
stub.some_method(cntl, &request, response, google::protobuf::NewCallback(OnRPCDone, response, cntl)); 

(2)、继承Google::protobuf::closure

使用NewCallback的缺点是要分配三次内存:response, controller, done。如果profiler证明这儿的内存分配有瓶颈,可以考虑自己继承Closure,把response/controller作为成员变量,这样可以把三次new合并为一次。但缺点就是代码不够美观,如果内存分配不是瓶颈,别用这种方法。

class OnRPCDone: public google::protobuf::Closure { 
public: 
    void Run() { 
        // unique_ptr会帮助我们在return时自动delete this,防止忘记。gcc 3.4下的unique_ptr是模拟版本。 
        std::unique_ptr<OnRPCDone> self_guard(this); 
 
        if (cntl->Failed()) { 
            // RPC失败了. response里的值是未定义的,勿用。 
        } else { 
            // RPC成功了,response里有我们想要的数据。开始RPC的后续处理。 
        } 
    } 
    MyResponse response; 
    brpc::Controller cntl; 
} 
OnRPCDone* done = new OnRPCDone; 
MyService_Stub stub(&channel); 
MyRequest request;  // 你不用new request,即使在异步访问中. 
request.set_foo(...); 
done->cntl.set_timeout_ms(...); 
stub.some_method(&done->cntl, &request, &done->response, done); 

六、client设置

client的设置主要包括如下三个部分

(1)、brpc::ChannelOptions: 定义在src/brpc/channel.h中,用于初始化Channel,一旦初始化成功无法修改

(2)、brpc::Controller: 定义在src/brpc/controller.h中,用于在某次RPC中覆盖ChannelOptions中的选项,可根据上下文每次均不同

(3)、全局gflags:常用于调节一些底层代码的行为,一般不用修改。请自行阅读服务/flags页面中的说明

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

Controller的特点如下:

(1)、一个Controller只能有一个使用者,没有特殊说明的话,Controller中的方法默认线程不安全

(2)、因为不能被共享,所以一般不会用共享指针管理Controller,如果你用共享指针了,很可能意味着出错了

(3)、Controller创建于开始RPC前,析构于RPC结束后,常见几种模式:

(a)、同步RPC前Controller放栈上,出作用域后自行析构。注意异步RPC的Controller绝对不能放栈上,否则其析构时异步调用很可能还在进行中,从而引发未定义行为

(b)、异步RPC前new Controller,done中删除

1、线程数

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

2、超时

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

ChannelOptions.connect_timeout_ms是对应Channel上所有RPC的连接超时(单位毫秒)。-1表示等到连接建立或出错,此值被限制为不能超过timeout_ms。注意此超时独立于TCP的连接超时,一般来说前者小于后者,反之则可能在connect_timeout_ms未达到前由于TCP连接超时而出错。需要注意以下两点:

(1)、brpc中的超时是deadline,超过就意味着RPC结束,超时后没有重试。其他实现可能既有单次访问的超时,也有代表deadline的超时。迁移到brpc时请仔细区分。

(2)、RPC超时的错误码为ERPCTIMEDOUT (1008),ETIMEDOUT的意思是连接超时,且可重试。

3、重试

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

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

4、熔断

关于熔断机制后续会专门讲解

5、协议

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

6、连接方式

brpc支持以下几种连接方式:

(1)、短连接:每次RPC前建立连接,结束后关闭连接。由于每次调用得有建立连接的开销,这种方式一般用于偶尔发起的操作,而不是持续发起请求的场景。没有协议默认使用这种连接方式,http/1.0对连接的处理效果类似短链接。

(2)、连接池:每次RPC前取用空闲连接,结束后归还,一个连接上最多只有一个请求,一个client对一台server可能有多条连接。http/1.1和各类使用nshead的协议都是这个方式。

(3)、单连接:进程内所有client与一台server最多只有一个连接,一个连接上可能同时有多个请求,回复返回顺序和请求顺序不需要一致,这是baidu_std,hulu_pbrpc,sofa_pbrpc协议的默认选项

7、关闭连接池中的闲置连接

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

8、延迟关闭连接

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

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

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

9、log_id

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

10、附件

baidu_std和hulu_pbrpc协议支持附件,这段数据由用户自定义,不经过protobuf的序列化。站在client的角度,设置在Controller::request_attachment()的附件会被server端收到,response_attachment()则包含了server端送回的附件。附件不受压缩选项影响。http/h2协议中,附件对应message body,比如要POST的数据就设置在request_attachment()中

11、开启SSL

具体可以参考官方文档ssl_options.h

12、认证

client的认证一般分为两种:

(1)、基于请求的认证:每次请求都会带上认证信息。这种方式比较灵活,认证信息中可以含有本次请求中的字段,但是缺点是每次请求都会需要认证,性能上有所损失

(2)、基于连接的认证:当TCP连接建立后,client发送认证包,认证成功后,后续该连接上的请求不再需要认证。相比前者,这种方式灵活度不高(一般认证包里只能携带本机一些静态信息),但性能较好,一般用于单连接/连接池场景

13、重置

调用Reset方法可让Controller回到刚创建时的状态,别在RPC结束前重置Controller,行为是未定义的

14、压缩

set_request_compress_type()设置request的压缩方式,默认不压缩。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值