spdk-rpc命令原理-service端
目录
一、RPC命令概览
spdk作为云服务的底座软件,将自己的接口通过rpc(Remote Procedure Call)命令的方式暴漏出去,供上层类似于openstack的组件调用, 可以说rpc指令就相当于spdk对上层软件的api。具体优点个人总结如下:
- 隔离:将管控面(RPC管理)和数据面(I/O处理)分离,从而避免了管控面操作而阻塞高性能的I/O路径。
- 简单易用且跨平台: rpc命令通过rpc.py脚本暴漏,方便在shell、python、Go代码的调用,方便上层使用,并且也可以跨平台使用,比如spdk运行在linux上,而客户端运行在windows上。
- 安全性:默认使用域套接字方式,也支持tcp+tls加密通信。
- 易扩展: 开源spdk的rpc命令提供了一些基础能力如:实时添加、删除或修改存储组件(如 NVMe 设备、Bdev 模块、NVMe-oF 子系统等),无需重启 SPDK 服务,避免服务中断。除此之外,也可以根据自己项目的需求,创建符合自己项目的rpc命令。
这篇文章将通过gdb调试的方法介绍rpc命令的实现原理,初始化相关代码的实现细节,最后会新创建一个rpc命令来展示下rpc命令的具体步骤。调试过程会用到有spdk组件的环境,spdk代码为v24.05.x版本,环境搭建可以参考:spdk环境搭建。
二、RPC原理-service端流程
spdk的rpc命令是实现原理是通过JSON-RPC协议实现动态控制。 为了便于观察代码的具体实现,先通过一个rpc命令来展示下服务端的调用栈。在spdk代码中,通过rpc.py脚本调用bdev_null_create命令可以创建一个dev_null的模拟盘,调用命令前先用gdb挂住vhost进程,在bdev_null_create接口处打断点。命令如下:
python scripts/rpc.py bdev_null_create test_null 1000 512
断住之后的gdb的调用栈如下:
#0 bdev_null_create (bdev=0x7b7cf1150730, opts=0x7b7cf1150750) at bdev_null.c:245
#1 0x00005890fda82cda in rpc_bdev_null_create (request=0x508000004d20, params=0x512000029040) at bdev_null_rpc.c:104
#2 0x00005890fde43183 in jsonrpc_handler (request=0x508000004d20, method=0x512000029000, params=0x512000029040) at rpc.c:128
#3 0x00005890fde4883c in jsonrpc_server_handle_request (request=0x508000004d20, method=0x512000029000, params=0x512000029040) at jsonrpc_server_tcp.c:232
#4 0x00005890fde451d0 in parse_single_request (request=0x508000004d20, values=0x512000028fc0) at jsonrpc_server.c:126
#5 0x00005890fde45e80 in jsonrpc_parse_request (conn=0x7b7ceb99a830, json=0x7b7ceb99a848, size=159) at jsonrpc_server.c:263
#6 0x00005890fde48cc5 in jsonrpc_server_conn_recv (conn=0x7b7ceb99a830) at jsonrpc_server_tcp.c:294
#7 0x00005890fde499c3 in spdk_jsonrpc_server_poll (server=0x7b7ceb99a800) at jsonrpc_server_tcp.c:425
#8 0x00005890fde43887 in spdk_rpc_server_accept (server=0x511000015940) at rpc.c:258
#9 0x00005890fde179d3 in rpc_subsystem_poll_servers (arg=0x0) at rpc.c:35
#10 0x00005890fde2ae64 in thread_execute_timed_poller (thread=0x51900000a580, poller=0x514000004440, now=93959206139684) at thread.c:1020
#11 0x00005890fde2b50f in thread_poll (thread=0x51900000a580, max_msgs=0, now=93959206139684) at thread.c:1110
#12 0x00005890fde2bac2 in spdk_thread_poll (thread=0x51900000a580, max_msgs=0, now=93959206139684) at thread.c:1173
#13 0x00005890fdd6faf7 in _reactor_run (reactor=0x522000011400) at reactor.c:914
#14 0x00005890fdd6ff15 in reactor_run (arg=0x522000011400) at reactor.c:952
#15 0x00005890fdd70723 in spdk_reactors_start () at reactor.c:1068
#16 0x00005890fdd67a24 in spdk_app_start (opts_user=0x7b7cf1200020, start_fn=0x5890fda789c2 <vhost_started>, arg1=0x0) at app.c:980
#17 0x00005890fda78c09 in main (argc=3, argv=0x7fff5b3fde08) at vhost.c:77
简单梳理下可分成三层,再结合具体的spdk代码梳理流程如下:
----------------------------------------------------spdk rpc的poller为入口--↓
-->rpc_subsystem_poll_servers
-->spdk_rpc_server_accept
-->spdk_jsonrpc_server_poll------------------------------------------------------------json-rpc的调用栈--↓
-->spdk_jsonrpc_server_poll #遍历每个 json rpc_对象conn-->jsonrpc_server_accept #当有链接来时
-->accept() #无连接时返回-1,但错误码为EWOULDBLOCK或
EAGAIN,表示无异常
-->jsonrpc_server_conn_recv
|-->recv # 对应套接字recv具体的数据
|-->jsonrpc_parse_request # 解析数据
|-->spdk_json_parse
|-->conn->outstanding_queue++ #处理的队列深度+1
|-->spdk_json_parse #将传入json解析成数据
|-->parse_single_request
-->jsonrpc_server_handle_request
-->request->conn->server->handle_request ---> jsonrpc_handler------------------------------------------------------------调用到具体的方法--↓
-->jsonrpc_handler
-->_get_rpc_method #根据传入的字符串,从g_rpc_methods中找到对应的方法-->m = m->is_alias_of, #支持别名,传入别名会找到对应的方法
-->m->func ---> rpc_bdev_null_create
-->bdev_null_create
第一层:rpc_subsystem_poll_servers是在spdk_rpc_initialize接口中初始化rpc时创建的poller(持续关注,后面会写一篇关于poller原理和实现的文章),这里可以理解成一个定时器,每隔4ms就会调用一次rpc_subsystem_poll_servers接口。
第二层:json-rpc的具体的通信实现是在spdk_jsonrpc_server_poll中完成的。gdb+debug日志抓了多次之后,总结下具体流程如下:

服务端的jsonrpc_server_accept接口监听到套接字中有数据之后,会从server->free_conns链表头部取出一个 struct spdk_jsonrpc_server_conn 缓存,将接收到的fd存入conn->fd中。后续在jsonrpc_server_conn_send接口中会从conn->fd中接收数据,接收到的就是json-rpc数据,抓去了一个完整数据如下所示:
{
"jsonrpc": "2.0", //表示协议是json2.0
"method": "bdev_null_create", //方法是 bdev_null_create
"id": 1,
"params": { //该方法的参数
"name": "test_null",
"num_blocks": 2048000,
"block_size": 512
}
}
接收到数据之后,随后jsonrpc_parse_request中实现了对json字符串的解析,并且这里会封装成一个request,后续调用结果保存的request中。
第三层:根据传入的方法(字符串)找到先前注册到rpc模块中的接口rpc_bdev_null_create接口,完整rpc_bdev_null_create的调用。
注册方法如下所示:
SPDK_RPC_REGISTER("bdev_null_create", rpc_bdev_null_create, SPDK_RPC_RUNTIME)
#define SPDK_RPC_REGISTER(method, func, state_mask) \
static void __attribute__((constructor(1000))) rpc_register_##func(void) \
{ \
spdk_rpc_register_method(method, func, state_mask); \
}
void spdk_rpc_register_method(const char *method, spdk_rpc_method_handler func, uint32_t state_mask)
{
struct spdk_rpc_method *m;
m = _get_rpc_method_raw(method);
m = calloc(1, sizeof(struct spdk_rpc_method));
m->name = strdup(method);
m->func = func;
m->state_mask = state_mask;
SLIST_INSERT_HEAD(&g_rpc_methods, m, slist); //插入到链表g_rpc_methods中
}
SPDK_RPC_REGISTER宏展开,其中constructor修饰的意思是该接口在main函数之前自动执行接口rpc_register_rpc_bdev_null_create(void) 。spdk_rpc_register_method中将函数名称和对应的方法名称封装成一个spdk_rpc_method, 然后插入到全局链表g_rpc_methods中。 待后续rpc命令解析之后,根据解析出来的字符串执行对应的方法。
查询在_get_rpc_method中完成,调用在jsonrpc_handler中,具体实现如下:
static struct spdk_rpc_method *
_get_rpc_method(const struct spdk_json_val *method)
{
struct spdk_rpc_method *m;
SLIST_FOREACH(m, &g_rpc_methods, slist) {
if (spdk_json_strequal(method, m->name)) {
return m;
}
}
return NULL;
}
static void
jsonrpc_handler(struct spdk_jsonrpc_request *request,
const struct spdk_json_val *method,
const struct spdk_json_val *params)
{
struct spdk_rpc_method *m;
m = _get_rpc_method(method);
if (m->is_alias_of != NULL) {
m = m->is_alias_of;
}
m->func(request, params);
}
三、RPC原理-service端初始化
看下rpc服务端初始化代码,我们先用gdb拉起一个vhost进程( gdb vhost),在spdk_rpc_initialize 打个断点再运行,查看下调用栈:
#0 spdk_rpc_initialize (listen_addr=0x5555561ad8e0 "/var/tmp/spdk.sock", opts=0x7ffff4e57d20) at rpc.c:135
#1 0x00005555559e5142 in app_do_spdk_subsystem_init (rc=0, arg1=0x0) at app.c:428
#2 0x00005555559e739a in bootstrap_fn (arg1=0x0) at app.c:632
#3 0x0000555555aaea80 in msg_queue_run_batch (thread=0x51900000a580, max_msgs=8) at thread.c:854
#4 0x0000555555ab02ca in thread_poll (thread=0x51900000a580, max_msgs=0, now=27714167741290) at thread.c:1076
#5 0x0000555555ab0ac2 in spdk_thread_poll (thread=0x51900000a580, max_msgs=0, now=27714167741290) at thread.c:1173
#6 0x00005555559f4af7 in _reactor_run (reactor=0x517000007400) at reactor.c:914
#7 0x00005555559f4f15 in reactor_run (arg=0x517000007400) at reactor.c:952
#8 0x00005555559f5723 in spdk_reactors_start () at reactor.c:1068
#9 0x00005555559eca24 in spdk_app_start (opts_user=0x7ffff5000020, start_fn=0x5555556fd9c2 <vhost_started>, arg1=0x0) at app.c:980
#10 0x00005555556fdc09 in main (argc=1, argv=0x7fffffffe408) at vhost.c:77
rpc服务的初始化时跟随者rpc子系统初始化的,具体的初始化流程如下:
-->app_do_spdk_subsystem_init
-->spdk_rpc_initialize
|-->spdk_rpc_server_listen
| -->_spdk_rpc_listen
| |-->open(server->lock_path, ...); # 打开/var/tmp/spdk.sock
| |-->spdk_jsonrpc_server_listen
| | |-->初始化server->free_conns链表,max为64
| | |-->socket, setsockopt, bind, listen
|-->注册一个poller,间隔4ms调用 rpc_subsystem_poll_servers
初始化流程中分析,可以看到在rpc执行过程中用到的rpc_subsystem_poll_servers注册,以及server->free_conns链表的初始化流程等等。
篇幅原因先介绍到这里,字太多看着比较费劲儿。 下篇介绍下rpc的客户端实现。
更多推荐


所有评论(0)