Skip to content

Commit ae7a5de

Browse files
authored
Fix/min folder occupied space cache (#17996)
* fix(consensus): cache occupied space in MinFolderOccupiedSpaceFirstStrategy MinFolderOccupiedSpaceFirstStrategy recomputed every folder's occupied space via a full Files.walk on every folder selection. While receiving a snapshot made of hundreds of thousands of tiny files, this turned each per-file allocation into a full directory-tree scan, making the cost quadratic. addPeer stalled (observed ~1 file / 4-5s) and syncLag stayed high. Cache the occupied space per folder and only recompute it periodically: an incremental selection counter is kept and, once a count threshold or a time interval is reached, the cached state is reset and the occupied space is recomputed. Selection semantics (pick the least occupied folder) are preserved while the number of full directory scans is bounded. Add a mocked unit test (DirectoryStrategyTest) and a real-filesystem integration test (MinFolderOccupiedSpaceFirstStrategyRealFsTest) covering the caching, the count-based refresh and the reset. * fix(consensus): make occupied space cache refresh configurable
1 parent cbe5847 commit ae7a5de

6 files changed

Lines changed: 318 additions & 9 deletions

File tree

iotdb-core/datanode/src/test/java/org/apache/iotdb/db/conf/directories/strategy/DirectoryStrategyTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,47 @@ public void testMinFolderOccupiedSpaceFirstStrategy()
140140

141141
PowerMockito.when(JVMCommonUtils.getOccupiedSpace(dataDirList.get(minIndex)))
142142
.thenReturn(Long.MAX_VALUE);
143+
// The occupied space is cached, so the new value is reflected only after a cache refresh.
144+
minFolderOccupiedSpaceFirstStrategy.invalidateCache();
143145
minIndex = getIndexOfMinOccupiedSpace();
144146
for (int i = 0; i < dataDirList.size(); i++) {
145147
assertEquals(minIndex, minFolderOccupiedSpaceFirstStrategy.nextFolderIndex());
146148
}
147149
}
148150

151+
@Test
152+
public void testMinFolderOccupiedSpaceFirstStrategyCachesOccupiedSpace()
153+
throws DiskSpaceInsufficientException, IOException {
154+
MinFolderOccupiedSpaceFirstStrategy strategy = new MinFolderOccupiedSpaceFirstStrategy();
155+
// Disable the time-based refresh so the count-based refresh is the only trigger under test.
156+
strategy.setRefreshIntervalMs(Long.MAX_VALUE);
157+
strategy.setRefreshSelectionThreshold(3);
158+
strategy.setFolders(dataDirList);
159+
160+
int minIndex = getIndexOfMinOccupiedSpace();
161+
// The first selection builds the cache via a single round of getOccupiedSpace calls.
162+
assertEquals(minIndex, strategy.nextFolderIndex());
163+
assertEquals(1, strategy.getSelectionsSinceRefresh());
164+
165+
// Mutate the occupied space of every folder so that a different folder becomes the least
166+
// occupied one. While the cache is still valid the selection must not change, which proves the
167+
// strategy is no longer re-walking the directory tree on every selection.
168+
for (int i = 0; i < dataDirList.size(); i++) {
169+
boolean available = !fullDirIndexSet.contains(i);
170+
PowerMockito.when(JVMCommonUtils.getOccupiedSpace(dataDirList.get(i)))
171+
.thenReturn(available ? (long) (dataDirList.size() - i) : Long.MAX_VALUE);
172+
}
173+
assertEquals(minIndex, strategy.nextFolderIndex());
174+
assertEquals(minIndex, strategy.nextFolderIndex());
175+
assertEquals(3, strategy.getSelectionsSinceRefresh());
176+
177+
// The threshold has been reached, so the next selection resets the state, recomputes the
178+
// occupied space and reflects the mutated values.
179+
int newMinIndex = getIndexOfMinOccupiedSpace();
180+
assertEquals(newMinIndex, strategy.nextFolderIndex());
181+
assertEquals(1, strategy.getSelectionsSinceRefresh());
182+
}
183+
149184
private int getIndexOfMinOccupiedSpace() throws IOException {
150185
int index = -1;
151186
long minOccupied = Long.MAX_VALUE;

iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,18 @@ topology_probing_timeout_ratio=0.5
765765
# Datatype: double(percentage)
766766
disk_space_warning_threshold=0.05
767767

768+
# The refresh interval in milliseconds for the occupied-space cache used by
769+
# MinFolderOccupiedSpaceFirstStrategy. The default is 60000 (60 seconds).
770+
# effectiveMode: restart
771+
# Datatype: long
772+
min_folder_occupied_space_cache_refresh_interval_ms=60000
773+
774+
# The number of folder selections after which MinFolderOccupiedSpaceFirstStrategy refreshes its
775+
# occupied-space cache. The default is 1000.
776+
# effectiveMode: restart
777+
# Datatype: int
778+
min_folder_occupied_space_cache_refresh_selection_threshold=1000
779+
768780
# Purpose: for data partition repair
769781
# The number of threads used for parallel scanning in the partition table recovery
770782
# effectiveMode: restart

iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ public class CommonConfig {
185185
/** Disk Monitor. */
186186
private double diskSpaceWarningThreshold = 0.05;
187187

188+
/** Refresh interval for MinFolderOccupiedSpaceFirstStrategy occupied-space cache. */
189+
private long minFolderOccupiedSpaceCacheRefreshIntervalMs = 60_000L;
190+
191+
/** Refresh selection threshold for MinFolderOccupiedSpaceFirstStrategy occupied-space cache. */
192+
private int minFolderOccupiedSpaceCacheRefreshSelectionThreshold = 1000;
193+
188194
/** Time partition origin in milliseconds. */
189195
private long timePartitionOrigin = 0;
190196

@@ -781,6 +787,26 @@ public void setDiskSpaceWarningThreshold(double diskSpaceWarningThreshold) {
781787
this.diskSpaceWarningThreshold = diskSpaceWarningThreshold;
782788
}
783789

790+
public long getMinFolderOccupiedSpaceCacheRefreshIntervalMs() {
791+
return minFolderOccupiedSpaceCacheRefreshIntervalMs;
792+
}
793+
794+
public void setMinFolderOccupiedSpaceCacheRefreshIntervalMs(
795+
long minFolderOccupiedSpaceCacheRefreshIntervalMs) {
796+
this.minFolderOccupiedSpaceCacheRefreshIntervalMs =
797+
minFolderOccupiedSpaceCacheRefreshIntervalMs;
798+
}
799+
800+
public int getMinFolderOccupiedSpaceCacheRefreshSelectionThreshold() {
801+
return minFolderOccupiedSpaceCacheRefreshSelectionThreshold;
802+
}
803+
804+
public void setMinFolderOccupiedSpaceCacheRefreshSelectionThreshold(
805+
int minFolderOccupiedSpaceCacheRefreshSelectionThreshold) {
806+
this.minFolderOccupiedSpaceCacheRefreshSelectionThreshold =
807+
minFolderOccupiedSpaceCacheRefreshSelectionThreshold;
808+
}
809+
784810
public boolean isReadOnly() {
785811
return status == NodeStatus.ReadOnly;
786812
}

iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,31 @@ public void loadCommonProps(TrimProperties properties) throws IOException {
230230
String.valueOf(config.getDiskSpaceWarningThreshold()))
231231
.trim()));
232232

233+
long minFolderOccupiedSpaceCacheRefreshIntervalMs =
234+
Long.parseLong(
235+
properties
236+
.getProperty(
237+
"min_folder_occupied_space_cache_refresh_interval_ms",
238+
String.valueOf(config.getMinFolderOccupiedSpaceCacheRefreshIntervalMs()))
239+
.trim());
240+
if (minFolderOccupiedSpaceCacheRefreshIntervalMs > 0) {
241+
config.setMinFolderOccupiedSpaceCacheRefreshIntervalMs(
242+
minFolderOccupiedSpaceCacheRefreshIntervalMs);
243+
}
244+
245+
int minFolderOccupiedSpaceCacheRefreshSelectionThreshold =
246+
Integer.parseInt(
247+
properties
248+
.getProperty(
249+
"min_folder_occupied_space_cache_refresh_selection_threshold",
250+
String.valueOf(
251+
config.getMinFolderOccupiedSpaceCacheRefreshSelectionThreshold()))
252+
.trim());
253+
if (minFolderOccupiedSpaceCacheRefreshSelectionThreshold > 0) {
254+
config.setMinFolderOccupiedSpaceCacheRefreshSelectionThreshold(
255+
minFolderOccupiedSpaceCacheRefreshSelectionThreshold);
256+
}
257+
233258
config.setTimestampPrecision(
234259
properties.getProperty("timestamp_precision", config.getTimestampPrecision()).trim());
235260

iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/MinFolderOccupiedSpaceFirstStrategy.java

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,56 @@
1818
*/
1919
package org.apache.iotdb.commons.disk.strategy;
2020

21+
import org.apache.iotdb.commons.conf.CommonDescriptor;
2122
import org.apache.iotdb.commons.exception.DiskSpaceInsufficientException;
2223
import org.apache.iotdb.commons.i18n.UtilMessages;
2324
import org.apache.iotdb.commons.utils.JVMCommonUtils;
25+
import org.apache.iotdb.commons.utils.TestOnly;
2426

2527
import java.io.IOException;
2628

29+
/**
30+
* Selects the folder with the least occupied space.
31+
*
32+
* <p>Computing the occupied space of a folder requires a full {@code Files.walk} of its directory
33+
* tree, which is very expensive when a folder holds a huge number of (small) files, e.g. while a
34+
* snapshot consisting of hundreds of thousands of tiny files is being received. Re-walking on every
35+
* selection turned the per-file cost into a full-tree scan and made the overall cost quadratic.
36+
*
37+
* <p>To avoid that, the occupied space of every folder is cached and only recomputed periodically.
38+
* Between two refreshes an incremental selection counter is maintained; once enough folders have
39+
* been selected (or enough time has elapsed) the cached state is reset and the occupied space is
40+
* recomputed. This keeps the selection semantics (pick the least occupied folder) while bounding
41+
* the number of full directory scans.
42+
*/
2743
public class MinFolderOccupiedSpaceFirstStrategy extends DirectoryStrategy {
2844

45+
private long refreshIntervalMs =
46+
CommonDescriptor.getInstance().getConfig().getMinFolderOccupiedSpaceCacheRefreshIntervalMs();
47+
private int refreshSelectionThreshold =
48+
CommonDescriptor.getInstance()
49+
.getConfig()
50+
.getMinFolderOccupiedSpaceCacheRefreshSelectionThreshold();
51+
52+
/** Cached occupied space per folder, captured at the last refresh. */
53+
private long[] cachedOccupiedSpace;
54+
55+
/** Incremental count of selections made since the last refresh. */
56+
private int selectionsSinceRefresh;
57+
58+
/** Timestamp (ms) of the last refresh; a negative value means the cache must be (re)built. */
59+
private long lastRefreshTimeMs = -1;
60+
2961
@Override
3062
public int nextFolderIndex() throws DiskSpaceInsufficientException {
3163
return getMinOccupiedSpaceFolder();
3264
}
3365

34-
private int getMinOccupiedSpaceFolder() throws DiskSpaceInsufficientException {
66+
private synchronized int getMinOccupiedSpaceFolder() throws DiskSpaceInsufficientException {
67+
if (needRefresh()) {
68+
refreshOccupiedSpace();
69+
}
70+
3571
int minIndex = -1;
3672
long minSpace = Long.MAX_VALUE;
3773

@@ -43,14 +79,7 @@ private int getMinOccupiedSpaceFolder() throws DiskSpaceInsufficientException {
4379
if (!JVMCommonUtils.hasSpace(folder)) {
4480
continue;
4581
}
46-
47-
long space = 0;
48-
try {
49-
space = JVMCommonUtils.getOccupiedSpace(folder);
50-
} catch (IOException e) {
51-
LOGGER.error(UtilMessages.CANNOT_CALCULATE_OCCUPIED_SPACE, folder, e);
52-
continue;
53-
}
82+
long space = cachedOccupiedSpace[i];
5483
if (space < minSpace) {
5584
minSpace = space;
5685
minIndex = i;
@@ -61,6 +90,61 @@ private int getMinOccupiedSpaceFolder() throws DiskSpaceInsufficientException {
6190
throw new DiskSpaceInsufficientException(folders);
6291
}
6392

93+
selectionsSinceRefresh++;
6494
return minIndex;
6595
}
96+
97+
private boolean needRefresh() {
98+
if (cachedOccupiedSpace == null || cachedOccupiedSpace.length != folders.size()) {
99+
return true;
100+
}
101+
if (lastRefreshTimeMs < 0 || selectionsSinceRefresh >= refreshSelectionThreshold) {
102+
return true;
103+
}
104+
return System.currentTimeMillis() - lastRefreshTimeMs >= refreshIntervalMs;
105+
}
106+
107+
/** Recompute the occupied space of every folder and reset the incremental state. */
108+
private void refreshOccupiedSpace() {
109+
if (cachedOccupiedSpace == null || cachedOccupiedSpace.length != folders.size()) {
110+
cachedOccupiedSpace = new long[folders.size()];
111+
}
112+
for (int i = 0; i < folders.size(); i++) {
113+
String folder = folders.get(i);
114+
if (isUnavailableFolder(folder) || !JVMCommonUtils.hasSpace(folder)) {
115+
// Folder is not a selection candidate; keep it deprioritized without paying for a walk.
116+
cachedOccupiedSpace[i] = Long.MAX_VALUE;
117+
continue;
118+
}
119+
try {
120+
cachedOccupiedSpace[i] = JVMCommonUtils.getOccupiedSpace(folder);
121+
} catch (IOException e) {
122+
LOGGER.error(UtilMessages.CANNOT_CALCULATE_OCCUPIED_SPACE, folder, e);
123+
cachedOccupiedSpace[i] = Long.MAX_VALUE;
124+
}
125+
}
126+
selectionsSinceRefresh = 0;
127+
lastRefreshTimeMs = System.currentTimeMillis();
128+
}
129+
130+
@TestOnly
131+
public void setRefreshIntervalMs(long refreshIntervalMs) {
132+
this.refreshIntervalMs = refreshIntervalMs;
133+
}
134+
135+
@TestOnly
136+
public void setRefreshSelectionThreshold(int refreshSelectionThreshold) {
137+
this.refreshSelectionThreshold = refreshSelectionThreshold;
138+
}
139+
140+
/** Forces the next selection to recompute the occupied space of every folder. */
141+
@TestOnly
142+
public synchronized void invalidateCache() {
143+
this.lastRefreshTimeMs = -1;
144+
}
145+
146+
@TestOnly
147+
public synchronized int getSelectionsSinceRefresh() {
148+
return selectionsSinceRefresh;
149+
}
66150
}

0 commit comments

Comments
 (0)