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

来源:互联网 发布:自学电脑编程 编辑:程序博客网 时间:2024/05/29 19:55

    远程进程调用(Remote Procedure Call,RPC)是构建分布式系统时所使用的一种常见组件。它使得客户端可以像进行本地调用一样进行远程的过程调用,即客户端可以忽略消息传递的细节,而专注于过程调用的效果。

    Portal RPC是Lustre 的RPC组件,它构建于LNET之上,提供客户端和服务器之间进行消息通信的接口。

2.1      消息通信连接的建立

    进行Portal RPC通信的是位于服务器和客户端的两个消息终端。服务器上的消息终端称为出口端(export),而客户端上的消息中断称为入口端(import)。

    入口端是通过类型为obd_import的对象描述的。这个对象由class_new_import()函数新建。MGC、MDC、OSC的启动函数mgc_setup()、mdc_setup()和osc_setup(),都会调用一个通用的函数client_obd_setup()。在这个函数里,将会调用class_new_import()函数为该客户端新建一个入口端。

    在客户端新建了入口端之后,为了完成与服务器的连接,会紧接着调用client_connect_import()函数向对应服务器发送一个请求消息。请求消息由类型为ptlrpc_request的对象描述,client_connect_import()函数小心地设置它的rq_import字段,这样当回复被接收到后,就可以知道该回复是属于哪个入口端了。

    与入口端相对的出口端使用类型为obd_export的对象来描述。在服务端,对来自每个客户端的连接,都有一个对应的出口对象。这个出口对象并不在事先创建好,而是在接收到客户端连接请求时新建。

    MGS、MDT、OST的在处理连接请求时都会调用通用的target_handle_connect()函数。在这个函数中,因为存在客户端的连接恢复等情况,因此需要进行复杂的处理。在此,我们只分析最简单的情形,即服务端首次接收到来自该客户端的连接请求。在这种情况下,该函数通过obd_connect()这个通用的封装函数,调用各服务定制的连接函数,如mgs_connect、mdt_obd_connect和filter_connect。这些函数又会调用通用的class_connect()函数,最终通过class_new_export()函数新建一个出口端。这样,在连接请求的处理过程中,该入口端对应的出口端就被创建完成了。随后,target_handle_connect()函数将会把回复消息的句柄通过lustre_msg_set_handle()设置为出口端的句柄。

    当服务端的这个连接回复消息被客户端接收到时,作为在client_connect_import()中被注册的处理函数,ptlrpc_connect_interpret()将被调用。这个函数使用lustre_msg_get_handle()函数从回复消息中取得服务端设置的句柄,作为远端句柄(remotehandle)放在obd_import对象的imp_remote_handle字段中。自此以后,客户端通过ptl_send_rpc()函数发送消息时,都用lustre_msg_set_handle()函数把消息的句柄设置为远端句柄的值。这样,在服务端接收到在请求消息后,请求处理函数ptlrpc_server_handle_req_in()可以从消息中获得这个句柄,并由此取得对应的出口端。

2.2      消息的发送与接收

    Portal RPC既支持同步的消息通信也支持异步的消息通信。在同步消息通信中,请求发送进程挂起直至消息的回复接受完成。在异步消息通信中,请求发送者不等待回复,而交由Portal RPC的守护进程处理回复。通信可以以单一消息或消息集合的方式进行。

    下表给出了可以用来发送消息的一些函数。

表 RPC发送函数列表

函数

描述

ptl_send_rpc()

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

ptlrpc_queue_wait()

同步的发送方法,只有在请求发送完,且接收到回复后才返回。

ptlrpcd_add_req()

完全异步的发送方法,请求在加入Portal RPC守护进程处理的队列中后,即返回。随后的处理由守护进程完成。

ptlrpc_set_wait()

同时发送多条消息的方法,只有在它得到集合中所有消息的回复后才会返回。集合里的消息是ptlrpc_set_add_req()函数事先放入的。

    在上述函数中,无论是同步发送还是异步发送函数,最终的发送操作都由ptl_send_rpc()函数调用LNET层提供的接口函数来完成。

    当消息被LNET层软件发送/接受完毕或超时时,就会在事件队列中新添一个事件。事件可以有很多不同的类型,其中一种经常会被用到的事件类型是NET_EVENT_UNLINK。该类事件可以发生在以下几种时机:

  •  请求/回复长时间未发送,发生超时。此时由ptlrpc_unregister_reply()函数、ptlrpc_unregister_bulk()函数、ptlrpc_abort_bulk()函数直接调用LNetMDUnlink()函数。
  • 发送/接受失败,即LNetPut/LNetGet返回非零值。
  • LNET路由创建失败或停止时。
  • PTLRPC服务停止时。此时由ptlrpc_unregister_service调用LNetMDUnlink()函数。

    LNetMDUnlink()函数首先调用lnet_build_unlink_event()函数在MD的事件队列中添加一个事件,然后调用lnet_enq_event_locked ()函数处理这个事件。至于这里MD的含义将在LNET一节讲到。

    lnet_enq_event_locked()处理事件的方式是调用Portal RPC注册的回调函数。注册回调函数的过程发生在ptlrpc_ni_init()函数里。它过LNetEQAlloc()函数将回调函数注册为ptlrpc_master_callback()。ptlrpc_master_callback会根据LNET事件的回调ID调用以下回调函数中的某一个。

表 PTLRPC回调函数列表

函数

描述

注册者

request_out_callback()

客户端发出请求回调函数

ptlrpc_prep_req_pool()

 

reply_in_callback()

客户端接回复回调函数

ptlrpc_prep_req_pool()

 

client_bulk_callback()

客户端的块传输已完成

ptlrpc_prep_bulk_imp()

 

request_in_callback()

服务端接收请求回调函数

ptlrpc_grow_req_bufs()

 

reply_out_callback()

服务端发出回复回调函数

ptlrpc_send_reply()

 

server_bulk_callback()

服务端块传输完成回调函数

ptlrpc_prep_bulk_exp()

 

    从客户端发送一个请求,到它接受到回复,就是整个RPC过程。在这个RPC过程中,客户端会产生两次回调,回调函数分别是request_out_callback()和reply_in_callback()。在请求发送完成时调用的回调函数是request_out_callback()函数。如果请求被成功发送,那么LENT层被激发的事件类型是LNET_EVENT_SEND。否则,如果请求发送失败,被激发的事件类型是LNET_EVENT_UNLINK。在两种情况下,request_out_callback()函数都会将ptlrpc_request类型的请求释放掉。但是只有在事件类型是LNET_EVENT_UNLINK的情况下,request_out_callback()才会唤醒发送进程,以返回出错,在发送成功的情况下,发送进程不会被唤醒。

    与此相比,reply_in_callback()回调函数则不管事件类型是LNET_EVENT_UNLINK还是LNET_EVENT_PUT都会唤醒接收进程。同样地,作为RPC最终完成时调用的回调函数,client_bulk_callback()也会不论如何都唤醒进程。

    消息的发送是Porltal RPC调用LNET的API发起的,而消息的接受流程则呈现相反的方向。LENT层在接收完一个消息之后,将调用lnet_finalize()函数。这个函数会调用lnet_enq_event_locked()函数在MD的事件队列中新建一个事件。注意到,lnet_enq_event_locked()函数的调用也发生在上面已经提到过的LNetMDUnlink()函数中。通过lnet_enq_event_locked()函数,LNET调用了Portal RPC注册的回调函数,也就将接受到的消息通知到了上层。因此,可以说回调函数是LNET将消息通知到Portal RPC的方法。同时,回调这种方式,作为软件层间的一种通知方式,被广泛应用在Lustre的各个地方。在以后的内容中,我们会更深刻地理解到这一点。

2.3      消息处理服务

    在Lustre中的服务器一般将自己的工作分为若干种服务。每个服务都用类型为ptlrpc_service的对象来描述。该对象的初始化是由ptlrpc_init_svc()函数完成的。ptlrpc_init_svc()函数将接受的req_portal和rep_portal参数分别被赋值到ptlrpc_service对象的srv_req_portal字段和srv_rep_portal字段中。它们是请求/回复能被正确发送到目标的关键。

    这两个字段分别对应了服务的请求portal和回复portal。Portal直接翻译出来是入口的意思,在这里的含义类似于IP地址端口。我们知道,TCP或UDP服务器会在众所周知的端口上等待请求,客户端尝试连接这些固定端口,以获得某种服务。在Portal RPC中,portal扮演着与IP端口类似的角色。

    在诸如mgs_setup()、mdt_start_ptlrpc_service()、ost_setup()、ldlm_setup()、等服务启动函数中,除了调用ptlrpc_init_svc()函数外,还要调用ptlrpc_start_threads()函数,为服务创建线程池。线程池中的所有线程都各用一个类型为ptlrpc_thread的对象来描述,它们共用同样的处理函数。

    我们需要注意到,一个服务启动函数可能会创建多种服务。每个服务有其独占的请求portal和的线程池。例如,OST创建了线程名前缀为ll_ost、ll_ost_creat和ll_ost_io的三个线程池,这些线程池分别使用了OST_REQUEST_PORTAL、OST_CREATE_PORTAL和OST_IO_PORTAL,不过它们使用了相同的回复portal:OST_IO_PORTAL。

    在客户端发送请求时,为了让请求能发送到正确的服务,它小心地填充ptlrpc_request对象的rq_request_portal,使其值等于对应的ptlrpc_service服务对象的srv_req_portal字段。

    服务端接收到客户端发起的RPC请求后,会根据请求消息的类型,调用对应的请求处理函数,并把处理结果以回复消息的方式发给客户端。这符合经典的RPC调用流程。而在服务通过ptlrpc_send_reply()函数发送回复消息时,也将从固定的回复portal发出。

    服务器之所以要创建多个不同的线程池,使用不同的portal,提供分立的服务,其主要原因是为了防止饥饿。假如使用统一线程池和portal,我们可以试想这样一种情况:众多客户端进行频繁大I/O,portal的被大量I/O请求消息占据,此时其他类型请求可能无法获得portal的及时发送和线程池的及时处理,从而出现大量请求处理超时。服务的分离避免了这一问题。同时,服务的分离也避免了在一些情况下可能出现的死锁。在死锁情况下,所有线程都在等待某些资源变成可用,虽然它们等待的资源可能不同,但结果是谁都无法处理新请求。

2.4      消息的格式

    Portal RPC的每个消息,不管是请求消息还是回复消息,都由一个由类型为ptlrpc_request对象描述。这个对象包含了关于该消息的所有信息。Portal RPC的每个消息对应着一个固定的消息格式。消息的格式由类型为req_format对象给出。每种消息格式不仅包括了请求消息的格式,而且包括了回复消息的格式。ptlrpc_request对象拥有关于类型为req_capsule的rq_pill字段。这个对象不仅有一个指向req_format对象的rc_fmt字段,还用rc_loc字段来标识该消息是请求消息还是回复消息。

    Portal RPC所有可能用到的消息格式都被定义在了全局数组req_formats中。数组中的每个元素的类型都是req_format。所有req_capsule对象的rc_fmt字段都指向这个数组中的某个元素。

    req_format对象描述的不是一种消息的格式,而是一对请求及其回复消息的格式。Portal RPC的每个请求消息都有固定的格式,而对于每种格式的请求消息,其回复消息的格式是固定的。req_format对象就描述了一对消息的格式,它的rf_fields字段是一个数组,其中每个元素就对应了一种消息的格式,每个消息格式由一个类型为req_msg_field的对象数组来描述。

    请求消息及其回复消息都被分成多个固定的域(field),每个域都对应了一个req_msg_field对象。每种消息格式各自对应着某个类型为req_msg_field的对象数组,这个数组中的每个元素就对应着消息的一个域。

    综合上面的知识,我们可以知道,对于每个由ptlrpc_request对象描述的消息,为了获得它的格式,我们可以首先从ptlrpc_request对象的rq_pill字段中获得req_capsule对象,然后从req_capsule对象的rc_loc字段获知该消息是请求消息还是回复消息,随后从req_format的rf_fields数组中选取对应的元素,最后从这个元素获得对各个域的描述。

    为了使得不同字节序的节点间正常通信,消息的传送是以“线上格式”进行的。在每个消息终端,消息都会被打包成线上格式,或者从线上格式解包。下图以LDLM_CANCEL消息为例,给出了上面所讲到的各个对象之间的关系。


图 消息格式中各个对象的关系

1.5      最简单消息:ping消息

ping消息是Portal RPC最简单和最基本的消息。它的消息格式是RQF_OBD_PING。这个消息格式的请求和回复都是ping消息。Ping消息只包含一个域,RMF_PTLRPC_BODY。实质上,Portal RPC发出的所有种类的消息都以RMF_PTLRPC_BODY作为第一个域。

ping消息由客户端首先发出,服务端在接受到这种消息之后,也立即回复一个ping消息。消息包的发起者是一个被称为pinger的内核线程。pinger的线程名被设定为ll_ping。它维护了一个包含所有入口端的链表pinger_imports。客户端在尝试与服务端进行网络连接时都将把入口端加入这个链表,这一事件发生在client_connect_import()函数调用ptlrpc_pinger_add_import()时。

pinger每个一段时间就轮询一次这个链表,但不会在每次轮询都向每个入口端发送一个消息。假如这样做,在某个入口端非常忙碌的时候,将会给这个入口端带来不必要的额外压力。pinger只有在发现入口端未能以足够频率发送请求的时候,才在该入口端发送额外的ping消息。

一个已连接好但长期保持空闲状态的入口端,需要pinger的这种机制来维持连接的畅通。否则,可以想象,服务端将无法辨别客户端是否已离线,因此为了确保资源不被离线客户端永久占用,服务器能做的只有将之驱逐。在服务端上,负责这项任务的是一个名字为ll_evictor的线程。

我们应该注意到,对于那些未连接好的入口端,pinger发送没有必要发送无用的ping消息,而是直接发送RQF_MDS_CONNECT格式的消息。

综上所述,pinger有两种作用,一方面是客户端用来尝试与服务端网络连通的方法,一方面是客户端确保在长期空闲时不被服务端驱逐的方法。

1.6      正在研究的课题:网络请求调度优化

网络请求调度(Network Request Scheduler,NRS)优化是一个研究在服务端对接收到的RPC进行重新调度的课题。这个课题早在2008年就已经在Lustre社区被提出来的,直到现在(2012年)仍然不断地被研究。

网络请求调度优化基于一个简单的事实,就是在服务器端上,由于请求在接收到之后会在队列中停留一段时间,因而呈现请求的接受和处理的两阶段现象。由于这一现象的存在,服务器可以重新调度这些请求,以达到以下目的:

l 通过请求的重排序,向客户端提供一致的性能,避免饥饿的出现。

l 通过请求的重排序,优化负载模式,使得后端文件系统更容易优化性能。

l 平衡网络流量,提高吞吐率。

l 控制处理资源和网络资源的分配,提供文件系统层的复制质量控制。

为了达到这些目标,可以实现以下策略:

l FIFO。这是现有的服务方式,当时需要包装为一种网络请求调度策略。

l 基于对象的轮询(Object-Based Round Robin,OBRR),即根据文件的便宜量,按照属于某个数据对象的方式将RPC组合成一组。这种策略可以通过减少磁盘寻道而提高吞吐率。

l  基于客户端的轮询(Client-Based Round Robin,CBRR),即根据请求所属的客户端,将RPC组合成一组。这种策略可以平衡网络流量。

l 基于进程ID的轮询(PID-BasedRound Robin,PBRR),即根据发起I/O的应用的NID::PID,将RPC组合成一组。NID是Lustre标识节点的方法。与基于客户端的轮询策略类似,这种策略可以平衡网络流量。此外,该策略还可以利用到客户端访问的空间局部性。

l 区分客户端或用户优先次序的策略。这种策略可以通过优先处理众多负载中相对重要的那一部分,而提供一种服务质量控制方法。在这种策略中,可以通过采用不同的方法计算请求的重要性,从而实现不同的目标。

1.7      总结

Portal RPC是Lustre中极为重要的一个组成部分。它一方面主要是Lustre这个集群文件系统的通信部件,一方面也可以作为各层之间异步传递消息的方式(参看ptlrpc_prep_fakereq()函数)。

Portal RPC的性能对Lustre的性能有着非常大的影响。因此,Lustre社区一方面通过研究网络请求调度,以尝试优化I/O性能,一方面通过把ptlrpcd的单线程、单队列的服务模式改成多线程、多队列的服务模式,以减少对元数据性能的影响。对于后者,可以通过对比Lustre-1.8和Lustre-2.2的相关代码可以发现这些优化的痕迹。

本文尽量全面地介绍了Portal RPC的设计原理和实现细节。但是Portal RPC是如此复杂的一个软件层,本文远远没有设计到它的全部内容,其中包括网络重连、请求重发等异常处理。这将在以后的内容中加以介绍。


本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article

原创粉丝点击