-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-14491: [19/N] Combine versioned store RocksDB instances into one #13431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,15 +16,33 @@ | |
| */ | ||
| package org.apache.kafka.streams.state.internals; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import org.apache.kafka.streams.processor.ProcessorContext; | ||
| import org.apache.kafka.streams.processor.internals.ProcessorContextUtils; | ||
| import org.apache.kafka.streams.state.internals.metrics.RocksDBMetricsRecorder; | ||
|
|
||
| /** | ||
| * A {@link Segments} implementation which uses a single underlying RocksDB instance. | ||
| * Regular segments with {@code segmentId >= 0} expire according to the specified | ||
| * retention period. "Reserved" segments with {@code segmentId < 0} do not expire | ||
| * and are completely separate from regular segments in that methods such as | ||
| * {@link #getSegmentForTimestamp(long)}, {@link #getOrCreateSegment(long, ProcessorContext)}, | ||
| * {@link #getOrCreateSegmentIfLive(long, ProcessorContext, long)}, | ||
| * {@link #segments(long, long, boolean)}, and {@link #allSegments(boolean)} | ||
| * only return regular segments and not reserved segments. The methods {@link #flush()} | ||
| * and {@link #close()} flush and close both regular and reserved segments, due to | ||
| * the fact that both types of segments share the same physical RocksDB instance. | ||
| * To create a reserved segment, use {@link #createReservedSegment(long, String)} instead. | ||
| */ | ||
| public class LogicalKeyValueSegments extends AbstractSegments<LogicalKeyValueSegment> { | ||
|
|
||
| private final RocksDBMetricsRecorder metricsRecorder; | ||
| private final RocksDBStore physicalStore; | ||
|
|
||
| // reserved segments do not expire, and are tracked here separately from regular segments | ||
| private final Map<Long, LogicalKeyValueSegment> reservedSegments = new HashMap<>(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only one reserved segment is needed for the versioned store implementation (used for the latest value store) but I've chosen to implement support for reserved segments more generally in this class. If we think this is unnecessarily complex, I can update this to be a single |
||
|
|
||
| LogicalKeyValueSegments(final String name, | ||
| final String parentDir, | ||
| final long retentionPeriod, | ||
|
|
@@ -41,6 +59,12 @@ public LogicalKeyValueSegment getOrCreateSegment(final long segmentId, | |
| if (segments.containsKey(segmentId)) { | ||
| return segments.get(segmentId); | ||
| } else { | ||
| if (segmentId < 0) { | ||
| throw new IllegalArgumentException( | ||
| "Negative segment IDs are reserved for reserved segments, " | ||
| + "and should be created through createReservedSegment() instead"); | ||
| } | ||
|
|
||
| final LogicalKeyValueSegment newSegment = new LogicalKeyValueSegment(segmentId, segmentName(segmentId), physicalStore); | ||
|
|
||
| if (segments.put(segmentId, newSegment) != null) { | ||
|
|
@@ -51,6 +75,26 @@ public LogicalKeyValueSegment getOrCreateSegment(final long segmentId, | |
| } | ||
| } | ||
|
|
||
| LogicalKeyValueSegment createReservedSegment(final long segmentId, | ||
| final String segmentName) { | ||
| if (segmentId >= 0) { | ||
| throw new IllegalArgumentException("segmentId for a reserved segment must be negative"); | ||
| } | ||
|
|
||
| final LogicalKeyValueSegment newSegment = new LogicalKeyValueSegment(segmentId, segmentName, physicalStore); | ||
|
|
||
| if (reservedSegments.put(segmentId, newSegment) != null) { | ||
| throw new IllegalStateException("LogicalKeyValueSegment already exists."); | ||
| } | ||
|
|
||
| return newSegment; | ||
| } | ||
|
|
||
| // VisibleForTesting | ||
| LogicalKeyValueSegment getReservedSegment(final long segmentId) { | ||
| return reservedSegments.get(segmentId); | ||
| } | ||
|
|
||
| @Override | ||
| public void openExisting(final ProcessorContext context, final long streamTime) { | ||
| metricsRecorder.init(ProcessorContextUtils.getMetricsImpl(context), context.taskId()); | ||
|
|
@@ -71,6 +115,24 @@ public void flush() { | |
| public void close() { | ||
| // close the logical segments first to close any open iterators | ||
| super.close(); | ||
|
|
||
| // same for reserved segments | ||
| for (final LogicalKeyValueSegment segment : reservedSegments.values()) { | ||
| segment.close(); | ||
| } | ||
| reservedSegments.clear(); | ||
|
|
||
| physicalStore.close(); | ||
| } | ||
|
|
||
| @Override | ||
| public String segmentName(final long segmentId) { | ||
| if (segmentId < 0) { | ||
| throw new IllegalArgumentException( | ||
| "Negative segment IDs are reserved for reserved segments, " | ||
| + "which have custom names that should not be accessed from this method"); | ||
| } | ||
|
|
||
| return super.segmentName(segmentId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,7 +90,7 @@ public class RocksDBStore implements KeyValueStore<Bytes, byte[]>, BatchWritingS | |
| private static final long BLOCK_CACHE_SIZE = 50 * 1024 * 1024L; | ||
| private static final long BLOCK_SIZE = 4096L; | ||
| private static final int MAX_WRITE_BUFFERS = 3; | ||
| private static final String DB_FILE_DIR = "rocksdb"; | ||
| static final String DB_FILE_DIR = "rocksdb"; | ||
|
|
||
| final String name; | ||
| private final String parentDir; | ||
|
|
@@ -124,7 +124,6 @@ public class RocksDBStore implements KeyValueStore<Bytes, byte[]>, BatchWritingS | |
| protected Position position; | ||
| private OffsetCheckpoint positionCheckpoint; | ||
|
|
||
| // VisibleForTesting | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cleanup is unrelated to the changes in this PR -- I happened to notice that this method is labeled as visible for testing but is actually called in a number of places from production code, so I've removed the misleading comment. |
||
| public RocksDBStore(final String name, | ||
| final String metricsScope) { | ||
| this(name, DB_FILE_DIR, new RocksDBMetricsRecorder(metricsScope, name)); | ||
|
|
@@ -424,7 +423,10 @@ void deleteRange(final Bytes keyFrom, final Bytes keyTo) { | |
|
|
||
| validateStoreOpen(); | ||
|
|
||
| // End of key is exclusive, so we increment it by 1 byte to make keyTo inclusive | ||
| // End of key is exclusive, so we increment it by 1 byte to make keyTo inclusive. | ||
| // RocksDB's deleteRange() does not support a null upper bound so in the event | ||
| // of overflow from increment(), the operation cannot be performed and an | ||
| // IndexOutOfBoundsException will be thrown. | ||
| dbAccessor.deleteRange(keyFrom.get(), Bytes.increment(keyTo).get()); | ||
| } | ||
|
|
||
|
|
@@ -756,7 +758,7 @@ public ManagedKeyValueIterator<Bytes, byte[]> all(final boolean forward) { | |
|
|
||
| @Override | ||
| public ManagedKeyValueIterator<Bytes, byte[]> prefixScan(final Bytes prefix) { | ||
| final Bytes to = Bytes.increment(prefix); | ||
| final Bytes to = incrementWithoutOverflow(prefix); | ||
| return new RocksDBRangeIterator( | ||
| name, | ||
| db.newIterator(columnFamily), | ||
|
|
@@ -821,4 +823,20 @@ public Options getOptions() { | |
| public Position getPosition() { | ||
| return position; | ||
| } | ||
|
|
||
| /** | ||
| * Same as {@link Bytes#increment(Bytes)} but {@code null} is returned instead of throwing | ||
| * {@code IndexOutOfBoundsException} in the event of overflow. | ||
| * | ||
| * @param input bytes to increment | ||
| * @return A new copy of the incremented byte array, or {@code null} if incrementing would | ||
| * result in overflow. | ||
| */ | ||
| static Bytes incrementWithoutOverflow(final Bytes input) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that negative segment ID is allowed (only for reserved segments within |
||
| try { | ||
| return Bytes.increment(input); | ||
| } catch (final IndexOutOfBoundsException e) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need
incrementWithoutOverflowhere, of could we just usenull?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a logical segment, a range query with
toBound == nullmeans that all data in the segment (after the specifiedfromBound) should be returned. But data from other segments should NOT be returned, which is why we need to specify the incremented prefix as the upper bound for the range scan in the physical store. Except in the edge case where incrementing the prefix would cause overflow, in which case usingnullfor the upper bound is correct. Thus,incrementWithoutOverflow()is correct here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean
tohere, which is the input parameter (nottoBoundwhich is the physical bound for the store)?But if we overflow, and set
toBound = null(as returned byincrementWithoutOverflow()) would it not imply we scan the physical store to its end, beyond the current logical segment bound?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sorry, that's what I meant.
If the prefix overflows then that means that this is the last possible segment in the store, in which case scanning to the end of the physical store is the same as scanning to the end of the segment, so that's actually exactly what we want. (This only works because all prefixes are the same length -- a bunch of the other logic in this class would break too if that were not the case, though, so it's a fine assumption.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Thanks for clarifying -- this make sense!