-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-15168: Handle overlapping remote log segments in RemoteLogMetadata cache #14004
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 |
|---|---|---|
|
|
@@ -100,17 +100,29 @@ void handleSegmentWithCopySegmentStartedState(RemoteLogSegmentId remoteLogSegmen | |
|
|
||
| void handleSegmentWithCopySegmentFinishedState(Long startOffset, RemoteLogSegmentId remoteLogSegmentId, | ||
| Long leaderEpochEndOffset) { | ||
| // If there are duplicate segments uploaded due to leader-election, then mark them as unreferenced. | ||
| // Duplicate segments can be uploaded when the previous leader had tier-lags and the next leader uploads the | ||
| // segment for the same leader-epoch which is a super-set of previously uploaded segments. | ||
| // (eg) | ||
| // case-1: Duplicate segment | ||
| // L0 uploaded segment S0 with offsets 0-100 and L1 uploaded segment S1 with offsets 0-200. | ||
| // We will mark the segment S0 as duplicate and add it to unreferencedSegmentIds. | ||
| // case-2: Overlapping segments | ||
| // L0 uploaded segment S0 with offsets 10-90 and L1 uploaded segment S1 with offsets 5-100, S2-101-200, | ||
| // and so on. When the consumer request for segment with offset 95, it should get the segment S1 and not S0. | ||
| Map.Entry<Long, RemoteLogSegmentId> lastEntry = offsetToId.lastEntry(); | ||
| while (lastEntry != null && lastEntry.getKey() >= startOffset && highestLogOffset <= leaderEpochEndOffset) { | ||
|
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. Hi, I have two questions here:
What kafka does in that case? when Broker-1 is up, it checks that new leader epoch change took place, and it removes messages up to latest synced message and replicate missing messages from Broker-2. That means previous non replicated messages on Broker-1 is gone, but in tiered storage it will be duplicate segment. Are we ok with that? If no, we should remove check for
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.
When B2 elected as unclean leader, it will have 2 segments [1-11] and [12-101], where the second segment [12 - 101] contains messages written by more than one leader epoch (LE0:12-80 and LE1: 81-101). B2 cannot be able to upload the segment S2 (12:101) as the highest offset uploaded so far in the remote storage for LE0 is 112. B2 can only be able to upload from segments which contain offset 113. When B1 was brought back as replica, it truncates itself up-to the largest common log prefix offset (target-offset) including the leader-epoch-checkpoint file so it cannot serve the uploaded segments from remote storage. If the target-offset is lesser than the local-log-start-offset, then the uploaded remote log segments will be cleared eventually when smallest (leader_epoch, start_offset) in the leader-epoch-checkpoint file is greater the unreferenced segment epoch.
In remote storage, the records for the offsets from [81-112] will be maintained for both LE0 and LE1. With KIP-320, the fetcher can detect and handle the log truncation themselves. Thanks for the detailed review! Let me know if I'm missing something.
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. Thinking more on this, we may have to update the logic to find the copiedOffset such that it's minimum of (end_offset_for_that_epoch_in_checkpoint_file, highest_remote_offset). In this case, when B2 was leader, copied_offset should be min(80, 112) where 80 is end_offset for LE0 and 112 is the highest_remote_offset for LE0. So, that B2 can able to upload the segment S2 (12:101) 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. Hi, thank you for explanation, sounds reasonable. Sorry for being too questionable. One more point: will do How I see it:
Of course this example is an edge case and configuration should be quite agressive for that in terms for retention times etc. But are the any drawbacks by removing 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. Test example
Member
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. Why would segment data for [151L,190L] is lost? This will be part of the next leader epoch state(viz 1) which should contain this segment. @kamalcph @Nickstery Am I missing anything here?
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. @satishd @Nickstery Correct me If I'm wrong: The logic to find the (The case mentioned in the test can happen when acks is set to 1) The logic to find the copied offset traverses the checkpoint file from latest-epoch. So, when B2 tries to find the copied offset: For epoch(1), there won't any uploaded segments, so it returns empty. So, B2 will skip the segment S2 (101-190) which means there is a data loss from [151-190] @Nickstery This can be fixed if we update the logic to find the
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.
Assume there are multiple back-to-back unclean leader elections happened and only one replica is available at any point of time. Both B1 and B2 may not be aware of all the leader-epochs. If the consumer reads the data from the beginning of the topic, then we should be able to serve the respective remote log segments for the (epoch, start_offset) present in both B1 and B2. This patch only mark the segment as unreferenced if the current segment is a superset of all the previously uploaded segments for the same epoch which means the message is same across the segments.
Member
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. Thanks @kamalcph for the detailed explanation. It is better to return the epoch along with highest-offset and then check the minimum of the respective end offset in the local leader epoch chain for that epoch and the highest offset uploaded to remote storage for that epoch.
Member
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. Filed https://issues.apache.org/jira/browse/KAFKA-15241 to followup on this. |
||
| offsetToId.remove(lastEntry.getKey()); | ||
| unreferencedSegmentIds.add(lastEntry.getValue()); | ||
| lastEntry = offsetToId.lastEntry(); | ||
| } | ||
|
|
||
| // Add the segment epochs mapping as the segment is copied successfully. | ||
| RemoteLogSegmentId oldEntry = offsetToId.put(startOffset, remoteLogSegmentId); | ||
| offsetToId.put(startOffset, remoteLogSegmentId); | ||
|
|
||
| // Remove the metadata from unreferenced entries as it is successfully copied and added to the offset mapping. | ||
| unreferencedSegmentIds.remove(remoteLogSegmentId); | ||
|
|
||
| // Add the old entry to unreferenced entries as the mapping is removed for the old entry. | ||
| if (oldEntry != null) { | ||
| unreferencedSegmentIds.add(oldEntry); | ||
| } | ||
|
|
||
| // Update the highest offset entry for this leader epoch as we added a new mapping. | ||
| if (highestLogOffset == null || leaderEpochEndOffset > highestLogOffset) { | ||
| highestLogOffset = leaderEpochEndOffset; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| /* | ||
| * 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.kafka.server.log.remote.metadata.storage; | ||
|
|
||
| import org.apache.kafka.common.TopicIdPartition; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.Uuid; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentId; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentMetadata; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteResourceNotFoundException; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| class RemoteLogLeaderEpochStateTest { | ||
|
|
||
| TopicPartition tp = new TopicPartition("topic", 0); | ||
| TopicIdPartition tpId = new TopicIdPartition(Uuid.randomUuid(), tp); | ||
| RemoteLogLeaderEpochState epochState = new RemoteLogLeaderEpochState(); | ||
|
|
||
| @Test | ||
| void testListAllRemoteLogSegmentsOnEmpty() throws RemoteResourceNotFoundException { | ||
| assertFalse(epochState.listAllRemoteLogSegments(Collections.emptyMap()).hasNext()); | ||
| } | ||
|
|
||
| @Test | ||
| void testListAllRemoteLogSegmentsShouldThrowErrorForUnknownSegmentId() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(0L, segmentId1, 10L); | ||
| assertThrows(RemoteResourceNotFoundException.class, | ||
| () -> epochState.listAllRemoteLogSegments(Collections.singletonMap(segmentId2, null))); | ||
| } | ||
|
|
||
| @Test | ||
| void testListAllRemoteLogSegmentsShouldReturnSortedSegments() throws RemoteResourceNotFoundException { | ||
| Map<RemoteLogSegmentId, RemoteLogSegmentMetadata> segmentIdToMetadataMap = new HashMap<>(); | ||
|
|
||
| // copy started but never finished so marked as unreferenced | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentStartedState(segmentId1); | ||
| segmentIdToMetadataMap.put(segmentId1, createRemoteLogSegmentMetadata(segmentId1, 0L)); | ||
|
|
||
| // copy finished successfully | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(5L, segmentId2, 10L); | ||
| segmentIdToMetadataMap.put(segmentId2, createRemoteLogSegmentMetadata(segmentId2, 5L)); | ||
|
|
||
| // copy finished successfully, but overwritten by the next segment upload so marked as unreferenced. | ||
| RemoteLogSegmentId segmentId3 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(11L, segmentId3, 100L); | ||
| segmentIdToMetadataMap.put(segmentId3, createRemoteLogSegmentMetadata(segmentId3, 11L)); | ||
|
|
||
| // copy finished successfully | ||
| RemoteLogSegmentId segmentId4 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(9L, segmentId4, 150L); | ||
| segmentIdToMetadataMap.put(segmentId4, createRemoteLogSegmentMetadata(segmentId4, 9L)); | ||
|
|
||
| // segments should be sorted by start-offset | ||
| List<RemoteLogSegmentId> expectedList = Arrays.asList(segmentId1, segmentId2, segmentId4, segmentId3); | ||
| List<RemoteLogSegmentId> actualList = new ArrayList<>(); | ||
| epochState.listAllRemoteLogSegments(segmentIdToMetadataMap) | ||
| .forEachRemaining(metadata -> actualList.add(metadata.remoteLogSegmentId())); | ||
| assertEquals(expectedList, actualList); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentStartedState() { | ||
| RemoteLogSegmentId segmentId = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentStartedState(segmentId); | ||
| assertEquals(1, epochState.unreferencedSegmentIds().size()); | ||
| assertTrue(epochState.unreferencedSegmentIds().contains(segmentId)); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentFinishedState() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(101L, segmentId2, 200L); | ||
|
|
||
| assertEquals(2, epochState.referencedSegmentIds().size()); | ||
| assertEquals(segmentId1, epochState.floorEntry(90L)); | ||
| assertEquals(segmentId2, epochState.floorEntry(150L)); | ||
| assertTrue(epochState.unreferencedSegmentIds().isEmpty()); | ||
| assertEquals(200L, epochState.highestLogOffset()); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentFinishedStateForOverlappingSegments() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(5L, segmentId2, 150L); | ||
|
|
||
| assertEquals(1, epochState.referencedSegmentIds().size()); | ||
| assertEquals(segmentId2, epochState.floorEntry(11L)); | ||
| assertEquals(1, epochState.unreferencedSegmentIds().size()); | ||
| assertTrue(epochState.unreferencedSegmentIds().contains(segmentId1)); | ||
| assertEquals(150L, epochState.highestLogOffset()); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentFinishedStateForMultipleOverlappingSegments() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId3 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId4 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
|
|
||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(5L, segmentId2, 150L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(148L, segmentId3, 155L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(4L, segmentId4, 200L); | ||
|
|
||
| assertEquals(1, epochState.referencedSegmentIds().size()); | ||
| assertEquals(segmentId4, epochState.floorEntry(11L)); | ||
| assertEquals(3, epochState.unreferencedSegmentIds().size()); | ||
| assertTrue(epochState.unreferencedSegmentIds().containsAll(Arrays.asList(segmentId1, segmentId2, segmentId3))); | ||
| assertEquals(200L, epochState.highestLogOffset()); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentFinishedStateForDuplicateSegments() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId2, 100L); | ||
|
|
||
| assertEquals(segmentId2, epochState.floorEntry(11L)); | ||
| assertEquals(1, epochState.unreferencedSegmentIds().size()); | ||
| assertTrue(epochState.unreferencedSegmentIds().contains(segmentId1)); | ||
| assertEquals(100L, epochState.highestLogOffset()); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithCopySegmentFinishedStateForSegmentsWithSameStartOffset() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId2, 150L); | ||
|
|
||
| assertEquals(segmentId2, epochState.floorEntry(11L)); | ||
| assertEquals(segmentId2, epochState.floorEntry(111L)); | ||
| assertEquals(1, epochState.unreferencedSegmentIds().size()); | ||
| assertEquals(150L, epochState.highestLogOffset()); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithDeleteSegmentStartedState() { | ||
| RemoteLogSegmentId segmentId1 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| RemoteLogSegmentId segmentId2 = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(10L, segmentId1, 100L); | ||
| epochState.handleSegmentWithCopySegmentFinishedState(101L, segmentId2, 200L); | ||
| assertEquals(2, epochState.referencedSegmentIds().size()); | ||
|
|
||
| epochState.handleSegmentWithDeleteSegmentStartedState(10L, segmentId1); | ||
| epochState.handleSegmentWithDeleteSegmentStartedState(101L, segmentId2); | ||
| assertTrue(epochState.referencedSegmentIds().isEmpty()); | ||
| assertEquals(2, epochState.unreferencedSegmentIds().size()); | ||
| assertTrue(epochState.unreferencedSegmentIds().containsAll(Arrays.asList(segmentId1, segmentId2))); | ||
| } | ||
|
|
||
| @Test | ||
| void handleSegmentWithDeleteSegmentFinishedState() { | ||
| RemoteLogSegmentId segmentId = new RemoteLogSegmentId(tpId, Uuid.randomUuid()); | ||
| epochState.handleSegmentWithCopySegmentStartedState(segmentId); | ||
| assertEquals(1, epochState.unreferencedSegmentIds().size()); | ||
|
|
||
| epochState.handleSegmentWithDeleteSegmentFinishedState(segmentId); | ||
| assertTrue(epochState.unreferencedSegmentIds().isEmpty()); | ||
| } | ||
|
|
||
| private RemoteLogSegmentMetadata createRemoteLogSegmentMetadata(RemoteLogSegmentId remoteLogSegmentId, | ||
| long startOffset) { | ||
| RemoteLogSegmentMetadata metadata = mock(RemoteLogSegmentMetadata.class); | ||
| when(metadata.remoteLogSegmentId()).thenReturn(remoteLogSegmentId); | ||
| when(metadata.startOffset()).thenReturn(startOffset); | ||
| return metadata; | ||
| } | ||
| } |
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.
Please add an example of overlapping segments int he doc here for reader's clarity.
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.
+1