Skip to content

Commit ac1e178

Browse files
pranavylboroknagyz
authored andcommitted
IMPALA-14523: JDBC Scan Parallelism with Shared Cursor Fetching
Impala’s current JDBC DataSourceScan creates a separate JDBC connection per scanner thread, but since JDBC cursors are inherently sequential, this limits effective parallelism and introduces unnecessary connection and JNI overhead. As a result, even with MT_DOP enabled, scans do not scale efficiently and can suffer from poor throughput for large datasets. This patch introduces a shared JDBC connection model per fragment, allowing multiple scanner threads to cooperatively fetch from a single cursor. A new SharedJdbcConnection abstraction is added and owned by DataSourceScanPlanNode, ensuring the connection is opened and closed exactly once. Multiple DataSourceScanNode instances invoke FetchBatch() concurrently, where cursor movement is serialized while row materialization happens in parallel, and EOS is tracked via an atomic flag to avoid redundant fetches. Planner and scheduler changes ensure all JDBC scan ranges are pinned to a single executor using a new is_data_source_scan flag, and one virtual scan range is generated per MT_DOP instance. On the Java side, batch fetching with minimal locking is introduced, and JdbcDataSource is updated for safe concurrent access and improved materialization, reducing overhead and improving scan throughput. For instance, for query: WITH recent_orders AS ( SELECT o.o_orderkey, o.o_custkey, o.o_totalprice, o.o_orderdate FROM tpch_jdbc.orders o WHERE o.o_orderdate >= '1995-01-01' ) SELECT c.c_custkey, c.c_name, c.c_mktsegment, COUNT(ro.o_orderkey) AS num_orders, SUM(ro.o_totalprice) AS total_spent, AVG(ro.o_totalprice) AS avg_order_value, RANK() OVER (ORDER BY SUM(ro.o_totalprice) DESC) AS spend_rank, CASE WHEN SUM(ro.o_totalprice) > 500000 THEN 'VIP' WHEN SUM(ro.o_totalprice) > 100000 THEN 'Premium' ELSE 'Regular' END AS customer_tier, s.s_name AS supplier_name, s.s_acctbal AS supplier_balance FROM tpch_jdbc.customer c JOIN recent_orders ro ON c.c_custkey = ro.o_custkey JOIN tpch_jdbc.supplier s ON c.c_nationkey = s.s_nationkey GROUP BY c.c_custkey, c.c_name, c.c_mktsegment, s.s_name, s.s_acctbal ORDER BY total_spent DESC LIMIT 5; It takes around 260 sec with mt_dop =1 while with mt_dop =7, it takes close to 77 sec. Also added JdbcJniWaitTime, JdbcCursorFetchTime, JdbcLockWaitTime and JdbcJniCallCount for better analysis of jdbc queries, in query profile. Testing: Some planner tests are included in jdbc-parallel.test and benchmark tests are added in test_ext_data_sources.py. Generated by: Some codes and comments are generated with the help of claude-4.6-sonnet-medium-thinking. Change-Id: I3b25b99f5cb77d32c111ba37c0a01378ffdc1107 Reviewed-on: http://gerrit.cloudera.org:8080/24133 Tested-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com> Reviewed-by: Zoltan Borok-Nagy <boroknagyz@cloudera.com>
1 parent 536f7c6 commit ac1e178

13 files changed

Lines changed: 2639 additions & 172 deletions

File tree

be/src/exec/data-source-scan-node.cc

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "util/jni-util.h"
3838
#include "util/periodic-counter-updater.h"
3939
#include "util/runtime-profile-counters.h"
40+
#include "util/time.h"
4041
#include "util/ubsan.h"
4142

4243
#include "common/names.h"
@@ -52,6 +53,26 @@ namespace impala {
5253
PROFILE_DEFINE_COUNTER(NumExternalDataSourceGetNext, DEBUG, TUnit::UNIT,
5354
"The total number of calls to ExternalDataSource::GetNext()");
5455

56+
PROFILE_DEFINE_TIMER(JdbcJniWaitTime, UNSTABLE,
57+
"Aggregate wall-clock time this scanner thread spent blocked inside the full "
58+
"JNI/Java JDBC round-trip (Thrift serialization + fetchLock wait + JDBC cursor "
59+
"fetch + Thrift deserialization). Use JdbcCursorFetchTime and JdbcLockWaitTime "
60+
"for the Java-reported sub-components.");
61+
62+
PROFILE_DEFINE_TIMER(JdbcCursorFetchTime, UNSTABLE,
63+
"Aggregate time this scanner thread held the Java-side fetchLock while iterating "
64+
"the JDBC cursor, as reported by the Java layer. Directly shows JDBC cursor "
65+
"utilisation; divide by JdbcJniWaitTime to get the cursor-active fraction.");
66+
67+
PROFILE_DEFINE_TIMER(JdbcLockWaitTime, UNSTABLE,
68+
"Aggregate time this scanner thread waited to acquire the Java-side fetchLock "
69+
"before gaining access to the JDBC cursor, as reported by the Java layer. "
70+
"High values indicate cursor contention between concurrent scanner threads.");
71+
72+
PROFILE_DEFINE_COUNTER(JdbcJniCallCount, DEBUG, TUnit::UNIT,
73+
"Number of JNI GetNext() calls issued to the JDBC data source by "
74+
"this scanner thread.");
75+
5576
// $0 = num expected cols, $1 = actual num columns
5677
const string ERROR_NUM_COLUMNS = "Data source returned unexpected number of columns. "
5778
"Expected $0 but received $1. This likely indicates a problem with the data source "
@@ -73,9 +94,80 @@ const string ERROR_MEM_LIMIT_EXCEEDED = "DataSourceScanNode::$0() failed to allo
7394
// Size of an encoded TIMESTAMP
7495
const size_t TIMESTAMP_SIZE = sizeof(int64_t) + sizeof(int32_t);
7596

76-
DataSourceScanNode::DataSourceScanNode(ObjectPool* pool, const ScanPlanNode& pnode,
77-
const DescriptorTbl& descs)
97+
// -----------------------------------------------------------------------
98+
// SharedJdbcConnection
99+
// -----------------------------------------------------------------------
100+
101+
Status SharedJdbcConnection::Open(const string& jar_path, const string& class_name,
102+
const string& api_version, const string& init_string,
103+
const extdatasource::TOpenParams& params, extdatasource::TOpenResult* result) {
104+
ref_count_.fetch_add(1, std::memory_order_relaxed);
105+
106+
std::unique_lock<std::mutex> lk(open_mu_);
107+
if (!open_done_) {
108+
// First caller: initialize the executor and open the Java data source.
109+
// All N C++ scanner threads share this single connection.
110+
Status s = executor_.Init(jar_path, class_name, api_version, init_string);
111+
if (s.ok()) s = executor_.Open(params, result);
112+
if (s.ok()) s = Status(result->status);
113+
if (s.ok()) scan_handle_ = result->scan_handle;
114+
open_status_ = s;
115+
open_done_ = true;
116+
open_cv_.notify_all();
117+
return s;
118+
}
119+
120+
// Subsequent callers: wait for the first caller to finish, then reuse.
121+
open_cv_.wait(lk, [this] { return open_done_; });
122+
result->__set_scan_handle(scan_handle_);
123+
return open_status_;
124+
}
125+
126+
Status SharedJdbcConnection::FetchBatch(extdatasource::TGetNextResult* result) {
127+
// Fast path: another thread already exhausted the stream.
128+
if (eos_.load(std::memory_order_acquire)) {
129+
result->__set_eos(true);
130+
return Status::OK();
131+
}
132+
133+
extdatasource::TGetNextParams params;
134+
params.__set_scan_handle(scan_handle_);
135+
136+
// Multiple threads may call GetNext() concurrently. Serialization is provided by
137+
// the Java-side fetchLock in JdbcRecordIterator::fetchBatch(), so only one thread
138+
// holds the JDBC cursor at a time. Batch conversion happens in parallel in C++.
139+
RETURN_IF_ERROR(executor_.GetNext(params, result));
140+
if (result->eos) eos_.store(true, std::memory_order_release);
141+
return Status(result->status);
142+
}
143+
144+
Status SharedJdbcConnection::Close(const extdatasource::TCloseParams& params,
145+
extdatasource::TCloseResult* result) {
146+
// Only the last active instance closes the shared JDBC connection.
147+
if (ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
148+
// Guard against the case where Open() was never called or failed: only invoke
149+
// executor_.Close() if the Java data source was successfully opened.
150+
std::unique_lock<std::mutex> lk(open_mu_);
151+
bool should_close = open_done_ && open_status_.ok();
152+
lk.unlock();
153+
if (should_close) return executor_.Close(params, result);
154+
}
155+
return Status::OK();
156+
}
157+
158+
// DataSourceScanPlanNode
159+
Status DataSourceScanPlanNode::CreateExecNode(RuntimeState* state,
160+
ExecNode** node) const {
161+
*node = state->obj_pool()->Add(
162+
new DataSourceScanNode(state->obj_pool(), *this, state->desc_tbl()));
163+
return Status::OK();
164+
}
165+
166+
//DataSourceScanNode
167+
DataSourceScanNode::DataSourceScanNode(ObjectPool* pool,
168+
const DataSourceScanPlanNode& pnode, const DescriptorTbl& descs)
78169
: ScanNode(pool, pnode, descs),
170+
plan_node_(pnode),
79171
data_src_node_(pnode.tnode_->data_source_node),
80172
tuple_idx_(0),
81173
num_rows_(0),
@@ -90,14 +182,17 @@ Status DataSourceScanNode::Prepare(RuntimeState* state) {
90182
tuple_desc_ = state->desc_tbl().GetTupleDescriptor(data_src_node_.tuple_id);
91183
DCHECK(tuple_desc_ != NULL);
92184

93-
data_source_executor_.reset(new ExternalDataSourceExecutor());
94-
RETURN_IF_ERROR(data_source_executor_->Init(data_src_node_.data_source.hdfs_location,
95-
data_src_node_.data_source.class_name, data_src_node_.data_source.api_version,
96-
data_src_node_.init_string));
185+
// Point at the plan node's shared connection. The object is owned by the plan node
186+
// and lives for the lifetime of the fragment; no construction needed here.
187+
shared_conn_ = &plan_node_.shared_conn;
97188

98189
cols_next_val_idx_.resize(tuple_desc_->slots().size(), 0);
99190
num_ext_data_source_get_next_ =
100191
PROFILE_NumExternalDataSourceGetNext.Instantiate(runtime_profile_);
192+
jdbc_jni_wait_timer_ = PROFILE_JdbcJniWaitTime.Instantiate(runtime_profile_);
193+
jdbc_cursor_fetch_timer_ = PROFILE_JdbcCursorFetchTime.Instantiate(runtime_profile_);
194+
jdbc_lock_wait_timer_ = PROFILE_JdbcLockWaitTime.Instantiate(runtime_profile_);
195+
jdbc_jni_call_count_ = PROFILE_JdbcJniCallCount.Instantiate(runtime_profile_);
101196
return Status::OK();
102197
}
103198

@@ -107,7 +202,8 @@ Status DataSourceScanNode::Open(RuntimeState* state) {
107202
RETURN_IF_ERROR(ExecNode::Open(state));
108203
RETURN_IF_CANCELLED(state);
109204

110-
// Prepare the schema for TOpenParams.row_schema
205+
// Build TOpenParams. All N scanner threads share the single Java data source stream,
206+
// with fetch serialization provided by the Java-side fetchLock in JdbcRecordIterator.
111207
vector<extdatasource::TColumnDesc> cols;
112208
for (const SlotDescriptor* slot: tuple_desc_->slots()) {
113209
extdatasource::TColumnDesc col;
@@ -128,10 +224,18 @@ Status DataSourceScanNode::Open(RuntimeState* state) {
128224
params.__set_batch_size(FLAGS_data_source_batch_size);
129225
params.__set_predicates(data_src_node_.accepted_predicates);
130226
params.__set_clean_dbcp_ds_cache(state->query_options().clean_dbcp_ds_cache);
227+
228+
// The first caller across all N scanner threads initializes the connection;
229+
// all others block on open_cv_ until it is ready and then reuse it.
131230
TOpenResult result;
132-
RETURN_IF_ERROR(data_source_executor_->Open(params, &result));
133-
RETURN_IF_ERROR(Status(result.status));
134-
scan_handle_ = result.scan_handle;
231+
RETURN_IF_ERROR(shared_conn_->Open(
232+
data_src_node_.data_source.hdfs_location,
233+
data_src_node_.data_source.class_name,
234+
data_src_node_.data_source.api_version,
235+
data_src_node_.init_string,
236+
params, &result));
237+
scan_handle_ = shared_conn_->scan_handle();
238+
135239
return GetNextInputBatch();
136240
}
137241

@@ -161,15 +265,30 @@ Status DataSourceScanNode::ValidateRowBatchSize() {
161265
}
162266

163267
Status DataSourceScanNode::GetNextInputBatch() {
164-
input_batch_.reset(new TGetNextResult());
165268
next_row_idx_ = 0;
166-
// Reset all the indexes into the column value arrays to 0
167269
Ubsan::MemSet(cols_next_val_idx_.data(), 0, sizeof(int) * cols_next_val_idx_.size());
168-
TGetNextParams params;
169-
params.__set_scan_handle(scan_handle_);
270+
271+
input_batch_.reset(new TGetNextResult());
272+
// Short-circuit: another scanner thread already reached EOS on the shared stream.
273+
if (shared_conn_->eos()) {
274+
input_batch_->__set_eos(true);
275+
return Status::OK();
276+
}
277+
// FetchBatch() serializes the JNI call via the Java-side fetchLock; this scan node
278+
// then materializes the returned batch in parallel while others may be fetching.
170279
COUNTER_ADD(num_ext_data_source_get_next_, 1);
171-
RETURN_IF_ERROR(data_source_executor_->GetNext(params, input_batch_.get()));
172-
RETURN_IF_ERROR(Status(input_batch_->status));
280+
COUNTER_ADD(jdbc_jni_call_count_, 1);
281+
{
282+
SCOPED_TIMER(jdbc_jni_wait_timer_);
283+
RETURN_IF_ERROR(shared_conn_->FetchBatch(input_batch_.get()));
284+
}
285+
if (input_batch_->__isset.cursor_fetch_time_ns) {
286+
COUNTER_ADD(jdbc_cursor_fetch_timer_, input_batch_->cursor_fetch_time_ns);
287+
}
288+
if (input_batch_->__isset.lock_wait_time_ns) {
289+
COUNTER_ADD(jdbc_lock_wait_timer_, input_batch_->lock_wait_time_ns);
290+
}
291+
173292
RETURN_IF_ERROR(ValidateRowBatchSize());
174293
if (!InputBatchHasNext() && !input_batch_->eos) {
175294
// The data source should have set eos, but if it didn't we should just log a
@@ -416,7 +535,9 @@ void DataSourceScanNode::Close(RuntimeState* state) {
416535
TCloseParams params;
417536
params.__set_scan_handle(scan_handle_);
418537
TCloseResult result;
419-
Status status = data_source_executor_->Close(params, &result);
538+
// shared_conn_->Close() only calls executor_.Close() when the last active
539+
// DataSourceScanNode in the fragment decrements the reference count to zero.
540+
Status status = shared_conn_->Close(params, &result);
420541
if (!status.ok()) state->LogError(status.msg());
421542
ScanNode::Close(state);
422543
}

be/src/exec/data-source-scan-node.h

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,128 @@
1919
#ifndef IMPALA_EXEC_DATA_SOURCE_SCAN_NODE_H_
2020
#define IMPALA_EXEC_DATA_SOURCE_SCAN_NODE_H_
2121

22+
#include <atomic>
23+
#include <condition_variable>
24+
#include <mutex>
2225
#include <string>
26+
#include <vector>
2327
#include <boost/scoped_ptr.hpp>
2428

2529
#include "common/global-types.h"
26-
#include "exec/scan-node.h"
30+
#include "common/status.h"
2731
#include "exec/external-data-source-executor.h"
32+
#include "exec/scan-node.h"
2833
#include "runtime/descriptors.h"
2934
#include "runtime/mem-pool.h"
3035

3136
#include "gen-cpp/ExternalDataSource_types.h"
3237

3338
namespace impala {
3439

40+
class DataSourceScanNode;
3541
class Tuple;
3642

43+
/// Shared JDBC connection used by all DataSourceScanNode instances within the same
44+
/// fragment. A single ExternalDataSourceExecutor is opened once and then called
45+
/// concurrently by N MT_DOP scanner threads. Fetch serialization is provided by the
46+
/// Java-side fetchLock in JdbcRecordIterator; once a thread returns from FetchBatch()
47+
/// it materializes its batch in parallel with other threads.
48+
class SharedJdbcConnection {
49+
public:
50+
SharedJdbcConnection() {}
51+
52+
/// Opens the JDBC connection. Thread-safe: the first caller initializes the executor
53+
/// and opens the Java data source; all subsequent callers block until the first caller
54+
/// finishes and then reuse the same connection.
55+
Status Open(const std::string& jar_path, const std::string& class_name,
56+
const std::string& api_version, const std::string& init_string,
57+
const extdatasource::TOpenParams& params, extdatasource::TOpenResult* result);
58+
59+
/// Fetches the next batch from the shared JDBC connection. Multiple threads may
60+
/// call this concurrently; serialization is handled by the Java-side fetchLock.
61+
/// Short-circuits immediately if eos() is already true.
62+
Status FetchBatch(extdatasource::TGetNextResult* result);
63+
64+
/// Decrements the open reference count. The last caller (ref_count_ reaches 0)
65+
/// closes the Java data source.
66+
Status Close(const extdatasource::TCloseParams& params,
67+
extdatasource::TCloseResult* result);
68+
69+
const std::string& scan_handle() const { return scan_handle_; }
70+
71+
/// Returns true once the JDBC stream has been fully consumed. All scanner threads
72+
/// check this before calling FetchBatch() to avoid redundant JNI calls.
73+
bool eos() const { return eos_.load(std::memory_order_acquire); }
74+
75+
private:
76+
ExternalDataSourceExecutor executor_;
77+
std::string scan_handle_;
78+
79+
/// Guards open_done_ / open_status_: used in Open() for one-time initialization
80+
/// and in Close() to check whether executor_.Close() should be called.
81+
std::mutex open_mu_;
82+
std::condition_variable open_cv_;
83+
bool open_done_ = false;
84+
Status open_status_;
85+
86+
/// Reference count: incremented in Open(), decremented in Close(). When it reaches
87+
/// zero, the last DSSN instance calls executor_.Close().
88+
std::atomic<int> ref_count_{0};
89+
90+
/// Set to true when the JDBC stream is exhausted. Threads check this before
91+
/// crossing the JNI bridge to avoid unnecessary calls after EOS.
92+
std::atomic<bool> eos_{false};
93+
};
94+
95+
/// Per-fragment plan node for data source scans. Created once per fragment (shared
96+
/// across all DataSourceScanNode instances), it owns the SharedJdbcConnection so that
97+
/// all N MT_DOP instances share a single JDBC connection with parallel batch conversion.
98+
class DataSourceScanPlanNode : public ScanPlanNode {
99+
public:
100+
virtual Status CreateExecNode(RuntimeState* state, ExecNode** node) const override;
101+
102+
/// Owned by the plan node; shared by all DataSourceScanNode instances in the fragment.
103+
/// Mutable so that Open() (called from exec nodes) can initialize it on first call.
104+
mutable SharedJdbcConnection shared_conn;
105+
};
106+
37107
/// Scan node for external data sources. The external data source jar is loaded
38-
/// in Prepare() (via an ExternalDataSourceExecutor), and then the data source
39-
/// is called to receive row batches when necessary. This node converts the
40-
/// rows stored in a thrift structure to RowBatches. The external data source is
41-
/// closed in Close().
108+
/// in Prepare() (via SharedJdbcConnection), and then the data source is called to
109+
/// receive row batches when necessary. N MT_DOP instances of this node share a single
110+
/// SharedJdbcConnection; fetch serialization is handled by the Java-side fetchLock
111+
/// while batch conversion/materialization runs in parallel across all N threads.
42112
class DataSourceScanNode : public ScanNode {
43113
public:
44-
DataSourceScanNode(
45-
ObjectPool* pool, const ScanPlanNode& pnode, const DescriptorTbl& descs);
114+
DataSourceScanNode(ObjectPool* pool, const DataSourceScanPlanNode& pnode,
115+
const DescriptorTbl& descs);
46116

47117
~DataSourceScanNode();
48118

49-
/// Load the data source library and create the ExternalDataSourceExecutor.
119+
/// Initialize the shared JDBC connection (once per fragment) and profile counters.
50120
virtual Status Prepare(RuntimeState* state) override;
51121

52-
/// Open the data source and initialize the first row batch.
122+
/// Opens the data source via the shared connection and fetches the first batch.
53123
virtual Status Open(RuntimeState* state) override;
54124

55-
/// Fill the next row batch, calls GetNext() on the external scanner.
125+
/// Fill the next row batch from the external scanner.
56126
virtual Status GetNext(RuntimeState* state, RowBatch* row_batch, bool* eos) override;
57127

58128
/// NYI
59129
virtual Status Reset(RuntimeState* state, RowBatch* row_batch) override;
60130

61-
/// Close the scanner, and report errors.
131+
/// Close the scanner via the shared connection.
62132
virtual void Close(RuntimeState* state) override;
63133

64134
protected:
65135
/// Write debug string of this into out.
66136
virtual void DebugString(int indentation_level, std::stringstream* out) const override;
67137

68138
private:
69-
/// Used to call the external data source.
70-
boost::scoped_ptr<ExternalDataSourceExecutor> data_source_executor_;
139+
/// Reference to the per-fragment plan node; owns shared_conn.
140+
const DataSourceScanPlanNode& plan_node_;
141+
142+
/// Non-owning pointer to the plan node's shared JDBC connection.
143+
SharedJdbcConnection* shared_conn_ = nullptr;
71144

72145
/// Thrift structure describing the data source scan node.
73146
const TDataSourceScanNode data_src_node_;
@@ -78,7 +151,8 @@ class DataSourceScanNode : public ScanNode {
78151
/// Tuple index in tuple row.
79152
int tuple_idx_;
80153

81-
/// The opaque handle returned by the data source for the scan.
154+
/// The opaque handle returned by the data source for the scan (shared across all
155+
/// instances — all call getNext() on the same Java object using the same handle).
82156
std::string scan_handle_;
83157

84158
/// The current result from calling GetNext() on the data source. Contains the
@@ -101,11 +175,25 @@ class DataSourceScanNode : public ScanNode {
101175
/// The total number of calls to ExternalDataSource::GetNext().
102176
RuntimeProfile::Counter* num_ext_data_source_get_next_;
103177

178+
/// Wall-clock time this thread spent blocked in the full JNI/Java JDBC round-trip.
179+
RuntimeProfile::Counter* jdbc_jni_wait_timer_ = nullptr;
180+
181+
/// Time this thread held the Java-side fetchLock while iterating the JDBC cursor,
182+
/// as reported by the Java layer. Reflects pure JDBC cursor utilisation.
183+
RuntimeProfile::Counter* jdbc_cursor_fetch_timer_ = nullptr;
184+
185+
/// Time this thread waited to acquire the Java-side fetchLock, as reported by the
186+
/// Java layer. High values indicate cursor contention across scanner threads.
187+
RuntimeProfile::Counter* jdbc_lock_wait_timer_ = nullptr;
188+
189+
/// Number of JNI GetNext() calls issued by this scan node.
190+
RuntimeProfile::Counter* jdbc_jni_call_count_ = nullptr;
191+
104192
/// Materializes the next row (next_row_idx_) into tuple. 'local_tz' is used as the
105193
/// local time-zone for materializing 'TYPE_TIMESTAMP' slots.
106194
Status MaterializeNextRow(const Timezone* local_tz, MemPool* mem_pool, Tuple* tuple);
107195

108-
/// Gets the next batch from the data source, stored in input_batch_.
196+
/// Fetches the next batch from the data source via the shared JDBC connection.
109197
Status GetNextInputBatch();
110198

111199
/// Validate row_batch_ contains the correct number of columns and that columns

0 commit comments

Comments
 (0)