KAFKA-20979: Fix another gap in index metadata sync logic - #23316
KAFKA-20979: Fix another gap in index metadata sync logic#23316mimaison wants to merge 5 commits into
Conversation
| public void close() throws IOException { | ||
| flush(); // Ensure the index content is flushed to disk as LogSegment.close may append an entry | ||
| trimToValidSize(true); | ||
| flush(false); // Ensure the index content is flushed to disk as LogSegment.close may append an entry |
There was a problem hiding this comment.
This call seems unnecessary. The second flush(true) flushes both the content and the metadata and should be enough.
There was a problem hiding this comment.
trimToValidSize replaces mmap with a new mapping, so the first flush ensures force() is invoked on the previous mmap before it gets swapped out.
The javadoc of MappedByteBuffer.force only guarantees that "all changes made to the buffer since it was created" are written to the device, so changes made through the previous buffer are not strictly covered by calling force() on the new one.
WDYT?
There was a problem hiding this comment.
Hmm, mmap maps page cache into user memory space. All writes to mmap are reflected in page cache. unmapping destroys the reference in user memory, but doesn't destroy page cache. So, remapping should preserve the data in page cache without flushing.
If that assumption is not true, we have problems elsewhere. When we roll a segment, we call trimToValidSize() on a rolled segment without flushing too. Flushing only happens later in the background job.
There was a problem hiding this comment.
If that assumption is not true, we have problems elsewhere. When we roll a segment, we call trimToValidSize() on a rolled segment without flushing too. Flushing only happens later in the background job.
makes sense. I guess we are all betting on the page cache anyway 😄
There was a problem hiding this comment.
Yes that's the reason I kept the two flush() calls. I did not want risking changing the behavior, and potentially breaking other things. It seems flushing an already flushed file is relatively cheap.
If both of you agree a single flush(true) after trimToValidSize() is fine, I'm happy to make the change.
There was a problem hiding this comment.
If both of you agree a single flush(true) after trimToValidSize() is fine, I'm happy to make the change.
+1
| * @param metadata true if the metadata should be flushed as well | ||
| */ | ||
| public void flush() { | ||
| public void flush(boolean metadata) throws IOException { |
There was a problem hiding this comment.
All callers now set metadata to true. Could we remove metadata?
There's a test in |
| try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE)) { | ||
| channel.force(true); | ||
| } | ||
| dirtyMetadata = false; |
There was a problem hiding this comment.
This seems problematic. During a control shutdown, LogManager first calls flush() on each log segment and then calls close() on each. The flush() call will clear dirtyMetadata. But close() could add an entry to index and needs to call flush() again. Since dirtyMetadata is cleared, the second flush() won't flush the metadata.
There was a problem hiding this comment.
I’m fine with removing the dirty flag if my nitpick introduces any risk. However, I may be missing something in Jun’s comment:
Since dirtyMetadata is cleared, the second flush() won't flush the metadata.
In close(), maybeAppend only writes index content through the mmap and does not change the file size, so mmap.force() covers it.
If the file size does change, trimToValidSize sets the dirty flag again, so the second flush() would still force the metadata. Am I missing a case?
| inLock(() -> { | ||
| if (mmap != null) { | ||
| mmap.force(); | ||
| try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE)) { |
There was a problem hiding this comment.
I’m fine with removing the dirty flag if my nitpick introduces any risk. However, I may be missing something in Jun’s comment:
Since dirtyMetadata is cleared, the second flush() won't flush the metadata.
In close(), maybeAppend only writes index content through the mmap and does not change the file size, so mmap.force() covers it.
If the file size does change, trimToValidSize sets the dirty flag again, so the second flush() would still force the metadata. Am I missing a case?
@chia7712 : You are right. The earlier dirty flag logic is correct. I overlooked that it's set to true on the resize() call. It does have a minor issue. The contract for flush() is that we need to flush both the data and the metadata of the index if they are dirty. When an index file is initialized, dirtyMetadata is set to false. This is unintuitive since the index initialization changes the file length.
The current PR does introduce an unnecessary file metadata sync on the last segment during close since LogManager first calls flush() on each log segment and then calls close() on each segment, which triggers a second flush() call. The file metadata sync on the first flush() call is unnecessary.
Currently, the issue is that we flush a rolled segment in a slightly different way from the flush during shutdown. When we roll a segment, we call LogSegment.onBecomeInactiveSegment(), which appends the last timeindex entry if necessary. When flushing a rolled segment, we just call segment.flush() since the extra timeindex entry has been added. However, during shutdown, LogManager first calls flush() on each log segment and call close on each segment. It depends on close() to add the last timeindex entry on the active and to call flush again. I am thinking the following approach to fix this issue in a cleaner way.
During shutdown, LogSegment will call LogSegment.onBecomeInactiveSegment() on the active segment and then call flush. LogSegment.close() won't call timeIndex().maybeAppend to add the last index entry and AbstractIndex.close() won't call flush. This way, we can decouple flush and close. The benefits are (1) only flushing the file metadata once during shutdown; (2) consistent approach when flushing a rolled segment and the active segment; and (3) avoiding unnecessary flush call when LogSegment is closed in LogLoader.load() and LocalLog.splitSegment(). What do you think?
| // Split the segment | ||
| List<LogSegment> newSegments = logAndSegment.log.splitOverflowedSegment(logAndSegment.segment); | ||
|
|
||
| logAndSegment.log.close(); |
There was a problem hiding this comment.
Could we add a comment on why this needs to be done earlier?
Addressing comments from #23258
Reviewers: Chia-Ping Tsai chia7712@gmail.com, Jun Rao
junrao@gmail.com