|
3.3 底层通信库$ R: ~( g% o% ]+ r& ?
网络通信模块是分布式系统中最底层的模块, 它直接支撑了上层分布式环境下复杂的进程间通信( Inter-Process/ J! C) l O* l$ ~+ R
Communication, IPC) 逻辑, 是所有分布式系统的基础。 远程过程调用( Remote Procedure Call, RPC) 是一种常用的分布式网络
- F5 `$ o4 A' E& ]- x6 O通信协议, 它允许运行于一台计算机的程序调用另一台计算机的子程序, 同时将网络的通信细节隐藏起来, 使得用户无须额外地% z" ^! o8 z0 o* z3 t! R3 U$ L
为这个交互作用编程。 由于RPC大大简化了分布式程序开发, 因此备受欢迎。
, C6 C4 Q3 y& z$ A8 i作为一个分布式系统, Hadoop实现了自己的RPC通信协议, 它是上层多个分布式子系统( 如MapReduce、 YARN、 HDFS等)% W* A. h, Y0 A" J) b6 ]: C
公用的网络通信模块。 本节首先从框架设计及实现等方面介绍Hadoop RPC, 接着介绍RPC框架在Hadoop YARN中的应用。; R U5 E+ B: F7 F/ L
3.3.1 RPC通信模型
. Q" u7 {: J! l' f" ~, `RPC是一种通过网络从远程计算机上请求服务, 但不需要了解底层网络技术的协议。 RPC 协议假定某些传输协议( 如TCP; E9 V2 G m* E7 Y0 f
或UDP等) 已经存在, 并通过这些传输协议为通信程序之间传递访问请求或者应答信息。 在OSI 网络通信模型中, RPC 跨越了传; r$ p0 @4 I9 L% J: @
输层和应用层。 RPC 使得开发分布式应用程 序更加容易 [6] 。' X1 K0 @" W8 l. s0 M6 k0 X A
RPC 通常采用客户机/服务器模型。 请求程序是一个客户机, 而服务提供程序则是一个服务器。 一个典型的RPC框架如图3-10 J8 N6 _) P/ P! W
所示, 主要包括以下几个部分:* ~# o3 f: i) P q; @( B$ C4 F5 I
❑通信模块 。 两个相互协作的通信模块实现请求-应答协议, 它们在客户和服务器之间传递请求和应答消息, 一般不会对数
6 w+ p8 \! r: X _( C& d/ m据包进行任何处理。 请求–应答协议的实现方式有同步方式和异步方式两种。
8 M2 Q' e( l8 X; l如图3-1所示, 同步模式下客户端程序一直阻塞到服务器端发送的应答请求到达本地; 而异步模式不同, 客户端将请求发送
. F9 @# _( R+ e% L0 t到服务器端后, 不必等待应答返回, 可以做其他事情, 待服务器端处理完请求后, 主动通知客户端。 在高并发应用场景中, 一般# u5 y: @7 g' F! E$ _5 G
采用异步模式以降低访问延迟和提高带宽利用率。
: T8 d) B8 k, E3 U) V* w K9 Y图3-1 同步模式与异步模式对比
, ]% {* w5 @2 J. B& }❑Stub程序 。 客户端和服务器端均包含Stub程序, 可将之看做代理程序。 它使得远程函数调用表现得跟本地调用一样, 对用2 S7 @% f* f- k& m3 l; K6 Q
户程序完全透明。 在客户端, 它表现得就像一个本地程序, 但不直接执行本地调用, 而是将请求信息通过网络模块发送给服务器; H# s) ~0 H; h; n3 U- M
端。 此外, 当服务器发送应答后, 它会解码对应结果。 在服务器端, Stub程序依次进行解码请求消息中的参数、 调用相应的服务
4 g& t: ?: e( e+ n过程和编码应答结果的返回值等处理。 ]8 P- q/ b M0 M$ F+ h
❑调度程序 。 调度程序接收来自通信模块的请求消息, 并根据其中的标识选择一个Stub程序进行处理。 通常客户端并发请求6 L$ ^" v) @% s" a8 U
量比较大时, 会采用线程池提高处理效率。
% r( t/ ?1 ?( B" @❑客户程序/服务过程 。 请求的发出者和请求的处理者。 如果是单机环境, 客户程序可直接通过函数调用访问服务过程, 但* t/ M/ `1 r& \% F# f- i4 K, V/ W
在分布式环境下, 需要考虑网络通信, 这不得增加通信模块和Stub程序( 保证函数调用的透明性) 。
8 I: H3 b/ P* v4 o通常而言, 一个RPC请求从发送到获取处理结果, 所经历的步骤( 见图3-2) 下所示。
. ~! x+ w/ E9 a5 ^2 Q2 C( e5 B/ \1) 客户程序以本地方式调用系统产生的Stub程序;
! w, w- F6 V0 r# Y+ r* Z2) 该Stub程序将函数调用信息按照网络通信模块的要求封装成消息包, 并交给通信模块发送到远程服务器端。
* c. R- g2 r7 T( o1 h- `3) 远程服务器端接收此消息后, 将此消息发送给相应的Stub程序;
7 _+ I# W1 w |/ `& F% H4 C9 |4) Stub程序拆封消息, 形成被调过程要求的形式, 并调用对应函数;# X# Z8 s& M* w- f6 X/ E
5) 被调用函数按照所获参数执行, 并将结果返回给Stub程序;( ~9 d6 |3 v( n" Q) i9 U
6) Stub程序将此结果封装成消息, 通过网络通信模块逐级地传送给客户程序。1 U$ z% z8 o% O+ j$ j: r
图3-2 RPC通用架构( }( n+ B- i/ N; q# H) E0 G
3.3.2 Hadoop RPC的特点概述& Z, ~$ A: z. K. ]3 W
RPC实际上是分布式计算中C/S( Client/Server) 模型的一个应用实例, 对于Hadoop RPC而言, 它具有以下几个特点。: k! U- R L; E! T
❑透明性 。 这是所有RPC框架最根本的特点, 即当用户在一台计算机的程序调用另外一台计算机上的子程序时, 用户自身
+ n) M2 ?3 q i+ G+ i不应感觉到其间涉及跨机器间的通信, 而是感觉像是在执行一个本地调用。
2 e) `1 t B0 o6 y( p( s& [❑高性能 。 Hadoop各个系统( 如HDFS、 YARN、 MapReduce等) 均采用了Master/Slave结构, 其中, Master实际上是一个RPC
R% s; |& O1 R7 O \Server, 它负责处理集群中所有Slave发送的服务请求, 为了保证Master的并发处理能力, RPC Server应是一个高性能服务器, 能够
* G, H$ h- V1 u' u% a. f高效地处理来自多个Client的并发RPC请求。
4 R3 n8 r, w2 T8 n- {% q6 h❑可控性 。 JDK中已经自带了一个RPC框架—RMI( Remote Method Invocation, 远程方法调用) , 之所以不直接使用该框
8 U4 ^. b& L3 x3 d' c架, 主要是考虑到RPC是Hadoop最底层最核心的模块之一, 保证其轻量级、 高性能和可控性显得尤为重要, 而RMI重量级过大且
# I3 T. c* }; v* @( x- v$ x用户可控之处太少( 如网络连接、 超时和缓冲等均 难以定制或者修改) [7] 。% `7 y6 ^/ H5 H
3.3.3 RPC总体架构
* U0 u1 m3 ?5 z& z6 u+ R同其他RPC框架一样, Hadoop RPC主要分为四个部分, 分别是序列化层、 函数调用层、 网络传输层和服务器端处理框架,
9 \+ n4 s5 J) C+ p1 w/ E! v. Z具体实现机制如下:
" }$ o! G3 P9 a7 W❑序列化层 。 序列化主要作用是将结构化对象转为字节流以便于通过网络进行传输或写入持久存储, 在RPC框架中, 它主2 P3 l4 h. z! M& l! z5 G4 g
要用于将用户请求中的参数或者应答转化成字节流以便跨机器传输。 前面介绍的Protocol Buffers和Apache Avro均可用在序列化9 Y% v7 x3 K/ a! x. v
层, Hadoop本身也提供了一套序列化框架, 一个类只要实现Writable接口即可支持对象序列化与反序列化。# O' i, S, `% y. x2 H E
❑函数调用层 。 函数调用层主要功能是定位要调用的函数并执行该函数, Hadoop RPC采用了Java反射机制与动态代理实现
1 m3 e( r0 @# A7 Z q9 W了函数调用。& r+ P" ]6 m5 s7 S* t; B
❑网络传输层 。 网络传输层描述了Client与Server之间消息传输的方式, Hadoop RPC 采用了基于TCP/IP 的Socket 机制。$ ^2 f) g, U2 H' e" m. ?8 E
❑服务器端处理框架 。 服务器端处理框架可被抽象为网络I/O模型, 它描述了客户端与服务器端间信息交互方式, 它的设计
- G& `# _5 Q b( U" y x0 g# l/ y直接决定着服务器端的并发处理能力, 常见的网络I/O模型有阻塞式I/O、 非阻塞式I/O、 事件驱动I/O等, 而Hadoop RPC采用了基. f- h# a9 o! o$ W+ z9 y4 R" h
于Reactor设计模式的事件驱动I/O模型。' M' l& Y% ~/ T! l
Hadoop RPC总体架构如图3-3所示, 自下而上可分为两层, 第一层是一个基于Java NIO ( New I/O) 实现的客户机–服务器
9 y2 z$ U4 |1 D( C/S) 通信模型。 其中, 客户端将用户的调用方法及其参数封装成请求包后发送到服务器端。 服务器端收到请求包后, 经解( B/ V0 z7 a4 G2 W: k1 n
包、 调用函数、 打包结果等一系列操作后, 将结果返回给客户端。 为了增强Sever端的扩展性和并发处理能力, Hadoop RPC采用
' C) m) k5 _; Z了基于事件驱动的Reactor设计模式, 在具体实现时, 用到了JDK提供的各种功能包, 主要包括java.nio( NIO) 、
# [0 Y- n' `' j0 Cjava.lang.reflect( 反射机制和动态代理) 、 java.net( 网络编程库) 等。 第二层是供更上层程序直接调用的RPC接口, 这些接口底层, M4 J9 d* r! A* S) w' M) N' [. F6 M+ F
即为C/S通信模型。
- D) l6 M; h: m8 L8 n图3-3 Hadoop RPC总体架构4 F+ ~5 p+ E; N- c+ e
3.3.4 Hadoop RPC使用方法
. h2 O) T6 ~+ Q" c/ P7 ?4 |Hadoop RPC对外主要提供了两种接口( 见类org.apache.hadoop.ipc.RPC) , 分别是:" Q7 ]" Q, f' z/ s4 D
❑public static <T>ProtocolProxy <T>getProxy/waitForProxy(…): 构造一个客户端代理对象( 该对象实现了某个协议) , 用于向7 f3 n+ d7 t6 g
服务器发送RPC请求。) P$ L# |& K! x! ^% d. {/ @7 Z2 {& L
❑public static Server RPC.Builder (Configuration).build(): 为某个协议( 实际上是Java接口) 实例构造一个服务器对象, 用于处理
8 e6 X$ }" S1 ]/ o7 O5 w; @0 U客户端发送的请求。
, M/ n1 g6 k* M; R通常而言, 使用Hadoop RPC可分为以下4个步骤。
6 J7 v" J; l8 I! {. U; w1.定义RPC协议' `* `* [$ Z2 m0 e
RPC协议是客户端和服务器端之间的通信接口, 它定义了服务器端对外提供的服务接口。 如下所示, 我们定义一个
: B* R+ [2 |' W- V) GClientProtocol通信接口, 声明了echo()和add()两个方法。 需要注意的是, Hadoop中所有自定义RPC接口都需要继承
0 U7 h: R- ]% w! w2 xVersionedProtocol接口, 它描述了协议的版本信息。* ?9 H( l, r6 Y7 Z, E2 \
interface ClientProtocol extends org.apache.hadoop.ipc.VersionedProtocol {
+ A4 e1 `( K: c) E2 t//版本号, 默认情况下, 不同版本号的RPC Client和Server之间不能相互通信
) g9 [. x: Y1 v! J( \3 _public static final long versionID = 1L;
4 J3 I7 i: `. _. t' J! x* sString echo(String value) throws IOException;4 N" \# h0 U: B0 Z$ j
int add(int v1, int v2) throws IOException;( X: P6 i+ @" z# K. U9 h
}' f' Z5 d1 s* B1 l. [3 G
2.实现RPC协议
# q: ]/ |, g/ AHadoop RPC协议通常是一个Java接口, 用户需要实现该接口。 对ClientProtocol接口进行简单的实现如下所示:
/ ]0 N$ P y7 U, upublic static class ClientProtocolImpl implements ClientProtocol {) d7 J! [3 K* S
//重载的方法, 用于获取自定义的协议版本号,
4 Y+ X1 O4 ^/ r, q# qpublic long getProtocolVersion(String protocol, long clientVersion) {
; A( M3 N# k9 |9 \' Y- }return ClientProtocol.versionID;4 X: A+ N: v2 y% v P& P7 O
}* ^9 Q! s9 q. \; r e+ L" H/ `
//重载的方法, 用于获取协议签名5 I. [( C, j3 g- H5 p( E
public ProtocolSignature getProtocolSignature(String protocol, long clientVersion,
: F5 m5 P: D3 ?5 yinthashcode) {
7 |9 U$ k. Z. U7 d8 b9 E/ [ |return new ProtocolSignature(ClientProtocol.versionID, null);
! i8 s4 \" a1 @ Y+ V. X}p
4 Q' A/ }8 v$ E0 O. O5 t1 Vublic String echo(String value) throws IOException {: z# ] n6 R" t7 `2 ?
return value;: ^, }0 E# s, |5 e% {
}
) o" f4 D& c- f: `public int add(int v1, int v2) throws IOException {
5 Y7 ^4 _0 ]* H: }, {9 \+ Y4 Q$ |1 Wreturn v1 + v2;
/ R' Q2 w7 Y, U}
3 @ F4 F1 l9 k* {- I}
/ b( K; X# ~" h9 G3.构造并启动RPC Server4 T3 n6 K+ ]* Q' h8 v
直接使用静态类Builder构造一个RPC Server, 并调用函数start()启动该Server:! B4 X# w/ s% U/ @5 Q6 A) P* O
Server server = new RPC.Builder(conf).setProtocol(ClientProtocol.class)
, Q: Q6 t. T$ ]8 x, n" w% U.setInstance(new ClientProtocolImpl()).setBindAddress(ADDRESS).setPort(0); ~" w1 b5 Z6 x6 I* H7 ]
.setNumHandlers(5).build();
/ z- o0 v% s# ?$ Gserver.start();
/ u! n1 w* A! j其中, BindAddress( 由函数setBindAddress设置) 和Port( 由函数setPort设置, 0表示由系统随机选择一个端口号) 分别表示服
1 u O6 {" q+ h8 h) Q* \: ^务器的host和监听端口号, 而NnumHandlers( 由函数setNumHandlers设置) 表示服务器端处理请求的线程数目。 到此为止, 服务器/ T X% A; n; t5 I- K% x
处理监听状态, 等待客户端请求到达。7 b, j5 N& W/ H# h* c! V E) z6 S
4.构造RPC Client并发送RPC请求
' e9 d l \6 | W使用静态方法getProxy构造客户端代理对象, 直接通过代理对象调用远程端的方法, 具体如下所示:% ^# C' [9 _4 v/ z
proxy = (ClientProtocol)RPC.getProxy(
# g }& _. q4 s2 ~3 xClientProtocol.class, ClientProtocol.versionID, addr, conf); j d1 h' @% T. l0 J
int result = proxy.add(5, 6);
: ]( p7 ~( [: Z0 xString echoResult = proxy.echo("result");
% o4 t5 j" B; C- K" O经过以上四步, 我们便利用Hadoop RPC搭建了一个非常高效的客户机–服务器网络模型。 接下来, 我们将深入到Hadoop RPC
) t' z1 i4 i; j9 j& U- W内部, 剖析它的设计原理及技巧。 : f# E# H, o9 q- F' W& }
' q" v; f3 _" \9 |4 l
: m c3 j& Q# w
|
|