RPC 即远程过程调用协议(Remote Procedure Call Protocol),可以让我们像调用本地对象一样发起
远程调用。
HTTP1.1核心问题在于:在同一个TCP连接中,没办法区分response是属于哪个请求,一旦多个请求返
回的文本内容混在一起,则没法区分数据归属于哪个请求,所以请求只能一个个串行排队发送。这直接
导致了TCP资源的闲置。
HTTP2为了解决这个问题,提出了 流 的概念,每一次请求对应一个流,有一个唯一ID,用来区分不同的
请求。基于流的概念,进一步提出了 帧 ,一个请求的数据会被分成多个帧,方便进行数据分割传输,每
个帧都唯一属于某一个流ID,将帧按照流ID进行分组,即可分离出不同的请求。这样同一个TCP连接中
就可以同时并发多个请求,不同请求的帧数据可穿插在一起,根据流ID分组即可。HTTP2.0基于这种二
进制协议的乱序模式 (Duplexing),直接解决了HTTP1.1的核心痛点,通过这种复用TCP连接的方式,不
用再同时建多个连接,提升了TCP的利用效率。
Client是对 Stub 封装;通过 Stub 可以真正的调用 RPC 请求。
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
std::string SayHello(const std::string& user) {
...
private:
std::unique_ptr<Greeter::Stub> stub_;
};
Channel 提供一个与特定 gRPC server 的主机和端口建立的连接。
Stub 就是在 Channel 的基础上创建而成的。
target_str = "localhost:50051";
auto channel =
grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials());
GreeterClient greeter(channel);
std::string user("world");
std::string reply = greeter.SayHello(user);
Server 端需要实现对应的 RPC,所有的 RPC 组成了 Service。
class GreeterServiceImpl final : public Greeter::Service {
Status SayHello(ServerContext* context, const HelloRequest* request,
HelloReply* reply) override {
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
return Status::OK;
}
};
Server 的创建需要一个 Builder,添加上监听的地址和端口,注册上该端口上绑定的服务,最后构建出
Server 并启动
ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// builder.SetSyncServerOption(ServerBuilder::MIN_POLLERS,2);
// builder.SetSyncServerOption(ServerBuilder::MAX_POLLERS,4);
builder.RegisterService(&service);
std::unique_ptr<Server> server(builder.BuildAndStart());
不管是哪种类型 RPC,都是由 Client 发起请求。
补充多线程:
builder.SetSyncServerOption(ServerBuilder::MIN_POLLERS,2);
builder.SetSyncServerOption(ServerBuilder::MAX_POLLERS,4);
得益于SO_REUSEPORT参数,同一个listenfd可以被放到多个epoll中进行监听
当一个链接成功建立后会生成acceptfd,这个acceptfd会被随机的分配到现有的epoll中,目前grpc的分配策略是轮询(round-robin)
min poller, max poller,自动根据调用的请求的频次进行自动伸缩poller。
服务端: 一个server可以有多个service,在proto文件对应service;
客户端: 一个channel (对应一个链路)可以供多个stub使用;就是不同的stub共用一个链路
在该模式中,当客户端调用服务器端的远程方法时,客户端发送请求至服务器端并获得一个响应,与响应一起发送的还有状态细节以及 trailer 元数据。
在服务器端流 RPC 模式中,服务器端在接收到客户端的请求消息后,会发回一个响应的序列。这种多个响应所组成的序列也被称为“流”。在将所有的服务器端响应发送完毕之后,服务器端会以 trailer 元数据的形式将其状态发送给客户端,从而标记流的结束。
client
server
客户端会发送多个请求给服务器端,而不再是单个请求。服务器端则会发送一个响应给客户端。但是,服务器端不一定要等到从客户端接收到所有消息后才发送响应。基于这样的逻辑,我们可以在接收到流中的一条消息或几条消息之后就发送响应,也可以在读取完流中的所有消息之后再发送响应。
client
sever
在双向流 RPC 模式中,客户端以消息流的形式发送请求到服务器端,服务器端也以消息流的形式进行响应。调用必须由客户端发起,但在此之后,通信完全基于 gRPC 客户端和服务器端的应用程序逻辑。
client:
server
不管是 Client 还是 Server,异步 gRPC 都是利用 CompletionQueue API 进行异步操作。基本的流程:
1、绑定一个 CompletionQueue 到一个 RPC 调用
2、利用唯一的 void* Tag 进行读写
3、调用 CompletionQueue::Next() 等待操作完成,完成后通过唯一的 Tag 来判断对应什么请求/返回进行后续操作
greeter_async_client.cc 中是异步 Client 的 Demo,其中只有一次请求,逻辑简单。
1、创建 CompletionQueue
2、创建 RPC (既 ClientAsyncResponseReader ),这里有两种方式:
-----stub_->PrepareAsyncSayHello() + rpc->StartCall()
-----stub_->AsyncSayHello()
3、调用 rpc->Finish() 设置请求消息 reply 和唯一的 tag 关联,将请求发送出去
4、使用 cq.Next() 等待 Completion Queue 返回响应消息体,通过 tag 关联对应的请求
class GreeterClient {
public:
explicit GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
std::string SayHello(const std::string& user) {
HelloRequest request;
request.set_name(user);
HelloReply reply;
ClientContext context;
Status status;
//创建 CompletionQueue
CompletionQueue cq;
//创建 RPC (既 ClientAsyncResponseReader ):stub_->PrepareAsyncSayHello() + rpc->StartCall()
stub_->PrepareAsyncSayHello(&context, request, &cq));
rpc->StartCall();
//调用 rpc->Finish() 设置请求消息 reply 和唯一的 tag =1关联,将请求发送出去
rpc->Finish(&reply, &status, (void*)1);
void* got_tag;
bool ok = false;
//使用 cq.Next() 等待 Completion Queue 返回响应消息体,通过 tag 关联对应的请求
GPR_ASSERT(cq.Next(&got_tag, &ok));
// Act upon the status of the actual RPC.
if (status.ok()) {
return reply.message();
} else {
return "RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
1、创建一个 CallData,初始构造列表中将状态设置为 CREATE
2、构造函数中,调用 Process()成员函数,调用 service_->RequestSayHello() 后,状态变更为PROCESS:
传入 ServerContext ctx_
传入 HelloRequest request_
传入 ServerAsyncResponseWriter responder_
传入 ServerCompletionQueue* cq_
将对象自身的地址作为 tag 传入
该动作,能将事件加入事件循环,可以在 CompletionQueue 中等待
3、收到请求, cq->Next() (监听请求的,Next里面最终会调用epoll_wait)的阻塞结束并返回,得到 tag,既上次传入的 CallData 对象地址
4、用 tag 对应 CallData 对象的 Proceed() ,此时状态为 Process
创建新的 CallData 对象以接收新请求
处理消息体并设置 reply_
将状态设置为 FINISH
调用 responder_.Finish() 将返回发送给客户端
该动作,能将事件加入到事件循环,可以在 CompletionQueue 中等待
5、发送完毕, cq->Next() 的阻塞结束并返回,得到 tag。现实中,如果发送有异常应当有其他相关的处理
6、调用 tag 对应 CallData 对象的 Proceed() ,此时状态为 FINISH, delete this 清理自己,一条消息处理完成。
class ServerImpl final {
private:
class CallData {
public:
// 创建一个 CallData,初始构造列表中将状态设置为 CREATE
CallData(Greeter::AsyncService* service, ServerCompletionQueue* cq)
: service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
//构造函数中,调用 Process()成员函数,调用 service_->RequestSayHello() 后,状态变更为PROCESS
Proceed();}
void Proceed() {
if (status_ == CREATE) {
//构造函数中,调用 Process()成员函数,调用 service_->RequestSayHello() 后,状态变更为PROCESS
status_ = PROCESS;
// 传入 ServerContext ctx_
// 传入 HelloRequest request_
// 传入 ServerAsyncResponseWriter responder_
// 传入 ServerCompletionQueue* cq_
// 将对象自身的地址作为 tag 传入
//该动作,能将事件加入事件循环,可以在 CompletionQueue 中等待
service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_,this);
}
//收到请求, cq->Next() 的阻塞结束并返回,得到 tag,既上次传入的 CallData 对象地址
//调用 tag 对应 CallData 对象的 Proceed() ,此时状态为 Process
else if (status_ == PROCESS) {
//创建新的 CallData 对象以接收新请求
new CallData(service_, cq_);
//处理消息体并设置 reply_
std::string prefix("Hello ");
reply_.set_message(prefix + request_.name());
// 将状态设置为 FINISH
status_ = FINISH;
//调用 responder_.Finish() 将返回发送给客户端
//该动作,能将事件加入到事件循环,可以在 CompletionQueue 中等待
responder_.Finish(reply_, Status::OK, this);
}//发送完毕, cq->Next() 的阻塞结束并返回,得到 tag。现实中,如果发送有异常应当有其他相关的处理
else {
GPR_ASSERT(status_ == FINISH);
// 调用 tag 对应 CallData 对象的 Proceed() ,此时状态为 FINISH, delete this 清理自己,一条消息处理完成
delete this;
}
}
private:
Greeter::AsyncService* service_;
ServerCompletionQueue* cq_;
ServerContext ctx_;
HelloRequest request_;
HelloReply reply_;
ServerAsyncResponseWriter<HelloReply> responder_;
enum CallStatus { CREATE, PROCESS, FINISH };
CallStatus status_;
};
private:
std::unique_ptr<ServerCompletionQueue> cq_;
Greeter::AsyncService service_;
std::unique_ptr<Server> server_;
};
异步处理的epoll方式和同步是类似的,但对于rpc函数的响应提供了更灵活的处理机制,可以将一些耗
时的处理逻辑放到外部的线程池进行处理
关系图:
右侧 RPC 为创建的对象中的内存容,左侧使用相同颜色的小块进行代替。
以下 CallData 并非 gRPC 中的概念,而是异步 Server 在实现过程中为了方便进行的封装,其中的
Status 也是在异步调用过程中自定义的、用于转移的状态。
2、同一个类定义不同的处理函数,需要一个变量定义不同的函数的处理
放在同一个类中,如果定义3个变量,那么每个变量都要多个不属于自己的参数,浪费空间!
●同步异步客户端、同步异步服务器交叉测试。
●同步服务端不同线程数量测试
●异步服务端不同线程数量测试
先思考服务器线程数量对于性能的影响。
查看线程数
grep ’ processor’ /proc/cpuinfo | sort -u | wc -1
注意,此处查看的线程数是总的线程数,可以理解为逻辑cpu的数量,这里本人获取的结果为4
小结论“: 服务端:能够自动调整epolI线程数量,最好设置为2- cpu逻辑数量
同步客户端而言,不能充分使用带宽
req ->
等待这段时间不能发请求
< resp
结论:
1、处理函数是cpu密集型
1个epoll------对应线程池1个线程
2个epoll------对应线程池2个线程(如果是阻塞的任务,可以多开几个线程)
epoll + pool = cpu逻辑核数(能并发的线程数量)
2、处理io密集型,有阻塞的存在
增加pool线程池线程数量,
或者考虑用协程(支持多线程)
如何研究线程模型:
listen
accept/ grpc_accept4 , 建议在grpc_accept4打断点
epoll_ctl
epoll_wait