Skip to content

Commit 536f7c6

Browse files
csringhoferImpala Public Jenkins
authored andcommitted
IMPALA-13475: Mem limit deferred RPC processing parallelism
Before this patch KrpcDataStreamRecvr::SenderQueue::GetBatch() didn't consider the mem requirement of the batches when initiating the deserialization of deferred RCPs (EnqueueDeserializeTask) and tried to deserialize as many batches in parallel as possible, based on FLAGS_datastream_service_num_deserialization_threads. This meant that some of the awakened threads could be rejected due to hitting mem limit. In this case the deferred RPC was put back in queue. Besides wasting resources this could also lead to starving bigger RPCs by rejecting them and letting smaller RPCs take over. The patch introduces the concept of "pending deferred RPCs", which are RPCs where processing was requested, but the deserialization thread didn't pick up the RPCs yet. The deserialized size of pending deferred RPCs is considered already taken and only N deserializations are started if the first N RPC's payload would fit to memory (with the exception of empty queue where at least 1 is started). The state flow of deferred RPCs became the following where each transition should be able to succeed: 1. no deserialization thread started (in deferred_rpcs_) 2. deserialization thread started (in pending_deferred_rpcs_) 3. currently deserialized (tracked in num_pending_enqueue_) 4. enqueued Note that it is still possible for batches to take over in 3 if their deserialization is faster. Merging exchanges need special handling as those had multiple queues with shared mem limit before this patch. The patch splits the mem limit evenly among the queues to allow them enforcing the "respect mem limit, with the exception when empty" rule individually, as at least one batch in queue per sender is critical to allow the merge sort to progress. Mem limit of closed queues is reused by counting the active queues in atomic int 'num_active_queues_' and using this to calculate the share of total mem limit a queue can use. There is still some risk of perf regression in skewed merging exchanges where one sender may "need" the queue space more than others. Some parts are generated by copilot + Claude Sonnet 4.6 Change-Id: I4d8e2b07a19e4d30210d7903c01e75fba632a1de Reviewed-on: http://gerrit.cloudera.org:8080/22217 Reviewed-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com> Tested-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com>
1 parent 11b5fd6 commit 536f7c6

8 files changed

Lines changed: 343 additions & 176 deletions

File tree

be/src/runtime/krpc-data-stream-mgr.cc

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ DEFINE_int32(datastream_sender_timeout_ms, 120000, "(Advanced) The time, in ms,
6060
"elapse before a plan fragment will time-out trying to send the initial row batch.");
6161
DEFINE_int32(datastream_service_num_deserialization_threads, 16,
6262
"Number of threads for deserializing RPC requests deferred due to the receiver "
63-
"not ready or the soft limit of the receiver is reached.");
63+
"not ready or the soft limit of the receiver is reached. "
64+
"If 0, it will be set to number of CPU cores.");
6465
DEFINE_int32(datastream_service_deserialization_queue_size, 10000,
6566
"Number of deferred RPC requests that can be enqueued before being processed by a "
6667
"deserialization thread.");
@@ -69,10 +70,13 @@ using std::mutex;
6970
namespace impala {
7071

7172
KrpcDataStreamMgr::KrpcDataStreamMgr(MetricGroup* metrics)
72-
: deserialize_pool_("data-stream-mgr", "deserialize",
73-
FLAGS_datastream_service_num_deserialization_threads,
74-
FLAGS_datastream_service_deserialization_queue_size,
75-
boost::bind(&KrpcDataStreamMgr::DeserializeThreadFn, this, _1, _2)) {
73+
: num_deserialization_threads_(
74+
FLAGS_datastream_service_num_deserialization_threads > 0
75+
? FLAGS_datastream_service_num_deserialization_threads : CpuInfo::num_cores()),
76+
deserialize_pool_("data-stream-mgr", "deserialize",
77+
num_deserialization_threads_,
78+
FLAGS_datastream_service_deserialization_queue_size,
79+
boost::bind(&KrpcDataStreamMgr::DeserializeThreadFn, this, _1, _2)) {
7680
MetricGroup* dsm_metrics = metrics->GetOrCreateChildGroup("datastream-manager");
7781
num_senders_waiting_ =
7882
dsm_metrics->AddGauge("senders-blocked-on-recvr-creation", 0L);
@@ -473,4 +477,11 @@ KrpcDataStreamMgr::~KrpcDataStreamMgr() {
473477
deserialize_pool_.Join();
474478
}
475479

480+
TransmitDataCtx::TransmitDataCtx(
481+
const TransmitDataRequestPB* request, TransmitDataResponsePB* response,
482+
kudu::rpc::RpcContext* rpc_context)
483+
: request(request), response(response), rpc_context(rpc_context),
484+
deserialized_size(RowBatch::GetDeserializedSize(request->row_batch_header())) { }
485+
486+
476487
} // namespace impala

be/src/runtime/krpc-data-stream-mgr.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ struct TransmitDataCtx {
177177
/// has been responded to. Not owned.
178178
kudu::rpc::RpcContext* rpc_context;
179179

180+
/// Memory needed by the batch after deserialization.
181+
int64_t deserialized_size;
182+
180183
TransmitDataCtx(const TransmitDataRequestPB* request, TransmitDataResponsePB* response,
181-
kudu::rpc::RpcContext* rpc_context)
182-
: request(request), response(response), rpc_context(rpc_context) { }
184+
kudu::rpc::RpcContext* rpc_context);
183185
};
184186

185187
/// Context for an EndDataStream() RPC. This structure is constructed when the RPC is
@@ -288,6 +290,8 @@ class KrpcDataStreamMgr : public CacheLineAligned {
288290
/// Waits for maintenance thread and sender response thread pool to finish.
289291
~KrpcDataStreamMgr();
290292

293+
int num_deserialization_threads() const { return num_deserialization_threads_; }
294+
291295
private:
292296
friend class KrpcDataStreamRecvr;
293297
friend class DataStreamTest;
@@ -320,6 +324,9 @@ class KrpcDataStreamMgr : public CacheLineAligned {
320324
int64_t enqueue_time_ns;
321325
};
322326

327+
/// Sets the number of threads in deserialize_pool_.
328+
const int num_deserialization_threads_;
329+
323330
/// Set of threads which deserialize buffered row batches, and deliver them to their
324331
/// receivers. Used only if RPCs were deferred when their channel's batch queue was
325332
/// full or if the receiver was not yet prepared.

be/src/runtime/krpc-data-stream-recvr.cc

Lines changed: 231 additions & 113 deletions
Large diffs are not rendered by default.

be/src/runtime/krpc-data-stream-recvr.h

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,6 @@ class KrpcDataStreamRecvr {
161161
/// sender queue. Called from KrpcDataStreamMgr.
162162
void RemoveSender(int sender_id);
163163

164-
/// Return true if the addition of a new batch of size 'batch_size' would exceed the
165-
/// total buffer limit.
166-
bool ExceedsLimit(int64_t batch_size) {
167-
return num_buffered_bytes_.Load() + batch_size > total_buffer_limit_;
168-
}
169-
170164
/// Return the current number of deferred RPCs.
171165
int64_t num_deferred_rpcs() const { return num_deferred_rpcs_.Load(); }
172166

@@ -180,11 +174,6 @@ class KrpcDataStreamRecvr {
180174
TUniqueId fragment_instance_id_;
181175
PlanNodeId dest_node_id_;
182176

183-
/// Soft upper limit on the total amount of buffering in bytes allowed for this stream
184-
/// across all sender queues. We defer processing of incoming RPCs once the amount of
185-
/// buffered data exceeds this value.
186-
const int64_t total_buffer_limit_;
187-
188177
/// Row schema. Not owned.
189178
const RowDescriptor* row_desc_;
190179

@@ -196,12 +185,20 @@ class KrpcDataStreamRecvr {
196185
/// from the fragment execution thread.
197186
bool closed_;
198187

199-
/// Current number of bytes held across all sender queues.
200-
AtomicInt32 num_buffered_bytes_;
201-
202188
/// Current number of outstanding deferred RPCs across all sender queues.
203189
AtomicInt64 num_deferred_rpcs_;
204190

191+
/// Total buffer limit across all sender queues as passed to the constructor. Each
192+
/// queue derives its effective per-queue limit at runtime by dividing this value by
193+
/// the current value of 'num_active_queues_' below, so that finished queues' budget
194+
/// is automatically absorbed by the remaining active queues.
195+
const int64_t total_buffer_limit_;
196+
197+
/// Number of sender queues whose sender has not yet finished. Decremented atomically
198+
/// in RemoveSender(). Queues read this to compute their effective buffer limit.
199+
/// Only meaningful when is_merging_ is true.
200+
AtomicInt32 num_active_queues_;
201+
205202
/// Memtracker for payloads of deferred Rpcs in the sender queue(s). This must be
206203
/// accessed with a sender queue's lock held to avoid race with Close() of the queue.
207204
boost::scoped_ptr<MemTracker> deferred_rpc_tracker_;
@@ -296,6 +293,9 @@ class KrpcDataStreamRecvr {
296293
/// Total wall-clock time in which the 'deferred_rpcs_' queues are not empty.
297294
RuntimeProfile::Counter* total_has_deferred_rpcs_timer_;
298295

296+
/// Total wall-clock time in which the 'pending_deferred_rpcs_' queues are not empty.
297+
RuntimeProfile::Counter* total_has_pending_deferred_rpcs_timer_;
298+
299299
/// Summary stats of time which RPCs spent in KRPC service queue before
300300
/// being dispatched to the RPC handlers.
301301
RuntimeProfile::SummaryStatsCounter* dispatch_timer_;

be/src/runtime/row-batch.cc

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,9 @@ void RowBatch::SetMemTracker(MemTracker* new_tracker) {
393393
mem_tracker_ = new_tracker;
394394
}
395395

396-
int64_t RowBatch::GetDeserializedSize(const RowBatchHeaderPB& header,
397-
const kudu::Slice& tuple_offsets) {
398-
DCHECK_EQ(tuple_offsets.size() % sizeof(int32_t), 0);
399-
return header.uncompressed_size() +
400-
(tuple_offsets.size() / sizeof(int32_t)) * sizeof(Tuple*);
396+
int64_t RowBatch::GetDeserializedSize(const RowBatchHeaderPB& header) {
397+
return header.uncompressed_size()
398+
+ header.num_rows() * header.num_tuples_per_row() * sizeof(Tuple*);
401399
}
402400

403401
int64_t RowBatch::GetDeserializedSize(const OutboundRowBatch& batch) {

be/src/runtime/row-batch.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,7 @@ class RowBatch {
366366
/// less than the deserialized size.
367367
static int64_t GetSerializedSize(const OutboundRowBatch& batch);
368368
static int64_t GetDeserializedSize(const OutboundRowBatch& batch);
369-
static int64_t GetDeserializedSize(const RowBatchHeaderPB& header,
370-
const kudu::Slice& tuple_offsets);
369+
static int64_t GetDeserializedSize(const RowBatchHeaderPB& header);
371370

372371
int ALWAYS_INLINE num_rows() const { return num_rows_; }
373372
int ALWAYS_INLINE capacity() const { return capacity_; }

be/src/util/spinlock.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class SpinLock {
4646
}
4747

4848
/// Verify that the lock is held.
49-
void DCheckLocked() { DCHECK(l_.IsHeld()); }
49+
void DCheckLocked() const { DCHECK(l_.IsHeld()); }
5050

5151
private:
5252
/// The underlying SpinLock from gutil.

tests/custom_cluster/test_exchange_deferred_batches.py

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,50 +17,84 @@
1717

1818
from __future__ import absolute_import, division, print_function
1919
from builtins import range
20+
import time
2021
import pytest
2122
from tests.common.custom_cluster_test_suite import CustomClusterTestSuite
2223
from tests.common.skip import SkipIfBuildType
24+
from tests.util.cancel_util import cancel_query_and_validate_state
2325

2426

2527
@SkipIfBuildType.not_dev_build
2628
class TestExchangeDeferredBatches(CustomClusterTestSuite):
2729

28-
@classmethod
29-
def setup_class(cls):
30-
if cls.exploration_strategy() != 'exhaustive':
31-
pytest.skip('runs only in exhaustive')
32-
super(TestExchangeDeferredBatches, cls).setup_class()
30+
@classmethod
31+
def setup_class(cls):
32+
if cls.exploration_strategy() != 'exhaustive':
33+
pytest.skip('runs only in exhaustive')
34+
super(TestExchangeDeferredBatches, cls).setup_class()
3335

34-
@pytest.mark.execute_serially
35-
@CustomClusterTestSuite.with_args(
36-
"--stress_datastream_recvr_delay_ms=3000"
37-
+ " --exchg_node_buffer_size_bytes=1024"
38-
+ " --datastream_service_num_deserialization_threads=1"
39-
+ " --impala_slow_rpc_threshold_ms=500")
40-
def test_exchange_small_buffer(self, vector):
41-
"""Exercise the code which handles deferred row batches. In particular,
42-
the exchange buffer is set to a small value to cause incoming row batches
43-
to be deferred at the receiver. Also, use a single deserialization thread
44-
to limit the speed in which the deferred row batches are dequeued. These
45-
settings help expose the race in IMPALA-8239 when there is any error
46-
deserializing deferred row batches."""
36+
@pytest.mark.execute_serially
37+
@CustomClusterTestSuite.with_args(
38+
"--stress_datastream_recvr_delay_ms=3000"
39+
+ " --exchg_node_buffer_size_bytes=1024"
40+
+ " --datastream_service_num_deserialization_threads=1"
41+
+ " --impala_slow_rpc_threshold_ms=500")
42+
def test_exchange_small_buffer(self, vector):
43+
"""Exercise the code which handles deferred row batches. In particular,
44+
the exchange buffer is set to a small value to cause incoming row batches
45+
to be deferred at the receiver. Also, use a single deserialization thread
46+
to limit the speed in which the deferred row batches are dequeued. These
47+
settings help expose the race in IMPALA-8239 when there is any error
48+
deserializing deferred row batches."""
4749

48-
TEST_QUERY = "select count(*) from tpch.lineitem t1, tpch.lineitem t2 " +\
49-
"where t1.l_orderkey = t2.l_orderkey"
50-
EXPECTED_RESULT = ['30012985']
50+
TEST_QUERY = "select count(*) from tpch.lineitem t1, tpch.lineitem t2 " +\
51+
"where t1.l_orderkey = t2.l_orderkey"
52+
EXPECTED_RESULT = ['30012985']
5153

52-
for i in range(10):
53-
# Simulate row batch insertion failure. This triggers IMPALA-8239.
54-
debug_action = 'RECVR_ADD_BATCH:FAIL@0.8'
55-
self.execute_query_expect_failure(self.client, TEST_QUERY,
56-
query_options={'debug_action': debug_action})
54+
for i in range(10):
55+
# Simulate row batch insertion failure. This triggers IMPALA-8239.
56+
debug_action = 'RECVR_ADD_BATCH:FAIL@0.8'
57+
self.execute_query_expect_failure(self.client, TEST_QUERY,
58+
query_options={'debug_action': debug_action})
5759

58-
for i in range(10):
59-
# Simulate row batch insertion failure. This triggers IMPALA-8239.
60-
debug_action = 'RECVR_UNPACK_PAYLOAD:FAIL@0.8'
61-
self.execute_query_expect_failure(self.client, TEST_QUERY,
62-
query_options={'debug_action': debug_action})
60+
for i in range(10):
61+
# Simulate row batch insertion failure. This triggers IMPALA-8239.
62+
debug_action = 'RECVR_UNPACK_PAYLOAD:FAIL@0.8'
63+
self.execute_query_expect_failure(self.client, TEST_QUERY,
64+
query_options={'debug_action': debug_action})
6365

64-
# Do a run with no debug action to make sure things are sane.
65-
result = self.execute_query(TEST_QUERY, vector.get_value('exec_option'))
66-
assert result.data == EXPECTED_RESULT
66+
# Do a run with no debug action to make sure things are sane.
67+
result = self.execute_query(TEST_QUERY, vector.get_value('exec_option'))
68+
assert result.data == EXPECTED_RESULT
69+
70+
@pytest.mark.execute_serially
71+
@CustomClusterTestSuite.with_args(
72+
" --exchg_node_buffer_size_bytes=32768"
73+
+ " --datastream_service_num_deserialization_threads=16")
74+
def test_parallel_deserialization_buffer_limit(self, vector):
75+
"""The test reproduces the scenario in IMPALA-13475 - delay is added to
76+
the receiver to cause most TransmitData RPCs to be deferred and buffer
77+
size is set to a small enough value where the queue can only hold 2 RowBatches
78+
which is not enough for a row batch from all 3 senders. This means that the
79+
receiver must limit the number of parallel deserialization tasks started to
80+
ensure that the queue has enough memory for the result of all started tasks."""
81+
82+
TEST_QUERY = "select count(*) from tpch.lineitem t1, tpch.lineitem t2 " +\
83+
"where t1.l_orderkey = t2.l_orderkey"
84+
EXPECTED_RESULT = ['30012985']
85+
86+
# There are ~1100 row batches per exchange node under the join, with 1ms
87+
# sleep they combine to guaranteed >2s run time.
88+
query_options = {'debug_action': 'RECVR_ADD_BATCH:SLEEP@1'}
89+
start = time.time()
90+
result = self.execute_query(TEST_QUERY, query_options=query_options)
91+
elapsed = time.time() - start
92+
assert result.data == EXPECTED_RESULT
93+
assert elapsed > 2, \
94+
"Query should take >2s due to RECVR_ADD_BATCH:SLEEP@1, got %.2fs" % elapsed
95+
96+
# Test that the query can be cancelled at several points. The query takes around
97+
# 3sec due to the debug_action.
98+
for cancel_delay in [0.5, 1.0, 1.5, 2.0, 2.5]:
99+
cancel_query_and_validate_state(TEST_QUERY,
100+
query_options, vector.get_value('table_format'), cancel_delay)

0 commit comments

Comments
 (0)