Impala3.4源码阅读笔记(九)解析ScanNode(下)

前言

本文为笔者个人阅读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开发其他数据源的扫描节点和扫描器,那么KuduScanNodeKuduScanner是很好的参考。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值