This is an automated email from the ASF dual-hosted git repository.
gavinchou 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 a6751eb3ac6 [improvement](compaction) Support lazy chunked row ID
conversion (#67333)
a6751eb3ac6 is described below
commit a6751eb3ac6b4667c3291544c5cfb1fae48e1556
Author: meiyi <[email protected]>
AuthorDate: Fri Sep 11 12:22:30 2026 +0800
[improvement](compaction) Support lazy chunked row ID conversion (#67333)
Problem Summary:
Later, we will support parallel compaction, which means one compaction
sub task only handles somes rows of the input rowsets.
Now, Row ID conversion allocated a dense destination entry for every
source row, which wastes memory when a compaction reads only sparse
source ranges.
Add an opt-in lazy chunked mode that allocates mappings only for touched
rows.
---
be/src/storage/merger.cpp | 8 +-
be/src/storage/rowid_conversion.h | 140 +++++++++++++++++++++++-------
be/test/storage/rowid_conversion_test.cpp | 77 +++++++++++++++-
3 files changed, 189 insertions(+), 36 deletions(-)
diff --git a/be/src/storage/merger.cpp b/be/src/storage/merger.cpp
index 5348aa3af4a..97f8e12090e 100644
--- a/be/src/storage/merger.cpp
+++ b/be/src/storage/merger.cpp
@@ -139,8 +139,8 @@ Status Merger::vmerge_rowsets(BaseTabletSPtr tablet,
ReaderType reader_type,
if (reader_params.record_rowids && block.rows() > 0) {
std::vector<uint32_t> segment_num_rows;
RETURN_IF_ERROR(dst_rowset_writer->get_segment_num_rows(&segment_num_rows));
-
stats_output->rowid_conversion->add(reader.current_block_row_locations(),
- segment_num_rows);
+ RETURN_IF_ERROR(stats_output->rowid_conversion->add(
+ reader.current_block_row_locations(), segment_num_rows));
}
output_rows += block.rows();
@@ -329,8 +329,8 @@ Status Merger::vertical_compact_one_group(
if (is_key && reader_params.record_rowids && block.rows() > 0) {
std::vector<uint32_t> segment_num_rows;
RETURN_IF_ERROR(dst_rowset_writer->get_segment_num_rows(&segment_num_rows));
-
stats_output->rowid_conversion->add(reader.current_block_row_locations(),
- segment_num_rows);
+ RETURN_IF_ERROR(stats_output->rowid_conversion->add(
+ reader.current_block_row_locations(), segment_num_rows));
}
output_rows += block.rows();
block.clear_column_data();
diff --git a/be/src/storage/rowid_conversion.h
b/be/src/storage/rowid_conversion.h
index e26acd8ce86..d6fd51d7367 100644
--- a/be/src/storage/rowid_conversion.h
+++ b/be/src/storage/rowid_conversion.h
@@ -17,7 +17,10 @@
#pragma once
+#include <algorithm>
#include <map>
+#include <memory>
+#include <utility>
#include <vector>
#include "common/cast_set.h"
@@ -35,12 +38,14 @@ namespace doris {
// destination rowset.
class RowIdConversion {
public:
+ enum class Mode { DENSE, LAZY_CHUNKED };
+
struct DestinationRowId {
uint32_t segment_pos;
uint32_t row_id;
};
- RowIdConversion() = default;
+ explicit RowIdConversion(Mode mode = Mode::DENSE) : _mode(mode) {}
~RowIdConversion() { RELEASE_THREAD_MEM_TRACKER(_seg_rowid_map_mem_used); }
Status init_segment_map(const RowsetId& src_rowset_id, const
std::vector<uint32_t>& segment_ids,
@@ -49,36 +54,30 @@ public:
for (size_t i = 0; i < num_rows.size(); i++) {
auto src_segment = std::pair<RowsetId, uint32_t> {src_rowset_id,
segment_ids[i]};
auto iter = _segment_to_id_map.find(src_segment);
- // Each segment-group reader initializes all source segments, so
reuse existing maps.
+ // A segment-group reader can be reopened, so reuse existing
source-segment maps.
if (iter != _segment_to_id_map.end()) {
- DORIS_CHECK_LT(iter->second, _segments_rowid_map.size());
- DORIS_CHECK_EQ(_segments_rowid_map[iter->second].size(),
num_rows[i]);
+ DORIS_CHECK_LT(iter->second, _segment_num_rows.size());
+ DORIS_CHECK_EQ(_segment_num_rows[iter->second], num_rows[i]);
continue;
}
constexpr size_t RESERVED_MEMORY = 10 * 1024 * 1024; // 10M
- if
(doris::GlobalMemoryArbitrator::is_exceed_hard_mem_limit(RESERVED_MEMORY)) {
- return Status::MemoryLimitExceeded(fmt::format(
- "RowIdConversion init_segment_map failed, process
memory exceed limit or "
- "sys available memory less than low water mark , {}, "
- "consuming "
- "tracker:<{}>, peak used {}, current used {}.",
- doris::GlobalMemoryArbitrator::process_mem_log_str(),
- doris::thread_context()
- ->thread_mem_tracker_mgr->limiter_mem_tracker()
- ->label(),
- doris::thread_context()
- ->thread_mem_tracker_mgr->limiter_mem_tracker()
- ->peak_consumption(),
- doris::thread_context()
- ->thread_mem_tracker_mgr->limiter_mem_tracker()
- ->consumption()));
- }
+ RETURN_IF_ERROR(check_memory_limit(RESERVED_MEMORY));
- uint32_t id = cast_set<uint32_t>(_segments_rowid_map.size());
+ uint32_t id = cast_set<uint32_t>(_segment_num_rows.size());
auto insert_result = _segment_to_id_map.emplace(src_segment, id);
DORIS_CHECK(insert_result.second);
_id_to_segment_map.push_back(src_segment);
+ _segment_num_rows.push_back(num_rows[i]);
+ if (_mode == Mode::LAZY_CHUNKED) {
+ _lazy_segments_rowid_map.emplace_back();
+ auto& chunks = _lazy_segments_rowid_map.back();
+ chunks.resize((cast_set<size_t>(num_rows[i]) + ROWS_PER_CHUNK
- 1) /
+ ROWS_PER_CHUNK);
+ track_lazy_mem_usage(chunks.capacity() *
sizeof(std::unique_ptr<RowIdPair[]>));
+ continue;
+ }
+
std::vector<std::pair<uint32_t, uint32_t>> vec(
num_rows[i], std::pair<uint32_t, uint32_t>(UINT32_MAX,
UINT32_MAX));
@@ -96,8 +95,8 @@ public:
const RowsetId& get_dst_rowset_id() const { return _dst_rowst_id; }
// add row id to the map
- void add(const std::vector<RowLocation>& rss_row_ids,
- const std::vector<uint32_t>& dst_segments_num_row) {
+ Status add(const std::vector<RowLocation>& rss_row_ids,
+ const std::vector<uint32_t>& dst_segments_num_row) {
for (auto& item : rss_row_ids) {
if (item.row_id == -1) {
continue;
@@ -109,9 +108,16 @@ public:
_cur_dst_segment_pos++;
_cur_dst_segment_rowid = 0;
}
- _segments_rowid_map[id][item.row_id] =
- std::pair<uint32_t, uint32_t> {_cur_dst_segment_pos,
_cur_dst_segment_rowid++};
+ if (_mode == Mode::DENSE) {
+ _segments_rowid_map[id][item.row_id] = std::pair<uint32_t,
uint32_t> {
+ _cur_dst_segment_pos, _cur_dst_segment_rowid++};
+ continue;
+ }
+ RowIdPair* destination = nullptr;
+ RETURN_IF_ERROR(get_or_create_lazy_destination(id, item.row_id,
&destination));
+ *destination = {_cur_dst_segment_pos, _cur_dst_segment_rowid++};
}
+ return Status::OK();
}
// Get the destination segment position and row id. The physical
destination segment id is
@@ -122,11 +128,24 @@ public:
if (iter == _segment_to_id_map.end()) {
return -1;
}
- const auto& rowid_map = _segments_rowid_map[iter->second];
- if (src.row_id >= rowid_map.size()) {
+ const RowIdPair* destination = nullptr;
+ if (_mode == Mode::DENSE) {
+ const auto& rowid_map = _segments_rowid_map[iter->second];
+ if (src.row_id >= rowid_map.size()) {
+ return -1;
+ }
+ destination = &rowid_map[src.row_id];
+ } else {
+ const auto id = iter->second;
+ if (src.row_id >= _segment_num_rows[id]) {
+ return -1;
+ }
+ destination = get_lazy_destination(id, src.row_id);
+ }
+ if (destination == nullptr) {
return -1;
}
- auto& [dst_segment_pos, dst_rowid] = rowid_map[src.row_id];
+ const auto& [dst_segment_pos, dst_rowid] = *destination;
if (dst_segment_pos == UINT32_MAX && dst_rowid == UINT32_MAX) {
return -1;
}
@@ -138,10 +157,13 @@ public:
const std::vector<std::vector<std::pair<uint32_t, uint32_t>>>&
get_rowid_conversion_map()
const {
+ DORIS_CHECK(_mode == Mode::DENSE);
return _segments_rowid_map;
}
- const std::map<std::pair<RowsetId, uint32_t>, uint32_t>&
get_src_segment_to_id_map() {
+ size_t memory_usage() const { return _seg_rowid_map_mem_used; }
+
+ const std::map<std::pair<RowsetId, uint32_t>, uint32_t>&
get_src_segment_to_id_map() const {
return _segment_to_id_map;
}
@@ -155,6 +177,52 @@ public:
}
private:
+ using RowIdPair = std::pair<uint32_t, uint32_t>;
+ using LazySegmentRowIdMap = std::vector<std::unique_ptr<RowIdPair[]>>;
+ // A 4096-row chunk uses 32 KiB, balancing sparse-range waste and
allocation overhead.
+ static constexpr uint32_t ROWS_PER_CHUNK = 4096;
+
+ Status check_memory_limit(size_t reserved_memory) const {
+ if
(!doris::GlobalMemoryArbitrator::is_exceed_hard_mem_limit(reserved_memory)) {
+ return Status::OK();
+ }
+ return Status::MemoryLimitExceeded(fmt::format(
+ "RowIdConversion allocation failed, process memory exceed
limit or sys available "
+ "memory less than low water mark, {}, consuming tracker:<{}>,
peak used {}, "
+ "current used {}.",
+ doris::GlobalMemoryArbitrator::process_mem_log_str(),
+
doris::thread_context()->thread_mem_tracker_mgr->limiter_mem_tracker()->label(),
+ doris::thread_context()
+ ->thread_mem_tracker_mgr->limiter_mem_tracker()
+ ->peak_consumption(),
+ doris::thread_context()
+ ->thread_mem_tracker_mgr->limiter_mem_tracker()
+ ->consumption()));
+ }
+
+ Status get_or_create_lazy_destination(uint32_t segment_id, uint32_t row_id,
+ RowIdPair** destination) {
+ DORIS_CHECK_LT(segment_id, _segment_num_rows.size());
+ DORIS_CHECK_LT(row_id, _segment_num_rows[segment_id]);
+ auto& chunks = _lazy_segments_rowid_map[segment_id];
+ auto& chunk = chunks[row_id / ROWS_PER_CHUNK];
+ if (chunk == nullptr) {
+ constexpr size_t CHUNK_BYTES = ROWS_PER_CHUNK * sizeof(RowIdPair);
+ RETURN_IF_ERROR(check_memory_limit(CHUNK_BYTES));
+ chunk = std::make_unique<RowIdPair[]>(ROWS_PER_CHUNK);
+ std::fill_n(chunk.get(), ROWS_PER_CHUNK, RowIdPair {UINT32_MAX,
UINT32_MAX});
+ track_lazy_mem_usage(CHUNK_BYTES);
+ }
+ *destination = &chunk[row_id % ROWS_PER_CHUNK];
+ return Status::OK();
+ }
+
+ const RowIdPair* get_lazy_destination(uint32_t segment_id, uint32_t
row_id) const {
+ const auto& chunks = _lazy_segments_rowid_map[segment_id];
+ const auto& chunk = chunks[row_id / ROWS_PER_CHUNK];
+ return chunk == nullptr ? nullptr : &chunk[row_id % ROWS_PER_CHUNK];
+ }
+
void track_mem_usage(size_t delta_std_pair_cap) {
_std_pair_cap += delta_std_pair_cap;
@@ -165,14 +233,26 @@ private:
_seg_rowid_map_mem_used = new_size;
}
+ void track_lazy_mem_usage(size_t bytes) {
+ CONSUME_THREAD_MEM_TRACKER(bytes);
+ _seg_rowid_map_mem_used += bytes;
+ }
+
private:
// the first level vector: index indicates src segment.
// the second level vector: index indicates row id of source segment,
// value indicates destination segment position and row id.
// <UINT32_MAX, UINT32_MAX> indicates current row not exist.
std::vector<std::vector<std::pair<uint32_t, uint32_t>>>
_segments_rowid_map;
+ // The first-level index indicates the internal source segment id.
+ // The second-level index is source row_id / ROWS_PER_CHUNK and selects a
lazy chunk.
+ // The chunk offset is source row_id % ROWS_PER_CHUNK.
+ // The value indicates destination segment position and row id.
+ std::vector<LazySegmentRowIdMap> _lazy_segments_rowid_map;
+ std::vector<uint32_t> _segment_num_rows;
size_t _seg_rowid_map_mem_used {0};
size_t _std_pair_cap {0};
+ Mode _mode;
// Map source segment to 0 to n
std::map<std::pair<RowsetId, uint32_t>, uint32_t> _segment_to_id_map;
diff --git a/be/test/storage/rowid_conversion_test.cpp
b/be/test/storage/rowid_conversion_test.cpp
index 661381c33fa..8c5fa1ab8f6 100644
--- a/be/test/storage/rowid_conversion_test.cpp
+++ b/be/test/storage/rowid_conversion_test.cpp
@@ -534,7 +534,7 @@ TEST_F(TestRowIdConversion, Basic) {
rowid_conversion.set_dst_rowset_id(dst_rowset);
std::vector<uint32_t> dst_segment_num_rows = {4, 3, 4};
- rowid_conversion.add(rss_row_ids, dst_segment_num_rows);
+ ASSERT_TRUE(rowid_conversion.add(rss_row_ids, dst_segment_num_rows).ok());
int res = 0;
src_rowset.init(0);
@@ -601,7 +601,7 @@ TEST_F(TestRowIdConversion,
ConvertDestinationPositionToPhysicalSegmentId) {
RowIdConversion rowid_conversion;
ASSERT_TRUE(rowid_conversion.init_segment_map(input_rowset_id, {10},
{1}).ok());
rowid_conversion.set_dst_rowset_id(output_rowset_id);
- rowid_conversion.add({RowLocation(input_rowset_id, 10, 0)}, {1});
+ ASSERT_TRUE(rowid_conversion.add({RowLocation(input_rowset_id, 10, 0)},
{1}).ok());
DeleteBitmap input_delete_bitmap(1);
input_delete_bitmap.add({input_rowset_id, 10, 5}, 0);
@@ -622,6 +622,79 @@ TEST_F(TestRowIdConversion,
ConvertDestinationPositionToPhysicalSegmentId) {
EXPECT_EQ(dst.segment_id, 100);
}
+TEST_F(TestRowIdConversion, LazyChunkedOnlyAllocatesTouchedRows) {
+ constexpr uint32_t NUM_ROWS = 1'000'000;
+ RowsetId src_rowset;
+ src_rowset.init(1);
+ RowsetId dst_rowset;
+ dst_rowset.init(2);
+
+ RowIdConversion rowid_conversion(RowIdConversion::Mode::LAZY_CHUNKED);
+ ASSERT_TRUE(rowid_conversion.init_segment_map(src_rowset, {10},
{NUM_ROWS}).ok());
+ rowid_conversion.set_dst_rowset_id(dst_rowset);
+ ASSERT_TRUE(rowid_conversion
+ .add({RowLocation(src_rowset, 10, 0),
+ RowLocation(src_rowset, 10, NUM_ROWS - 1)},
+ {1, 1})
+ .ok());
+
+ RowIdConversion::DestinationRowId dst;
+ ASSERT_EQ(rowid_conversion.get(RowLocation(src_rowset, 10, 0), &dst), 0);
+ EXPECT_EQ(dst.segment_pos, 0);
+ EXPECT_EQ(dst.row_id, 0);
+ ASSERT_EQ(rowid_conversion.get(RowLocation(src_rowset, 10, NUM_ROWS - 1),
&dst), 0);
+ EXPECT_EQ(dst.segment_pos, 1);
+ EXPECT_EQ(dst.row_id, 0);
+ EXPECT_EQ(rowid_conversion.get(RowLocation(src_rowset, 10, NUM_ROWS / 2),
&dst), -1);
+ EXPECT_LT(rowid_conversion.memory_usage(), 1024 * 1024);
+}
+
+TEST_F(TestRowIdConversion, LazyChunkedHandlesMultipleSegmentsAndBoundaries) {
+ constexpr uint32_t CHUNK_SIZE = 4096;
+ RowsetId first_rowset;
+ first_rowset.init(1);
+ RowsetId second_rowset;
+ second_rowset.init(2);
+
+ RowIdConversion rowid_conversion(RowIdConversion::Mode::LAZY_CHUNKED);
+ ASSERT_TRUE(
+ rowid_conversion.init_segment_map(first_rowset, {10, 11},
{CHUNK_SIZE + 1, 2}).ok());
+ ASSERT_TRUE(rowid_conversion.init_segment_map(second_rowset, {10},
{CHUNK_SIZE * 2 + 1}).ok());
+ const size_t memory_usage = rowid_conversion.memory_usage();
+ ASSERT_TRUE(
+ rowid_conversion.init_segment_map(first_rowset, {10, 11},
{CHUNK_SIZE + 1, 2}).ok());
+ EXPECT_EQ(rowid_conversion.get_src_segment_to_id_map().size(), 3);
+ EXPECT_EQ(rowid_conversion.memory_usage(), memory_usage);
+
+ ASSERT_TRUE(rowid_conversion
+ .add({RowLocation(first_rowset, 10, CHUNK_SIZE - 1),
+ RowLocation(first_rowset, 10, CHUNK_SIZE),
+ RowLocation(first_rowset, 11, 1),
+ RowLocation(second_rowset, 10, CHUNK_SIZE * 2)},
+ {2, 2})
+ .ok());
+
+ auto expect_destination = [&](const RowsetId& rowset_id, uint32_t
segment_id, uint32_t row_id,
+ uint32_t segment_pos, uint32_t
destination_row_id) {
+ RowIdConversion::DestinationRowId destination;
+ ASSERT_EQ(rowid_conversion.get(RowLocation(rowset_id, segment_id,
row_id), &destination),
+ 0);
+ EXPECT_EQ(destination.segment_pos, segment_pos);
+ EXPECT_EQ(destination.row_id, destination_row_id);
+ };
+ expect_destination(first_rowset, 10, CHUNK_SIZE - 1, 0, 0);
+ expect_destination(first_rowset, 10, CHUNK_SIZE, 0, 1);
+ expect_destination(first_rowset, 11, 1, 1, 0);
+ expect_destination(second_rowset, 10, CHUNK_SIZE * 2, 1, 1);
+
+ RowIdConversion::DestinationRowId destination;
+ EXPECT_EQ(rowid_conversion.get(RowLocation(first_rowset, 10, 0),
&destination), -1);
+ EXPECT_EQ(rowid_conversion.get(RowLocation(second_rowset, 10, CHUNK_SIZE),
&destination), -1);
+ EXPECT_EQ(rowid_conversion.get(RowLocation(first_rowset, 10, CHUNK_SIZE +
1), &destination),
+ -1);
+ EXPECT_EQ(rowid_conversion.get(RowLocation(first_rowset, 12, 0),
&destination), -1);
+}
+
TEST_F(TestRowIdConversion,
SingleRowsetGroupedCompactionRowIdConversionIsComplete) {
constexpr int64_t num_segments = 5;
constexpr int64_t rows_per_segment = 1500;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]