在glusterfs中,gluster与glusterd通信请求对卷的操作、集群的操作、状态的查看等;glusterd与glusterfsd通信完成对卷的操作,集群的操作,状态的查看;glusterfs与glusterfsd通信完成文件的存储。所有这些通信都是通过内部的RPC模块来完成的。
有关RPC的相关概念、协议等这里不展开描述,有兴趣的可以看看这两篇文章(1, 2)。
=========================================
从代码的组织来看,RPC的服务端逻辑上可分为四层,server-app、rpc-server、rpc-transport、protocol,每一层都提供相应的接口供上一层调用,同时,上一层会提供回调函数供下一层来调用;同样,RPC的客户端逻辑上也可分为四层,cli-app、rpc-cli、rpc-transport、protocol。目前,protocol提供了tcp(socket)和rdma两种方式,且都以动态库的方式提供,rpc-transport会根据配置加载不同的动态库。我们以gluster与glusterd的通信并选用tcp的方式为例来看看RPC相关流程。
1. 服务端的初始化
关键流程如图所示:
需要注意的是:rpc_transport_load时会根据协议的类型加载(使用dlopen)不同的动态库,调用socket_listen时将fd与回调函数添加事件驱动器中。当有读写事件时,事件驱动器回调socket_server_event_handler函数(用于服务端的accept)或者socket_event_handler函数(用于一般的请求),然后依次回调rpc_transport_notify、rpcsvc_notify处理RPC请求。
2. 客户端的初始化
关键流程:
socket_connect函数会将fd以及回调处理函数注册到事件驱动器中。
3. 一次完整的RPC流程
(1) 客户端发送RPC请求
关键的流程与数据结构:
客户端通过调用rpc_clnt_submit函数发送RPC请求,该函数又会一层层调用,最终在socket_submit_request中通过writev将请求发送出去。在调用rpc_clnt_submit时会准备好RPC所需要的相关数据,例如程序号,程序版本号,过程号,参数信息等等,然后逐层按照接口组织好相关的数据。
例如: 执行 gluster volume info命令,其内部关键代码:
int32_t gf_clie_1_get_volume(call_frame_t * frame, xlator_t * this) { ... ret = cli_cmd_submit(&req, frame, cli_rpc_prog, //包含程序名,程序号,程序版本号等信息 GLUSTER_CLI_GET_VOLUME, //过程号 NULL, this, gf_cli3_1_get_volume_cbk, //结果处理回调函数 (xdrproc_t)xdr_gf_cli_req); .. } int cli_cmd_submit(void *req, call_frame_t * frame, rpc_clnt_prog_t * prog, int procnum, struct iobref * iobref, xlator_t * this, fop_cbk_fn_t cbkfn, xdrproc_t xdrproc) { ... ret = cli_submit_request(req, frame, prog, procnum, NULL, this, cbkfn, xdrproc); ... } int cli_submit_request(void * req, call_frame_t * frame, rpc_clnt_prog_t * prog, int procnum, struct iobref * iobref, xlator_t * this, fop_cbk_fn_t cbkfn, xdrproc_t xdrproc) { ... ret = rpc_clnt_submit(global_rpc, prog, procnum, cbkfn, &iov, count, NULL, 0, iobref, frame, NULL, 0, NULL, 0, NULL) } int rpc_clnt_submit(struct rpc_clnt * rpc, rpc_clnt_prog_t * prog, int procnum, fop_cbk_fn_t cbkfn, struct iovec * proghdr, int proghdrcount, struct iovec * progpayload, int progpayloadcount, struct iobref * iobref, void * frame, struct iovec * rsphdr, int rsphdr_count, struct iovec * rsp_payload,int rsp_payload_count, struct iobref * rsp_iobref) { struct iobuf * request_iob = NULL; rpc_transport_req_t req; ... request_iob = rpc_clnt_record(rpc, frame, prog, procnum, proglen, &rpchdr, callid); req.msg.rpchdr = &rpchdr; req.msg.rpchdrcount = 1; req.msg.proghdr = proghdr; req.msg.proghdrcount = proghdrcount; req.msg.progpayload = progpayload; req.msg.progpayloadcount = progpayloadcount; req.msg.iobref = iobref; ... ret = rpc_transport_submit_request(rpc->conn.trans, &req); ... } int32_t rpc_transport_submit_request(rpc_transport_t * this, rpc_transport_req_t * req) { ret = this->ops->submit_request(this, req); } int32_t socket_submit_request(rpc_transport_t * this, rpc_transport_req_t * req) { struct ioq * entry = NULL; entry = __socket_ioq_new(this, &req->msg); ret = __socket_ioq_churn_entry(this, entry); ... } int __socket_ioq_churn_entry(rpc_transport_t *this, struct ioq * entry) { ret = __socket_writev(this, entry->pending_vector, entry->pending_count, &entry->pending_vector, &entry->pending_count); ... } int __socket_writev(rpc_transport_t * this, struct iovec * vector, int count, struct iovec **pending_vector, int *pendint_count) { ret = __socket_rwv(this, vector, count, pending_vector, pending_count, NULL, 1); ... } int __socket_rwv(rpc_transport_t *this, struct iovec *vector, int count, struct iovec **pending_vector, int * pending_count, size_t * bytes, int write) { int opcount = 0; struct iovec * opvector = NULL; opvector = vector; opcount = count; while(opcount) { if(write) { ret = wrtiev(sock, opvector, opcount); } ... } ... }
(2) 服务端处理RPC请求
服务端收到请求后,从socket_event_handler回调到rpc_transport_notify,再回调到rpcsvc_notify,最终调用rpcsvc_handle_rpc_call函数。在这个函数中,解析客户端RPC请求中包含的程序号,过程号以及相关参数等,然后根据这些程序号,过程号找到对应的处理函数。而这些处理函数就是先前通过rpcsvc_program_register函数注册的。对应的处理函数处理完成后调用相关函数答复客户端。
注: actor并不是一个真正的函数,仅标识不同RPC请求的处理函数.
(3) 客户端处理RPC的回复
客户端在发送请求时,会将请求的相关信息缓存下来,当收到服务器的回应后,再根据程序号、过程号找到对应的请求信息,然后调用相应的回调函数对请求结果进行处理。
========================================
总结:文本仅仅讲述了RPC的大概运行流程,其中还有很多细节以及相关点都未涉及到,例如状态机、锁机制、frame概念、xdr等等,后续需要进一步展开学习研究。