Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hugegraph-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Extract the distribution package and edit `conf/application.yml`:
| `grpc.port` | `8500` | gRPC server port for client connections |
| `raft.address` | `127.0.0.1:8510` | Raft service address for this Store node |
| `raft.snapshotInterval` | `1800` | Raft snapshot interval in seconds (30 minutes) |
| `raft.maxSegmentFileSize` | `1073741824` | Maximum bytes per newly allocated segment log file when `raft.useRocksDBSegmentLogStorage=true`; nonempty files retain their size and incompatible blank allocations are isolated on restart |
| `server.port` | `8520` | REST API port for management and metrics |
| `app.data-path` | `./storage` | Directory for RocksDB data storage (supports multiple paths for multi-disk setups) |
| `app.fake-pd` | `false` | Enable built-in PD mode for standalone testing (not for production) |
Expand Down
32 changes: 32 additions & 0 deletions hugegraph-store/docs/deployment-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ raft:
address: 192.168.1.20:8510 # Raft RPC address
snapshotInterval: 1800 # Snapshot interval (seconds)
disruptorBufferSize: 1024 # Raft log buffer
maxSegmentFileSize: 1073741824 # 1 GiB per new segment log file
max-log-file-size: 10737418240 # Max log file: 10GB

# Data Storage
Expand All @@ -342,6 +343,37 @@ app:
fake-pd: false # Use real PD cluster
```

With `raft.useRocksDBSegmentLogStorage=true` (the default),
`raft.maxSegmentFileSize` controls newly allocated segment files. The default is
1 GiB, preserving the effective size used before this setting was connected to
the log storage factory. Previously declared 64 MiB defaults were ignored; an
explicit 64 MiB value now takes effect. For a small development cluster,
`16777216` selects 16 MiB. An explicit `raft.maxSegmentFileSize` takes priority;
when it is unset, the existing `-Djraft.log_storage.segment.max.size.bytes`
system property remains a fallback, followed by 1 GiB if neither is set. The
distribution's default placeholder preserves this fallback too.
Existing segments containing logs retain their original size and remain readable after
this setting changes. On restart, an incompatible **blank preallocated** segment is
atomically renamed to `<sequence>.s.blank-<old-size>-<unique-id>` before JRaft loads its
allocation pool. JRaft then allocates blank segments at the configured size. This also
avoids a write failure after increasing the size when an entry fits the new limit but
does not fit an old blank segment. No nonempty segment is renamed or resized.

Blank detection follows JRaft 1.3.x's segment header format and runs after RocksDB has
acquired the log directory lock, before segment mapping and background allocation.
Unknown or incomplete headers are retained and fail startup with a diagnostic when
their size differs; the code does not guess that sparse or zero-filled files are empty.
Recheck this compatibility handling when upgrading JRaft. Renamed files are retained
for diagnosis and are outside JRaft's normal log retention. Renaming does not reclaim
their allocated blocks; review them during offline maintenance before deleting them.
Keep the configured size larger than the largest encoded Raft entry, including
entries that a newly added Store must receive. Reading an existing large segment
does not make a smaller segment large enough for new writes or replica catch-up.
The limit is per segment, not a limit on total Raft log or database disk usage.
Segment files may be sparse: compare allocated filesystem blocks with apparent
file sizes when investigating disk consumption, and allow space for RocksDB WAL,
snapshots, compaction and container images as well.

File: `hugegraph-store/conf/application-pd.yml` (RocksDB tuning)

```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.apache.hugegraph.store.raft.PartitionStateMachine;
import org.apache.hugegraph.store.raft.RaftClosure;
import org.apache.hugegraph.store.raft.RaftOperation;
import org.apache.hugegraph.store.raft.RaftSegmentLogStorage;
import org.apache.hugegraph.store.raft.RaftStateListener;
import org.apache.hugegraph.store.raft.RaftTaskHandler;
import org.apache.hugegraph.store.raft.util.RaftUtils;
Expand Down Expand Up @@ -218,11 +219,7 @@ public synchronized boolean init(PartitionEngineOptions opts) {
nodeOptions.setServiceFactory(new DefaultJRaftServiceFactory() {
@Override
public LogStorage createLogStorage(final String uri, final RaftOptions raftOptions) {
if (options.getRaftOptions().isUseRocksDBSegmentLogStorage()) {
return new RocksDBSegmentLogStorage(uri, raftOptions);
} else {
return new RocksDBLogStorage(uri, raftOptions);
}
return PartitionEngine.createLogStorage(uri, raftOptions, raft);
}
});
// Initial cluster
Expand Down Expand Up @@ -267,6 +264,15 @@ public LogStorage createLogStorage(final String uri, final RaftOptions raftOptio
return this.started;
}

static LogStorage createLogStorage(String uri, RaftOptions raftOptions,
HgStoreEngineOptions.RaftOptions storeRaftOptions) {
if (storeRaftOptions.isUseRocksDBSegmentLogStorage()) {
return new RaftSegmentLogStorage(RocksDBSegmentLogStorage.builder(uri, raftOptions)
.setMaxSegmentFileSize(storeRaftOptions.getMaxSegmentFileSize()));
}
return new RocksDBLogStorage(uri, raftOptions);
}

public HgStoreEngine getStoreEngine() {
return this.storeEngine;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ public static class RaftOptions {
*/
private double aveLogEntrySizeRatio = 0.95;
private boolean useRocksDBSegmentLogStorage = true;
private int maxSegmentFileSize = 64 * 1024 * 1024;
private int maxSegmentFileSize = Integer.getInteger("jraft.log_storage.segment.max.size.bytes",
1024 * 1024 * 1024);
}

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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.hugegraph.store.raft;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.UUID;

import com.alipay.sofa.jraft.option.LogStorageOptions;
import com.alipay.sofa.jraft.storage.log.RocksDBSegmentLogStorage;
import com.alipay.sofa.jraft.storage.log.SegmentFile;
import com.alipay.sofa.jraft.util.Utils;

import lombok.extern.slf4j.Slf4j;

/**
* Normalize incompatible blank allocations before JRaft 1.3.x loads its segment pool.
*/
@Slf4j
public final class RaftSegmentLogStorage extends RocksDBSegmentLogStorage {

private final Path segmentsPath;
private final int segmentSize;
private boolean normalized;
private boolean normalizationFailed;
private boolean normalizationFailureClosed;

public RaftSegmentLogStorage(Builder builder) {
super(builder.getPath(), builder.getRaftOptions(), builder.getValueSizeThreshold(),
builder.getMaxSegmentFileSize(), builder.getPreAllocateSegmentCount(),
builder.getKeepInMemorySegmentCount(), builder.getCheckpointIntervalMs(),
builder.getWriteExecutor());
this.segmentsPath = Path.of(builder.getPath(), "segments");
this.segmentSize = builder.getMaxSegmentFileSize();
}

@Override
public synchronized boolean init(LogStorageOptions options) {
this.normalizationFailed = false;
this.normalizationFailureClosed = false;
boolean initialized = super.init(options);
if (!initialized && this.normalizationFailed) {
// The hook ran after RocksDB.open() acquired its directory lock. Release it on
// normalization failure too; JRaft's segment allocator has not been started yet.
this.shutdown();
}
return initialized;
}

@Override
public synchronized void shutdown() {
// NodeImpl.afterShutdown() can repeat cleanup after init() has already closed a
// failed normalization attempt. Keep that path idempotent without changing normal
// successful-storage shutdown behavior.
if (!this.normalizationFailureClosed) {
super.shutdown();
this.normalizationFailureClosed = this.normalizationFailed;
}
}

@Override
protected boolean onInitLoaded() {
if (!this.normalized) {
try {
this.normalizeBlankSegments();
this.normalized = true;
} catch (IOException e) {
this.normalizationFailed = true;
log.error("Cannot normalize blank Raft segments in {} for size {}; files retained",
this.segmentsPath, this.segmentSize, e);
return false;
}
}
this.normalizationFailed = false;
return super.onInitLoaded();
}

@Override
protected void onShutdown() {
if (!this.normalizationFailed) {
super.onShutdown();
}
}

private void normalizeBlankSegments() throws IOException {
if (!Files.exists(this.segmentsPath, LinkOption.NOFOLLOW_LINKS)) {
return;
}
if (!Files.isDirectory(this.segmentsPath, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Not a segment directory: " + this.segmentsPath);
}
// RocksDB already holds the directory lock. At startup no segment has been mapped
// and no allocator is running. Do not normalize again from reset(): the immutable
// size has not changed and its existing allocator can still be creating files.
try (DirectoryStream<Path> files = Files.newDirectoryStream(this.segmentsPath, "*.s")) {
for (Path file : files) {
if (!file.getFileName().toString().matches("[0-9]+\\.s")) {
continue;
}
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Not a regular segment file: " + file);
}
long size = Files.size(file);
if (size == this.segmentSize || !this.isBlank(file)) {
continue;
}
Path isolated = file.resolveSibling(file.getFileName() + ".blank-" + size + "-" +
UUID.randomUUID());
Files.move(file, isolated, StandardCopyOption.ATOMIC_MOVE);
log.warn("Isolated blank Raft segment {} ({} bytes) as {}; new allocations use {} bytes",
file, size, isolated, this.segmentSize);
}
}
// Persist renames before JRaft can reuse an old blank's numeric filename. Also sync
// on a retry where a previous attempt renamed every blank but failed before this step.
Utils.fsync(this.segmentsPath.toFile());
}

private boolean isBlank(Path file) throws IOException {
ByteBuffer header = ByteBuffer.allocate(SegmentFile.HEADER_SIZE);
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS)) {
while (header.hasRemaining()) {
if (channel.read(header) < 0) {
throw new IOException("Incomplete segment header: " + file);
}
}
}
header.flip();
// JRaft 1.3.x SegmentHeader: two magic bytes, firstLogIndex, reserved zero.
// Never infer emptiness from a corrupt header, apparent size or sparse blocks.
if (header.get() != 0x20 || header.get() != 0x20) {
throw new IOException("Unrecognized segment header: " + file);
}
long firstLogIndex = header.getLong();
if (firstLogIndex != -99L) {
return false;
}
if (header.getLong() != 0L) {
throw new IOException("Unrecognized blank segment header: " + file);
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ raft:
# raft cache queue size
disruptorBufferSize: 1024
address: 127.0.0.1:8510
# Maximum bytes per newly allocated segment log file (1 GiB).
# Keep this above the largest encoded Raft entry, including replica catch-up.
maxSegmentFileSize: ${jraft.log_storage.segment.max.size.bytes:1073741824}
max-log-file-size: 600000000000
# Snapshot generation interval, in seconds
snapshotInterval: 1800
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public class Raft {
private double aveLogEntrySizeRation;
@Value("${raft.useRocksDBSegmentLogStorage:true}")
private boolean useRocksDBSegmentLogStorage;
@Value("${raft.maxSegmentFileSize:67108864}")
@Value("${raft.maxSegmentFileSize:${jraft.log_storage.segment.max.size.bytes:1073741824}}")
private int maxSegmentFileSize;
@Value("${raft.maxReplicatorInflightMsgs:256}")
private int maxReplicatorInflightMsgs;
Expand Down
1 change: 1 addition & 0 deletions hugegraph-store/hg-store-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
<include>**/IpUtilTest.java</include>
<include>**/PartitionStateMachineTest.java</include>
<include>**/PartitionEngineErrorTest.java</include>
<include>**/PartitionLogStorageTest.java</include>
<include>**/PartitionEngineMembershipTest.java</include>
<include>**/BatchGraphIsolationTest.java</include>
<include>**/GraphIDManagerTest.java</include>
Expand Down
Loading
Loading