共包含5部分
一. RPC与http比较
二. RPC开源框架thrift
三. 安装thrift,及python样例代码
四. thrift-python 常用类剖析
五. 生产环境使用样例
一. RPC与http比较
RPC过程:
RPC是一种C/S架构的服务模型,server端提供接口供client调用,client端向server端发送数据,server端接收client端的数据进行相关计算并将结果返回给client端。
rpc与http比较:
传输效率:
-
RPC,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
-
HTTP,如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理
性能消耗,主要在于序列化和反序列化的耗时
-
RPC,可以基于thrift实现高效的二进制传输
-
HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能
二. RPC开源框架 Thrift:
Thrift通过一个中间语言(IDL, 接口定义语言)来定义RPC的接口和数据类型,然后通过一个编译器生成不同语言的代码,并由生成的代码负责RPC协议层和传输层的实现。
Thrift实际上是实现了C/S模式,通过代码生成工具将接口定义文件生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)便可以了。其中protocol(协议层, 定义数据传输格式,可以为二进制或者XML等)和transport(传输层,定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。
Thrift交互流程:
1.在Client和Server的最顶层都是用户自定义的处理逻辑,也就是说用户只需要编写用户逻辑,就可以完成整套的RPC调用流程。用户逻辑的下一层是Thrift自动生成的代码,这些代码主要用于结构化数据的解析,发送和接收,同时服务器端的自动生成代码中还包含了RPC请求的转发
2.TTransport负责以字节流方式发送和接收Message,是底层IO模块在Thrift框架中的实现,每一个底层IO模块都会有一个对应TTransport来负责Thrift的字节流(Byte Stream)数据在该IO模块上的传输。例如TSocket对应Socket传输,TFileTransport对应文件传输。
3.TProtocol主要负责结构化数据组装成Message,或者从Message结构中读出结构化数据。TProtocol将一个有类型的数据转化为字节流以交给TTransport进行传输,或者从TTransport中读取一定长度的字节数据转化为特定类型的数据。
4.TServer负责接收Client的请求,并将请求转发到Processor进行处理。TServer主要任务就是高效的接受Client的请求,特别是在高并发请求的情况下快速完成请求。
5.Processor(或者TProcessor)负责对Client的请求做出响应,包括RPC请求转发,调用参数解析和用户逻辑调用,返回值写回等处理步骤。Processor是服务器端从Thrift框架转入用户逻辑的关键流程。Processor同时也负责向Message结构中写入数据或者读出数据。
利用Thrift用户只需要做三件事:
1) 利用IDL定义数据结构及服务
2) 利用代码生成工具将(1)中的IDL编译成对应语言(如PYTHON),编译后得到基本的框架代码
3) 在(2)中框架代码基础上完成完整代码
三.安装thrift,及python示例
centos环境安装thrift
1.下载boost https://www.boost.org/
./bootstrap.sh
./b2 install
2.下载thrift http://thrift.apache.org/download
./configure
make
make install
使用thrift,python示例
1.编写IDL
test_thrift.thrift
样例:
typedef i32 int
struct pair {
1: string str1,
2: string str2
}
service test_thrift
{
int test_api(
1:int n1,
2:int n2)
int test_pair(
1: pair n1
)
pair test_pair2(
1: int n1
)
}
2.编译:thrift -gen py test_thrift.thrift
会在同目录下生成 gen-py 目录,目录下test_thrift文件夹为编译好的调用库
-out可将gen-py替换为指定目录,需要先建立好目录
例:thrift -gen py -out thrift_gen eslib_rpc.thrift
3.服务器端样例代码:
server.py:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from test_thrift import test_thrift
from test_thrift.ttypes import *
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer
class RpcServerHandler(object):
def test_api(self, n1, n2):
print 'test_api'
print n1, n2
return n1 + n2
def test_pair(self, n1):
print 'test_pair'
print n1
return int(n1.str1)
def test_pair2(self, n1):
print 'test_pair2'
print n1
result = pair('100', '200')
print result
return result
if __name__ == '__main__':
handler = RpcServerHandler()
processor = test_thrift.Processor(handler)
transport = TSocket.TServerSocket(port=9080)
tfactory = TTransport.TBufferedTransportFactory()
pfactory1 = TBinaryProtocol.TBinaryProtocolFactory()
server_bin = TServer.TSimpleServer(processor, transport, tfactory, pfactory1)
print('Starting the server...')
server_bin.serve()
print('done.')
4.客户端样例代码
#!/usr/bin/env python
# encoding: utf-8
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol import TBinaryProtocol
from test_thrift import test_thrift
from test_thrift.ttypes import pair
def rpc_client():
transport = TSocket.TSocket("127.0.0.1", 9080)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = test_thrift.Client(protocol)
transport.open()
return transport, client
transport, client = rpc_client()
result = client.test_api(1, 2)
print result
new_pair = pair('10', '20')
result = client.test_pair(new_pair)
print result
result = client.test_pair2(1)
print result
transport.close()
四.thrift-python 常用类剖析
TServer
主要任务是接收Client的请求,并转到某个TProcessor上进行请求处理,针对不同的访问规模,Thrift提供了不同的TServer模型:
1. TSimpleServer:使用阻塞IO的单线程服务器
2. TThreadedServer:使用阻塞IO的多线程服务器,每一个请求都在一个线程里处理,并发访问情况下会有很多线程同时在运行
3. TThreadPoolServer:使用阻塞IO的多线程服务器,使用线程池处理线程
4. TNonBlockingServer: 使用非阻塞IO的多线程服务器,使用少量线程即可完成大并发量的请求响应,比如使用TFramedTransport
Thrift使用livevent作为服务的事件驱动器,libevent就是epoll更高级的封装,处理大量请求的话,主要在TThreadedServer与TNonblockingServer中进行选择。TNonblockingServer能够使用少量线程处理大量并发连接,但是延迟较高,TThreadedServer的延迟较低。实际中,TThreadedServer的吞吐量可能会比TNonblockingServer高,但是TThreadedServer的CPU占用要高很多
TTransport
Thrift最底层的传出可以使用socket,File,Zip来实现,TTransport是与底层数据传输紧密相关的传输层,每一种支持的底层传输方式都存在对应的TTransport。在TTransport这一层,数据按字节流方式处理,即传输层看到的是一个个字节,并把这些字节按照顺序发送和接收。TTransport并不了解它传输的数据是什么类型,实际上传输层也不关心,只需要按照字节方式对数据进行发送和接收即可。数据类型的解析在TProtocol这一层完成。
TTransport有以下几个类:
1. TSocket: 使用阻塞的TCP Socket进行数据传输
2. THttpTransport:采用Http传输协议进行数据传输
3. TFileTransport:文件、日志传输类,允许client将文件传给server,允许server将收到的数据写到文件中
4.TZlibTransport:与其他的TTransport配合使用,压缩后对数据进行传输,或者将收到的数据解压
TProtocol
主要任务是把TTransport中的字节流转化成数据流,在TProtocol这一层就会出现具有数据类型的数据,如整形,浮点数,字符串,结构体等,TProtocol只会按照指定类型将数据读出和写入,和数据的真正用途,在Thrift自动生成的Server和Client中处理
Thrift可以让用户选客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本text和二进制binnary,为了节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议居多
常用协议如下:
1. TBinaryProtocol:二进制格式
2. TComactProtocol:高效率的、密集的二进制编码格式
3. TJSONProtocol:使用JSON的数据编码协议进行数据传输
4. TSimpleJSONProtocol:提供JSON只写协议,生成的文件很容易通过脚本语言解析
5. TDebugProtocol: 使用易懂的可读文本格式,便于debug
TCompactProtocol 高效的编码方式,使用了类似于ProtocolBuffer的Variable-Length Quantity (VLQ) 编码方式,主要思路是对整数采用可变长度,同时尽量利用没有使用Bit。对于一个int32并不保证一定是4个字节编码,实际中可能是1个字节,也可能是5个字节,但最多是五个字节。TCompactProtocol并不保证一定是最优的,但多数情况下都会比TBinaryProtocol性能要更好。
TProcessor/Processor
主要对TServer中的一次请求的InputProtocol和OutputProtcol中写入用户逻辑的返回值
Processor是TServer从Thrift框架转到用户逻辑的关键流程,同时Client所有的RPC调用都会经过TProcessor.process处理并转发
TProcessor对于一次RPC调用的处理过程:
1.TServer接收到RPC请求后,调用TProcessor.process进行处理
2.TProcessor.process首先调用TTransport.readMessageBegin接口,读出RPC调用的名称和RPC调用的类型,如果RPC调用类型是RPC Call,则调用TProcessor.process_fn继续处理,对于未知的RPC调用类型,则抛出异常
3. TProcessor.process_fn根据RPC调用名称到自己的processMap中查找对应的RPC处理函数,对于存在的RPC处理函数,调用处理函数继续进行请求响应,不存在抛异常
4. PRC处理函数调用RPC请求参数的解析类,从TProtocol中读入数据完成参数解析,将参数放到一个Struct中,Thrift会检查参数字段id和类型是否匹配,对于不符合要求的参数都会跳过。这样,RPC接口发生变化之后,旧的处理函数在不做修改的情况下,可以通过跳过不认识的参数,继续提供服务,进而在RPC框架中提供接口的多Version支持
5.完成参数解析后,调用用户逻辑,完成真正的请求响应
6.用户逻辑的返回值使用返回值打包类进行打包,写入TProtocol
ThriftClient
ThriftClient跟TProcessor一样,主要操作InputProtocol和OutputProtocol
1. send,将用户的调用参数作为一个整体的Struct写入TProcotol,并发送到TServer
2. send结束后,ThriftClient立刻进入Receive状态等待TServer的响应,对应TServer返回的响应,使用返回值解析类进行返回值解析
五. 生产环境使用样例
背景:analysisOpenApi为单独的web服务,eslib为服务中一个子模块(调用方法为直接引入调用函数),现将eslib拆分出来成为rpc调用的子服务
1.编写IDL
IDL文件:eslib_rpc.thrift
struct eslib_struct{
1: string items_html,
2: string items_qs_ans,
3: string items_qs_exp
} # 定义rpc调用时的指定参数类型
service eslib_rpc_interface # 定义rpc服务调用接口
{
eslib_struct get_items_render_result(
1: string items_info,
2: string subject
), # 返回结构为上面定义的eslib_struct,传入参数为两个string
string render_items(1: string item_ids, 2: string subject, 3: bool add_answer, 4: string start), # 返回结构为string
}
2.执行thrift
①在analysisOpenApi(需要调用rpc服务的客户端)项目下创建文件夹(thrift_gen)用来存储thrift生成的文件
②在analysisOpenApi项目下执行:thrift -gen py -out thrift_gen eslib_rpc.thrift
会在创建的thrift_gen下生成:
其中子目录名eslib_rpc对应执行的thrift文件的名称,eslib_rpc_interface.py文件中为thrift文件中service中定义的各个接口及client,ttypes.py中为thrift文件中定义的数据结构(eslib_struct)
③在analysisOpenApi项目下编写client:(约定rpc客户端与服务端使用9080端口交互)
#!/usr/bin/env python
# encoding: utf-8
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol import TBinaryProtocol
from thrift_gen.eslib_rpc import eslib_rpc
from thrift_gen.eslib_rpc import ttypes
def rpc_client(): # 创建rpc客户端
transport = TSocket.TSocket("127.0.0.1", 9080)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = eslib_rpc.Client(protocol)
transport.open()
return transport, client
def get_items_render_result(items_info, subject): # 接口调用
transport, client = rpc_client()
result = client.get_items_render_result(items_info, subject)
transport.close()
return result
def render_items(item_ids, subject, add_answer, start): # 接口调用
# item_ids json([str])
# start = json([int])
transport, client = rpc_client()
result = client.render_items(item_ids, subject, add_answer, start)
transport.close()
return result
④ 在eslib项目下(rpc服务端)执行①②,会在eslib下生成同样的目录文件
⑤ 在eslib项目下编写service:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
thrift:
thrift -gen py -out thrift_gen eslib_rpc.thrift
"""
from thrift_gen.eslib_rpc import eslib_rpc
from thrift_gen.eslib_rpc.ttypes import *
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer
import time
import config
import json
import argparse
import logging
class EslibHandler(object):
def get_items_render_result(self, items_info, subject):
pass
# 业务逻辑
return None # return 结果为thrift中定义的接口返回格式
def def render_items(self, item_ids, subject, add_answer, start):
pass
# 业务逻辑
return None # return 结果为thrift中定义的接口返回格式
def rpc_instance(): # 初始化rpc服务
handler = EslibHandler() # 定义接受调用的handler
processor = eslib_rpc.Processor(handler)
transport = TSocket.TServerSocket(port=config.get('rpc_port'))
tfactory = TTransport.TBufferedTransportFactory()
pfactory = TBinaryProtocol.TBinaryProtocolFactory()
server = TServer.TThreadPoolServer(processor, transport, tfactory, pfactory)
return server
if __name__ == '__main__': # 启动rpc服务
argp = argparse.ArgumentParser()
argp.add_argument('--rpc_port', default=9080, type=int)
args = argp.parse_args()
config.load_config('server.conf')
config.update('rpc_port', args.rpc_port)
rpc_server = rpc_instance()
logging.basicConfig(level=logging.DEBUG,
format='[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
logging.info('{} Starting the server... {}'.format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
config.get('rpc_port')))
rpc_server.serve()
logging.info('done.')
启动eslib服务即可,在analysisOpenApi客户端即可通过rpc接口调用eslib