This is an automated email from the ASF dual-hosted git repository. JackieTien97 pushed a commit to branch rc/2.0.11 in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit f538fae5945c2f109baee95b69a2b2bcfa5ea4a2 Author: Yongzao <[email protected]> AuthorDate: Fri Sep 4 16:07:05 2026 +0800 Bound PartitionInfo snapshot buffers via config_node_snapshot_buffer_size_max (#18580) (cherry picked from commit fd398988672d84d12163103b251a0cea6891a07e) --- example/pipe-count-point-processor/pom.xml | 2 +- integration-test/pom.xml | 10 +- iotdb-client/jdbc/pom.xml | 2 +- .../iotdb/confignode/conf/ConfigNodeConfig.java | 20 ++ .../confignode/conf/ConfigNodeDescriptor.java | 8 + .../persistence/partition/PartitionInfo.java | 23 +- .../confignode/conf/ConfigNodeConfigTest.java | 44 +++ .../confignode/persistence/PartitionInfoTest.java | 65 +++++ .../resources/conf/edge/iotdb-system.properties | 5 + .../conf/iotdb-system.properties.template | 5 + .../apache/iotdb/commons/i18n/CommonMessages.java | 4 +- .../apache/iotdb/commons/i18n/CommonMessages.java | 4 +- .../snapshot/ReusableBufferedOutputStream.java | 134 +++++++++ .../commons/snapshot/SnapshotStreamFactory.java | 130 +++++++++ .../snapshot/SnapshotStreamFactoryTest.java | 307 +++++++++++++++++++++ pom.xml | 12 +- 16 files changed, 750 insertions(+), 25 deletions(-) diff --git a/example/pipe-count-point-processor/pom.xml b/example/pipe-count-point-processor/pom.xml index de6979de7ec..51899beb9c4 100644 --- a/example/pipe-count-point-processor/pom.xml +++ b/example/pipe-count-point-processor/pom.xml @@ -59,7 +59,7 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.2</version> - <configuration /> + <configuration/> </plugin> </plugins> </build> diff --git a/integration-test/pom.xml b/integration-test/pom.xml index 955184f2b26..e7259080410 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -29,17 +29,17 @@ <artifactId>integration-test</artifactId> <name>IoTDB: Integration-Test</name> <properties> - <integrationTest.excludedGroups /> + <integrationTest.excludedGroups/> <integrationTest.edgePackage>${maven.multiModuleProjectDirectory}/distribution/target/apache-iotdb-${project.version}-edge-bin.zip</integrationTest.edgePackage> <integrationTest.forkCount>1</integrationTest.forkCount> - <integrationTest.includedGroups /> + <integrationTest.includedGroups/> <integrationTest.launchNodeInSameJVM>true</integrationTest.launchNodeInSameJVM> <integrationTest.nodeMaxHeapSize>200m</integrationTest.nodeMaxHeapSize> <integrationTest.nodeNewHeapSize>200m</integrationTest.nodeNewHeapSize> <integrationTest.randomSelectWriteNode>true</integrationTest.randomSelectWriteNode> <integrationTest.readAndVerifyWithMultiNode>true</integrationTest.readAndVerifyWithMultiNode> <integrationTest.dataRegionPerDataNode>0</integrationTest.dataRegionPerDataNode> - <integrationTest.testEnv /> + <integrationTest.testEnv/> <lightWeightStandaloneMode.configNodeConsensus>Simple</lightWeightStandaloneMode.configNodeConsensus> <!-- -DClusterConfigurations values configured here --> <lightWeightStandaloneMode.configNodeNumber>1</lightWeightStandaloneMode.configNodeNumber> @@ -719,7 +719,7 @@ <activeByDefault>false</activeByDefault> </activation> <properties> - <integrationTest.excludedGroups /> + <integrationTest.excludedGroups/> <integrationTest.includedGroups>org.apache.iotdb.itbase.category.DailyIT</integrationTest.includedGroups> <integrationTest.launchNodeInSameJVM>false</integrationTest.launchNodeInSameJVM> <integrationTest.randomSelectWriteNode>true</integrationTest.randomSelectWriteNode> @@ -761,7 +761,7 @@ <activeByDefault>false</activeByDefault> </activation> <properties> - <integrationTest.excludedGroups /> + <integrationTest.excludedGroups/> <integrationTest.includedGroups>org.apache.iotdb.itbase.category.ManualIT</integrationTest.includedGroups> <integrationTest.launchNodeInSameJVM>false</integrationTest.launchNodeInSameJVM> <integrationTest.randomSelectWriteNode>true</integrationTest.randomSelectWriteNode> diff --git a/iotdb-client/jdbc/pom.xml b/iotdb-client/jdbc/pom.xml index 5e717b50849..d9b5c8f546c 100644 --- a/iotdb-client/jdbc/pom.xml +++ b/iotdb-client/jdbc/pom.xml @@ -262,7 +262,7 @@ </goals> </pluginExecutionFilter> <action> - <ignore /> + <ignore/> </action> </pluginExecution> </pluginExecutions> diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeConfig.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeConfig.java index f49525ea7da..c1baa98209c 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeConfig.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeConfig.java @@ -26,6 +26,7 @@ import org.apache.iotdb.common.rpc.thrift.TEndPoint; import org.apache.iotdb.commons.client.property.ClientPoolProperty.DefaultProperty; import org.apache.iotdb.commons.conf.CommonDescriptor; import org.apache.iotdb.commons.conf.IoTDBConstant; +import org.apache.iotdb.commons.i18n.CommonMessages; import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; import org.apache.iotdb.confignode.manager.load.balancer.RegionBalancer; import org.apache.iotdb.confignode.manager.load.balancer.router.leader.AbstractLeaderBalancer; @@ -245,6 +246,9 @@ public class ConfigNodeConfig { private long configNodeRatisConsensusLogAppenderBufferSize = 16 * 1024 * 1024L; private long schemaRegionRatisConsensusLogAppenderBufferSize = 16 * 1024 * 1024L; + /** Max size (in bytes) of the in-memory buffer used when taking/loading ConfigNode snapshots. */ + private long configNodeSnapshotBufferSizeMax = 4 * 1024 * 1024L; + /** * RatisConsensus protocol, trigger a snapshot when ratis_snapshot_trigger_threshold logs are * written. @@ -973,6 +977,22 @@ public class ConfigNodeConfig { schemaRegionRatisConsensusLogAppenderBufferSize; } + public long getConfigNodeSnapshotBufferSizeMax() { + return configNodeSnapshotBufferSizeMax; + } + + public void setConfigNodeSnapshotBufferSizeMax(long configNodeSnapshotBufferSizeMax) { + if (configNodeSnapshotBufferSizeMax > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + String.format( + CommonMessages + .EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E, + Integer.MAX_VALUE, + configNodeSnapshotBufferSizeMax)); + } + this.configNodeSnapshotBufferSizeMax = configNodeSnapshotBufferSizeMax; + } + public long getSchemaRegionRatisSnapshotTriggerThreshold() { return schemaRegionRatisSnapshotTriggerThreshold; } diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeDescriptor.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeDescriptor.java index 16dfd4150f1..b4e8b6f9d89 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeDescriptor.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/conf/ConfigNodeDescriptor.java @@ -28,6 +28,7 @@ import org.apache.iotdb.commons.exception.BadNodeUrlException; import org.apache.iotdb.commons.log.LoggerPeriodicalLogReducer; import org.apache.iotdb.commons.pipe.config.PipeDescriptor; import org.apache.iotdb.commons.schema.SchemaConstant; +import org.apache.iotdb.commons.snapshot.SnapshotStreamFactory; import org.apache.iotdb.commons.utils.NodeUrlUtils; import org.apache.iotdb.confignode.i18n.ConfigNodeMessages; import org.apache.iotdb.confignode.manager.load.balancer.RegionBalancer; @@ -403,6 +404,13 @@ public class ConfigNodeDescriptor { loadRatisConsensusConfig(properties); loadCQConfig(properties); + + conf.setConfigNodeSnapshotBufferSizeMax( + Long.parseLong( + properties.getProperty( + "config_node_snapshot_buffer_size_max", + String.valueOf(conf.getConfigNodeSnapshotBufferSizeMax())))); + SnapshotStreamFactory.setBufferSizeMax(conf.getConfigNodeSnapshotBufferSizeMax()); } private void loadRatisConsensusConfig(TrimProperties properties) { diff --git a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/partition/PartitionInfo.java b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/partition/PartitionInfo.java index af9429e5953..adfdd5298a4 100644 --- a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/partition/PartitionInfo.java +++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/partition/PartitionInfo.java @@ -30,6 +30,7 @@ import org.apache.iotdb.commons.partition.DataPartitionTable; import org.apache.iotdb.commons.partition.SchemaPartitionTable; import org.apache.iotdb.commons.schema.table.Audit; import org.apache.iotdb.commons.snapshot.SnapshotProcessor; +import org.apache.iotdb.commons.snapshot.SnapshotStreamFactory; import org.apache.iotdb.commons.utils.PathUtils; import org.apache.iotdb.confignode.consensus.request.read.partition.CountTimeSlotListPlan; import org.apache.iotdb.confignode.consensus.request.read.partition.GetDataPartitionPlan; @@ -80,11 +81,11 @@ import org.apache.tsfile.utils.ReadWriteIOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; import java.util.BitSet; @@ -119,9 +120,6 @@ public class PartitionInfo implements SnapshotProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(PartitionInfo.class); - // Allocate 8MB buffer for load snapshot of PartitionInfo - private static final int PARTITION_TABLE_BUFFER_SIZE = 32 * 1024 * 1024; - /** For Cluster Partition. */ // For allocating Regions private final AtomicInteger nextRegionGroupId; @@ -993,9 +991,11 @@ public class PartitionInfo implements SnapshotProcessor { // snapshot operation. File tmpFile = new File(snapshotFile.getAbsolutePath() + "-" + UUID.randomUUID()); + // The write buffer is bounded by config_node_snapshot_buffer_size_max, so a small partition + // table no longer allocates a fixed 32MB buffer per snapshot. try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile); - BufferedOutputStream bufferedOutputStream = - new BufferedOutputStream(fileOutputStream, PARTITION_TABLE_BUFFER_SIZE); + OutputStream bufferedOutputStream = + SnapshotStreamFactory.createOutputStream(fileOutputStream); TIOStreamTransport tioStreamTransport = new TIOStreamTransport(bufferedOutputStream)) { TProtocol protocol = new TBinaryProtocol(tioStreamTransport); @@ -1049,9 +1049,12 @@ public class PartitionInfo implements SnapshotProcessor { return; } - try (final BufferedInputStream fileInputStream = - new BufferedInputStream( - Files.newInputStream(snapshotFile.toPath()), PARTITION_TABLE_BUFFER_SIZE); + // The read buffer is sized from the file size and capped by + // config_node_snapshot_buffer_size_max, + // so loading a snapshot never allocates more than the configured cap. + try (final InputStream fileInputStream = + SnapshotStreamFactory.createInputStream( + Files.newInputStream(snapshotFile.toPath()), snapshotFile.length()); final TIOStreamTransport tioStreamTransport = new TIOStreamTransport(fileInputStream)) { final TProtocol protocol = new TBinaryProtocol(tioStreamTransport); // before restoring a snapshot, clear all old data diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/conf/ConfigNodeConfigTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/conf/ConfigNodeConfigTest.java new file mode 100644 index 00000000000..da016c5e163 --- /dev/null +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/conf/ConfigNodeConfigTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.confignode.conf; + +import org.apache.iotdb.commons.snapshot.SnapshotStreamFactory; + +import org.junit.Assert; +import org.junit.Test; + +public class ConfigNodeConfigTest { + + @Test + public void testSnapshotBufferSizeMaxDefault() { + final ConfigNodeConfig configNodeConfig = new ConfigNodeConfig(); + // The code default must stay aligned with SnapshotStreamFactory's default cap. + Assert.assertEquals( + SnapshotStreamFactory.DEFAULT_BUFFER_SIZE_MAX, + configNodeConfig.getConfigNodeSnapshotBufferSizeMax()); + + configNodeConfig.setConfigNodeSnapshotBufferSizeMax(256 * 1024L); + Assert.assertEquals(256 * 1024L, configNodeConfig.getConfigNodeSnapshotBufferSizeMax()); + Assert.assertThrows( + IllegalArgumentException.class, + () -> configNodeConfig.setConfigNodeSnapshotBufferSizeMax((long) Integer.MAX_VALUE + 1)); + Assert.assertEquals(256 * 1024L, configNodeConfig.getConfigNodeSnapshotBufferSizeMax()); + } +} diff --git a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/PartitionInfoTest.java b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/PartitionInfoTest.java index afccb0c0eba..21744963066 100644 --- a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/PartitionInfoTest.java +++ b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/PartitionInfoTest.java @@ -29,6 +29,7 @@ import org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot; import org.apache.iotdb.commons.partition.DataPartitionTable; import org.apache.iotdb.commons.partition.SchemaPartitionTable; import org.apache.iotdb.commons.partition.SeriesPartitionTable; +import org.apache.iotdb.commons.snapshot.SnapshotStreamFactory; import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlanType; import org.apache.iotdb.confignode.consensus.request.read.region.GetRegionInfoListPlan; import org.apache.iotdb.confignode.consensus.request.write.database.DatabaseSchemaPlan; @@ -66,6 +67,8 @@ public class PartitionInfoTest { private static PartitionInfo partitionInfo; private static final File snapshotDir = new File(BASE_OUTPUT_PATH, "snapshot"); + private long originalSnapshotBufferSizeMax; + public enum testFlag { DataPartition(20), SchemaPartition(30); @@ -87,6 +90,10 @@ public class PartitionInfoTest { if (!snapshotDir.exists()) { snapshotDir.mkdirs(); } + // Run the snapshot round-trips of this class with a small buffer cap, proving that snapshot + // correctness does not depend on a large fixed buffer. + originalSnapshotBufferSizeMax = SnapshotStreamFactory.getBufferSizeMax(); + SnapshotStreamFactory.setBufferSizeMax(64 * 1024); } @After @@ -95,6 +102,7 @@ public class PartitionInfoTest { if (snapshotDir.exists()) { FileUtils.deleteDirectory(snapshotDir); } + SnapshotStreamFactory.setBufferSizeMax(originalSnapshotBufferSizeMax); } @Test @@ -152,6 +160,63 @@ public class PartitionInfoTest { Assert.assertEquals(partitionInfo, partitionInfo1); } + @Test + public void testSnapshotWithWriteBufferSmallerThanSnapshot() throws TException, IOException { + partitionInfo.generateNextRegionGroupId(); + + // Set StorageGroup + partitionInfo.createDatabase( + new DatabaseSchemaPlan( + ConfigPhysicalPlanType.CreateDatabase, new TDatabaseSchema("root.test"))); + + // Create a SchemaRegion + CreateRegionGroupsPlan createRegionGroupsReq = new CreateRegionGroupsPlan(); + final TRegionReplicaSet schemaRegionReplicaSet = + generateTRegionReplicaSet( + testFlag.SchemaPartition.getFlag(), + generateTConsensusGroupId( + testFlag.SchemaPartition.getFlag(), TConsensusGroupType.SchemaRegion)); + createRegionGroupsReq.addRegionGroup("root.test", schemaRegionReplicaSet); + partitionInfo.createRegionGroups(createRegionGroupsReq); + + // Create a DataRegion + createRegionGroupsReq = new CreateRegionGroupsPlan(); + final TRegionReplicaSet dataRegionReplicaSet = + generateTRegionReplicaSet( + testFlag.DataPartition.getFlag(), + generateTConsensusGroupId( + testFlag.DataPartition.getFlag(), TConsensusGroupType.DataRegion)); + createRegionGroupsReq.addRegionGroup("root.test", dataRegionReplicaSet); + partitionInfo.createRegionGroups(createRegionGroupsReq); + + // Create a data partition table far larger than the 64KB write buffer configured in setup(), + // so the buffered snapshot stream must flush and wrap around many times. + final CreateDataPartitionPlan createDataPartitionPlan = new CreateDataPartitionPlan(); + final Map<String, DataPartitionTable> dataPartitionMap = new HashMap<>(); + final Map<TSeriesPartitionSlot, SeriesPartitionTable> slotInfo = new HashMap<>(); + final TConsensusGroupId dataRegionId = dataRegionReplicaSet.getRegionId(); + for (int seriesSlot = 0; seriesSlot < 2000; seriesSlot++) { + final Map<TTimePartitionSlot, List<TConsensusGroupId>> relationInfo = new HashMap<>(); + for (int timeSlot = 0; timeSlot < 8; timeSlot++) { + relationInfo.put(new TTimePartitionSlot(timeSlot), Collections.singletonList(dataRegionId)); + } + slotInfo.put(new TSeriesPartitionSlot(seriesSlot), new SeriesPartitionTable(relationInfo)); + } + dataPartitionMap.put("root.test", new DataPartitionTable(slotInfo)); + createDataPartitionPlan.setAssignedDataPartition(dataPartitionMap); + partitionInfo.createDataPartition(createDataPartitionPlan); + + Assert.assertTrue(partitionInfo.processTakeSnapshot(snapshotDir)); + + // The snapshot must actually be larger than the 64KB buffer for this test to be meaningful. + final File snapshotFile = new File(snapshotDir, "partition_info.bin"); + Assert.assertTrue(snapshotFile.length() > 64 * 1024); + + final PartitionInfo partitionInfo1 = new PartitionInfo(); + partitionInfo1.processLoadSnapshot(snapshotDir); + Assert.assertEquals(partitionInfo, partitionInfo1); + } + @Test public void testGetRegionType() { diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties index a4686233b71..01cac68af52 100644 --- a/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties +++ b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties @@ -169,6 +169,11 @@ schema_region_ratis_preserve_logs_num_when_purge=200 config_node_ratis_periodic_snapshot_interval=1800 schema_region_ratis_periodic_snapshot_interval=1800 +# Cap the snapshot I/O buffer of the ConfigNode at 8KB (stock default: 4MB). +# Snapshots are tiny on an edge node, so this keeps their transient heap +# allocation negligible. +config_node_snapshot_buffer_size_max=8192 + # ---- realtime pipe sync out of the box ---- # The pipe memory pool is 10% of the heap (~22MB at the default 224M budget), # while the stock pipe memory estimates are sized for datacenter nodes: each diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template index 5ccbf54da96..f457bc72f45 100644 --- a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template +++ b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template @@ -2068,6 +2068,11 @@ config_node_ratis_snapshot_trigger_threshold=400000 schema_region_ratis_snapshot_trigger_threshold=400000 data_region_ratis_snapshot_trigger_threshold=400000 +# max size (in byte) of the in-memory buffer used when taking/loading ConfigNode snapshot files +# effectiveMode: restart +# Datatype: long +config_node_snapshot_buffer_size_max=4194304 + # allow flushing Raft Log asynchronously # effectiveMode: restart # Datatype: Boolean diff --git a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java index ae97d09ddc2..72a41048a34 100644 --- a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java +++ b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java @@ -222,5 +222,7 @@ public final class CommonMessages { public static final String EXCEPTION_THE_ORDER_BY_CLAUSE_OF_THE_DATA_ARGUMENT_MUST_CONTAIN_EXACTLY_THE_TIME_COLUMN_SPECIFIED_BY_THE_TIMECOL_ARGUMENT_4375BAE9 = "The ORDER BY clause of the DATA argument must contain exactly the time column specified by the TIMECOL argument."; public static final String EXCEPTION_UNSUPPORTED_M4_VALUE_TYPE_AF0EF286 = "Unsupported M4 value type: "; public static final String EXCEPTION_DISK_SPACE_WARNING_THRESHOLD_MUST_BE_IN_0_1_BUT_WAS_7B345766 = "disk_space_warning_threshold must be in [0, 1), but was "; - + public static final String + EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E = + "Snapshot buffer size must not exceed %d bytes, but was %d."; } diff --git a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java index 71c45ccaf3f..1d0dd06b38b 100644 --- a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java +++ b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java @@ -215,5 +215,7 @@ public final class CommonMessages { public static final String EXCEPTION_THE_ORDER_BY_CLAUSE_OF_THE_DATA_ARGUMENT_MUST_CONTAIN_EXACTLY_THE_TIME_COLUMN_SPECIFIED_BY_THE_TIMECOL_ARGUMENT_4375BAE9 = "DATA 参数的 ORDER BY 子句必须仅包含 TIMECOL 参数指定的时间列。"; public static final String EXCEPTION_UNSUPPORTED_M4_VALUE_TYPE_AF0EF286 = "不支持的 M4 值类型:"; public static final String EXCEPTION_DISK_SPACE_WARNING_THRESHOLD_MUST_BE_IN_0_1_BUT_WAS_7B345766 = "disk_space_warning_threshold 必须在 [0, 1) 范围内,但实际为 "; - + public static final String + EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E = + "快照缓冲区大小不得超过 %d 字节,但实际为 %d。"; } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/ReusableBufferedOutputStream.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/ReusableBufferedOutputStream.java new file mode 100644 index 00000000000..e18922cac06 --- /dev/null +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/ReusableBufferedOutputStream.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.commons.snapshot; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; + +/** + * A {@link java.io.BufferedOutputStream} variant whose backing byte array is borrowed from and + * returned to {@link SnapshotStreamFactory}'s per-thread pool instead of being freshly allocated + * per stream. Behavior follows {@link java.io.BufferedOutputStream}: {@link #close()} propagates a + * flush failure while still closing the underlying stream. + * + * <p>Writes larger than the buffer bypass it entirely, exactly like the JDK implementation would + * after flushing. + */ +final class ReusableBufferedOutputStream extends OutputStream { + + private final OutputStream out; + private final int bufferSize; + + /** Lazily borrowed from {@link SnapshotStreamFactory} on the first write. */ + private byte[] buffer; + + private int count; + + private boolean closed; + + ReusableBufferedOutputStream(final OutputStream out, final int bufferSize) { + this.out = Objects.requireNonNull(out); + this.bufferSize = bufferSize; + } + + @Override + public void write(final int b) throws IOException { + ensureOpen(); + ensureBuffer(); + if (count >= buffer.length) { + flushBuffer(); + } + buffer[count++] = (byte) b; + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + ensureOpen(); + Objects.checkFromIndexSize(off, len, b.length); + if (len == 0) { + return; + } + if (buffer == null && len >= bufferSize) { + // Large write while the buffer has not even been allocated: skip the buffer entirely + // instead of allocating it just to flush it immediately. + out.write(b, off, len); + return; + } + ensureBuffer(); + if (len >= buffer.length) { + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buffer.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buffer, count, len); + count += len; + } + + @Override + public void flush() throws IOException { + ensureOpen(); + flushBuffer(); + out.flush(); + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + try (final OutputStream outputStream = out) { + flush(); + } finally { + closed = true; + releaseBuffer(); + } + } + + private void ensureBuffer() { + if (buffer == null) { + buffer = SnapshotStreamFactory.acquireBuffer(bufferSize); + } + } + + private void ensureOpen() throws IOException { + if (closed) { + throw new IOException(); + } + } + + private void flushBuffer() throws IOException { + if (count > 0) { + out.write(buffer, 0, count); + count = 0; + } + } + + private void releaseBuffer() { + count = 0; + if (buffer != null) { + SnapshotStreamFactory.releaseBuffer(buffer); + buffer = null; + } + } +} diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactory.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactory.java new file mode 100644 index 00000000000..ceb1b008e28 --- /dev/null +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactory.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.commons.snapshot; + +import org.apache.iotdb.commons.i18n.CommonMessages; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; + +/** + * Creates the buffered streams used to write and read ConfigNode snapshot files. + * + * <p>The buffer size is bounded by {@link #bufferSizeMax}, which is configurable through {@code + * config_node_snapshot_buffer_size_max} (0 disables buffering). This replaces the previous fixed + * 32MB buffer of {@code PartitionInfo}: memory-constrained deployments can lower the cap, while the + * default keeps snapshot I/O fast for large partition tables. Write buffers are pooled per thread + * through {@link SoftReference}s, so consecutive snapshots on the same thread reuse the array + * instead of re-allocating it. + */ +public final class SnapshotStreamFactory { + + /** Default upper bound of a snapshot stream buffer, 4MB. */ + public static final long DEFAULT_BUFFER_SIZE_MAX = 4 * 1024 * 1024L; + + private static volatile long bufferSizeMax = DEFAULT_BUFFER_SIZE_MAX; + + /** Thread-local pool of reusable write buffers, kept only as long as GC allows. */ + private static final ThreadLocal<SoftReference<byte[]>> WRITE_BUFFER_POOL = + ThreadLocal.withInitial(() -> new SoftReference<>(null)); + + private SnapshotStreamFactory() { + // Utility class + } + + /** + * Set the upper bound of snapshot stream buffers, in bytes. Values below or equal to zero disable + * buffering entirely (the raw stream is returned unchanged). Thread-safe; takes effect on the + * next stream creation. + */ + public static void setBufferSizeMax(final long sizeInBytes) { + if (sizeInBytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + String.format( + CommonMessages + .EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E, + Integer.MAX_VALUE, + sizeInBytes)); + } + bufferSizeMax = Math.max(0L, sizeInBytes); + } + + public static long getBufferSizeMax() { + return bufferSizeMax; + } + + /** + * Wrap {@code raw} with a buffered output stream whose buffer is at most {@link #bufferSizeMax} + * bytes. The buffer is reusable, so the same thread writing several snapshots does not repeatedly + * allocate it. + * + * @param raw the raw stream to buffer + * @return a buffered stream, or {@code raw} itself if buffering is disabled + */ + public static OutputStream createOutputStream(final OutputStream raw) { + final int bufferSize = (int) bufferSizeMax; + return bufferSize <= 0 ? raw : new ReusableBufferedOutputStream(raw, bufferSize); + } + + /** + * Wrap {@code raw} with a buffered input stream whose buffer is sized to at most {@code + * fileSize}, capped by {@link #bufferSizeMax}. Reading never allocates more than the configured + * cap, no matter how large the snapshot file is. + * + * @param raw the raw stream to buffer + * @param fileSize size of the file being read, in bytes + * @return a buffered stream, or {@code raw} itself if buffering is disabled or the file is empty + */ + public static InputStream createInputStream(final InputStream raw, final long fileSize) { + final long bufferSize = Math.min(fileSize, bufferSizeMax); + return bufferSize <= 0 ? raw : new BufferedInputStream(raw, (int) bufferSize); + } + + /** + * Borrow a reusable buffer of at least {@code minSize} bytes that does not exceed the current + * cap. If the thread's pool holds a large enough buffer that still fits within {@link + * #bufferSizeMax}, it is handed out (and removed from the pool, so concurrent borrowers never see + * the same array); otherwise a new buffer is allocated. + */ + static byte[] acquireBuffer(final int minSize) { + final long cap = bufferSizeMax; + final SoftReference<byte[]> reference = WRITE_BUFFER_POOL.get(); + final byte[] cached = reference == null ? null : reference.get(); + // Reuse only a buffer that fits the current cap: after the cap has been lowered, a larger + // pooled buffer must not be handed out again, since the backing buffer may never exceed + // bufferSizeMax. + if (cached != null && cached.length >= minSize && cached.length <= cap) { + // Take ownership of the cached buffer so a nested borrower allocates its own instead of + // silently sharing the array. + WRITE_BUFFER_POOL.set(new SoftReference<>(null)); + return cached; + } + return new byte[minSize]; + } + + /** Return a buffer to the thread's pool for reuse by a later snapshot on the same thread. */ + static void releaseBuffer(final byte[] buffer) { + if (buffer != null) { + WRITE_BUFFER_POOL.set(new SoftReference<>(buffer)); + } + } +} diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactoryTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactoryTest.java new file mode 100644 index 00000000000..e4e33e5e83f --- /dev/null +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactoryTest.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.commons.snapshot; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Random; + +public class SnapshotStreamFactoryTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private long originalBufferSizeMax; + + @Before + public void setUp() { + originalBufferSizeMax = SnapshotStreamFactory.getBufferSizeMax(); + } + + @After + public void tearDown() { + // The buffer size cap is process-global; restore it so tests do not leak state into each + // other. + SnapshotStreamFactory.setBufferSizeMax(originalBufferSizeMax); + } + + @Test + public void testDisabledBufferingReturnsRawStreams() { + SnapshotStreamFactory.setBufferSizeMax(0); + + final OutputStream rawOut = new NullOutputStream(); + final InputStream rawIn = new NullInputStream(); + Assert.assertSame(rawOut, SnapshotStreamFactory.createOutputStream(rawOut)); + Assert.assertSame(rawIn, SnapshotStreamFactory.createInputStream(rawIn, 1024)); + } + + @Test + public void testBufferSizeMaximumBoundary() { + SnapshotStreamFactory.setBufferSizeMax(Integer.MAX_VALUE); + Assert.assertEquals(Integer.MAX_VALUE, SnapshotStreamFactory.getBufferSizeMax()); + + Assert.assertThrows( + IllegalArgumentException.class, + () -> SnapshotStreamFactory.setBufferSizeMax((long) Integer.MAX_VALUE + 1)); + Assert.assertEquals(Integer.MAX_VALUE, SnapshotStreamFactory.getBufferSizeMax()); + } + + @Test + public void testRoundTripThroughBufferedStreams() throws IOException { + // 100KB of random data with a 64KB buffer: writes must wrap the buffer several times. + SnapshotStreamFactory.setBufferSizeMax(64 * 1024); + final byte[] data = new byte[100 * 1024]; + new Random(42).nextBytes(data); + + final File file = temporaryFolder.newFile(); + try (FileOutputStream fileOutputStream = new FileOutputStream(file); + OutputStream outputStream = SnapshotStreamFactory.createOutputStream(fileOutputStream)) { + // Write in 4KB chunks so the 64KB buffer is actually filled, flushed and wrapped around. + for (int offset = 0; offset < data.length; offset += 4096) { + outputStream.write(data, offset, Math.min(4096, data.length - offset)); + } + } + + final byte[] readBack = new byte[data.length]; + try (FileInputStream fileInputStream = new FileInputStream(file); + InputStream inputStream = + SnapshotStreamFactory.createInputStream(fileInputStream, file.length())) { + int offset = 0; + while (offset < readBack.length) { + final int read = inputStream.read(readBack, offset, readBack.length - offset); + if (read < 0) { + break; + } + offset += read; + } + Assert.assertEquals(readBack.length, offset); + } + Assert.assertArrayEquals(data, readBack); + } + + @Test + public void testLargeWriteBypassesBuffer() throws IOException { + // A single write larger than the buffer must bypass it and still land correctly on the + // underlying stream. + SnapshotStreamFactory.setBufferSizeMax(64 * 1024); + final byte[] data = new byte[1024 * 1024]; + new Random(7).nextBytes(data); + + final File file = temporaryFolder.newFile(); + try (FileOutputStream fileOutputStream = new FileOutputStream(file); + OutputStream outputStream = SnapshotStreamFactory.createOutputStream(fileOutputStream)) { + outputStream.write(data); + } + Assert.assertEquals(data.length, file.length()); + + final byte[] readBack = new byte[data.length]; + try (InputStream inputStream = new FileInputStream(file)) { + int offset = 0; + while (offset < readBack.length) { + final int read = inputStream.read(readBack, offset, readBack.length - offset); + if (read < 0) { + break; + } + offset += read; + } + Assert.assertEquals(readBack.length, offset); + } + Assert.assertTrue(Arrays.equals(data, readBack)); + } + + @Test + public void testWriteBufferReuse() { + // Use an explicit cap so the pool semantics do not depend on state left by other tests. + SnapshotStreamFactory.setBufferSizeMax(128 * 1024); + final byte[] first = SnapshotStreamFactory.acquireBuffer(64 * 1024); + SnapshotStreamFactory.releaseBuffer(first); + + // The released buffer is handed out again for a request that fits. + final byte[] second = SnapshotStreamFactory.acquireBuffer(64 * 1024); + Assert.assertSame(first, second); + SnapshotStreamFactory.releaseBuffer(second); + + // A borrowed buffer is removed from the pool: a concurrent borrow must not share it. + final byte[] borrowed = SnapshotStreamFactory.acquireBuffer(64 * 1024); + final byte[] other = SnapshotStreamFactory.acquireBuffer(64 * 1024); + Assert.assertNotSame(borrowed, other); + SnapshotStreamFactory.releaseBuffer(other); + + // A smaller request reuses the larger cached buffer while it still fits the cap. + final byte[] smaller = SnapshotStreamFactory.acquireBuffer(1024); + Assert.assertSame(other, smaller); + SnapshotStreamFactory.releaseBuffer(smaller); + + // A request larger than the cached buffer allocates a fresh one. + final byte[] tiny = new byte[32 * 1024]; + SnapshotStreamFactory.releaseBuffer(tiny); + final byte[] larger = SnapshotStreamFactory.acquireBuffer(64 * 1024); + Assert.assertNotSame(tiny, larger); + SnapshotStreamFactory.releaseBuffer(borrowed); + SnapshotStreamFactory.releaseBuffer(larger); + } + + @Test + public void testBufferNotReusedAfterCapDecrease() { + // Prime the pool with a large write buffer under a large cap. + SnapshotStreamFactory.setBufferSizeMax(128 * 1024); + final byte[] large = SnapshotStreamFactory.acquireBuffer(128 * 1024); + SnapshotStreamFactory.releaseBuffer(large); + + // Lower the cap: the pooled buffer now exceeds it and must not be handed out again. + SnapshotStreamFactory.setBufferSizeMax(8192); + final byte[] borrowed = SnapshotStreamFactory.acquireBuffer(8192); + Assert.assertNotSame(large, borrowed); + Assert.assertTrue(borrowed.length <= 8192); + SnapshotStreamFactory.releaseBuffer(borrowed); + + // The buffer allocated under the new cap is still reused for requests that fit it. + final byte[] again = SnapshotStreamFactory.acquireBuffer(8192); + Assert.assertSame(borrowed, again); + SnapshotStreamFactory.releaseBuffer(again); + } + + @Test + public void testCloseIsIdempotentAndWriteAfterCloseFailsOnFlush() throws IOException { + final File file = temporaryFolder.newFile(); + final OutputStream outputStream = + SnapshotStreamFactory.createOutputStream(new FileOutputStream(file)); + outputStream.write(1); + outputStream.close(); + // Double close must not throw. + outputStream.close(); + + // Writes and flushes after close must fail instead of silently losing data. + Assert.assertThrows(IOException.class, () -> outputStream.write(2)); + Assert.assertThrows(IOException.class, outputStream::flush); + // Closing an already closed stream remains idempotent. + outputStream.close(); + } + + @Test + public void testClosePropagatesFlushFailureAndClosesUnderlyingStream() throws IOException { + SnapshotStreamFactory.setBufferSizeMax(64); + final FailingOutputStream rawOut = new FailingOutputStream(false); + final OutputStream outputStream = SnapshotStreamFactory.createOutputStream(rawOut); + outputStream.write(1); + + final IOException exception = Assert.assertThrows(IOException.class, outputStream::close); + Assert.assertEquals("write failure", exception.getMessage()); + Assert.assertTrue(rawOut.closed); + } + + @Test + public void testCloseSuppressesUnderlyingCloseFailureAfterFlushFailure() throws IOException { + SnapshotStreamFactory.setBufferSizeMax(64); + final FailingOutputStream rawOut = new FailingOutputStream(true); + final OutputStream outputStream = SnapshotStreamFactory.createOutputStream(rawOut); + outputStream.write(1); + + final IOException exception = Assert.assertThrows(IOException.class, outputStream::close); + Assert.assertEquals("write failure", exception.getMessage()); + Assert.assertEquals(1, exception.getSuppressed().length); + Assert.assertEquals("close failure", exception.getSuppressed()[0].getMessage()); + Assert.assertTrue(rawOut.closed); + } + + @Test + public void testInputBufferNeverExceedsFileSizeOrCap() throws IOException { + // The read buffer of a stream created for a small file must be capped, and reading must + // still see the whole content. + final byte[] data = new byte[100]; + new Random(3).nextBytes(data); + final File file = temporaryFolder.newFile(); + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + fileOutputStream.write(data); + } + + SnapshotStreamFactory.setBufferSizeMax(64); + final byte[] readBack = new byte[data.length]; + try (FileInputStream fileInputStream = new FileInputStream(file); + InputStream inputStream = + SnapshotStreamFactory.createInputStream(fileInputStream, file.length())) { + int offset = 0; + while (offset < readBack.length) { + final int read = inputStream.read(readBack, offset, readBack.length - offset); + if (read < 0) { + break; + } + offset += read; + } + Assert.assertEquals(readBack.length, offset); + } + Assert.assertArrayEquals(data, readBack); + } + + /** OutputStream that discards everything, used to test the disabled-buffering fast path. */ + private static final class NullOutputStream extends OutputStream { + @Override + public void write(final int b) { + // discard + } + } + + /** InputStream that is always at EOF, used to test the disabled-buffering fast path. */ + private static final class NullInputStream extends InputStream { + @Override + public int read() { + return -1; + } + } + + private static final class FailingOutputStream extends OutputStream { + + private final boolean failOnClose; + private boolean closed; + + private FailingOutputStream(final boolean failOnClose) { + this.failOnClose = failOnClose; + } + + @Override + public void write(final int b) throws IOException { + throw new IOException("write failure"); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + throw new IOException("write failure"); + } + + @Override + public void close() throws IOException { + closed = true; + if (failOnClose) { + throw new IOException("close failure"); + } + } + } +} diff --git a/pom.xml b/pom.xml index f0b0c0d8c65..6b8d0a7ed1c 100644 --- a/pom.xml +++ b/pom.xml @@ -56,9 +56,9 @@ <airline.version>0.9</airline.version> <antlr4.version>4.13.2</antlr4.version> <!-- By default, the argLine is empty--> - <argLine /> + <argLine/> <awaitility.version>4.2.0</awaitility.version> - <boost.include.dir /> + <boost.include.dir/> <bouncycastle.version>1.84</bouncycastle.version> <caffeine.version>2.9.3</caffeine.version> <cglib.version>3.3.0</cglib.version> @@ -141,13 +141,13 @@ <clang.format.version>17.0.6</clang.format.version> <swagger.version>2.2.50</swagger.version> <thrift.exec-cmd.executable>chmod</thrift.exec-cmd.executable> - <thrift.exec.absolute.path /> + <thrift.exec.absolute.path/> <thrift.version>0.23.0</thrift.version> <xz.version>1.9</xz.version> <zstd-jni.version>1.5.6-3</zstd-jni.version> <tsfile.version>2.4.0</tsfile.version> <i18n.locale>en</i18n.locale> - <tsfile.locale.opt /> + <tsfile.locale.opt/> </properties> <!-- if we claim dependencies in dependencyManagement, then we do not claim @@ -840,7 +840,7 @@ <importOrder> <order>org.apache.iotdb,,jakarta,javax,java,\#</order> </importOrder> - <removeUnusedImports /> + <removeUnusedImports/> </java> <lineEndings>UNIX</lineEndings> </configuration> @@ -1092,7 +1092,7 @@ <phase>validate</phase> <configuration> <rules> - <dependencyConvergence /> + <dependencyConvergence/> </rules> </configuration> </execution>
