深入理解Lustre文件系统-第9篇 Portal RPC

本文详细介绍了PortalRPC的基本机制,包括客户端和服务端接口的使用方法、块数据传输的过程及错误恢复机制。并以LDLM发送机制为例,展示了如何利用PortalRPC API进行RPC通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Portal RPC为如下内容提供了基础机制:

  • 通过输入口发送请求,接受请求
  • 通过输出口接收和处理请求,发送请求
  • 执行块数据传输
  • 错误恢复

9.1   客户端接口

我们将首先探讨Portal RPC的接口,而不深入到实现细节中。我们将用LDLM的发送机制作为例子。对这个实例,LDLM向客户端发送一个阻塞ASTRPC(ldlm_server_blocking_ast),该客户端是一个给定的锁的占有者和管理者。这个例子帮助我们更好地理解客户端怎样使用Portal RPC API。

首先,我们下面给出的那样准备大小。

structldlm_request *req;

__u32 size[] = {[MSG_PTLRPC_BODY_OFF] = sizeof(struct ptlrpc_body),

[DLM_LOCKREQ_OFF] = sizeof (*body) };

请求可以被看成一连串的记录,第一个记录的偏移量是0,而第二个记录的偏移量是2,如此继续。一旦确定了大小,我们可以调用ptlrpc_prep_req()。这个函数的原型是:

structptlrpc_request *

ptlrpc_prep_req(struct obd_import *imp, __u32 version, int opcode,

int count, __u32 *lengths, char **bufs)

要进行RPC通信,客户端需要一个输入口,而它在连接阶段就创建了。*imp指针就指向这个输入口,而version则指出Portal RPC内部使用的用来打包数据的版本。这里的打包是传输线上的格式(on-the-wireformat),定义了在网络报文中,缓冲实际上处于哪个位置。为了区分它们的布局(layout)请求,每个子系统定义了版本号——例如,MDC和MDS定义了它们的版本,MGC和MGS则定义了另外一个。

opcode确定了这个请求是对于什么的请求。每个子系统定义了一些操作(更多信息,参看lustre_idl.h)。count就是这个请求所需的缓冲数,而*length是一个数组,其中每个元素确定了对应请求缓冲的大小。最后一个参数signalsPortalRPC to accept (copy the data over) and process the incoming packet as is(??)。对于我们的例子,这个函数按如下形式被调用:

req =ptlrpc_prep_req(lock->l_export->exp_imp_reverse,

      LUSTRE_DLM_VERSION, LDLM_BL_CALLBACK, 2,size, NULL);

这种调用表明,请求了两个缓冲,而每个缓冲的大小由参数列表中的size表示。

除了内部管理(housekeeping)之外,上述调用还分配了请求缓冲,并将之存储在req->rq_reqmsg中。感兴趣的地址能够通过给出记录偏移量来得到:

body =lustre_msg_buf(req->rq_reqmsg, DLM_LOCKREQ_OFF, sizeof(*body));

在服务器端,我们能够看到有着类似输入参数的相同的帮助方法,它被用来取得感兴趣的字段。一旦得到了缓冲结构体,就可以进一步地填入请求所需的字段。在所有这些完成之后,有几种方法来发送请求,如下所述:

ptl_send_rpc() 发送RPC的一种简单形式,它不等待回复,在失败发生时也不重发。这不是一个发送RPC的优选方法。

ptlrpcd_add_req()一个完全异步的RPC发送方法,由ptl-rpcd守护进程处理。

ptlrpc_set_wait()一个同步地发送多条消息的方法,只有在它得到所有回复时才会返回。首先,使用ptlrpc_set_add_req()来将请求放入一个未初始化集合里(pre-initialized set),该集合里面包含了一个或者多个需要一齐发送的请求。

ptlrpc_queue_wait()可能是最常用的发送RPC的方法。它是同步的,只有在RPC的请求发送完,且接收到回复后才返回。

在调用RPC请求之后的最后一步是,通过调用ptlrpc_req_finished(req)来释放资源引用。

9.2   服务器端接口

服务器端使用Portal RPC的方法和客户端完全不同。首先,它使用如下函数初始化服务:

structptlrpc_service * ptlrpc_init_svc (

      int nbufs, /* num of buffers to allocate*/

      int bufsize, /* size of above requestedbuffer */

      int max_req_size, /* max request sizeserver will accept */

      int max_reply_size, /* max reply sizeserver will send */

      int req_portal, /* req service port inlnet */

      int rep_portal, /* rep service port inlnet */

      int watchdog_factor, /* wait time forhandler to finish */

      svc_handler_t handler, /* service handlerfunction */

      char *name, /* service name */

      cfs_proc_dir_entry_t *proc_entry, /* forprocfs */

      svcreq_printfn_t svcreq_printfn, /* forprocfs */

      int min_threads, /* min # of threads tostart */

      int max_threads, /* max # of threads tostart */

      char *threadname /* thread name prefix */

)

一旦调用返回,请求就可以进入,就将调用注册好的处理函数。通常,服务器将手头的工作分为几类。对每类,它创建一个不同的线程池。这些线程共享同样的处理函数。使用不同的池的原因是为了防止饿死。在一些情况下,多个线程池也防止了死锁,在死锁情况下,为处理一个新的RPC,所有线程都在等待某个资源变成可用。

9.3   块传输

客户端首先发送一个块RPC请求。让我们假设这是一个写请求。它里面包含了对传输什么的描述。现在服务器处理请求,分配空间,然后控制数据传输。服务端的下一个RPC将把数据传输到先前分配的空间里。osc_brw_pre_request()里所做的就是上述过程的一个实例。让我们看一下这个过程:

1. 块传输的初始化从上述的准备工作开始着手。然而,因为我们是在从一个已分配好的池里请求,所以准备请求有点不同,如果请求本身可能和内存不足的情况相关,那么这就是导致内存不足的那种情形(?)。

req =ptlrpc_pre_req_pool(cli->cl_import, LUSTRE_OST_VERSION,

opc, 4, size, null, pool)

这里的opc可能是,比如说,OST_WRITE。

2. 接着,我们制定服务入口。在接入口结构中,有一个入口,作为默认情况,请求将由这个入口处理。但是,在这种情况下,让我们假设请求将由一个特定的入口处理:

req->rq_request_portal= OST_IO_PORTAL;

3. 然后,我们需要准备块请求。我们传入指向请求的指针、页数目、类型和目的入口。返回的是对这个请求的块描述符。注意,块请求将传入另外一个的入口:

structptlrpc_bulk_desc desc = ptlrpc_prep_bulk_imp(req, page_count,

BULK_GET_SOURCE, OST_BULK_PORTAL);

4. 对于每个需要传输的页,我们调用ptlrpc_prep_bulk_page(),将该页及时地加到块描述符中。这是请求里面的一个标志,它表明这是一个块请求,而我们需要检查这个描述符,以取得页的布局信息。

structptlrpc_request {

      ...

      struct ptlrpc_bulk_desc *rq_bulk;

      ...

}

在服务器端,整体的准备结构是类似的,但是现在准备的是输出口,而不是输入口。在ost处理函数中的ost_brw_read()里可以看到一个例子:

desc =ptlrpc_prep_bulk_exp(req, npages, BULK_PUT_SOURCE, OST_BULK_PORTAL);

服务器端也需要为每个块页做准备。然后,服务器端就可以开始传输:

rc =ptlrpc_start_bulk_transfer(desc);

在此时,从客户端发来的第一个RPC请求已经由服务器处理了,而服务器已为块数据传输做好了准备。现在客户端可以像我们在此节开始时提及的那样开始块RPC传输。

NRS优化

另外一个需要指出的是,在服务器端,我们接到了大量的描述符,这些描述符描述了待读或待写的页布局。如果在相同的方向上,存在邻接的读或者写,就提供了一个优化的机会。如果确实存在,那么它们可以被聚合和同时处理。这就是网络请求调度(Network Request Scheduler, NRS)的课题。这也显示出两阶段块传输的重要性,它使我们有了一个想法:在输入输出数据时,并不立马取得数据,而是重新组织,以获得更好的性能。两阶段操作的另外一个原因是,随着服务初始化的增加,就会分配一定量的缓冲空间。当客户端请求到达时,在进一步处理之前,可以将它们先缓存在这个空间里,因为为了容纳潜在的块传输而预先分配一大块空间并不是优选的方法。另外,不用大的数据块覆盖服务器的缓冲空间是非常重要的,在这种情形下,两阶段操作也有作用。

9.4   错误恢复:客户端观点

大多数的恢复机制都在Portal RPC层实现。我们以一个从高层传递下来的portal RPC请求作为开始。在Portal RPC内部,输入口维护了两个链表,它们对我们的探讨非常重要。它们就是sending和replay链表。输入口同时维护了imp->states和imp->flags。可能的状态是:full、connecting、disconnecting和 recovery,可能的标志是invalid、active和inactive。

在检查输入口的健康状态后,发送请求将继续。这些步骤序列如下:

1. 发送RPC请求,然后将它存入在正在发送链表,并在客户端开始obd计时器。

2. 如果服务器在计时耗尽之前回复,而请求又是可重复的,那么将之从发送链表删除,并加入重复链表。如果请求是不可重复的,那么在接收到回复时,将之从发送链表中删除。

从服务器端发来的回复并不一定意味着它已经将数据写入了磁盘(假设请求改变了盘上数据)。所以,我们必须等待从服务器端发出的事务提交(一个事务号),它意味着现在变更已经安全地提交给磁盘了。这种上一次服务器提交的事物号通常附加在(piggbacked)在各个服务器回复上。

通常,从MDC到MDS的请求是可重复的,但是OSC到OST的请求则不是,而这仅仅在异步日志更新未使能时才是对的。这里有两个原因:

  • 首先。从OSC到OST的数据请求(读或写)可能非常大,为供重复,将他们保存在内存里,这对于内存是一个非常大的负担。
  • OST只使用直接IO(最少现在如此)。带有事务号的回复本身就是对提交已完成的足够保障。

3. 如果计时器超时,客户端将这个输入口状态从full转变为disconnect。现在pinger插入一脚,如果服务器对pinger有响应,那么客户端将试着重连(以reconnect标志连接)。

4. 如果重连成功,那么我们开始恢复过程。我们现在将状态标记为recovery,并首先开始发送重复链表中的其请求,然后是发送正在发送链表中的请求。

关于pinger的关键之处是,如果请求以足够的频率发送,那就不需要用pinger了。只有在客户端有一个较长的空闲时期,pinger才用来和服务器保持活跃连接,从而防止由于无活动而被驱逐。在另一方面,如果客户端由于任何原因而离线,服务器将不会被客户端ping,服务器可能也会驱逐这个客户端。

本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article
### 三级标题:RPC 基本概念 远程过程调用(Remote Procedure Call,简称 RPC)是一种在分布式系统中广泛使用的通信机制,允许一个程序像调用本地函数一样调用另一个网络节点上的程序[^5]。RPC 的核心思想是隐藏底层的网络通信细节,使得开发者可以专注于业务逻辑的设计与实现。 在 RPC 模型中,通常包含客户端(Client)、服务端(Server)、以及中间的网络通信组件。客户端通过调用本地代理(Stub)来发起请求,该请求被序列化并通过网络传输到服务端。服务端接收到请求后进行反序列化,并执行对应的服务逻辑,最终将结果返回给客户端[^2]。 ### 三级标题:RPC 使用场景 RPC 主要应用于以下几种典型场景: 1. **微服务架构**:在现代的微服务系统中,各个服务之间需要频繁地进行通信。RPC 提供了一种高效、透明的跨服务调用方式,能够显著提升系统的整体性能和可维护性。 2. **高性能计算**:当系统需要处理大量的并发请求时,使用高效的 RPC 协议(如 gRPC 或 Dubbo)可以有效降低网络延迟并提高吞吐量。 3. **异构系统集成**:许多 RPC 框架支持多种编程语言和平台,这使得不同技术栈构建的服务可以在统一的接口下协同工作。 4. **分布式事务管理**:在复杂的分布式系统中,多个服务可能需要共同完成一个事务。RPC 可以作为这些服务之间的协调工具,确保数据的一致性和完整性。 ### 三级标题:RPC 框架实现 #### 1. 核心组件 一个完整的 RPC 框架通常包括以下几个关键组件: - **协议层**:定义了消息的格式和交互规则。常见的协议有 HTTP/REST、gRPC 和 Dubbo 协议等。 - **序列化机制**:负责将数据结构转换为字节流以便在网络上传输,常用的序列化方式包括 JSON、Protocol Buffers、Thrift 和 Hessian 等。 - **网络通信**:基于 TCP/IP 或 HTTP/2 实现可靠的数据传输。 - **服务注册与发现**:用于动态管理和查找可用的服务实例。 - **负载均衡与容错机制**:确保请求能够在多个服务实例之间合理分配,并在出现故障时提供自动恢复能力。 #### 2. 实现示例 以下是一个简化版的 RPC 调用流程的 Python 示例代码: ```python import socket import json # 定义服务端 class RpcServer: def __init__(self, host='localhost', port=8080): self.host = host self.port = port self.services = {} def register_service(self, name, func): self.services[name] = func def start(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((self.host, self.port)) s.listen() print("Server is listening...") while True: conn, addr = s.accept() with conn: data = conn.recv(1024) if not data: break request = json.loads(data.decode()) service_name = request['service'] args = request['args'] result = self.services[service_name](*args) conn.sendall(json.dumps(result).encode()) # 定义客户端 class RpcClient: def __init__(self, host='localhost', port=8080): self.host = host self.port = port def call(self, service_name, *args): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((self.host, self.port)) request = json.dumps({'service': service_name, 'args': args}) s.sendall(request.encode()) data = s.recv(1024) return json.loads(data.decode()) # 示例服务 def add(a, b): return a + b # 启动服务器并注册服务 server = RpcServer() server.register_service('add', add) # server.start() # 注释掉以避免阻塞主线程 # 客户端调用 client = RpcClient() result = client.call('add', 2, 3) print(f"Result: {result}") ``` #### 3. 高级特性与优化 为了提升 RPC 框架的性能和可靠性,通常会引入以下高级功能: - **异步调用**:支持非阻塞式的调用方式,提升系统的响应速度和资源利用率。 - **缓存机制**:对于重复的请求,可以通过缓存减少对后端服务的压力。 - **安全机制**:采用 SSL/TLS 加密通信,防止数据被窃取或篡改。 - **监控与日志**:记录详细的调用日志,便于故障排查和性能分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值