RPC和序列化

0.094字数 4186阅读 2577

1. RPC

1.1 简介

RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。

问题:为什么要用RPC接口,而不是http接口?

通过RPC能解耦服务,这才是使用RPC的真正目的。RPC的原理主要用到了动态代理模式,至于http协议,只是传输协议而已

RPC是一个软件结构概念,是构建分布式应用的理论基础。就好比为啥你家可以用到发电厂发出来的电?是因为电是可以传输的。至于用铜线还是用铁丝还是其他 种类的导线,也就是用http还是用其他协议的问题了

1.2 RPC结构

调用过程:

1)服务消费方(client)调用以本地调用方式调用服务;

2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

3)client stub找到服务地址,并将消息发送到服务端;

4)server stub收到消息后进行解码;

5)server stub根据解码结果调用本地的服务;

6)本地服务执行并将结果返回给server stub;

7)server stub将返回结果打包成消息并发送至消费方;

8)client stub接收到消息,并进行解码;

9)服务消费方得到最终结果。

1.3 RPC评判标准

真的像本地函数一样调用

使用简单,用户只需要关注业务即可

灵活,RPC调用的序列化方式可以自由定制,比如支持json,支持msgpack等方式

RPC性能:

第一次第二次第三次第四次第五次

RMI46244338452342813425

TCP69337088712577176423

thrift(TBinaryProtocol)22122229211920211953

thrift(TCompactProtocol)21482827235523702316

thrift(TJSONProtocol)27522861372627562403

https://github.com/wysure/wy/tree/master/src/main/java/com/wy/test/rpc

2.Thrift

2.1 thrift简介

Thrift是一个跨语言的服务部署框架,最初由Facebook于2007年开发,2008年进入Apache开源项目。Thrift通过一个中间语言(IDL, 接口定义语言)来定义RPC的接口和数据类型,然后通过一个编译器生成不同语言的代码(目前支持C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),并由生成的代码负责RPC协议层和传输层的实现。

2.2 thrift架构

Thrift实际上是实现了C/S模式,通过代码生成工具将接口定义文件生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)便可以了。其中protocol(协议层, 定义数据传输格式,可以为二进制或者XML等)和transport(传输层,定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。

协议层TProtocol:在传输协议上总体上划分为文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议为多数。

TBinaryProtocol– 二进制编码格式进行数据传输。

TCompactProtocol– 高效的编码方式,使用类似于protobuffer的Variable-Length Quantity (VLQ) 编码(可以节省传输空间,使数据的传输效率更高)对数据进行压缩。

TJSONProtocol– 使用JSON的数据编码协议进行数据传输。

TSimpleJSONProtocol– 这种节约只提供JSON只写的协议,适用于通过脚本语言解析

TDebugProtocol– 在开发的过程中帮助开发人员调试用的,以文本的形式展现方便阅读。

传输层TTransport

TSocket-使用阻塞式I/O进行传输,也是最常见的模式。

TFramedTransport-使用非阻塞方式,按块的大小,进行传输,类似于Java中的NIO。

TFileTransport-顾名思义按照文件的方式进程传输,虽然这种方式不提供Java的实现,但是实现起来非常简单。

TMemoryTransport-使用内存I/O,就好比Java中的ByteArrayOutputStream实现。

TZlibTransport-使用执行zlib压缩,不提供Java的实现。

TNonblockingTransport-使用非阻塞方式,用于构建异步客户端。

2.3 数据类型

Thrift支持五种数据类型:

Base Types:基本类型

bool:布尔值,true 或 false,对应 Java 的 boolean

byte:8 位有符号整数,对应 Java 的 byte

i16:16 位有符号整数,对应 Java 的 short

i32:32 位有符号整数,对应 Java 的 int

i64:64 位有符号整数,对应 Java 的 long

double:64 位浮点数,对应 Java 的 double

string:未知编码文本或二进制字符串,对应 Java 的 String

Struct:结构体类型

struct:定义公共的对象,类似于 C 语言中的结构体定义,在 Java 中是一个 JavaBean

Container:容器类型,即List、Set、Map

Exception:异常类型

Service:类似于面向对象的接口定义,里面包含一系列的方法

2.4 thrift序列化(二进制编码格式为例)

2.4.1 序列化过程

userInfo.thrift

namespace java com.test.user

struct userInfo {

1: required i32 userId;

2: optional string userName;

3: optional string extern;

}

序列化(TBinaryProtocol编码格式为例)

publicvoidwrite(org.apache.thrift.protocol.TProtocol oprot, userInfo struct)throwsorg.apache.thrift.TException {

struct.validate();//校验thrift文件中定义的required域即必传的值是不是有值

oprot.writeStructBegin(STRUCT_DESC);//开始写结构体

oprot.writeFieldBegin(USER_ID_FIELD_DESC);//开始写userId域 (序列号和域类型)

oprot.writeI32(struct.userId);//写userId数据

oprot.writeFieldEnd();//写域完成

if(struct.userName !=null) {

oprot.writeFieldBegin(USER_NAME_FIELD_DESC);

oprot.writeString(struct.userName);

oprot.writeFieldEnd();

}

if(struct.extern !=null) {

if(struct.isSetExtern()) {

oprot.writeFieldBegin(EXTERN_FIELD_DESC);

oprot.writeString(struct.extern);

oprot.writeFieldEnd();

}

}

oprot.writeFieldStop();//向序列化的文件里面写入一个字节的0表示序列化结束

oprot.writeStructEnd();//写结构体完成

}

}

writeFieldBegin()方法

publicvoidwriteStructBegin(TStruct struct) {

}

publicvoidwriteStructEnd() {

}

publicvoidwriteFieldBegin(TField field)throwsTException {

this.writeByte(field.type);//类型

this.writeI16(field.id);//序号

}

publicvoidwriteFieldEnd() {

}

publicvoidwriteFieldStop()throwsTException {

this.writeByte((byte)0);

}

thrift序列化后的文件里面只有域的类型以及域的数字序号,没有域的名称,因此与JSON/XML这种序列化工具相比,thrift序列化后生成的文件体积要小很多

反序列化

publicvoidread(org.apache.thrift.protocol.TProtocol iprot, userInfo struct)throwsorg.apache.thrift.TException {

org.apache.thrift.protocol.TField schemeField;//由域的类型type及域的数字序号id构成的一个类

iprot.readStructBegin();//构造struct对象

while(true)

{

schemeField = iprot.readFieldBegin();//从序列化文件中构造一个TField类型的对象

if(schemeField.type == org.apache.thrift.protocol.TType.STOP) {

break;

}

switch(schemeField.id) {

case1:// USER_ID

if(schemeField.type == org.apache.thrift.protocol.TType.I32) {

struct.userId = iprot.readI32();

struct.setUserIdIsSet(true);

}else{

org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);//域的数字序号相同,但是域的类型不同,则会跳过给该域赋值

}

break;

case2:// USER_NAME

if(schemeField.type == org.apache.thrift.protocol.TType.STRING) {

struct.userName = iprot.readString();

struct.setUserNameIsSet(true);

}else{

org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);

}

break;

case3:// EXTERN

if(schemeField.type == org.apache.thrift.protocol.TType.STRING) {

struct.extern = iprot.readString();

struct.setExternIsSet(true);

}else{

org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);

}

break;

default:

org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type);//域的数字序号未找到,跳过给该域赋值

}

iprot.readFieldEnd();

}

iprot.readStructEnd();

// check for required fields of primitive type, which can't be checked in the validate method

if(!struct.isSetUserId()) {

throwneworg.apache.thrift.protocol.TProtocolException("Required field 'userId' was not found in serialized data! Struct: "+ toString());

}

struct.validate();

}

publicTStruct readStructBegin() {

returnnewTStruct();

}

publicvoidreadStructEnd() {

}

publicTField readFieldBegin()throwsTException {

bytetype =this.readByte();

shortid = type ==0?0:this.readI16();

returnnewTField("", type, id);

}

publicvoidreadFieldEnd() {

}

Thrift的向后兼容性,需要满足一定的条件

域的数字序号不能改变

域的类型不能改变

满足了上面的两点,无论你增加还是删除域,都可以实现向后兼容

2.4.2 序列化结果

struct示例

struct SerializeModel {

1: required bool testBoolean;

2: requiredbytetestByte;

3: required i16 testShort;

4: required i32 testInt;

5: required i64 testLong;

6: requireddoubletestDouble;

7: required string testString;

8: optional list testList;

}

SerializeModel serializeModel =newSerializeModel();

serializeModel.setTestBoolean(true);

serializeModel.setTestByte((byte)1);

serializeModel.setTestDouble(123.456);

serializeModel.setTestInt(2345);

serializeModel.setTestLong(123456789L);

serializeModel.setTestShort((short)10);

serializeModel.setTestString("what a fuck boy!");

[2,0,1,1,3,0,2,1,6,0,3,0,10,8,0,4,0,0,9,41,10,0,5,0,0,0,0,7,91, -51,21,4,0,6,64,94, -35,47,26, -97, -66,119,11,0,7,0,0,0,16,119,104,97,116,32,97,32,102,117,99,107,32,98,111,121,33,0]

2testBoolean类型           (1byte)

0,1testBoolean序号           (2byte)

1testBoolean的值           (1byte)

3,                                      testByte类型              (1byte)

0,2testByte序号              (2byte)

1testByte的值              (1byte)

6testShort类型         (1byte)

0,3testShort序号         (2byte)

0,10testShort的值         (2byte)

8testInt类型               (1byte)

0,4testInt序号               (2byte)

0,0,9,41testInt的值               (4byte)

10testLong类型              (1byte)

0,5testLong序号              (2byte)

0,0,0,0,7,91, -51,21testLong的值              (8byte)

4testDouble类型            (1byte)

0,6testDouble序号            (2byte)

64,94, -35,47,26, -97, -66,119testDouble的值            (8byte)

11testString类型            (1byte)

0,7testString序号            (2byte)

0,0,0,16testString长度            (4byte)

119,104...111,121,33testString的值            (16byte)

0结束标识

org.apache.thrift.protocol.TType

packageorg.apache.thrift.protocol;

publicfinalclassTType {

publicstaticfinalbyteSTOP =0;

publicstaticfinalbyteVOID =1;

publicstaticfinalbyteBOOL =2;

publicstaticfinalbyteBYTE =3;

publicstaticfinalbyteDOUBLE =4;

publicstaticfinalbyteI16 =6;

publicstaticfinalbyteI32 =8;

publicstaticfinalbyteI64 =10;

publicstaticfinalbyteSTRING =11;

publicstaticfinalbyteSTRUCT =12;

publicstaticfinalbyteMAP =13;

publicstaticfinalbyteSET =14;

publicstaticfinalbyteLIST =15;

publicstaticfinalbyteENUM =16;

publicTType() {

}

}

3. 序列化

序列化: 将数据结构或对象转换成二进制串的过程

反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

https://tech.meituan.com/serialization_vs_deserialization.html

一个好的序列化协议至少应该考虑以下几点:

通用性

可读性

性能

可扩展

安全性

3.1 序列化协议性能指标

性能包括两个方面,时间复杂度和空间复杂度:

第一、空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。

第二、时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

3.2 序列化协议对比

3.2.1 XML

(1)优点

人机可读性好

可指定元素或特性的名称

(2)缺点

序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息。

类必须有一个将由 XmlSerializer 序列化的默认构造函数。

只能序列化公共属性和字段

不能序列化方法

文件庞大,文件格式复杂,传输占带宽

(3)使用场景

当做配置文件存储数据

实时数据转换

3.2.2 JSON

(1)优点

前后兼容性高

数据格式比较简单,易于读写

序列化后数据较小,可扩展性好,兼容性好

与XML相比,其协议比较简单,解析速度比较快

(2)缺点

数据的描述性比XML差

不适合性能要求为ms级别的情况

额外空间开销比较大

(3)适用场景(可替代XML)

跨防火墙访问

可调式性要求高的情况

基于Web browser的Ajax请求

传输数据量相对小,实时性要求相对低(例如秒级别)的服务

3.2.3 Thrift

(1)优点

序列化后的体积小, 速度快

支持多种语言和丰富的数据类型

对于数据字段的增删具有较强的兼容性

支持二进制压缩编码

(2)缺点

使用者较少

跨防火墙访问时,不安全

不具有可读性,调试代码时相对困难

不能与其他传输层协议共同使用(例如HTTP)

无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议

(3)适用场景

分布式系统的RPC解决方案

3.2.4 ProtoBuf

(1)优点

序列化后码流小,性能高

结构化数据存储格式(XML JSON等)

通过标识字段的顺序,可以实现协议的前向兼容

结构化的文档更容易管理和维护

(2)缺点

需要依赖于工具生成代码

支持的语言相对较少,官方只支持Java 、C++ 、Python

(3)适用场景

对性能要求高的RPC调用

具有良好的跨防火墙的访问属性

适合应用层对象的持久化

序列化性能对比:(一百万次SerializeModel的序列化)

字节流大小(byte)序列化耗时(ms)反序列化耗时(ms)

protoBuf41452197

gson13725501655

fastjson1376541020

jackson1378951122

java原生23616471626

thrift(TBinaryProtocol)66562555

thrift(TCompactProtocol)41492657

thrift(TJSONProtocol)13232943678

thrift(TSimpleJSONProtocol)1342742

https://github.com/wysure/wy/tree/master/src/main/java/com/wy/test/serialize

4. 账户thrift接口改造

4.1 背景

账户thrift接口通过IDL语法生成的Java代码,每次开发需要重新生成代码,一个Java文件里的代码动辄上万行,容易出错

接口不能与使用方共享Javabean,使用方不能直接看到接口定义,需要维护接口文档

账户重构需要废弃一些接口,IDL方式不能很好的标识被废弃的接口

4.2 方案

使用Facebook的开源项目swift,个人理解是Facebook基于Java注解对thrift做简单易用的改造

优点:

代码简洁

各系统间相互调用时可以共享Javabean,注释标记好和系统内使用无差异

支持Java的Deprecated

可以减少因不熟悉IDL语法而造成的bug

缺点:

不能跨语言

性能略有下降,约为原生thrift的百分之七十

thrift不支持泛型、重载,在idl可以做更好的限制,如果用注解,thrift新手可能会使用泛型导致问题

在跨语言和性能方面,我们的需求不大,因此选择了swift

4.3 遇到的问题

1. Enum生成问题

在枚举类型的get方法上,需要添加@ThriftEnumValue注解,否则枚举不能正常使用

这里主要是为了校验,该方法必须为public,必须为非静态方法,返回值必须非空,返回值必须为int或其包装类型

Method enumValueMethod =null;

for(Method method : enumClass.getMethods()) {

if(method.isAnnotationPresent(ThriftEnumValue.class)) {

Preconditions.checkArgument(

Modifier.isPublic(method.getModifiers()),

"Enum class %s @ThriftEnumValue method is not public: %s",

enumClass.getName(),

method);

Preconditions.checkArgument(

!Modifier.isStatic(method.getModifiers()),

"Enum class %s @ThriftEnumValue method is static: %s",

enumClass.getName(),

method);

Preconditions.checkArgument(

method.getTypeParameters().length ==0,

"Enum class %s @ThriftEnumValue method has parameters: %s",

enumClass.getName(),

method);

Class returnType = method.getReturnType();

Preconditions.checkArgument(

returnType ==int.class|| returnType == Integer.class,

"Enum class %s @ThriftEnumValue method does not return int or Integer: %s",

enumClass.getName(),

method);

enumValueMethod = method;

}

}

2. struct元素前序列号的问题

由于系统迭代造成的代码删改,很多struct元素前的序列号不连续,在改造过程中需要做兼容,做到上线后不影响线上业务

3. service请求参数序号问题

由于之前对IDL语法认识不够全面,导致在使用IDL方式生成thrift接口时忽视了序号的问题,在.thrift文件中更改了序号,但是并没有同步生成Java文件,造成了系统隐患,此次thrift接口改造中问题得以发现并解决

4. struct元素赋默认值问题

如果入参为null,返回默认值,如果入参不为空,返回原始入参

struct元素最好为包装类型,最好不要赋默认值

参考文档:

http://www.aiprograming.com/b/pengpeng/24

http://blog.csdn.net/column/details/thrift.html

http://zheming.wang/blog/2014/08/28/94D1F945-40EC-45E4-ABAF-3B32DFFE4043/

https://tech.meituan.com/serialization_vs_deserialization.html

推荐阅读更多精彩内容