-
Notifications
You must be signed in to change notification settings - Fork 15.4k
[KAFKA-14685] Refactor logic to handle OFFSET_MOVED_TO_TIERED_STORAGE error #13206
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 7 commits
4894131
63e2839
7851d09
42f0937
c29d421
95755c8
1742b8f
f937391
9610005
1f8d636
3adb0b0
d2a3e76
2e38027
9aa8af0
6525189
4b81be1
f6f4416
32d6e01
5d118a0
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 |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| * 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 kafka.server; | ||
|
|
||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.requests.FetchRequest; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| The replica alter log dirs tier state machine is unsupported but is provided to the ReplicaAlterLogDirsThread. | ||
| */ | ||
| public class ReplicaAlterLogDirsTierStateMachine implements TierStateMachine { | ||
|
|
||
| public PartitionFetchState start(TopicPartition topicPartition, | ||
| PartitionFetchState currentFetchState, | ||
| FetchRequest.PartitionData fetchPartitionData) throws Exception { | ||
| // JBOD is not supported with tiered storage. | ||
| throw new UnsupportedOperationException("Building remote log aux state is not supported in ReplicaAlterLogDirsThread."); | ||
| } | ||
|
|
||
| public Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition, | ||
| PartitionFetchState currentFetchState) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| /* | ||
| * 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 kafka.server; | ||
|
|
||
| import kafka.cluster.Partition; | ||
| import kafka.log.LeaderOffsetIncremented$; | ||
| import kafka.log.UnifiedLog; | ||
| import kafka.log.remote.RemoteLogManager; | ||
| import org.apache.kafka.common.KafkaException; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.message.OffsetForLeaderEpochResponseData.EpochEndOffset; | ||
| import org.apache.kafka.common.message.OffsetForLeaderEpochRequestData.OffsetForLeaderPartition; | ||
| import org.apache.kafka.common.protocol.Errors; | ||
| import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpointFile; | ||
| import org.apache.kafka.storage.internals.log.EpochEntry; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.StandardCopyOption; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| import org.apache.kafka.common.requests.FetchRequest.PartitionData; | ||
| import org.apache.kafka.common.utils.Utils; | ||
| import org.apache.kafka.server.common.CheckpointFile; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentMetadata; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteStorageException; | ||
| import org.apache.kafka.server.log.remote.storage.RemoteStorageManager; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import scala.Option; | ||
| import scala.Tuple2; | ||
| import scala.jdk.CollectionConverters; | ||
|
|
||
| /** | ||
| The replica fetcher tier state machine follows a state machine progression. | ||
|
|
||
| Currently, the tier state machine follows a synchronous execution, and we only need to start the machine. | ||
| There is no need to advance the state. | ||
|
|
||
| When started, the tier state machine will fetch the local log start offset of the | ||
| leader and then build the follower's remote log aux state until the leader's | ||
| local log start offset. | ||
| */ | ||
| public class ReplicaFetcherTierStateMachine implements TierStateMachine { | ||
| private final Logger log = LoggerFactory.getLogger(ReplicaFetcherTierStateMachine.class); | ||
|
|
||
| private LeaderEndPoint leader; | ||
| private ReplicaManager replicaMgr; | ||
|
|
||
| public ReplicaFetcherTierStateMachine(LeaderEndPoint leader, | ||
| ReplicaManager replicaMgr) { | ||
| this.leader = leader; | ||
| this.replicaMgr = replicaMgr; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Start the tier state machine for the provided topic partition. Currently, this start method will build the | ||
| * entire remote aux log state synchronously. | ||
| * | ||
| * @param topicPartition the topic partition | ||
| * @param currentFetchState the current PartitionFetchState which will | ||
| * be used to derive the return value | ||
| * @param fetchPartitionData the data from the fetch response that returned the offset moved to tiered storage error | ||
| * | ||
| * @return the new PartitionFetchState after the successful start of the | ||
| * tier state machine | ||
| */ | ||
| public PartitionFetchState start(TopicPartition topicPartition, | ||
|
mattwong949 marked this conversation as resolved.
|
||
| PartitionFetchState currentFetchState, | ||
| PartitionData fetchPartitionData) throws Exception { | ||
|
|
||
| Tuple2<Object, Object> epochAndLeaderLocalStartOffset = leader.fetchEarliestLocalOffset(topicPartition, currentFetchState.currentLeaderEpoch()); | ||
| int epoch = (int) epochAndLeaderLocalStartOffset._1; | ||
| long leaderLocalStartOffset = (long) epochAndLeaderLocalStartOffset._2; | ||
|
|
||
| long offsetToFetch = buildRemoteLogAuxState(topicPartition, currentFetchState.currentLeaderEpoch(), leaderLocalStartOffset, epoch, fetchPartitionData.logStartOffset); | ||
|
|
||
| Tuple2<Object, Object> fetchLatestOffsetResult = leader.fetchLatestOffset(topicPartition, currentFetchState.currentLeaderEpoch()); | ||
| long leaderEndOffset = (long) fetchLatestOffsetResult._2; | ||
|
|
||
| long initialLag = leaderEndOffset - offsetToFetch; | ||
|
|
||
| return PartitionFetchState.apply(currentFetchState.topicId(), offsetToFetch, Option.apply(initialLag), currentFetchState.currentLeaderEpoch(), | ||
| Fetching$.MODULE$, replicaMgr.localLogOrException(topicPartition).latestEpoch()); | ||
| } | ||
|
|
||
| /** | ||
| * This is currently a no-op but will be used for implementing async tiering logic in KAFKA-13560. | ||
| * | ||
| * @param topicPartition the topic partition | ||
| * @param currentFetchState the current PartitionFetchState which will | ||
| * be used to derive the return value | ||
| * | ||
| * @return the original PartitionFetchState | ||
| */ | ||
| public Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition, | ||
|
mattwong949 marked this conversation as resolved.
|
||
| PartitionFetchState currentFetchState) { | ||
| // No-op for now | ||
| return Optional.of(currentFetchState); | ||
| } | ||
|
|
||
| private EpochEndOffset fetchEarlierEpochEndOffset(Integer epoch, | ||
| TopicPartition partition, | ||
| Integer currentLeaderEpoch) { | ||
| int previousEpoch = epoch - 1; | ||
|
|
||
| // Find the end-offset for the epoch earlier to the given epoch from the leader | ||
| Map<TopicPartition, OffsetForLeaderPartition> partitionsWithEpochs = new HashMap<>(); | ||
| partitionsWithEpochs.put(partition, new OffsetForLeaderPartition().setPartition(partition.partition()).setCurrentLeaderEpoch(currentLeaderEpoch).setLeaderEpoch(previousEpoch)); | ||
|
|
||
| Option<EpochEndOffset> maybeEpochEndOffset = leader.fetchEpochEndOffsets(CollectionConverters.MapHasAsScala(partitionsWithEpochs).asScala()).get(partition); | ||
| if (maybeEpochEndOffset.isEmpty()) { | ||
| throw new KafkaException("No response received for partition: " + partition); | ||
| } | ||
|
|
||
| EpochEndOffset epochEndOffset = maybeEpochEndOffset.get(); | ||
| if (epochEndOffset.errorCode() != Errors.NONE.code()) { | ||
| throw Errors.forCode(epochEndOffset.errorCode()).exception(); | ||
| } | ||
|
|
||
| return epochEndOffset; | ||
| } | ||
|
|
||
| private List<EpochEntry> readLeaderEpochCheckpoint(RemoteLogManager rlm, | ||
| RemoteLogSegmentMetadata remoteLogSegmentMetadata) throws IOException, RemoteStorageException { | ||
| InputStream inputStream = rlm.storageManager().fetchIndex(remoteLogSegmentMetadata, RemoteStorageManager.IndexType.LEADER_EPOCH); | ||
| try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { | ||
| CheckpointFile.CheckpointReadBuffer<EpochEntry> readBuffer = new CheckpointFile.CheckpointReadBuffer<>("", bufferedReader, 0, LeaderEpochCheckpointFile.FORMATTER); | ||
| return readBuffer.read(); | ||
| } | ||
| } | ||
|
|
||
| private void buildProducerSnapshotFile(File snapshotFile, | ||
| RemoteLogSegmentMetadata remoteLogSegmentMetadata, | ||
| RemoteLogManager rlm) throws IOException, RemoteStorageException { | ||
| File tmpSnapshotFile = new File(snapshotFile.getAbsolutePath() + ".tmp"); | ||
| // Copy it to snapshot file in atomic manner. | ||
| Files.copy(rlm.storageManager().fetchIndex(remoteLogSegmentMetadata, RemoteStorageManager.IndexType.PRODUCER_SNAPSHOT), | ||
| tmpSnapshotFile.toPath(), StandardCopyOption.REPLACE_EXISTING); | ||
| Utils.atomicMoveWithFallback(tmpSnapshotFile.toPath(), snapshotFile.toPath(), false); | ||
| } | ||
|
|
||
| /** | ||
| * It tries to build the required state for this partition from leader and remote storage so that it can start | ||
| * fetching records from the leader. The return value is the next offset to fetch from the leader, which is the | ||
| * next offset following the end offset of the remote log portion. | ||
| */ | ||
|
mattwong949 marked this conversation as resolved.
|
||
| private Long buildRemoteLogAuxState(TopicPartition topicPartition, | ||
|
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. Have you considered refactoring this method to ease testability? This could be part of another PR too to avoid changing too many parts in one code refactor. |
||
| Integer currentLeaderEpoch, | ||
| Long leaderLocalLogStartOffset, | ||
| Integer epochForLeaderLocalLogStartOffset, | ||
| Long leaderLogStartOffset) throws IOException, RemoteStorageException { | ||
|
|
||
| UnifiedLog unifiedLog = replicaMgr.localLogOrException(topicPartition); | ||
|
|
||
| long nextOffset; | ||
|
|
||
| if (unifiedLog.remoteStorageSystemEnable() && unifiedLog.config().remoteLogConfig.remoteStorageEnable) { | ||
| if (replicaMgr.remoteLogManager().isEmpty()) throw new IllegalStateException("RemoteLogManager is not yet instantiated"); | ||
|
|
||
| RemoteLogManager rlm = replicaMgr.remoteLogManager().get(); | ||
|
|
||
| // Find the respective leader epoch for (leaderLocalLogStartOffset - 1). We need to build the leader epoch cache | ||
| // until that offset | ||
| long previousOffsetToLeaderLocalLogStartOffset = leaderLocalLogStartOffset - 1; | ||
| int targetEpoch; | ||
| // If the existing epoch is 0, no need to fetch from earlier epoch as the desired offset(leaderLogStartOffset - 1) | ||
| // will have the same epoch. | ||
| if (epochForLeaderLocalLogStartOffset == 0) { | ||
| targetEpoch = epochForLeaderLocalLogStartOffset; | ||
| } else { | ||
| // Fetch the earlier epoch/end-offset(exclusive) from the leader. | ||
| EpochEndOffset earlierEpochEndOffset = fetchEarlierEpochEndOffset(epochForLeaderLocalLogStartOffset, topicPartition, currentLeaderEpoch); | ||
| // Check if the target offset lies within the range of earlier epoch. Here, epoch's end-offset is exclusive. | ||
| if (earlierEpochEndOffset.endOffset() > previousOffsetToLeaderLocalLogStartOffset) { | ||
| // Always use the leader epoch from returned earlierEpochEndOffset. | ||
| // This gives the respective leader epoch, that will handle any gaps in epochs. | ||
| // For ex, leader epoch cache contains: | ||
| // leader-epoch start-offset | ||
| // 0 20 | ||
| // 1 85 | ||
| // <2> - gap no messages were appended in this leader epoch. | ||
| // 3 90 | ||
| // 4 98 | ||
| // There is a gap in leader epoch. For leaderLocalLogStartOffset as 90, leader-epoch is 3. | ||
| // fetchEarlierEpochEndOffset(2) will return leader-epoch as 1, end-offset as 90. | ||
| // So, for offset 89, we should return leader epoch as 1 like below. | ||
| targetEpoch = earlierEpochEndOffset.leaderEpoch(); | ||
| } else { | ||
| targetEpoch = epochForLeaderLocalLogStartOffset; | ||
| } | ||
| } | ||
|
|
||
| Optional<RemoteLogSegmentMetadata> maybeRlsm = rlm.fetchRemoteLogSegmentMetadata(topicPartition, targetEpoch, previousOffsetToLeaderLocalLogStartOffset); | ||
|
|
||
| if (maybeRlsm.isPresent()) { | ||
|
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. Note: if the rlmMetadata is unavailable for an extended period of time, the replica fetcher will keep retrying indefinitely to construct the starting fetch state for the partition. This will lead to an The asynchronous resolution of the correct fetch state from the remote storages (KAFKA-13560) will prevent the extra load on the replica fetcher thread itself. The consideration above applies to the RPCs which are made on the synchronous fetch path. |
||
| RemoteLogSegmentMetadata remoteLogSegmentMetadata = maybeRlsm.get(); | ||
| // Build leader epoch cache, producer snapshots until remoteLogSegmentMetadata.endOffset() and start | ||
| // segments from (remoteLogSegmentMetadata.endOffset() + 1) | ||
| // Assign nextOffset with the offset from which next fetch should happen. | ||
| nextOffset = remoteLogSegmentMetadata.endOffset() + 1; | ||
|
|
||
| // Truncate the existing local log before restoring the leader epoch cache and producer snapshots. | ||
| Partition partition = replicaMgr.getPartitionOrException(topicPartition); | ||
| partition.truncateFullyAndStartAt(nextOffset, false); | ||
|
|
||
| // Build leader epoch cache. | ||
| unifiedLog.maybeIncrementLogStartOffset(leaderLogStartOffset, LeaderOffsetIncremented$.MODULE$); | ||
| List<EpochEntry> epochs = readLeaderEpochCheckpoint(rlm, remoteLogSegmentMetadata); | ||
| if (unifiedLog.leaderEpochCache().isDefined()) { | ||
| unifiedLog.leaderEpochCache().get().assign(epochs); | ||
| } | ||
|
|
||
| log.debug("Updated the epoch cache from remote tier till offset: {} with size: {} for {}", leaderLocalLogStartOffset, epochs.size(), partition); | ||
|
|
||
| // Restore producer snapshot | ||
| File snapshotFile = UnifiedLog.producerSnapshotFile(unifiedLog.dir(), nextOffset); | ||
| buildProducerSnapshotFile(snapshotFile, remoteLogSegmentMetadata, rlm); | ||
|
|
||
| // Reload producer snapshots. | ||
| unifiedLog.producerStateManager().truncateFullyAndReloadSnapshots(); | ||
| unifiedLog.loadProducerState(nextOffset); | ||
| log.debug("Built the leader epoch cache and producer snapshots from remote tier for {}, " + | ||
| "with active producers size: {}, leaderLogStartOffset: {}, and logEndOffset: {}", | ||
| partition, unifiedLog.producerStateManager().activeProducers().size(), leaderLogStartOffset, nextOffset); | ||
| } else { | ||
| throw new RemoteStorageException("Couldn't build the state from remote store for partition: " + topicPartition + | ||
| ", currentLeaderEpoch: " + currentLeaderEpoch + | ||
| ", leaderLocalLogStartOffset: " + leaderLocalLogStartOffset + | ||
| ", leaderLogStartOffset: " + leaderLogStartOffset + | ||
| ", epoch: " + targetEpoch + | ||
| "as the previous remote log segment metadata was not found"); | ||
| } | ||
| } else { | ||
| // If the tiered storage is not enabled throw an exception back so that it will retry until the tiered storage | ||
| // is set as expected. | ||
| throw new RemoteStorageException("Couldn't build the state from remote store for partition " + topicPartition + ", as remote log storage is not yet enabled"); | ||
| } | ||
|
|
||
| return nextOffset; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * 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 kafka.server; | ||
|
Contributor
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. Should this be in the storage module under org.apache.kafka.server.log.remote.storage package? Eventually, we could move ReplicaFetcherTierStateMachine to the storage module too.
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. yup makes sense I'll move the interface over
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. actually I remember why I didn't initially put it in the storage package. it seems like it wouldn't be a simple change to move this to there because the TSM relies on the PartitionFetchState case class in AbstractFetcherThread.scala. it would've created a circular dependency across modules that I wanted to avoid. @junrao wdyt about keeping it in the core module in this PR |
||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.apache.kafka.common.requests.FetchRequest.PartitionData; | ||
|
|
||
| /** | ||
| * This interface defines the APIs needed to handle any state transitions | ||
| * related to tiering in AbstractFetcherThread. | ||
|
mattwong949 marked this conversation as resolved.
Outdated
|
||
| */ | ||
| public interface TierStateMachine { | ||
|
|
||
| /** | ||
| * Start the tier state machine for the provided topic partition. | ||
| * | ||
| * @param topicPartition the topic partition | ||
| * @param currentFetchState the current PartitionFetchState which will | ||
| * be used to derive the return value | ||
| * @param fetchPartitionData the data from the fetch response that returned the offset moved to tiered storage error | ||
| * | ||
| * @return the new PartitionFetchState after the successful start of the | ||
| * tier state machine | ||
| */ | ||
| PartitionFetchState start(TopicPartition topicPartition, | ||
| PartitionFetchState currentFetchState, | ||
| PartitionData fetchPartitionData) throws Exception; | ||
|
|
||
| /** | ||
| * Optionally advance the state of the tier state machine, based on the | ||
| * current PartitionFetchState. The decision to advance the tier | ||
| * state machine is implementation specific. | ||
| * | ||
| * @param topicPartition the topic partition | ||
| * @param currentFetchState the current PartitionFetchState which will | ||
| * be used to derive the return value | ||
| * | ||
| * @return the new PartitionFetchState if the tier state machine was advanced | ||
|
mattwong949 marked this conversation as resolved.
Outdated
|
||
| */ | ||
| Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition, | ||
| PartitionFetchState currentFetchState); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.