Skip to content

Commit ced3908

Browse files
authored
Reduce IoTConsensus snapshot transmission memory and log overhead (#18019)
- Reuse a single buffer across all files instead of allocating a 10MB buffer per file in SnapshotFragmentReader. - Only build/emit the full file list at debug level, and throttle the per-file progress log via a configurable interval (data_region_iot_snapshot_transmission_progress_log_interval_ms). - Add SnapshotFragmentReaderTest covering shared-buffer reuse.
1 parent c98bc5a commit ced3908

8 files changed

Lines changed: 195 additions & 30 deletions

File tree

‎iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/config/IoTConsensusConfig.java‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ public static class Replication {
325325
private final long regionMigrationSpeedLimitBytesPerSecond;
326326
private final long subscriptionWalRetentionSizeInBytes;
327327
private final long subscriptionWalRetentionTimeMs;
328+
private final long snapshotTransmissionProgressLogIntervalMs;
328329

329330
private Replication(
330331
int maxLogEntriesNumPerBatch,
@@ -342,7 +343,8 @@ private Replication(
342343
double maxMemoryRatioForQueue,
343344
long regionMigrationSpeedLimitBytesPerSecond,
344345
long subscriptionWalRetentionSizeInBytes,
345-
long subscriptionWalRetentionTimeMs) {
346+
long subscriptionWalRetentionTimeMs,
347+
long snapshotTransmissionProgressLogIntervalMs) {
346348
this.maxLogEntriesNumPerBatch = maxLogEntriesNumPerBatch;
347349
this.maxSizePerBatch = maxSizePerBatch;
348350
this.maxPendingBatchesNum = maxPendingBatchesNum;
@@ -359,6 +361,7 @@ private Replication(
359361
this.regionMigrationSpeedLimitBytesPerSecond = regionMigrationSpeedLimitBytesPerSecond;
360362
this.subscriptionWalRetentionSizeInBytes = subscriptionWalRetentionSizeInBytes;
361363
this.subscriptionWalRetentionTimeMs = subscriptionWalRetentionTimeMs;
364+
this.snapshotTransmissionProgressLogIntervalMs = snapshotTransmissionProgressLogIntervalMs;
362365
}
363366

364367
public int getMaxLogEntriesNumPerBatch() {
@@ -425,6 +428,10 @@ public long getSubscriptionWalRetentionTimeMs() {
425428
return subscriptionWalRetentionTimeMs;
426429
}
427430

431+
public long getSnapshotTransmissionProgressLogIntervalMs() {
432+
return snapshotTransmissionProgressLogIntervalMs;
433+
}
434+
428435
public static Replication.Builder newBuilder() {
429436
return new Replication.Builder();
430437
}
@@ -450,6 +457,11 @@ public static class Builder {
450457
private long regionMigrationSpeedLimitBytesPerSecond = 32 * 1024 * 1024L;
451458
private long subscriptionWalRetentionSizeInBytes = 0;
452459
private long subscriptionWalRetentionTimeMs = -1L;
460+
// Throttle the per-file snapshot-transmission progress log to at most once per this interval;
461+
// a snapshot may contain hundreds of thousands of files, so one INFO line per file is itself
462+
// a
463+
// heavy IO/string-building cost. A value <= 0 logs every file.
464+
private long snapshotTransmissionProgressLogIntervalMs = 5000L;
453465

454466
public Replication.Builder setMaxLogEntriesNumPerBatch(int maxLogEntriesNumPerBatch) {
455467
this.maxLogEntriesNumPerBatch = maxLogEntriesNumPerBatch;
@@ -535,6 +547,12 @@ public Builder setSubscriptionWalRetentionTimeMs(long subscriptionWalRetentionTi
535547
return this;
536548
}
537549

550+
public Builder setSnapshotTransmissionProgressLogIntervalMs(
551+
long snapshotTransmissionProgressLogIntervalMs) {
552+
this.snapshotTransmissionProgressLogIntervalMs = snapshotTransmissionProgressLogIntervalMs;
553+
return this;
554+
}
555+
538556
public Replication build() {
539557
return new Replication(
540558
maxLogEntriesNumPerBatch,
@@ -552,7 +570,8 @@ public Replication build() {
552570
maxMemoryRatioForQueue,
553571
regionMigrationSpeedLimitBytesPerSecond,
554572
subscriptionWalRetentionSizeInBytes,
555-
subscriptionWalRetentionTimeMs);
573+
subscriptionWalRetentionTimeMs,
574+
snapshotTransmissionProgressLogIntervalMs);
556575
}
557576
}
558577
}

‎iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java‎

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -382,33 +382,42 @@ public void takeSnapshot() throws ConsensusGroupModifyPeerException {
382382
public void transmitSnapshot(Peer targetPeer) throws ConsensusGroupModifyPeerException {
383383
File snapshotDir = new File(storageDir, newSnapshotDirName);
384384
List<File> snapshotPaths = stateMachine.getSnapshotFiles(snapshotDir);
385-
AtomicLong snapshotSizeSumAtomic = new AtomicLong();
386-
StringBuilder allFilesStr = new StringBuilder();
387-
snapshotPaths.forEach(
388-
file -> {
389-
long fileSize = file.length();
390-
snapshotSizeSumAtomic.addAndGet(fileSize);
391-
allFilesStr
392-
.append("\n")
393-
.append(file.getName())
394-
.append(" ")
395-
.append(humanReadableByteCountSI(fileSize));
396-
});
397-
final long snapshotSizeSum = snapshotSizeSumAtomic.get();
385+
long snapshotSizeSum = 0;
386+
for (File file : snapshotPaths) {
387+
snapshotSizeSum += file.length();
388+
}
398389
long transitedSnapshotSizeSum = 0;
399390
long transitedFilesNum = 0;
400391
long startTime = System.nanoTime();
392+
long lastProgressLogTime = startTime;
393+
// Throttle the per-file progress log to at most once per this interval; a snapshot may contain
394+
// hundreds of thousands of files, so one INFO line per file is itself a heavy cost.
395+
long progressLogIntervalNs =
396+
TimeUnit.MILLISECONDS.toNanos(
397+
config.getReplication().getSnapshotTransmissionProgressLogIntervalMs());
401398
logger.info(
402399
IoTConsensusMessages.SNAPSHOT_TRANSMISSION_START,
403400
snapshotPaths.size(),
404401
humanReadableByteCountSI(snapshotSizeSum),
405402
snapshotDir);
406-
logger.info(IoTConsensusMessages.SNAPSHOT_TRANSMISSION_ALL_FILES, allFilesStr);
403+
if (logger.isDebugEnabled()) {
404+
StringBuilder allFilesStr = new StringBuilder();
405+
for (File file : snapshotPaths) {
406+
allFilesStr
407+
.append("\n")
408+
.append(file.getName())
409+
.append(" ")
410+
.append(humanReadableByteCountSI(file.length()));
411+
}
412+
logger.debug(IoTConsensusMessages.SNAPSHOT_TRANSMISSION_ALL_FILES, allFilesStr);
413+
}
414+
ByteBuffer fragmentBuffer =
415+
ByteBuffer.allocate(SnapshotFragmentReader.DEFAULT_FILE_FRAGMENT_SIZE);
407416
try (SyncIoTConsensusServiceClient client =
408417
syncClientManager.borrowClient(targetPeer.getEndpoint())) {
409418
for (File file : snapshotPaths) {
410419
SnapshotFragmentReader reader =
411-
new SnapshotFragmentReader(newSnapshotDirName, file.toPath());
420+
new SnapshotFragmentReader(newSnapshotDirName, file.toPath(), fragmentBuffer);
412421
try {
413422
while (reader.hasNext()) {
414423
// TODO: zero copy ?
@@ -423,16 +432,20 @@ public void transmitSnapshot(Peer targetPeer) throws ConsensusGroupModifyPeerExc
423432
}
424433
transitedSnapshotSizeSum += reader.getTotalReadSize();
425434
transitedFilesNum++;
426-
logger.info(
427-
IoTConsensusMessages.SNAPSHOT_TRANSMISSION_PROGRESS,
428-
newSnapshotDirName,
429-
transitedFilesNum,
430-
snapshotPaths.size(),
431-
humanReadableByteCountSI(transitedSnapshotSizeSum),
432-
humanReadableByteCountSI(snapshotSizeSum),
433-
CommonDateTimeUtils.convertMillisecondToDurationStr(
434-
(System.nanoTime() - startTime) / 1_000_000),
435-
file);
435+
long now = System.nanoTime();
436+
if (now - lastProgressLogTime >= progressLogIntervalNs
437+
|| transitedFilesNum == snapshotPaths.size()) {
438+
lastProgressLogTime = now;
439+
logger.info(
440+
IoTConsensusMessages.SNAPSHOT_TRANSMISSION_PROGRESS,
441+
newSnapshotDirName,
442+
transitedFilesNum,
443+
snapshotPaths.size(),
444+
humanReadableByteCountSI(transitedSnapshotSizeSum),
445+
humanReadableByteCountSI(snapshotSizeSum),
446+
CommonDateTimeUtils.convertMillisecondToDurationStr((now - startTime) / 1_000_000),
447+
file);
448+
}
436449
} finally {
437450
reader.close();
438451
}

‎iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/snapshot/SnapshotFragmentReader.java‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
public class SnapshotFragmentReader {
2929

30-
private static final int DEFAULT_FILE_FRAGMENT_SIZE = 10 * 1024 * 1024;
30+
public static final int DEFAULT_FILE_FRAGMENT_SIZE = 10 * 1024 * 1024;
3131
private final String snapshotId;
3232
private final String filePath;
3333
private final SeekableByteChannel fileChannel;
@@ -36,12 +36,20 @@ public class SnapshotFragmentReader {
3636
private long totalReadSize;
3737
private SnapshotFragment cachedSnapshotFragment;
3838

39-
public SnapshotFragmentReader(String snapshotId, Path path) throws IOException {
39+
/**
40+
* The {@code buf} is supplied (and owned) by the caller so a single 10MB buffer can be reused
41+
* across every file of a snapshot transmission. Allocating a fresh 10MB buffer per file is
42+
* extremely wasteful when a snapshot contains hundreds of thousands of tiny files, multiplying GC
43+
* pressure and allocation cost. The buffer is fully reset via {@link ByteBuffer#clear()} on each
44+
* {@link #hasNext()} call, and each fragment is serialized synchronously before the next read, so
45+
* sharing it across files (and across readers) is safe.
46+
*/
47+
public SnapshotFragmentReader(String snapshotId, Path path, ByteBuffer buf) throws IOException {
4048
this.snapshotId = snapshotId;
4149
this.filePath = path.toAbsolutePath().toString();
4250
this.fileSize = Files.size(path);
4351
this.fileChannel = Files.newByteChannel(path);
44-
this.buf = ByteBuffer.allocate(DEFAULT_FILE_FRAGMENT_SIZE);
52+
this.buf = buf;
4553
}
4654

4755
public boolean hasNext() throws IOException {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.consensus.iot.snapshot;
21+
22+
import org.junit.Assert;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.TemporaryFolder;
26+
27+
import java.io.ByteArrayOutputStream;
28+
import java.io.IOException;
29+
import java.nio.ByteBuffer;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.util.Random;
33+
34+
/**
35+
* Regression test for the snapshot-transmission read path. The sending side reuses a single,
36+
* caller-supplied {@link ByteBuffer} across every file of a snapshot (instead of allocating a fresh
37+
* 10MB buffer per file), so this verifies that (1) a single buffer reused across many readers still
38+
* reconstructs every file byte-for-byte, and (2) the buffer is genuinely shared rather than
39+
* re-allocated per file.
40+
*/
41+
public class SnapshotFragmentReaderTest {
42+
43+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
44+
45+
@Test
46+
public void reusedBufferReadsEveryFileCorrectly() throws IOException {
47+
final Random random = new Random(42);
48+
// Deliberately use a tiny buffer so files span multiple fragments, and use file sizes that are
49+
// not multiples of the buffer size to exercise the partial-final-fragment path.
50+
final int bufferSize = 16;
51+
final int[] fileSizes = {0, 1, bufferSize, bufferSize + 1, 5 * bufferSize + 7};
52+
53+
byte[][] contents = new byte[fileSizes.length][];
54+
Path[] paths = new Path[fileSizes.length];
55+
for (int i = 0; i < fileSizes.length; i++) {
56+
contents[i] = new byte[fileSizes[i]];
57+
random.nextBytes(contents[i]);
58+
paths[i] = temporaryFolder.newFile("file-" + i).toPath();
59+
Files.write(paths[i], contents[i]);
60+
}
61+
62+
final ByteBuffer sharedBuffer = ByteBuffer.allocate(bufferSize);
63+
ByteBuffer firstReusedChunk = null;
64+
65+
for (int i = 0; i < paths.length; i++) {
66+
SnapshotFragmentReader reader = new SnapshotFragmentReader("snap", paths[i], sharedBuffer);
67+
try {
68+
ByteArrayOutputStream reconstructed = new ByteArrayOutputStream();
69+
while (reader.hasNext()) {
70+
SnapshotFragment fragment = reader.next();
71+
// Every fragment must be backed by the one shared buffer, proving it is reused and not
72+
// re-allocated per file.
73+
Assert.assertSame(sharedBuffer, fragment.getFileChunk());
74+
if (firstReusedChunk == null) {
75+
firstReusedChunk = fragment.getFileChunk();
76+
} else {
77+
Assert.assertSame(firstReusedChunk, fragment.getFileChunk());
78+
}
79+
80+
// Drain the fragment immediately, mirroring how the sender serializes each fragment
81+
// synchronously before the next read overwrites the shared buffer.
82+
ByteBuffer chunk = fragment.getFileChunk();
83+
byte[] bytes = new byte[chunk.remaining()];
84+
chunk.get(bytes);
85+
reconstructed.write(bytes);
86+
}
87+
Assert.assertArrayEquals(
88+
"File " + i + " was not reconstructed correctly",
89+
contents[i],
90+
reconstructed.toByteArray());
91+
Assert.assertEquals(contents[i].length, reader.getTotalReadSize());
92+
} finally {
93+
reader.close();
94+
}
95+
}
96+
}
97+
}

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,9 @@ public class IoTDBConfig {
11011101
private int maxPendingBatchesNum = 5;
11021102
private double maxMemoryRatioForQueue = 0.6;
11031103
private long regionMigrationSpeedLimitBytesPerSecond = 48 * 1024 * 1024L;
1104+
// Throttle the per-file snapshot-transmission progress log in IoTConsensus to at most once per
1105+
// this interval (ms). A value <= 0 logs every file.
1106+
private long dataRegionIotSnapshotTransmissionProgressLogIntervalMs = 5000L;
11041107

11051108
// IoTConsensusV2 Config
11061109
private int iotConsensusV2PipelineSize = 5;
@@ -1275,6 +1278,16 @@ public void setRegionMigrationSpeedLimitBytesPerSecond(
12751278
this.regionMigrationSpeedLimitBytesPerSecond = regionMigrationSpeedLimitBytesPerSecond;
12761279
}
12771280

1281+
public long getDataRegionIotSnapshotTransmissionProgressLogIntervalMs() {
1282+
return dataRegionIotSnapshotTransmissionProgressLogIntervalMs;
1283+
}
1284+
1285+
public void setDataRegionIotSnapshotTransmissionProgressLogIntervalMs(
1286+
long dataRegionIotSnapshotTransmissionProgressLogIntervalMs) {
1287+
this.dataRegionIotSnapshotTransmissionProgressLogIntervalMs =
1288+
dataRegionIotSnapshotTransmissionProgressLogIntervalMs;
1289+
}
1290+
12781291
public int getIotConsensusV2PipelineSize() {
12791292
return iotConsensusV2PipelineSize;
12801293
}

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,12 @@ private void loadIoTConsensusProps(TrimProperties properties) throws IOException
12861286
"region_migration_speed_limit_bytes_per_second",
12871287
ConfigurationFileUtils.getConfigurationDefaultValue(
12881288
"region_migration_speed_limit_bytes_per_second"))));
1289+
conf.setDataRegionIotSnapshotTransmissionProgressLogIntervalMs(
1290+
Long.parseLong(
1291+
properties.getProperty(
1292+
"data_region_iot_snapshot_transmission_progress_log_interval_ms",
1293+
ConfigurationFileUtils.getConfigurationDefaultValue(
1294+
"data_region_iot_snapshot_transmission_progress_log_interval_ms"))));
12891295
conf.setKeepSameDiskWhenLoadingSnapshot(
12901296
Boolean.parseBoolean(
12911297
properties.getProperty(

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/DataRegionConsensusImpl.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ private static ConsensusConfig buildConsensusConfig() {
178178
COMMON_CONF.getSubscriptionConsensusWalRetentionSizeInBytes())
179179
.setSubscriptionWalRetentionTimeMs(
180180
COMMON_CONF.getSubscriptionConsensusWalRetentionTimeMs())
181+
.setSnapshotTransmissionProgressLogIntervalMs(
182+
CONF.getDataRegionIotSnapshotTransmissionProgressLogIntervalMs())
181183
.build())
182184
.build())
183185
.setIoTConsensusV2Config(

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,13 @@ data_region_iot_max_memory_ratio_for_queue = 0.6
17231723
# Datatype: long
17241724
region_migration_speed_limit_bytes_per_second = 50331648
17251725

1726+
# The minimum interval (in ms) between two per-file progress logs while transmitting a snapshot in
1727+
# IoTConsensus. A snapshot may contain a huge number of files, so logging one line per file is
1728+
# costly; this throttles it to at most once per interval. A value <= 0 logs every file.
1729+
# effectiveMode: hot_reload
1730+
# Datatype: long
1731+
data_region_iot_snapshot_transmission_progress_log_interval_ms = 5000
1732+
17261733
# When loading snapshot, try keeping TsFiles in the same disk as the snapshot dir.
17271734
# This may reduce file copies but may also result in a worse disk load-balance
17281735
# effectiveMode: hot_reload

0 commit comments

Comments
 (0)