前言
本文为笔者个人阅读Apache Impala源码时的笔记,仅代表我个人对代码的理解,个人水平有限,文章可能存在理解错误、遗漏或者过时之处。如果有任何错误或者有更好的见解,欢迎指正。
正文
在前置文章Impala3.4源码阅读笔记(八)解析ScanNode(中)中,我们以Kudu扫描结点KuduScanNode
为例子介绍了多线程版本ScanNode
的结构定义和工作流程。本文将续接上文,继续介绍Scanner
的实现和原理。建议阅读完上文后再阅读本文。
实现扫描逻辑的Kudu扫描器KuduScanner
对于Kudu这种支持了MT版本的扫描结点和HdfsScanNode
这种支持多种文件格式的ScanNode
来说,将扫描的连接数据源、解析数据和物化行批逻辑从ScanNode
中抽象出来可以提高代码复用率,降低开发难度,这些抽象出来的类均命名为Scanner
,这些类的实现方式也是各扫描结点之间差异最大的部分。对于Kudu扫描结点来说,KuduScanner
就是这些扫描逻辑的抽象,我们首先看其定义:
class KuduScanner {
public:
KuduScanner(KuduScanNodeBase* scan_node, RuntimeState* state);
/// 执行开启逻辑,包括抽取从元组描述符中抽取时间戳类型、克隆表达式求值器。
Status Open();
/// 以传入的扫描令牌开启一个新的kudu::client::KuduScanner,
/// 如果该令牌没有需要扫描的行(比如被runtime filters全部过滤),
/// 则eos还会被设置为true。
Status OpenNextScanToken(const std::string& scan_token, bool* eos);
/// 获取下一个行批,当当前kudu::client::KuduScanner没有更多行时,设置eos为true。
Status GetNext(RowBatch* row_batch, bool* eos);
/// 调用Kudu接口Ping一下Kudu TabletServer,以便于扫描器在等待行批队列或其他情况时保持活动状态。
void KeepKuduScannerAlive();
/// 关闭扫描器,释放资源。
void Close();
private:
/// 处理count(*)查询,只向行批中写入行数而不物化数据,
/// 这种优化只在简单的情况下才有可能,例如查询没有谓词时。
Status GetNextWithCountStarOptimization(RowBatch* row_batch, bool* eos);
/// 优化处理关系投影为空的情况,例如count(*),通过向行批中批量添加行而不是逐个添加行来实现。
/// 如果在极少数情况下存在任何谓词,则对每一行求一次值,只有当谓词值为真时才向行批中添加一行。
Status HandleEmptyProjection(RowBatch* row_batch);
/// 从Kudu获取到的行批KuduScanBatch中解码数据并物化到impala的行批RowBatch中。
Status DecodeRowsIntoRowBatch(RowBatch* batch, Tuple** tuple_mem);
/// 从当前kudu::client::KuduScanner获取下一个Kudu行批KuduScanBatch。
Status GetNextScannerBatch();
/// 关闭当前的kudu::client::KuduScanner.
void CloseCurrentClientScanner();
/// 计算并返回当前行批的下一个元组的地址以写入下一行数据。
inline Tuple* next_tuple(Tuple* t) const {
uint8_t* mem = reinterpret_cast<uint8_t*>(t);
return reinterpret_cast<Tuple*>(mem + scan_node_->tuple_desc()->byte_size());
}
/// 用于构建错误字符串。
std::string BuildErrorString(const char* msg);
KuduScanNodeBase* scan_node_;
RuntimeState* state_;
/// 用于保存从扫描结点克隆的表达式求值器的对象池,与扫描器生命周期相同。
ObjectPool obj_pool_;
/// 用于表达式求值器分配内存的内存池。
boost::scoped_ptr<MemPool> expr_perm_pool_;
/// 用于表达式求值器保存表达式求值结果的内存池。
boost::scoped_ptr<MemPool> expr_results_pool_;
/// 根据当前扫描令牌创建的kudu::client::KuduScanner,用于连接Kudu获取Kudu行批的接口。
/// 开启新扫描令牌时使用KuduScanToken::DeserializeIntoScanner()创建一个新的KuduScanner。
boost::scoped_ptr<kudu::client::KuduScanner> scanner_;
/// 从kudu::client::KuduScanner获取到的Kudu行批,其中的数据会被解码物化到Impala行批中。
kudu::client::KuduScanBatch cur_kudu_batch_;
/// 已经从当前Kudu行批cur_kudu_batch_中读取的行数。
int cur_kudu_batch_num_read_;
/// 上次发送Keep Alive请求或成功完成RPC请求的时间。
int64_t last_alive_time_micros_;
/// 扫描器从扫描结点克隆的表达式求值器副本。
vector<ScalarExprEvaluator*> conjunct_evals_;
/// 扫描节点的元组描述符中的时间戳槽位描述符,用于转换Kudu的UNIXTIME_MICRO值。
vector<const SlotDescriptor*> timestamp_slots_;
};
可以发现KuduScanner
中调用了许多Kudu API完成扫描的基本功能和优化,这一块逻辑不是我们分析的重点,我们要着重分析的是Scanner
对象通用的一些逻辑,所以我们接下来只看一些KuduScanner
中关键的代码。按照扫描的流程,首先是开启一个新的扫描令牌的函数OpenNextScanToken
:
Status KuduScanner::OpenNextScanToken(const string& scan_token, bool* eos) {
DCHECK(scanner_ == NULL);
kudu::client::KuduScanner* scanner;
KUDU_RETURN_IF_ERROR(kudu::client::KuduScanToken::DeserializeIntoScanner(
scan_node_->kudu_client(), scan_token, &scanner),
BuildErrorString("Unable to deserialize scan token"));
scanner_.reset(scanner);
// 省略若干非关键代码……
if (scan_node_->filter_ctxs_.size() > 0) {
// 如果有runtime filters,则尝试将其下推到Kudu。
for (const FilterContext& ctx : scan_node_->filter_ctxs_) {
// KuduScanner支持min_max过滤器。
MinMaxFilter* filter = ctx.filter->get_min_max();
// 如果过滤器不总是为true(总是为true的过滤器无法过滤任何行),那可以尝试进行下推。
if (filter != nullptr && !filter->AlwaysTrue()) {
if (filter->AlwaysFalse()) {
// 如果filter结果总是为false,则会过滤任何行,我们可以直接跳过这个扫描。
CloseCurrentClientScanner();
*eos = true;
return Status::OK();
} else {
// 获取需要过滤的Kudu列的名字和类型。
auto it = ctx.filter->filter_desc().planid_to_target_ndx.find(scan_node_->id());
const TRuntimeFilterTargetDesc& target_desc =
ctx.filter->filter_desc().targets[it->second];
const string& col_name = target_desc.kudu_col_name;
DCHECK(col_name != "");
const ColumnType& col_type = ColumnType::FromThrift(target_desc.kudu_col_type);
// 从过滤器中拿到最小值和最大值,其支持的类型包括基础类型、字符串、时间戳和定点数。
const void* min = filter->GetMin();
const void* max = filter->GetMax();
// 对于整型,如果过滤器的类型与目标列的类型不相同,则必须进行隐式整数转换,
// 我们需要确保下推给Kudu的最小/最大值在目标列类型的范围内。
int64_t int_min;
int64_t int_max;
if (col_type.type != filter->type()) {
DCHECK(col_type.IsIntegerType());
// 调用过滤器的整型转换函数,若原最小最大值范围超过了转换后类型的最小最大值范围,
// 则所有行都被会过滤掉,因此我们可以跳过扫描。
if (!filter->GetCastIntMinMax(col_type, &int_min, &int_max)) {
CloseCurrentClientScanner();
*eos = true;
return Status::OK();
}
// 指向转换后的值。
min = &int_min;
max = &int_max;
}
// 调用CreateKuduValue方法创建Kudu值类型,然后调用AddConjunctPredicate下推谓词。
KuduValue* min_value;
RETURN_IF_ERROR(CreateKuduValue(col_type, min, &min_value));
KUDU_RETURN_IF_ERROR(
scanner_->AddConjunctPredicate(scan_node_->table_->NewComparisonPredicate(
col_name, KuduPredicate::ComparisonOp::GREATER_EQUAL, min_value)),
BuildErrorString("Failed to add min predicate"));
KuduValue* max_value;
RETURN_IF_ERROR(CreateKuduValue(col_type, max, &max_value));
KUDU_RETURN_IF_ERROR(
scanner_->AddConjunctPredicate(scan_node_->table_->NewComparisonPredicate(
col_name, KuduPredicate::ComparisonOp::LESS_EQUAL, max_value)),
BuildErrorString("Failed to add max predicate"));
}
}
}
}
// 如果扫描结点有limit限制,且没有谓词,那我们可以把limit限制下推到Kudu侧。
if (scan_node_->limit() != -1 && conjunct_evals_.empty()) {
KUDU_RETURN_IF_ERROR(scanner_->SetLimit(scan_node_->limit()),
BuildErrorString("Failed to set limit on scan"));
}
{
// SCOPED_TIMER2与SCOPED_TIMER相同,但是能同时更新两个计时器。
SCOPED_TIMER2(state_->total_storage_wait_timer(), scan_node_->kudu_client_time());
KUDU_RETURN_IF_ERROR(scanner_->Open(), BuildErrorString("Unable to open scanner"));
}
*eos = false;
return Status::OK();
}
开启了扫描令牌之后,就可以调用KuduScanner::GetNext
获取下一个行批了:
Status KuduScanner::GetNext(RowBatch* row_batch, bool* eos) {
SCOPED_TIMER(scan_node_->materialize_tuple_timer());
// 对于启用优化count(*)的扫描,只写入行数,不物化行。
if (scan_node_->optimize_count_star()) {
return GetNextWithCountStarOptimization(row_batch, eos);
}
// 扫描开始前,首先调用RowBtach::ResizeAndAllocateTupleBuffer函数申请缓冲区,
int64_t tuple_buffer_size;
uint8_t* tuple_buffer;
RETURN_IF_ERROR(
row_batch->ResizeAndAllocateTupleBuffer(state_, &tuple_buffer_size, &tuple_buffer));
// 将缓冲区指针转换为元组指针。
Tuple* tuple = reinterpret_cast<Tuple*>(tuple_buffer);
// 扫描的主循环逻辑,在eos或达到行批容量时退出。
while (!*eos) {
// 查询取消则直接退出。
RETURN_IF_CANCELLED(state_);
// 已读取Kudu行批行数小于Kudu行批总行数,即当前Kudu行批还有行可以读取时,
// 调用DecodeRowsIntoRowBatch将Kudu行批中的行尽可能地解码物化到Impala行批。
if (cur_kudu_batch_num_read_ < cur_kudu_batch_.NumRows()) {
RETURN_IF_ERROR(DecodeRowsIntoRowBatch(row_batch, &tuple));
// 达到行批容量时退出。
if (row_batch->AtCapacity()) break;
}
// 到达该处说明当前Kudu行批已经全部读取完毕,需要从Kudu获取新的Kudu行批,
// 若HasMoreRows为true说明当前扫描令牌还有行可读,那么在未达到limit限制的情况下,
// 调用GetNextScannerBatch获取下一个Kudu行批,然后继续循环。
if (scanner_->HasMoreRows() && !scan_node_->ReachedLimitShared()) {
RETURN_IF_ERROR(GetNextScannerBatch());
continue;
}
// 到达该处说明没有更多的Kudu行批,当前扫描令牌已经读取完毕,设置eos为true
// 并且可以关闭对应创建的kudu::client::KuduScanner。
CloseCurrentClientScanner();
*eos = true;
}
return Status::OK();
}
看KuduScanner::GetNext
代码可以发现其中有两个关键函数,分别是从Kudu行批解码行到Impala行批的DecodeRowsIntoRowBatch
和从Kudu获取Kudu行批的GetNextScannerBatch
,接下来我们分析这两个方法。按照调用顺序,第一次调用时KuduScanner::GetNext
还没有Kudu行批,所以首先调用到的是GetNextScannerBatch
:
Status KuduScanner::GetNextScannerBatch() {
SCOPED_TIMER2(state_->total_storage_wait_timer(), scan_node_->kudu_client_time());
int64_t now = MonotonicMicros();
// 直接调用kudu::client::KuduScanner::NextBatch获取下一个Kudu行批,写入cur_kudu_batch_。
KUDU_RETURN_IF_ERROR(scanner_->NextBatch(&cur_kudu_batch_),
BuildErrorString("Unable to advance iterator"));
// 更新相关计数器,并将当前Kudu行批已读行数重置。
COUNTER_ADD(scan_node_->kudu_round_trips(), 1);
cur_kudu_batch_num_read_ = 0;
COUNTER_ADD(scan_node_->rows_read_counter(), cur_kudu_batch_.NumRows());
last_alive_time_micros_ = now;
return Status::OK();
}
可以发现GetNextScannerBatch
比较简单,核心逻辑就是调用Kudu API获取下一个KuduRowBatch
。我们继续分析DecodeRowsIntoRowBatch
:
Status KuduScanner::DecodeRowsIntoRowBatch(RowBatch* row_batch, Tuple** tuple_mem) {
// 对于关系投影为空的情况,没有需要解码物化的槽位,直接调用HandleEmptyProjection优化处理。
if (scan_node_->tuple_desc()->slots().empty()) {
return HandleEmptyProjection(row_batch);
}
bool has_conjuncts = !conjunct_evals_.empty();
int num_rows = cur_kudu_batch_.NumRows();
// 解码物化的主循环,遍历Kudu行批中的行,计算谓词并将满足谓词条件的行深拷贝到row_batch中。
for (int krow_idx = cur_kudu_batch_num_read_; krow_idx < num_rows; ++krow_idx) {
// 根据Kudu行批内存地址和行size计算当前Kudu行数据偏移量,拿到当前行数据地址并转换为元组类型指针。
Tuple* kudu_tuple = const_cast<Tuple*>(
reinterpret_cast<const Tuple*>(cur_kudu_batch_.direct_data().data()
+ (krow_idx * scan_node_->row_desc()->GetRowSize())));
// 已读行数+1。
++cur_kudu_batch_num_read_;
// 对于时间戳类型的数据进行额外的处理和转换。
for (const SlotDescriptor* slot : timestamp_slots_) {
DCHECK(slot->type().type == TYPE_TIMESTAMP);
if (slot->is_nullable() && kudu_tuple->IsNull(slot->null_indicator_offset())) {
continue;
}
int64_t ts_micros = *reinterpret_cast<int64_t*>(
kudu_tuple->GetSlot(slot->tuple_offset()));
TimestampValue tv;
if (FLAGS_use_local_tz_for_kudu_timestamp) {
tv = TimestampValue::FromUnixTimeMicros(ts_micros, state_->local_time_zone());
} else {
tv = TimestampValue::UtcFromUnixTimeMicros(ts_micros);
}
if (tv.HasDateAndTime()) {
RawValue::Write(&tv, kudu_tuple, slot, NULL);
} else {
kudu_tuple->SetNull(slot->null_indicator_offset());
RETURN_IF_ERROR(state_->LogOrReturnError(
ErrorMsg::Init(TErrorCode::KUDU_TIMESTAMP_OUT_OF_RANGE,
scan_node_->table_->name(),
scan_node_->table_->schema().Column(slot->col_pos()).name())));
}
}
// 调用ExecNode::EvalConjuncts对Kudu元组计算谓词表达式结果,不满足谓词则继续循环,放弃该行。
// Kudu元组有与Impala元组相同的内存布局,所以可以直接调用Impala的谓词评估方法。
if (has_conjuncts && !ExecNode::EvalConjuncts(conjunct_evals_.data(),
conjunct_evals_.size(), reinterpret_cast<TupleRow*>(&kudu_tuple))) {
continue;
}
// 将通过谓词检查的元组数据深拷贝到Impala行批当前元组的地址。
kudu_tuple->DeepCopy(*tuple_mem, *scan_node_->tuple_desc(),
row_batch->tuple_data_pool());
// 获取行批中下一个TupleRow地址,TupleRow即行批中的一行,可以容纳多个元组指针。
TupleRow* row = row_batch->GetRow(row_batch->AddRow());
// 设置该行的第一个元组地址为当前元组地址。
row->SetTuple(0, *tuple_mem);
// 调用CommitLastRow向行批提交最后一行,只有调用了CommitLastRow才会将最后一行添加到行批。
row_batch->CommitLastRow();
// 如果已经达到行批容量或limit限制,退出。
if (row_batch->AtCapacity() || scan_node_->ReachedLimitShared()) break;
// 移动到元组缓冲区中的下一个元组地址。
*tuple_mem = next_tuple(*tuple_mem);
}
// 定期清空谓词表达式计算结果,释放内存。
expr_results_pool_->Clear();
// 检查状态,确保在谓词评估期间设置的错误状态可以被感知。
return state_->GetQueryStatus();
}
至此,KuduScanner
的结构定义和主要逻辑我们就分析完毕了,虽然是作为和Kudu直接连接的扫描器包含了很多Kudu特有的优化和API,但是其通用的Scanner
逻辑仍然很好的体现在几个主要函数中。如果要为Impala开发其他数据源的扫描节点和扫描器,那么KuduScanNode
和KuduScanner
是很好的参考。