This is an automated email from the ASF dual-hosted git repository.

liaoxin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 91d2bc2566c [opt](memory) limit max block bytes per batch in the write 
path (#61821)
91d2bc2566c is described below

commit 91d2bc2566c1d5b3620327168fe6ba263a799117
Author: hui lai <[email protected]>
AuthorDate: Thu Apr 9 11:33:48 2026 +0800

    [opt](memory) limit max block bytes per batch in the write path (#61821)
    
    ## Problem
    
    During import/load, file format readers accumulate rows into a block
    until `batch_size`
    (row count) is reached. When individual rows are large (e.g., wide
    schemas, complex nested
    types), a single batch can exceed hundreds of MBs, causing excessive
    memory pressure.
    
    ## Solution
    
    Add a new BE config `load_reader_max_block_bytes` (default **64MB**, set
    to `0` to disable)
    and enforce it across all load file format readers:
    
    | Reader | Mechanism |
    |---|---|
    | `CsvReader` | Track cumulative line bytes in the read loop; break
    early when limit reached |
    | `NewJsonReader` | Check `block->bytes()` in the loop guard before
    reading each JSON object |
    | `ParquetReader` | After each `next_batch()`, adaptively reduce
    `_batch_size` based on observed bytes/row |
    | `OrcReader` | Same adaptive reduction + recreate `_batch` with the new
    size for subsequent reads |
    
    ## Config
    // be.conf
    load_reader_max_block_bytes=67108864  # 64MB (default)
    load_reader_max_block_bytes=0           # disable limit
    
    ## Memory Test
    TVF load 5TB data with big block.
    
    before: It was canceled as soon as it was loaed
    <img width="1398" height="990" alt="image"
    
src="https://github.com/user-attachments/assets/bdd67fd5-c3af-4ca1-97fa-1efff9053c60";
    />
    
    
![img_v3_0210h_aca12dd0-596e-411f-8bd0-4d5cc8907d6g](https://github.com/user-attachments/assets/85988c20-74a9-4100-a42a-a5f0064969af)
    
    
    after: Stable load with over 20G of memory
    <img width="1410" height="912" alt="image"
    
src="https://github.com/user-attachments/assets/0f647690-8fea-407c-96a1-1dff2f5ef033";
    />
    
    
![img_v3_0210h_c178c480-8be4-4f50-bf1f-3ada2dc9dc0g](https://github.com/user-attachments/assets/3991ff07-d682-4462-9092-7ea2b4f1df9b)
---
 be/src/common/config.cpp                  |  4 ++++
 be/src/common/config.h                    |  4 ++++
 be/src/exec/scan/scanner.cpp              | 11 +++++++++++
 be/src/format/csv/csv_reader.cpp          | 14 ++++++++++++--
 be/src/format/json/new_json_reader.cpp    |  7 ++++++-
 be/src/format/orc/vorc_reader.cpp         | 23 +++++++++++++++++++++++
 be/src/format/orc/vorc_reader.h           |  4 ++++
 be/src/format/parquet/vparquet_reader.cpp | 18 ++++++++++++++++++
 be/src/format/parquet/vparquet_reader.h   |  4 ++++
 9 files changed, 86 insertions(+), 3 deletions(-)

diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp
index 1a7ac0cea48..1d7def9c66f 100644
--- a/be/src/common/config.cpp
+++ b/be/src/common/config.cpp
@@ -1727,6 +1727,10 @@ 
DEFINE_mBool(enable_auto_clone_on_mow_publish_missing_version, "false");
 // The maximum csv line reader output buffer size
 DEFINE_mInt64(max_csv_line_reader_output_buffer_size, "4294967296");
 
+// The maximum bytes of a single block returned by load file readers 
(CsvReader, NewJsonReader,
+// ParquetReader, OrcReader). Default is 64MB. Set to 0 to disable the limit.
+DEFINE_mInt64(load_reader_max_block_bytes, "67108864");
+
 // Maximum number of OpenMP threads allowed for concurrent vector index builds.
 // -1 means auto: use 80% of the available CPU cores.
 DEFINE_Int32(omp_threads_limit, "-1");
diff --git a/be/src/common/config.h b/be/src/common/config.h
index c5430a3ed0a..417212ed724 100644
--- a/be/src/common/config.h
+++ b/be/src/common/config.h
@@ -1785,6 +1785,10 @@ DECLARE_String(fuzzy_test_type);
 // The maximum csv line reader output buffer size
 DECLARE_mInt64(max_csv_line_reader_output_buffer_size);
 
+// The maximum bytes of a single block returned by load file readers 
(CsvReader, NewJsonReader,
+// ParquetReader, OrcReader). Default is 200MB. Set to 0 to disable the limit.
+DECLARE_mInt64(load_reader_max_block_bytes);
+
 // Maximum number of OpenMP threads available for concurrent index builds.
 // -1 means auto: use 80% of detected CPU cores.
 DECLARE_Int32(omp_threads_limit);
diff --git a/be/src/exec/scan/scanner.cpp b/be/src/exec/scan/scanner.cpp
index d60ed9b5017..fb6507c33ac 100644
--- a/be/src/exec/scan/scanner.cpp
+++ b/be/src/exec/scan/scanner.cpp
@@ -87,7 +87,18 @@ Status Scanner::get_block_after_projects(RuntimeState* 
state, Block* block, bool
         } else {
             
_origin_block.clear_column_data(row_descriptor.num_materialized_slots());
             const auto min_batch_size = std::max(state->batch_size() / 2, 1);
+            // For LOAD queries, use load_reader_max_block_bytes as the 
byte-based
+            // upper bound for block accumulation, so small blocks can still be
+            // merged to a reasonable size without exceeding the memory limit.
+            const int64_t load_max_bytes = (state->query_type() == 
TQueryType::LOAD &&
+                                            
config::load_reader_max_block_bytes > 0)
+                                                   ? 
config::load_reader_max_block_bytes
+                                                   : 0;
             while (_padding_block.rows() < min_batch_size && !*eos) {
+                if (load_max_bytes > 0 &&
+                    static_cast<int64_t>(_padding_block.bytes()) >= 
load_max_bytes) {
+                    break;
+                }
                 RETURN_IF_ERROR(get_block(state, &_origin_block, eos));
                 if (_origin_block.rows() >= min_batch_size) {
                     break;
diff --git a/be/src/format/csv/csv_reader.cpp b/be/src/format/csv/csv_reader.cpp
index 63045f8bfeb..b3aed7d9468 100644
--- a/be/src/format/csv/csv_reader.cpp
+++ b/be/src/format/csv/csv_reader.cpp
@@ -31,6 +31,7 @@
 #include <utility>
 
 #include "common/compiler_util.h" // IWYU pragma: keep
+#include "common/config.h"
 #include "common/consts.h"
 #include "common/status.h"
 #include "core/block/block.h"
@@ -315,12 +316,18 @@ Status CsvReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof) {
     }
 
     const int batch_size = std::max(_state->batch_size(), 
(int)_MIN_BATCH_SIZE);
+    const int64_t max_block_bytes =
+            (_state->query_type() == TQueryType::LOAD && 
config::load_reader_max_block_bytes > 0)
+                    ? config::load_reader_max_block_bytes
+                    : 0;
     size_t rows = 0;
+    size_t block_bytes = 0;
 
     bool success = false;
     bool is_remove_bom = false;
     if (_push_down_agg_type == TPushAggOp::type::COUNT) {
-        while (rows < batch_size && !_line_reader_eof) {
+        while (rows < batch_size && !_line_reader_eof &&
+               (max_block_bytes <= 0 || (int64_t)block_bytes < 
max_block_bytes)) {
             const uint8_t* ptr = nullptr;
             size_t size = 0;
             RETURN_IF_ERROR(_line_reader->read_line(&ptr, &size, 
&_line_reader_eof, _io_ctx));
@@ -348,6 +355,7 @@ Status CsvReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof) {
 
             RETURN_IF_ERROR(_validate_line(Slice(ptr, size), &success));
             ++rows;
+            block_bytes += size;
         }
         auto mutate_columns = block->mutate_columns();
         for (auto& col : mutate_columns) {
@@ -356,7 +364,8 @@ Status CsvReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof) {
         block->set_columns(std::move(mutate_columns));
     } else {
         auto columns = block->mutate_columns();
-        while (rows < batch_size && !_line_reader_eof) {
+        while (rows < batch_size && !_line_reader_eof &&
+               (max_block_bytes <= 0 || (int64_t)block_bytes < 
max_block_bytes)) {
             const uint8_t* ptr = nullptr;
             size_t size = 0;
             RETURN_IF_ERROR(_line_reader->read_line(&ptr, &size, 
&_line_reader_eof, _io_ctx));
@@ -387,6 +396,7 @@ Status CsvReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof) {
                 continue;
             }
             RETURN_IF_ERROR(_fill_dest_columns(Slice(ptr, size), block, 
columns, &rows));
+            block_bytes += size;
         }
         block->set_columns(std::move(columns));
     }
diff --git a/be/src/format/json/new_json_reader.cpp 
b/be/src/format/json/new_json_reader.cpp
index cecfcf3f0dc..f889a962c1c 100644
--- a/be/src/format/json/new_json_reader.cpp
+++ b/be/src/format/json/new_json_reader.cpp
@@ -204,8 +204,13 @@ Status NewJsonReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof)
     }
 
     const int batch_size = std::max(_state->batch_size(), 
(int)_MIN_BATCH_SIZE);
+    const int64_t max_block_bytes =
+            (_state->query_type() == TQueryType::LOAD && 
config::load_reader_max_block_bytes > 0)
+                    ? config::load_reader_max_block_bytes
+                    : 0;
 
-    while (block->rows() < batch_size && !_reader_eof) {
+    while (block->rows() < batch_size && !_reader_eof &&
+           (max_block_bytes <= 0 || (int64_t)block->bytes() < 
max_block_bytes)) {
         if (UNLIKELY(_read_json_by_line && _skip_first_line)) {
             size_t size = 0;
             const uint8_t* line_ptr = nullptr;
diff --git a/be/src/format/orc/vorc_reader.cpp 
b/be/src/format/orc/vorc_reader.cpp
index a1a46d85652..782783b9ac7 100644
--- a/be/src/format/orc/vorc_reader.cpp
+++ b/be/src/format/orc/vorc_reader.cpp
@@ -48,6 +48,7 @@
 #include "absl/strings/substitute.h"
 #include "cctz/civil_time.h"
 #include "cctz/time_zone.h"
+#include "common/config.h"
 #include "common/consts.h"
 #include "common/exception.h"
 #include "core/block/block.h"
@@ -2435,6 +2436,15 @@ Status OrcReader::_get_next_block_impl(Block* block, 
size_t* read_rows, bool* eo
         *read_rows = 0;
         return Status::OK();
     }
+
+    // Limit memory per batch for load paths: pre-shrink _batch_size using the 
bytes-per-row
+    // estimate from the previous batch so the current batch stays within 
load_reader_max_block_bytes
+    // (effective from call 2 onward; first batch is capped on the next call).
+    const int64_t max_block_bytes =
+            (_state != nullptr && _state->query_type() == TQueryType::LOAD &&
+             config::load_reader_max_block_bytes > 0)
+                    ? config::load_reader_max_block_bytes
+                    : 0;
     if (_push_down_agg_type == TPushAggOp::type::COUNT) {
         auto rows = std::min(get_remaining_rows(), (int64_t)_batch_size);
 
@@ -2451,6 +2461,15 @@ Status OrcReader::_get_next_block_impl(Block* block, 
size_t* read_rows, bool* eo
         return Status::OK();
     }
 
+    if (max_block_bytes > 0 && _load_bytes_per_row > 0 && _row_reader) {
+        size_t new_batch_size = std::max(
+                (size_t)1, (size_t)((int64_t)max_block_bytes / 
(int64_t)_load_bytes_per_row));
+        if (new_batch_size != _batch_size) {
+            _batch_size = new_batch_size;
+            _batch = _row_reader->createRowBatch(_batch_size);
+        }
+    }
+
     if (!_seek_to_read_one_line()) {
         *eof = true;
         return Status::OK();
@@ -2792,6 +2811,10 @@ Status OrcReader::_get_next_block_impl(Block* block, 
size_t* read_rows, bool* eo
 #endif
         *read_rows = block->rows();
     }
+
+    if (max_block_bytes > 0 && *read_rows > 0) {
+        _load_bytes_per_row = block->bytes() / *read_rows;
+    }
     return Status::OK();
 }
 
diff --git a/be/src/format/orc/vorc_reader.h b/be/src/format/orc/vorc_reader.h
index 4d7a73d19f6..86e6611c934 100644
--- a/be/src/format/orc/vorc_reader.h
+++ b/be/src/format/orc/vorc_reader.h
@@ -692,6 +692,10 @@ private:
     io::FileSystemProperties _system_properties;
     io::FileDescription _file_description;
     size_t _batch_size;
+    // Bytes-per-row estimate from the previous batch, used to pre-shrink 
_batch_size
+    // before reading so that oversized blocks are prevented from the current 
call onward.
+    // Zero means no prior data (first batch).
+    size_t _load_bytes_per_row = 0;
     int64_t _range_start_offset;
     int64_t _range_size;
     std::string _ctz;
diff --git a/be/src/format/parquet/vparquet_reader.cpp 
b/be/src/format/parquet/vparquet_reader.cpp
index 6de215608f7..106b6124282 100644
--- a/be/src/format/parquet/vparquet_reader.cpp
+++ b/be/src/format/parquet/vparquet_reader.cpp
@@ -26,6 +26,7 @@
 #include <functional>
 #include <utility>
 
+#include "common/config.h"
 #include "common/status.h"
 #include "core/block/block.h"
 #include "core/block/column_with_type_and_name.h"
@@ -719,6 +720,19 @@ Status ParquetReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof)
         return Status::OK();
     }
 
+    // Limit memory per batch for load paths.
+    // _load_bytes_per_row is updated after each batch so the *next* call 
pre-shrinks _batch_size
+    // before reading, ensuring the current batch is already within the limit 
(from call 2 onward).
+    const int64_t max_block_bytes =
+            (_state != nullptr && _state->query_type() == TQueryType::LOAD &&
+             config::load_reader_max_block_bytes > 0)
+                    ? config::load_reader_max_block_bytes
+                    : 0;
+    if (max_block_bytes > 0 && _load_bytes_per_row > 0) {
+        _batch_size = std::max((size_t)1,
+                               (size_t)((int64_t)max_block_bytes / 
(int64_t)_load_bytes_per_row));
+    }
+
     SCOPED_RAW_TIMER(&_reader_statistics.column_read_time);
     Status batch_st =
             _current_group_reader->next_batch(block, _batch_size, read_rows, 
&_row_group_eof);
@@ -735,6 +749,10 @@ Status ParquetReader::get_next_block(Block* block, size_t* 
read_rows, bool* eof)
                                      batch_st.to_string());
     }
 
+    if (max_block_bytes > 0 && *read_rows > 0) {
+        _load_bytes_per_row = block->bytes() / *read_rows;
+    }
+
     if (_row_group_eof) {
         auto column_st = _current_group_reader->merged_column_statistics();
         _column_statistics.merge(column_st);
diff --git a/be/src/format/parquet/vparquet_reader.h 
b/be/src/format/parquet/vparquet_reader.h
index 5172c8efdb9..bbdbcf23d82 100644
--- a/be/src/format/parquet/vparquet_reader.h
+++ b/be/src/format/parquet/vparquet_reader.h
@@ -341,6 +341,10 @@ private:
 
     // parquet file reader object
     size_t _batch_size;
+    // Bytes-per-row estimate from the previous batch, used to pre-shrink 
_batch_size
+    // before reading so that oversized blocks are prevented from the current 
call onward.
+    // Zero means no prior data (first batch).
+    size_t _load_bytes_per_row = 0;
     int64_t _range_start_offset;
     int64_t _range_size;
     const cctz::time_zone* _ctz = nullptr;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to