【ZooKeeper】序列化与协议

ZooKeeper 的客户端和服务端之间会进行一系列的 网络通信以实现数据的传输。对于一个网络通信,首先需要解决的就是对数据的序列化 和反序列化处理,在 ZooKeeper 中,使用了 Jute 这一序列化组件来进行数据的序列化和 反序列化操作。同时,为了实现一个高效的网络通信程序,良好的通信协议设计也是至 关重要的。

1 Jute 介绍

Jute 是 ZooKeeper 中的序列化组件,最初也是 Hadoop 中的默认序列化组件,其前身是 Hadoop Record IO 中的序列化组件,后来由于 Apache Avro具有出众的跨语言特性、 丰富的数据结构和对 MapReduce 的天生支持,并且能非常方便地用于 RPC 调用,从而 深深吸引了 Hadoop 因此Hadoop 从 0.21.0 版本开始,废弃了 Record IO, 使用了 Avro 这个序列化框架,同时 Jute 也从Hadoop 工程中被剥离出来,成为了独立的序列化组件。

ZooKeeper 则从第一个正式对外发布的版本 (0.0.1 版本)开始,就一直使用 Jute 组件来 进行网络数据传输和本地磁盘数据存储的序列化和反序列化工作,一直使用至今。其实 在前些年,ZooKeeper 官方也一直在寻求一种高性能的跨语言序列化组件,期间也多次 提出要替换ZooKeeper 的序列化组件。关于序列化组件的改造还需要追溯到 2008 年左 右,那时候 ZooKeeper官方就提出要使用类似于 Apache Avro 、 Thrift 或是 Google 的 protobuf 这样的组件来替换 Jute, 但是考虑到新老版本序列化组件的兼容性,官方团队 一直对替换序列化组件工作的推进持保守和观望态度。值得一提的是,在替换序列化组 件这件事上,ZooKeeper 官方团队曾经也有过类似于下面这样的方案:服务器开启两个 客户端服务端口,让包含新序列化组件的新版客户端连接单独的服务器端口,老版本的 客户端则连接另一个端口。但考虑到其实施的复杂性,这个想法设计一直没有落地。更 为有趣的是,ZooKeeper 开发团队曾经甚至考虑将“如何让依赖 Jute 组件的老版本客户端/服务器和依赖 Avro 组件的新版本客户端/服务器进行无缝通信”这个问题作为 Google Summer of Code 的题目。当然,另一个重要原因是针对 Avro 早期的发布版本, ZooKeeper 官方做了一个Jute 和 Avro 的性能测试,但是测试结果并不理想,因此也并没有决定使 用 Avro 。时 至 今日, Jute 的序列化能力都不曾是 ZooKeeper 的性能瓶颈。

总之,因为种种原因以及 2009 年以后 ZooKeeper 快速地被越来越多的系统使用,开发 团队需要将更多的精力放在解决更多优先级更高的需求和 Bug 修复上,以致于替换 Jute 序列化组件的工作一度被搁置——于是我们现在看到,在最新版本的 ZooKeeper 中,底 层依然使用了 Jute 这个古老的,并且似乎没有更多其他系统在使用的序列化组件。

2 使用 Jute 进行序列化

下面我们通过一个例子来看看如何使用 Jute 来完成 Java 对象的序列化和反序列化。假 设我们有一个实体类 MockReqHeader (代表了一个简单的请求头)

public class MockReqHeader implements Record {
	private long sessionld;
	private String type;
	public MockReqHeader() {}
	public MockReqHeader( long sessionld, String type ) { 
		this.sessionld = sessionld; this.type =	type;
	}
	public long getSessionId() { return sessionld;
	}
	public void setSessionldf long sessionld ) { 
		this.sessionld = sessionld;
	}
	public String getType() { return type;
	}
	public void setType( String m_ ){ type = m_;
	}
	public void serialize( OutputArchive a_, String tag ) throws java.io.IOException{
		a_.startRecord( this, tag );
		a_.writeLongf sessionld, "sessionld");
		a_.writeString( type, "type");
		a_.endRecord( this, tag );
	}
	public void deserialize( InputArchive a_, String tag ) throws java.io.lOException {
		a_.startRecord( tag );
		sessionld = a_.readLong( "sessionld' 1 );
		type = a_.readstring( "type");
		a_.endRecord( tag );
	}
}

上面即为一个非常简单的请求头定义,包含了两个成员变量: sessionld 和 type 。接下来我们看看如何使用 Jute 来进行序列化和反序列化。

//开始序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
new MockReqHeader( 0x34221eccb92a34el, "ping" ).serialize(boa, "header");

//这里通常是 TCP 网络传输对象
ByteBuffer bb = ByteBuffer.wrap(baos.toByteArray());
//开始反序列化
ByteBufferlnputStream bbis = new ByteBufferlnputStream(bb);

BinaryinputArchive bbia = BinarylnputArchive.getArchive(bbis);
MockReqHeader header2 = new MockReqHeader();
header2.deserialize(bbia, "header");
bbis.close();
baos.close();

上面这个代码片段演示了如何使用 Jute 来对 MockReqHeader 对象进行序列化和反序 列化,总的来说,大体可以分为 4 步。

  1. 实体类需要实现 Record 接口的 serialize 和 deserialize 方法。
  2. 构建一个序列化器 BinaryOutputArchive 。
  3. 序列化。
    调用实体类的 serialize 方法,将对象序列化到指定 tag 中去。例如在本例中 就将 MockReqHeader 对象序列化到 header 中去。
  4. 反序列化。
    调用实体类的 deserialize, 从指定的 tag 中反序列化出数据内容。

3 深入 Jute

从上面的讲解中可以看出,使用 Jute 来进行 Java 对象的序列化和反序列化是非常简单 的。接下去我们再通过 Record 序列化接口、序列化器和 Jute 配置文件三方面来深入了 解下 Jute 。

Record 接口

Jute 定义了自己独特的序列化格式 Record, ZooKeeper 中所有需要进行网络传输或是本地磁盘 存储的类型定义,都实现了该接口,其结构简单明了,操作灵活可变,是 Jute 序列化的核心。

Record 接口定义了两个最基本的方法,分别是 serialize 和 deserialize, 分别用于序 列化和反序列化:

package org.apache.jute;
import java.io.lOException;
public interface Record {
	public void serialize(OutputArchive archive, String tag) throws lOException; 
	public void deserialize(InputArchive archive, String tag) throws lOException; 
}

所有实体类通过实现 Record 接口的这两个方法,来定义自己将如何被序列化和反序列 化。其中 archive 是底层真正的序列化器和反序列化器,并且每个 archive 中可以 包含对多个对象的序列化和反序列化,因此两个接口方法中都标记了参数 tag, 用于向 序列化器和反序列化器标识对象自己的标记。例如在清单 7-10 所示的代码片段中,将 MockReqHeader 对象交付给 boa 序列化器进行序列化,并标记为 header 。

serialize 和 deserialize 的过程基本上是两 个相反的过程 ,serialize 过程就是将当前对象的各个成员变量以一定的标记 (tag) 写入到序列化器中去;而deserialize 过程则正好相反,是从反序列化器中根据指定 的标记 (tag) 将数据读取出来,并赋值给相应的成员变量。

OutputArchive 和 InputArchive

OutputArchive 和 InputArchive 分别是 Jute 底层的序列化器和反序列化器接口定 义。在最新版本的 Jute 中,分别有 BinaryOutputArchive/BinaryInputArchive>CsvOutputArchive/CsvlnputArchiveXmlOutputArchive/ XmllnputArchive 三种实现。无论哪种
实现,都是基于 Outputstream 和 InputStream 进行操作。

Binaryoutput Archive 对数据对象的序列化和反序列化,主要用于 进行网路传输和本地磁盘的存储,是 ZooKeeper 底层最主要的序列化方式。CsvOutputArchive 对数据的序列化,则更多的是方便数据对象的可视化展现,因此 被使用在toString 方法中。最后一种 XmlOutputArchive, 则是为了将数据对象以 XML 格式保存和还原,但是目前在 ZooKeeper 中基本没有被使用到。

zookeeper.jute

很多读者在阅读 ZooKeeper 的代码的过程中,都会发现一个有趣的现象,那就是在很多ZooKeeper 类的说明中,都写着 “File generated by hadoop record compiler. Do not edit.” 这是因为该类并不是 ZooKeeper 的开发人员编写的,而是通过 Jute 组件在编译过程中动 态生成的。在ZooKeeper 的 src 目录下,有一个名叫 zookeeper.jute 的文件:

module org.apache.zookeeper.data {
class Id {
	ustring scheme;
	ustring id;
}
class ACL {
	int perms;
	Id id;
}

在这个文件中定义了所有实体类的所属包名、类名以及该类的所有成员变量及其类型。 例如清单中的代码片段就分别定义了 org.apache.zookeeper.data.Id 和 org.apache.zookeeper.data, ACL两个类。

有了这个定义文件后,在源代码编译阶段, Jute 会使用不同的代码生成器来为这些类定 义生成实际编程语言 (Java 或 C/C++) 的类文件。以 Java 语言为例, Jute 会使用 JavaGenerator 来生成相应的类文件,这些类文件都会被存放在 src\java\generated 目 录下。需要注意的一点是,使用这种方式生成的类,都会实现 Record 接口。

4 通信协议

基于 TCP/IP 协议, ZooKeeper 实现了自己的通信协议来完成客户端与服务端、服务端与 服务端之间的网络通信。 ZooKeeper 通信协议整体上的设计非常简单,对于请求,主要 包含请求头和请求体,而对于响应,则主要包含响应头和响应体
在这里插入图片描述

协议解析:请求部分

我们首先来看请求协议的详细设计,下图为获取数据节点的请求协议
在这里插入图片描述

接下来,我们将从请求头和请求体两方面分别解析 ZooKeeper 请求的协议设计。

请求头: RequestHeader
请求头中包含了请求最基本的信息,包括 xid 和 type:

module org.apache.zookeeper.proto {
...
class RequestHeader { 
	int xid;
	int type;
} 
...

xid 用于记录客户端请求发起的先后序号,用来确保单个客户端请求的响应顺序。
type 代表请求的操作类型,常见的包括创建节点 (Opcode.create: 1), 删除节点 (OpCode. create: 2) 和获取节点数据 (Opcode. getData: 4) 等,所有这些操作 类型都被定义在类 org .apache.zookeeper.ZooDefs .OpCode 中。根据协议规定, 除非是“会话创建”请求,其他所有的客户端请求中都会带上请求头。

请求体: Request
协议的请求体部分是指请求的主体内容部分,包含了请求的所有操作内容。不同的请求 类型,其请求体部分的结构是不同的,下面我们以会话创建、获取节点数据和更新节点 数据这三个典型的请求体为例来对请求体进行详细分析。

  • ConnectRequest: 会话创建
    ZooKeeper 客户端和服务器在创建会话的时候,会发送 ConnectRequest 请求, 该请求体中包含了协议的版本号 protocolversion 、最近一次接收到的服务器 ZXfD lastZxidSeen 、会话超时时间 timeOut 、会话标识 sessionld 和会话密 码 passwd, 其数据结构定义如下:

    module org.apache.zookeeper.proto { 
    	class ConnectRequest { 
    		int protocolversion;
    		long LastZxidSeen;
    		int timeOut;
    		long sessionld;
    		buffer passwd;
    	}
    ...
    
  • GetDataRequest: 获取节点数据
    ZooKeeper 客户端在向服务器发送获取节点数据请求的时候,会发送 GetDataRequest 请求,该请求体中包含了数据节点的节点路径 path 和是否注册 Watcher 的标识 watch, 其数据结构定义如下:

    module org.apache.zookeeper.proto { 
    	class GetDataRequest { 
    		ustring path;
    		boolean watch;
    	}
    ...
    
  • SetDataRequest: 更新节点数据
    ZooKeeper 客户端在向服务器发送更新节点数据请求的时候,会发送 SetDataRequest 请求,该请求体中包含了数据节点的节点路径 Path 、数据内容 data 和节点数据的期望版本号 version, 其数据结构定义如下:

    module o rg.apache.zookeeper.proto (
    	class SetDataRequest {
    	ustring path;
    	buffer data;
    	int version;
    	}
    	...
    

以上介绍了常见的三种典型请求体定义,针对不同的请求类型, ZooKeeper 都会定义不 同的请求体,可以到 org. apache。zookeeper.proto 包下自行查看。

请求协议实例:获取节点数据

上面我们分别介绍了请求头和请求体的协议定义,现在我们通过一个客户端"获取节点 数据”的具体例子来进一步了解请求协议。

public class A_simple_get_data_request implements Watcher {
	public static void main(String[] args) throws Exception {
		ZooKeeper zk = new ZooKeeper("domainl.book.zookeeper", 5000,
										 new A_simple_get_data_ request());
		zk.getData("/$7_2_4/get_data", true, null);
	}
	public void process(WatchedEvent event) {}
}

清单是 一 个发 起 一次 简 单 的获 取 节点 数 据内容 请 求 的样 例 程 序。客户端调用 getData 接口,实际上就 是向 ZooKeeper 服务端发送了一个 GetDataRequest 请求。使用 Wireshark 归获取到 其发送的网络 TCP包,如图所示。
在这里插入图片描述
我们获取到了 ZooKeeper 客户端请求发出后,在 TCP 层数据传输的十六 进制表示,其中带下划线的部分就是对应的 GetDataRequest 请求,即 “[00,00,00,Id,00,00,00,01,00,00,00,04.00,00,00,10,2f,24.37,5f,32,5f,34.2f,67,65,74,5f,64,61,74,61,01] 通过 比对图中的 GetDataRequest 请求的完整协议定义,我们来分析下这个十六进制 字节数组的含义
在这里插入图片描述

表中分段解析了 ZooKeeper 客户端的 GetDataRequest 请求发送的数据,其他请 求也都类似

协议解析:响应部分

上面我们已经对 ZooKeeper 请求部分的协议进行了解析,接下来我们看看服务器端响应 的协议解析。我们首先来看响应协议的详细设计
在这里插入图片描述

响应头: ReplyHeader

响应头中包含了每一个响应最基本的信息,包括 xid 、 zxid 和 err:

module org.apache.zookeeper.proto {
	class ReplyHeader {
		int xid;
		long zxid;
		int err;
	} 

xid 和上文中提到的请求头中的 xid 是一致的,响应中只是将请求中的 xid 原值返回。 zxid代表 ZooKeeper 服务器上当前最新的事务 ID 。 err 则是一个错误码,当请求处理过程 中出现异常情况时,会在这个错误码中标识出来,常见的包括处理成功(Code.0K:0)、节点不存在 (Code.NONODE: 101) 和没有权限 (Code. N0AUTH: 102) 等,所有这 些错误码都被定义在类 org. apache. zookeeper. KeeperException. Code 中。

响应体: Response

协议的响应体部分是指响应的主体内容部分,包含了响应的所有返回数据。不同的响应 类型,其响应体部分的结构是不同的,下面我们以会话创建、获取节点数据和更新节点 数据这三个典型的响应体为例来对响应体进行详细分析。

  • ConnectResponse: 会话创建
    针对客户端的会话创建请求,服务端会返回客户端一个 ConnectResponse 响应, 该响应体中包含了协议的版本号 protocolversion. 会话的超时时间 timeOut> 会话标识 sessionld 和会话密码passwd, 其数据结构定义如下:
    module org.apache.zookeeper.proto (
    	class ConnectResponse {
    		int protocolVersion;
    		int timeOut;
    		long sessionld;
    		buffer passwd;
    	}
    
  • GetDataResponse: 获取节点数据
    针对客户端的获取节点数据请求,服务端会返回客户端一个 GetDataResponse 响应,该响应体中包含了数据节点的数据内容 data 和节点状态 stat, 其数据结 构定义如下:
    module org.apache.zookeeper.proto (
    	class GetDataResponse { 
    		buffer data;
    		org.apache.zookeeper.data.Stat stat;
    	}
    
  • SetDataResponse: 更新节点数据
    针对客户端的更新节点数据请求,服务端会返回客户端一个 SetDataResponse 响应,该响应体中包含了最新的节点状态 stat, 其数据结构定义如下:
    module org.apache.zookeeper.proto {
    	class SetDataResponse {
    		org.apache.zookeeper.data
    		Stat stat;
    	}
    

以上介绍了常见的三种典型响应体定义,针对不同的响应类型, ZooKeeper 都会定义不 同的响应体,读者可以到。 org.apache, zookeeper.proto 包下自行查看。

响应协议实例:获取节点数据

在上面的内容中,我们分别介绍了响应头和响应体的协议定义,现在我们再次通过上文 中提到的客户端“获取节点数据”的例子来对响应协议做一个实际分析。这里的测试用例还是使用清单中的示例程序,只是这次我们使用 Wireshark 获取到 服务端响应客户端时的网络 TCP 包,如图所示。
在这里插入图片描述

在图 7-16 中,我们获取到了 ZooKeeper 服务端响应发出之后,在 TCP 层数据传输的十 六进制表示,其中带下划线的部分就是对应的 GetDataResponse 响应,即“[00,00,00,63,00,00,00,05,00,00,00,00,00,00,00,04,00,00,00,00,00,00,00,Ob,69,27,6d,5f,63,6f,6e,74,65,6e,74,00,00,00,00,00,00,00,04,00,00,00,00,00,00,00,04,00,00,01,43,67,bd,Oe,08,00,00,01,43,67,bd,Oe,08,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,0b,00,00,00,00,00,00,00,00,00,00,00,04] w o 通过比对图 7-15 中的 GetDataResponse 响应完整协 议定义,我们来分析下这个十六进制字节数组的含义,如表所示。
在这里插入图片描述
在这里插入图片描述

其他 响应也都类似,感兴趣的读者可以使用相同的方法自行分析。

相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:马嘣嘣 返回首页