blob: b1ad34a628048ae1fcd00b7cea23b6d164b48661 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.impala.planner;
import java.util.List;
import org.apache.impala.analysis.Analyzer;
import org.apache.impala.analysis.Expr;
import org.apache.impala.analysis.SortInfo;
import org.apache.impala.analysis.TupleId;
import org.apache.impala.common.ImpalaException;
import org.apache.impala.service.BackendConfig;
import org.apache.impala.thrift.TExchangeNode;
import org.apache.impala.thrift.TExplainLevel;
import org.apache.impala.thrift.TPlanNode;
import org.apache.impala.thrift.TPlanNodeType;
import org.apache.impala.thrift.TQueryOptions;
import org.apache.impala.thrift.TSortInfo;
import com.google.common.base.Preconditions;
/**
* Receiver side of a 1:n data stream. Logically, an ExchangeNode consumes the data
* produced by its children. For each of the sending child nodes the actual data
* transmission is performed by the DataStreamSink of the PlanFragment housing
* that child node. Typically, an ExchangeNode only has a single sender child but,
* e.g., for distributed union queries an ExchangeNode may have one sender child per
* union operand.
*
* If a (optional) SortInfo field is set, the ExchangeNode will merge its
* inputs on the parameters specified in the SortInfo object. It is assumed that the
* inputs are also sorted individually on the same SortInfo parameter.
*/
public class ExchangeNode extends PlanNode {
// The serialization overhead per tuple in bytes when sent over an exchange.
// Currently it accounts only for the tuple_offset entry per tuple (4B) in a
// BE TRowBatch. If we modify the RowBatch serialization, then we need to
// update this constant as well.
private static final double PER_TUPLE_SERIALIZATION_OVERHEAD = 4.0;
// Empirically derived minimum estimate (in bytes) for the exchange node.
private static final int MIN_ESTIMATE_BYTES = 16 * 1024;
// The parameters based on which sorted input streams are merged by this
// exchange node. Null if this exchange does not merge sorted streams
private SortInfo mergeInfo_;
// Offset after which the exchange begins returning rows. Currently valid
// only if mergeInfo_ is non-null, i.e. this is a merging exchange node.
private long offset_;
private boolean isMergingExchange() {
return mergeInfo_ != null;
}
private boolean isBroadcastExchange() {
// If the output of the sink is not partitioned but the target fragment is
// partitioned, then the data exchange is broadcast.
Preconditions.checkState(!children_.isEmpty());
DataSink sink = getChild(0).getFragment().getSink();
if (sink == null) return false;
Preconditions.checkState(sink instanceof DataStreamSink);
DataStreamSink streamSink = (DataStreamSink) sink;
return !streamSink.getOutputPartition().isPartitioned() && fragment_.isPartitioned();
}
public ExchangeNode(PlanNodeId id, PlanNode input) {
super(id, "EXCHANGE");
offset_ = 0;
children_.add(input);
// Only apply the limit at the receiver if there are multiple senders.
if (input.getFragment().isPartitioned()) limit_ = input.limit_;
computeTupleIds();
}
@Override
public void computeTupleIds() {
clearTupleIds();
tupleIds_.addAll(getChild(0).getTupleIds());
tblRefIds_.addAll(getChild(0).getTblRefIds());
nullableTupleIds_.addAll(getChild(0).getNullableTupleIds());
}
@Override
public void init(Analyzer analyzer) throws ImpalaException {
super.init(analyzer);
Preconditions.checkState(conjuncts_.isEmpty());
}
@Override
public void computeStats(Analyzer analyzer) {
super.computeStats(analyzer);
Preconditions.checkState(children_.size() == 1);
cardinality_ = capCardinalityAtLimit(children_.get(0).getCardinality());
// Apply the offset correction if there's a valid cardinality
if (cardinality_ > -1) cardinality_ = Math.max(0, cardinality_ - offset_);
}
/**
* Set the parameters used to merge sorted input streams. This can be called
* after init().
*/
public void setMergeInfo(SortInfo info, long offset) {
mergeInfo_ = info;
offset_ = offset;
displayName_ = "MERGING-EXCHANGE";
}
@Override
protected String getNodeExplainString(String prefix, String detailPrefix,
TExplainLevel detailLevel) {
StringBuilder output = new StringBuilder();
output.append(String.format("%s%s [%s]\n", prefix,
getDisplayLabel(), getDisplayLabelDetail()));
if (offset_ > 0) {
output.append(detailPrefix + "offset: ").append(offset_).append("\n");
}
if (isMergingExchange() && detailLevel.ordinal() > TExplainLevel.MINIMAL.ordinal()) {
output.append(detailPrefix + "order by: ");
output.append(getSortingOrderExplainString(mergeInfo_.getSortExprs(),
mergeInfo_.getIsAscOrder(), mergeInfo_.getNullsFirstParams(),
mergeInfo_.getSortingOrder()));
}
return output.toString();
}
/**
* An Exchange simply moves rows over the network: its row width
* and cardinality are identical to its input. So, for standard
* level, there is no need to repeat these values. Retained in
* higher levels for backward compatibility.
*/
@Override
protected boolean displayCardinality(TExplainLevel detailLevel) {
return detailLevel.ordinal() >= TExplainLevel.EXTENDED.ordinal();
}
@Override
protected String getDisplayLabelDetail() {
// For the non-fragmented explain levels, print the data partition
// of the data stream sink that sends to this exchange node.
Preconditions.checkState(!children_.isEmpty());
DataSink sink = getChild(0).getFragment().getSink();
if (sink == null) return "";
if (isBroadcastExchange()) {
return "BROADCAST";
} else {
Preconditions.checkState(sink instanceof DataStreamSink);
DataStreamSink streamSink = (DataStreamSink) sink;
return streamSink.getOutputPartition().getExplainString();
}
}
/**
* Returns the average size of rows produced by 'exchInput' when serialized for
* being sent through an exchange.
*/
public static double getAvgSerializedRowSize(PlanNode exchInput) {
return exchInput.getAvgRowSize() +
(exchInput.getTupleIds().size() * PER_TUPLE_SERIALIZATION_OVERHEAD);
}
@Override
public void computeNodeResourceProfile(TQueryOptions queryOptions) {
// For non-merging exchanges, one row batch queue is maintained for row
// batches from all sender fragment instances. For merging exchange, one
// queue is created for the batches from each distinct sender. There is a
// soft limit on every row batch queue of FLAGS_exchg_node_buffer_size_bytes
// (default 10MB). There is also a deferred rpc queue which queues at max
// one rpc payload (containing the rowbatch) per sender in-case the row
// batch queue hits the previously mentioned soft limit. Actual memory used
// depends on the row size (that can vary a lot due to presence of var len
// strings) and on the rate at which rows are received and consumed from the
// exchange node which in turn depends on the complexity of the query and
// the system load. This makes it difficult to accurately estimate the
// memory usage at runtime. The following estimates assume that memory usage will
// lean towards the soft limits.
Preconditions.checkState(!children_.isEmpty());
Preconditions.checkNotNull(children_.get(0).getFragment());
int numSenders = children_.get(0).getFragment().getNumInstances(queryOptions.mt_dop);
long estimatedTotalQueueByteSize = estimateTotalQueueByteSize(numSenders);
long estimatedDeferredRPCQueueSize = estimateDeferredRPCQueueSize(queryOptions,
numSenders);
long estimatedMem = Math.max(
checkedAdd(estimatedTotalQueueByteSize, estimatedDeferredRPCQueueSize),
MIN_ESTIMATE_BYTES);
nodeResourceProfile_ = ResourceProfile.noReservation(estimatedMem);
}
// Returns the estimated size of the deferred batch queue (in bytes) by
// assuming that at least one row batch rpc payload per sender is queued.
private long estimateDeferredRPCQueueSize(TQueryOptions queryOptions,
int numSenders) {
long rowBatchSize = queryOptions.isSetBatch_size() && queryOptions.batch_size > 0
? queryOptions.batch_size : DEFAULT_ROWBATCH_SIZE;
// Set an upper limit based on estimated cardinality.
if (getCardinality() > 0) rowBatchSize = Math.min(rowBatchSize, getCardinality());
long avgRowBatchByteSize = Math.min(
(long) Math.ceil(rowBatchSize * getAvgSerializedRowSize(this)),
ROWBATCH_MAX_MEM_USAGE);
long deferredBatchQueueSize = avgRowBatchByteSize * numSenders;
return deferredBatchQueueSize;
}
// Returns the total estimated size (in bytes) of the row batch queues by
// assuming enough batches can be queued such that it hits the row batch
// queue's soft mem limit.
private long estimateTotalQueueByteSize(int numSenders) {
int numQueues = isMergingExchange() ? numSenders : 1;
long maxQueueByteSize = BackendConfig.INSTANCE.getBackendCfg().
exchg_node_buffer_size_bytes;
// TODO: Should we set a better default size here? This might be alot for
// queries without stats.
long estimatedTotalQueueByteSize = numQueues * maxQueueByteSize;
// Set an upper limit based on estimated cardinality.
if (hasValidStats()) {
long totalBytesToReceive = (long) Math.ceil(getAvgRowSize() * getCardinality());
// Assuming no skew in distribution during data shuffling.
long bytesToReceivePerExchNode = isBroadcastExchange() ? totalBytesToReceive
: totalBytesToReceive / getNumNodes();
estimatedTotalQueueByteSize = Math.min(bytesToReceivePerExchNode,
estimatedTotalQueueByteSize);
}
return estimatedTotalQueueByteSize;
}
@Override
public ExecPhaseResourceProfiles computeTreeResourceProfiles(
TQueryOptions queryOptions) {
// Don't include resources of child in different plan fragment.
return new ExecPhaseResourceProfiles(nodeResourceProfile_, nodeResourceProfile_);
}
@Override
protected void toThrift(TPlanNode msg) {
msg.node_type = TPlanNodeType.EXCHANGE_NODE;
msg.exchange_node = new TExchangeNode();
for (TupleId tid: tupleIds_) {
msg.exchange_node.addToInput_row_tuples(tid.asInt());
}
if (isMergingExchange()) {
TSortInfo sortInfo = new TSortInfo(
Expr.treesToThrift(mergeInfo_.getSortExprs()), mergeInfo_.getIsAscOrder(),
mergeInfo_.getNullsFirst(), mergeInfo_.getSortingOrder());
msg.exchange_node.setSort_info(sortInfo);
msg.exchange_node.setOffset(offset_);
}
}
}