Skip to main content

risingwave_meta/hummock/manager/compaction/
compaction_group_schedule.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Automatic group scheduling. Policies inspect observations and current state;
16//! topology changes and normalization retain their own transaction boundaries.
17
18use std::collections::HashSet;
19
20use itertools::Itertools;
21use risingwave_common::catalog::TableId;
22use thiserror_ext::AsReport;
23
24use super::CompactionGroupStatistic;
25use crate::hummock::error::Result;
26use crate::hummock::manager::HummockManager;
27use crate::hummock::table_write_throughput_statistic::TableWriteThroughputStatisticManager;
28
29mod merge_policy;
30mod normalize;
31mod topology;
32
33#[cfg(test)]
34mod tests;
35
36// Preserve the log target of transaction and normalization code moved below this module.
37const TRACE_TARGET: &str = module_path!();
38
39impl HummockManager {
40    /// Split the compaction group if the group is too large or contains high throughput tables.
41    pub async fn try_split_compaction_group(
42        &self,
43        table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
44        group: CompactionGroupStatistic,
45    ) {
46        if group
47            .compaction_group_config
48            .compaction_config
49            .disable_auto_group_scheduling
50            .unwrap_or(false)
51        {
52            return;
53        }
54        let mut hot_tables = group
55            .table_statistic
56            .keys()
57            .copied()
58            .filter(|&table_id| {
59                table_write_throughput_statistic_manager
60                    .latest_table_throughput(table_id)
61                    .is_some_and(|rate| rate > self.env.opts.table_high_write_throughput_threshold)
62            })
63            .peekable();
64        let group_max_size = (group.compaction_group_config.max_estimated_group_size() as f64
65            * self.env.opts.split_group_size_ratio) as u64;
66        if hot_tables.peek().is_none()
67            && (group.table_statistic.len() < 2 || group.group_size <= group_max_size)
68        {
69            return;
70        }
71
72        for table_id in hot_tables {
73            self.try_move_high_throughput_table_to_dedicated_cg(table_id)
74                .await;
75        }
76
77        // Plan size-based splits from current groups, not the snapshot used to select hot tables.
78        // Refresh even after a failed move: its first split may already have committed.
79        let table_ids = group.table_statistic.keys().copied().collect_vec();
80        for current in self
81            .calculate_compaction_group_statistic_for_tables(&table_ids)
82            .await
83        {
84            if !current
85                .compaction_group_config
86                .compaction_config
87                .disable_auto_group_scheduling
88                .unwrap_or(false)
89            {
90                self.try_split_huge_compaction_group(current).await;
91            }
92        }
93    }
94
95    /// Try to isolate a table already selected as hot by the scheduler.
96    async fn try_move_high_throughput_table_to_dedicated_cg(&self, table_id: TableId) {
97        // An earlier hot-table split in this round may have moved this table to a new group.
98        // Resolve its current parent instead of using the scheduler's original group snapshot.
99        let parent_group_id = self
100            .on_current_version(|version| {
101                let group_id = version
102                    .state_table_info
103                    .info()
104                    .get(&table_id)?
105                    .compaction_group_id;
106                (version
107                    .state_table_info
108                    .compaction_group_member_table_ids(group_id)
109                    .len()
110                    > 1)
111                .then_some(group_id)
112            })
113            .await;
114        let Some(parent_group_id) = parent_group_id else {
115            // The table was removed or is already in a single-table group, so no move is needed.
116            return;
117        };
118
119        let ret = self
120            .move_state_tables_to_dedicated_compaction_group_impl(
121                parent_group_id,
122                &[table_id],
123                Some(self.env.opts.partition_vnode_count),
124                true,
125            )
126            .await;
127        match ret {
128            Ok(split_result) => {
129                tracing::info!(
130                    "split state table [{}] from group-{} success table_vnode_partition_count {:?} split result {:?}",
131                    table_id,
132                    parent_group_id,
133                    self.env.opts.partition_vnode_count,
134                    split_result
135                );
136            }
137            Err(e) => {
138                tracing::info!(
139                    error = %e.as_report(),
140                    "failed to split state table [{}] from group-{}",
141                    table_id,
142                    parent_group_id,
143                )
144            }
145        }
146    }
147
148    pub async fn try_split_huge_compaction_group(&self, group: CompactionGroupStatistic) {
149        let group_max_size = (group.compaction_group_config.max_estimated_group_size() as f64
150            * self.env.opts.split_group_size_ratio) as u64;
151        let is_huge_hybrid_group =
152            group.group_size > group_max_size && group.table_statistic.len() > 1; // avoid split single table group
153        if is_huge_hybrid_group {
154            let mut accumulated_size = 0;
155            let mut table_ids = Vec::default();
156            for (table_id, table_size) in &group.table_statistic {
157                accumulated_size += table_size;
158                table_ids.push(*table_id);
159                // split if the accumulated size is greater than half of the group size
160                // avoid split a small table to dedicated compaction group and trigger multiple merge
161                let remaining_size = group.group_size.saturating_sub(accumulated_size);
162                if accumulated_size > group_max_size / 2
163                    && remaining_size > 0
164                    && table_ids.len() < group.table_statistic.len()
165                {
166                    let ret = self
167                        .move_state_tables_to_dedicated_compaction_group_impl(
168                            group.group_id,
169                            &table_ids,
170                            None,
171                            true,
172                        )
173                        .await;
174                    match ret {
175                        Ok(split_result) => {
176                            tracing::info!(
177                                "split_huge_compaction_group success {:?}",
178                                split_result
179                            );
180                            self.metrics
181                                .split_compaction_group_count
182                                .with_label_values(&[&group.group_id.to_string()])
183                                .inc();
184                            return;
185                        }
186                        Err(e) => {
187                            tracing::error!(
188                                error = %e.as_report(),
189                                "failed to split_huge_compaction_group table {:?} from group-{}",
190                                table_ids,
191                                group.group_id
192                            );
193
194                            return;
195                        }
196                    }
197                }
198            }
199        }
200    }
201
202    pub async fn try_merge_compaction_group(
203        &self,
204        table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
205        group: &CompactionGroupStatistic,
206        next_group: &CompactionGroupStatistic,
207        created_tables: &HashSet<TableId>,
208    ) -> Result<CompactionGroupStatistic> {
209        merge_policy::validate_group_merge(
210            group,
211            next_group,
212            created_tables,
213            table_write_throughput_statistic_manager,
214            &self.env.opts,
215        )?;
216
217        let result = self
218            .merge_compaction_group_impl(
219                group.group_id,
220                next_group.group_id,
221                Some(created_tables),
222                true,
223            )
224            .await;
225
226        match &result {
227            Ok(survivor) => {
228                tracing::info!(
229                    "merge groups {} and {} into group-{}",
230                    group.group_id,
231                    next_group.group_id,
232                    survivor.group_id,
233                );
234            }
235            Err(e) => {
236                tracing::info!(
237                    error = %e.as_report(),
238                    "failed to merge group-{} group-{}",
239                    next_group.group_id,
240                    group.group_id,
241                );
242            }
243        }
244
245        result
246    }
247}