[KAFKA-14685] Refactor logic to handle OFFSET_MOVED_TO_TIERED_STORAGE error#13206
Conversation
…also implemented in java. need to modify testing still
|
cc @satishd |
There was a problem hiding this comment.
Thanks for the PR @mattwong949! Few comments.
Have you considered refactoring out some of the tiering specific tests in AbstractFetcherThreadTest into a separate test suite? (i.e., ReplicaFetcherTierStateMachineTest.java)?
There was a problem hiding this comment.
Thanks @mattwong949 for the PR. I had an initial review, and left a few minor comments.
|
|
||
| Optional<RemoteLogSegmentMetadata> maybeRlsm = rlm.fetchRemoteLogSegmentMetadata(topicPartition, targetEpoch, previousOffsetToLeaderLocalLogStartOffset); | ||
|
|
||
| if (maybeRlsm.isPresent()) { |
There was a problem hiding this comment.
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 OffsetForLeaderEpoch and ListOffsets requests every time. If a large number of partitions are impacted, that will generate unnecessary inter-broker traffic on the cluster - although marginal most of the time. As an optimization, we could store the leader epoch associated to the leader's local log start offset - 1 which was retrieved here (we would still, however, need to query for the local log start offset on the leader on every iteration, and fetch the associated leader epoch if it has changed).
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.
| * 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. | ||
| */ | ||
| private Long buildRemoteLogAuxState(TopicPartition topicPartition, |
There was a problem hiding this comment.
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.
junrao
left a comment
There was a problem hiding this comment.
@mattwong949 : Thanks for the PR. A few comments below.
|
|
||
| def buildRemoteLog(topicPartition: TopicPartition, leaderLogStartOffset: Long): Unit = { | ||
| fetcher.truncateFullyAndStartAt(topicPartition, leaderState.localLogStartOffset) | ||
| replicaState.logStartOffset = leaderLogStartOffset |
There was a problem hiding this comment.
Here, we want to update the replicaState in the MockFetcherThread. Would it be better to call fetcher.replicaPartitionState to get the replicaState explicitly?
Also, adding a callback in a Mock class seems a bit complicated. Alternatively, we could override doWork() in MockFetcherThread so that it updates its replicaPartitionState from the partitionState in AbstractFetcherThread after each call. This way, we could get rid of the callback in mockTierStateMachine.
There was a problem hiding this comment.
I had tried to ensure that the MockTierStateMachine code would get invoked as part of the test as a sanity check that the TierStateMachine logic is getting called from handleOffsetsMovedToTieredStorage. I couldn't think of another way to get the replicaPartitionState updated from the MockTierStateMachine since it is contained in the MockFetcherThread.
I am not sure about the effectiveness of the test if we override the doWork from the MockFetcherThread to update the replicaPartitionState given my thoughts on trying to invoke the MockTierStateMachine code, but maybe I'm misunderstanding the alternative you mentioned.
There was a problem hiding this comment.
Well, buildRemoteLog() does two things (1) call fetcher.truncateFullyAndStartAt and (2) set replicaState.logStartOffset. For (1), since the truncation logic is moved to TierStateMachine, it probably should be done in MockTierStateMachine.start() directly. For (2), the existing test doesn't need to set replicaState.logStartOffset. So, it seems it's unnecessary? If address both (1) and (2), then the callback is not needed.
There was a problem hiding this comment.
hm ok I think I see what you're suggesting. Let me try the refactor w/o the callback. thanks Jun
showuon
left a comment
There was a problem hiding this comment.
@mattwong949 , made a 1st pass, left some comments. Thanks for the PR.
| debug(s"Received error ${Errors.OFFSET_MOVED_TO_TIERED_STORAGE}, " + | ||
| s"at fetch offset: ${currentFetchState.fetchOffset}, " + s"topic-partition: $topicPartition") | ||
| if (!handleOffsetsMovedToTieredStorage(topicPartition, currentFetchState, | ||
| fetchPartitionData.currentLeaderEpoch, partitionData.logStartOffset())) |
There was a problem hiding this comment.
For the logStartOffset, we used to retrieve from partitionData, which is from fetchResponse (from leader), and now, we changed to get from fetchPartitionData, which is from fetchRequest. Any reason we change it? I'm thinking the logStartOffset should still rely on the leader response to avoid some inconsistency. WDYT?
There was a problem hiding this comment.
ah i believe this was a mistake. I wanted the TierStateMachine.start to use the fetch response as well. thanks for the catch
| val (epoch, leaderStartOffset) = if (fetchFromLocalLogStartOffset) | ||
| leader.fetchEarliestLocalOffset(topicPartition, currentLeaderEpoch) else | ||
| leader.fetchEarliestOffset(topicPartition, currentLeaderEpoch) | ||
|
|
||
| val (_, leaderStartOffset) = leader.fetchEarliestOffset(topicPartition, currentLeaderEpoch) |
There was a problem hiding this comment.
Could you explain why we change this behavior? I think we should try fetch from local start offset if possible to save time to catch up. But after this change, we always fetch from log start offset (not local log start offset). Why should we change it?
There was a problem hiding this comment.
IIUC, this is correct. We should try to fetch from the leader log start offset instead of the local leader log start offset so that in the case where the leader log start offset < local leader log start offset, the leader returns an offset-move-to-tiered-storage error and the follower takes the related code path to reconstruct the local replica log prefix.
There was a problem hiding this comment.
+1 to @Hangleton 's comment. I restored this function back to the original logic, so this function is only called when first trying to fetch the leader log start offset (after starting fetch or after getting the offset out of range error). If the follower gets the OFFSET_MOVED_TO_TIERED_STORAGE error, it proceeds to other the code path to build the remote aux log state via TierStateMachine interface
| * In the first case, the follower's current log end offset is smaller than the leader's log start offset. So the | ||
| * follower should truncate all its logs, roll out a new segment and start to fetch from the current leader's log | ||
| * start offset. |
There was a problem hiding this comment.
I read through the code and comments, it is not correct. We're saying leaderEndOffset >= replicaEndOffset case here, not leaderStratOffset. The leaderStartOffset is another case under leaderEndOffset >= replicaEndOffset`. So, maybe change to:
In the first case, [if] the follower's current log end offset is smaller than the leader's log start offset, the follower should truncate all its logs, roll out a new segment and start to fetch from the current leader's log start offset since the data are all stale.
WDYT?
There was a problem hiding this comment.
I think this case refers to line 674. This comment reverts back to the original one, before the change introduced for TS. Agreed it could be made clearer though, perhaps by referencing explicitly the case below.
There was a problem hiding this comment.
Yes this was just a revert to the original comments and logic. but this makes sense, I can try to clear up the cases. thanks for the comment suggestion
|
sorry for the delay on any open comments, I'll be able to reply to and iterate on them this week |
…factor and fix build on scala 2.12
junrao
left a comment
There was a problem hiding this comment.
@mattwong949 : Thanks for the updated PR. A few more comments.
|
|
||
| def buildRemoteLog(topicPartition: TopicPartition, leaderLogStartOffset: Long): Unit = { | ||
| fetcher.truncateFullyAndStartAt(topicPartition, leaderState.localLogStartOffset) | ||
| replicaState.logStartOffset = leaderLogStartOffset |
There was a problem hiding this comment.
Well, buildRemoteLog() does two things (1) call fetcher.truncateFullyAndStartAt and (2) set replicaState.logStartOffset. For (1), since the truncation logic is moved to TierStateMachine, it probably should be done in MockTierStateMachine.start() directly. For (2), the existing test doesn't need to set replicaState.logStartOffset. So, it seems it's unnecessary? If address both (1) and (2), then the callback is not needed.
junrao
left a comment
There was a problem hiding this comment.
@mattwong949 : Thanks for the updated PR. One more followup comment.
| fetchPartitionData: FetchResponseData.PartitionData): PartitionFetchState = { | ||
| replicaState.log.clear() | ||
| replicaState.localLogStartOffset = 5 | ||
| replicaState.logEndOffset = 5 |
There was a problem hiding this comment.
Instead of the customizing for this test, I am wondering if we could make this more general. For example, could we pass in fetcher to MockTierStateMachine and call fetcher.truncateFullyAndStartAt() in MockTierStateMachine.start().
There was a problem hiding this comment.
hm yeah I see what you are saying, I can make this change. the only somewhat tricky part is that we pass in the MockTierStateMachine to the fetcher itself, so we'd have to pass the reference to the fetcher to the MockTierStateMachine after both are instantiated
There was a problem hiding this comment.
Got it. So, if we do that, it's the same as the callback logic you had earlier.
Here is another way to improve this. Since AbstractFetcherThread already exposes the partitionState through fetchState(), we could just get rid of replicaPartitionStates in MockFetcherThread and purely rely on the partitionState that's changed on every doWork() call. Will that work? Since that's a bigger change, we could probably just take the callback code you had in this PR and make the bigger change in a separate PR.
junrao
left a comment
There was a problem hiding this comment.
@mattwong949 : Thanks for the explanation. A couple of more comments.
| fetchPartitionData: FetchResponseData.PartitionData): PartitionFetchState = { | ||
| replicaState.log.clear() | ||
| replicaState.localLogStartOffset = 5 | ||
| replicaState.logEndOffset = 5 |
There was a problem hiding this comment.
Got it. So, if we do that, it's the same as the callback logic you had earlier.
Here is another way to improve this. Since AbstractFetcherThread already exposes the partitionState through fetchState(), we could just get rid of replicaPartitionStates in MockFetcherThread and purely rely on the partitionState that's changed on every doWork() call. Will that work? Since that's a bigger change, we could probably just take the callback code you had in this PR and make the bigger change in a separate PR.
| * limitations under the License. | ||
| */ | ||
|
|
||
| package kafka.server; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
yup makes sense I'll move the interface over
There was a problem hiding this comment.
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
…factor fix conflicts and change logic in ReplicaFetcherTierStateMachine
junrao
left a comment
There was a problem hiding this comment.
@mattwong949 : Thanks for the updated PR. LGTM
This PR adds the TierStateMachine interface to handle all state transitions related to tiered storage and building the remote log aux state.
The new interface supports a
startandmaybeAdvanceState. In theReplicaFetcherTierStateMachine, themaybeAdvanceStateis unused since the implementation is synchronous. Only thestartis needed. This PR keeps the addition of themaybeAdvanceStatebecause there is an existing task for building the remote log aux state in an asynchronous manner that will be able to use the full interface https://issues.apache.org/jira/browse/KAFKA-13560