目录

一、RPC命令概览

二、RPC原理-service端流程

三、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的客户端实现。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐