impala的rpc类型
impala在CDH 5.15.0(2.12.0社区版)中正式引入了kudu rpc框架,逐渐将主要使用的rpc替换为基于protobuf的krpc实现,在此之前,impala使用的是thrift框架,thrift框架的性能虽然较好,但稳定性较差,随着并发数上升,查询成功率会逐渐下降。
impala主要使用的rpc
admission-control(admission服务专用)
AdmitQuery:调度下发查询
GetQueryStatus:coordinator获取查询调度状态
ReleaseQuery:查询结束后,释放准入控制相关资源
ReleaseQueryBackends:某个be执行完成查询相关fragment后,释放此be上该fragment执行所占用的资源
CancelAdmission:在GetQueryStatus返回之前,取消查询调度
AdmissionHeartbeat:admission服务和coordinator之间的心跳rpc
control-service
ExecQueryFInstances:coordinator调用后,be(其他executor)异步地按照执行计划分片fragment开始执行查询
ReportExecStatus:向coordinator汇报be执行状态
CancelQueryFInstances:取消一个特定查询的fragment在be的执行
RemoteShutdown:shutdown特定be
data-stream-service
TransmitData:be之间传输数据
EndDataStream:数据传输结束后,调用此rpc结束传输
UpdateFilter:更新特定fragment的bloom filter
PublishFilter:广播全局bloom filter
impala rpc实现及执行过程(以DataStreamService为例)
通过protobuf描述文件,将rpc的请求体、响应体、请求参数结构体、rpc服务定义出来;
在protoc-gen-krpc.cc中继承了protobuf的CodeGenerator类,通过其中定义好的Generate函数,利用名称通配符,将定义好的服务、rpc、其他参数等等转化为临时cc和h文件,并通过预先在指定目录定义好的CMakeLists.txt对其进行编译;
最终生成的rpc服务相关代码文件格式为{protobuf_service_name}.service.h/cc、{protobuf_service_name}.pb.h/cc、{protobuf_service_name}.proxy.h/cc,其中各个rpc服务的类命名为{rpc_service_name}ServiceIf,impala中对于各个rpc的处理逻辑则继承此类再进行实现,如DataStreamService相关实现(省略注释和部分函数声明):
// EndDataStream和TransmitData是在data_stream_service.proto中定义的rpc
class DataStreamService : public DataStreamServiceIf {
public:
DataStreamService(MetricGroup* metric_group);
virtual void EndDataStream(const EndDataStreamRequestPB* request,
EndDataStreamResponsePB* response, kudu::rpc::RpcContext* context);
virtual void TransmitData(const TransmitDataRequestPB* request,
TransmitDataResponsePB* response, kudu::rpc::RpcContext* context);
};
rpc逻辑均定义完成后,impala通过KrpcDataStreamMgr、KrpcDataStreamRecvr和KrpcDataStreamSender类进行管理,在KrpcDataStreamMgr中定义了多个函数实现数据传输的相关逻辑,以下简要分析DataStreamService中TransmitData和EndDataStream的执行过程。
SQL的每个fragment的执行过程中,首先会调用GetNext获取当前fragment需要的RowBatch,随后会根据自身处于的执行计划节点类型调用不同的DataSink来处理RowBatch,当需要进行节点间数据传输时,如ExchangeNode节点,会通过KrpcDataStreamSender类中继承的Send函数进行数据传输的处理。DataSink共有5种(定义于DataSink.thrift):
enum TDataSinkType {
DATA_STREAM_SINK = 0 //管理网络数据流,用于节点间数据交换
TABLE_SINK = 1 //一般用于Hdfs、Kudu、Hbase等类型表的DML数据文件写入
HASH_JOIN_BUILDER = 2 //用于Join节点的执行,如runtime filter数据传输
PLAN_ROOT_SINK = 3 //用于管理coordinator节点的数据接收,即执行计划树的根节点数据流
NESTED_LOOP_JOIN_BUILDER = 4 //用于退化的Join节点执行
}
fragment在初始化时,会构造KrpcDataStreamSender对象,构造时会根据执行计划树来确定需要建立多少个与其他be进行通信的Channel,后续的数据传输通过Channel进行。
KrpcDataStreamSender首先将传入的RowBatch进行序列化,随后遍历所有构造好的Channel,调用Channel::TransmitData将序列化过的RowBatch通过注册过的TransmitData rpc发送到下游be。在rpc调用完成后,会自动调用TransmitDataCompleteCb的回调,用来收集rpc执行的metrics,如图中类似报错来源于此:
总体执行逻辑大概如图所示(省略部分调用):
Impala数据传输协议中的Sender和Recvr是多对多的关系,一个Sender可能有多个Recvr(如Broadcast),一个Recvr也可能接收到来自多个Sender的数据(如AggregationNode)。Sender与Recvr之间的数据流向主要依赖fragment_instance_id来确定,Sender在发送数据时,仅指定fragment_id即可,在fragment的Prepare阶段会创建Recvr,Sender在通过TransmitData的rpc发送数据前,会先通过一个recvr_map来确定Recvr,随后将数据发送给所有Recvr。Recvr接收到数据后,会先进行反序列化,并将数据放入batch_queue,如果batch_queue满了,会反压当前所有Sender-Recvr的rpc,待batch_queue消费后再继续反序列化数据。每个Recvr有一个sender_queues_数组,其中的每个元素都是存储来自多个不同sender的SenderQueue队列,每个队列中包含了Sender对应的batch_queue。在fragment的Open阶段,Recvr会开始调用GetBatch来消费sender_queues_中的各个元素的batch_queue,若SQL存在order by等需要排序的场景,执行计划中会存在SortNode节点,SortNode会使ExchangeNode中的一个is_merging_的flag=1,这使得Recvr会创建一个merger来对batch_queue中的数据进行排序后再合并,排序算法为堆排序。若GetBatch消费的速度较慢,赶不上Sender填充batch_queue的速度,batch_queue会迅速填满,此时Recvr进行rpc反压,暂时阻塞rpc直到处理完batch_queue的数据,日志中会打印如下输出:
在所有RowBatch发送结束后,即状态进入last batch sent,fragment会发送Eos信号给Recvr,此时会通过KrpcDataStreamSender::FlushFinal调用EndDataStream rpc表示数据传输结束。若rpc由于某种原因出现失败,在4.1版本,fragment会自动重新调度执行此rpc。
总结
本文首先列举了目前Impala内部使用Krpc实现的RPC,然后调研分析了DataStreamService中主要两个RPC:TransmitData和EndDataStream的执行过程和简要实现,并举例了目前线上常见的异常,希望借助此文可便于后续排查DataStreamService中的一些性能问题和故障。
参考资料
https://blog.cloudera.com/scalability-improvement-of-apache-impala-2-12-0-in-cdh-5-15-0/