KAFKA-15181: Improvements for TopicBaseRemoteLogMetadataManager#14127
Conversation
67f35bb to
1a86b92
Compare
|
Will check later this week |
showuon
left a comment
There was a problem hiding this comment.
Will continue review later. Thanks.
| } | ||
|
|
||
| public void ensureInitialized() throws InterruptedException { | ||
| if (!initializedLatch.await(2, TimeUnit.MINUTES)) { |
There was a problem hiding this comment.
I can see we will block for operations like findOffsetByTimestamp (invoked by clients), and the client request timeout default is 30 secs. I think the 2 mins wait is not appropriate here. Maybe 10 secs? Or any reason we chose 2 mins?
There was a problem hiding this comment.
In large clusters, we saw that it takes more than a minute to initialize. That's the reason we kept it 2 mins. We could also make this configurable. Thoughts?
cc @kamalcph
There was a problem hiding this comment.
Both FETCH and LIST_OFFSETS APIs will hit this method. If the partition is not initialised within the timeout (2 mins / 10 secs), then we throw RemoteResourceNotFoundException which will in turn throw UNKNOWN_SERVER_ERROR back to the caller.
Consumer will retry the FETCH request as UNKNOWN_SERVER_ERROR is retriable. The LIST_OFFSETS call can originate from both Consumer and Admin client. Consumer retries the request on receiving retriable error but Admin client won't. If the user tries to increase the request timeout from 30 secs to 2 min in AdminClient, then the LIST_OFFSETS request should succeed.
This timeout specified here is kind of upper boundary where we expect the RLMM to initialise the partition within that amount of time.
If the request timeout is configured to default value of 30 secs on the client, then the request gets timed out and the client retries the request. In case of AdminClient, the user gets to know that the request was failed due to TimeoutException and will increase the request timeout as appropriate.
There was a problem hiding this comment.
We can reduce the timeout to 10 seconds but should think of a solution for Admin#listOffsets call.
There was a problem hiding this comment.
Thanks for the explanation, @kamalcph !
One correction: UNKNOWN_SERVER_ERROR is unretriable. So if we throw RemoteResourceNotFoundException, the client won't be retried.
I think we don't have to consider if client will auto retry or not when facing retriable error. IMO, sending a request and returned a request timeout error without any explanation (suppose the 2 min timeout is not expired here, so no error thrown in the broker side), which is not good.
I think reduce to a small number (I think 10 secs is still too long, maybe 1 sec, or return error immediately if it's not initialized), and wrap the RemoteResourceNotFoundException with a retriable error, ex: ReplicaNotAvailableException, and add clear messages there. I think as long as we returned clear error reason with a retriable exception, this is already a good API. We don't want to block the request for a long time because we're waiting for some state change. Like COORDINATOR_LOAD_IN_PROGRESS error, it's clear that we returned this error and want the client retry later, instead of waiting in the broker for coordinator load completion. Does that make sense?
There was a problem hiding this comment.
The java client retries when it receives UNKNOWN_SERVER_ERROR not sure for other clients. As per the protocol contract, the clients are not expected to retry when it receives this error.
We also have one more API isInitialized that can be used instead of ensureInitialized if we want to unblock the caller immediately. This latch will only block the remote-log-reader and remote-log-deleter threads until the partition gets initialised and won't block the request handler threads.
The idea to increase the timeout upto 2 minutes was similar to delayed purgatory where the requests are hold until it gets served within the request timeout.
There was a problem hiding this comment.
Currently, the LIST_OFFSETS call is sync which is handled by the request handler threads so the request-handler-thread will be blocked here. Until KAFKA-13560 gets implemented, it's good to reduce the timeout to 1s (or) throw error immediately as suggested. Also, it will unblock the remote-log-reader threads where it can be utilised for other initialized partitions.
There was a problem hiding this comment.
Also, ListOffsetsHandler handles retriable error. So, we are good if we throw retryable error back to the client.
There was a problem hiding this comment.
@kamalcph , thanks for the update!
@abhijeetk88 , could you reduce the timeout to 1s (or) throw error immediately ( I think use isInitialized() direcrtly is better) as discussed above? Thanks.
There was a problem hiding this comment.
@showuon I have addressed this. Added a new retryable exception. Let me know if we need to rename it.
| // If the leader for a `__remote_log_metadata` partition is not available, then the call to `ListOffsets` | ||
| // will fail after the default timeout of 1 min. Added a delay of 5 min in between the retries to prevent the | ||
| // thread from aggressively fetching the list offsets. During this time, the recently reassigned | ||
| // user-topic-partitions won't be marked as initialized. | ||
| if (isOffsetsFetchFailed && lastFailedFetchOffsetsTimestamp + 300_000 < time.milliseconds()) { | ||
| fetchBeginAndEndOffsets(); | ||
| } |
There was a problem hiding this comment.
The 5 mins wait is too long. Like you said in the comment, timeout is 1 min, why can't we retry after 10 secs or so to have more chance to get the offsets? IMO, 10 secs for electing a leader for a partition is long enough, so we should reduce this timeout. Thought?
There was a problem hiding this comment.
When the leader is offline, it may take some time to come up. In that case, we want to avoid aggressively fetching the offsets. Otherwise, we will frequently call this method and get blocked for 1 min (until the API times out). This will prevent us from processing updates for the already assigned topic partitions.
It is basically a tradeoff (when the leader is offline) between whether we want to update for the newly assigned topic partitions as soon as possible or always be up-to-date for the already assigned topic partitions. We chose the second option here.
There was a problem hiding this comment.
Could we make this a ConsumerTask input parameter? Maybe later can be exposed as a configuration.
There was a problem hiding this comment.
Added input parameter
showuon
left a comment
There was a problem hiding this comment.
@abhijeetk88 , thanks for the PR. Left some comments.
| @Test | ||
| public void testCloseOnNoAssignment() throws InterruptedException { | ||
| thread.start(); | ||
| Thread.sleep(10); |
There was a problem hiding this comment.
Could you explain what this test is trying to verify? If we want to verify the consumer thread is closed, should we explicitly assert that?
There was a problem hiding this comment.
It is trying to verify that when there were no assignments made, the consumer thread still shuts down gracefully.
There was a problem hiding this comment.
Could we add some assertion to verify consumer task shuts down gracefully as you expected? In current test, even if the consumer task doesn't shutdown, it sill passes the test, right?
There was a problem hiding this comment.
I will add assertions in 'afterEach' method to ensure that the consumer thread shuts down gracefully
There was a problem hiding this comment.
It's still unclear in this test, because the assertion you added is not inside the test. Could you at least add an assertion inside the test, ex:
@Test
public void testCloseOnNoAssignment() throws InterruptedException {
thread.start();
Thread.sleep(10);
assertDoesNotThrow(() -> consumerTask.close(), "Close method threw exception");
This way, people can understand you want to verify it should be closed gracefully.
There was a problem hiding this comment.
Added assertion for clarity.
812f7c4 to
4121b0d
Compare
|
Thanks for the review @showuon. I have addressed your comments and responded to your questions. Please take a look. |
|
Apologies for joining the review late (I had to spend a few hours getting acquainted with the ConsumerTask code). I carried out a first pass on the changes to the main files and they make sense to me. I will circle back tomorrow morning and have another go through the tests. |
clolov
left a comment
There was a problem hiding this comment.
Okay, I finished my first pass through the tests and I have a couple of small comments/questions
| final List<TopicIdPartition> idPartitions = getIdPartitions("sample", 3); | ||
| final Map<TopicPartition, Long> endOffsets = idPartitions.stream() | ||
| .map(idp -> toRemoteLogPartition(partitioner.metadataPartition(idp))) | ||
| .collect(Collectors.toMap(Function.identity(), e -> 0L, (a, b) -> b)); |
There was a problem hiding this comment.
Is the merge function needed? If not, can we get rid of it, because its presence leads me to think that we can have multiple values associated with the same key and unless I am wrong that isn't the case here?
There was a problem hiding this comment.
The TopicIdPartitions are being converted to metadata topic partitions and this will result in multiple user topic partitions getting mapped to the same metadata topic partition. The merge function is required to handle multiple entries with the same key. Here we choose any value in the merge function, since all of the values will be 0.
| 0, 100, -1L, 0, | ||
| time.milliseconds(), SEG_SIZE, Collections.singletonMap(0, 0L)); | ||
| Assertions.assertThrows(ExecutionException.class, () -> rlmm().addRemoteLogSegmentMetadata(leaderSegmentMetadata).get(), | ||
| "org.apache.kafka.common.KafkaException: This consumer is not assigned to the target partition 0. Partitions currently assigned: []"); |
There was a problem hiding this comment.
I thought that the last argument to an assertThrows is the message you would like to return if the assertion fails rather than the message you would like to check is contained in the exception? Or am I misunderstanding the purpose of this message?
There was a problem hiding this comment.
Can you give a bit more explanation here, because I do not understand this? Are we expecting this to throw, in which case, why would it accept the metadata entry or are we not expecting it to throw?
There was a problem hiding this comment.
My bad, I actually want to assert that it fails with that message. I will change this.
We are trying to assert that adding the remote log segment metadata entry will throw an exception since the partition is not subscribed yet.
There was a problem hiding this comment.
Thanks @abhijeetk88. Took an initial look and left some comments. Most changes look good to me. Will have another look at ConsumerTask (without looking at git diff 😅 ) and tests tomorrow.
| private static final Logger log = LoggerFactory.getLogger(ConsumerTask.class); | ||
|
|
||
| private static final long POLL_INTERVAL_MS = 100L; | ||
| static long pollIntervalMs = 100L; |
There was a problem hiding this comment.
Is this really needed?
I can see value is overwritten on test setup to 10L, but I can manage to test successfully with default value 100L.
There was a problem hiding this comment.
It will take longer for the tests to run with higher poll intervals. That is the reason we want to override it in tests.
There was a problem hiding this comment.
Got it. Though, I'd argue a mutable static field may not be the best option here. Adding pollIntervalMs as input parameter/config may be better?
Also, isn't 100ms too often to poll again? Maybe a few seconds or higher may be a better default.
There was a problem hiding this comment.
+1 on pollIntervalMs should not be a static field here. We can also have this as a config if needed later.
There was a problem hiding this comment.
Added as an input parameter.
Also, it is a timeout value and not a poll interval. I have updated the field name. Let me know if the default value of 100 ms does not make sense for timeout.
There was a problem hiding this comment.
I'd suggest increasing the timeout to at least 5 seconds to reduce poll frequency when no new data is available.
| RemotePartitionMetadataEventHandler remotePartitionMetadataEventHandler, | ||
| public ConsumerTask(RemotePartitionMetadataEventHandler remotePartitionMetadataEventHandler, | ||
| RemoteLogMetadataTopicPartitioner topicPartitioner, | ||
| Path committedOffsetsPath, |
There was a problem hiding this comment.
Haven't look to deep into this, but could you elaborate on why this file is not needed anymore?
btw, COMMITTED_OFFSETS_FILE_NAME can be removed on ConsumerManager.
There was a problem hiding this comment.
We want to keep the implementation simple for now without checkpointing mechanism.
44a794f to
bcc6f56
Compare
| } | ||
| } | ||
|
|
||
| static class StartAndEndOffsetHolder { |
There was a problem hiding this comment.
nit: not sure the Holder adds much, but feel free to ignore this.
| static class StartAndEndOffsetHolder { | |
| static class StartAndEndOffsets { |
| // If the leader for a `__remote_log_metadata` partition is not available, then the call to `ListOffsets` | ||
| // will fail after the default timeout of 1 min. Added a delay of 5 min in between the retries to prevent the | ||
| // thread from aggressively fetching the list offsets. During this time, the recently reassigned | ||
| // user-topic-partitions won't be marked as initialized. | ||
| if (isOffsetsFetchFailed && lastFailedFetchOffsetsTimestamp + 300_000 < time.milliseconds()) { | ||
| fetchBeginAndEndOffsets(); | ||
| } |
There was a problem hiding this comment.
Could we make this a ConsumerTask input parameter? Maybe later can be exposed as a configuration.
| private static final Logger log = LoggerFactory.getLogger(ConsumerTask.class); | ||
|
|
||
| private static final long POLL_INTERVAL_MS = 100L; | ||
| static long pollIntervalMs = 100L; |
There was a problem hiding this comment.
Got it. Though, I'd argue a mutable static field may not be the best option here. Adding pollIntervalMs as input parameter/config may be better?
Also, isn't 100ms too often to poll again? Maybe a few seconds or higher may be a better default.
jeqo
left a comment
There was a problem hiding this comment.
Thanks @abhijeetk88. Adding a few more comments while taking another round of reviews on the tests.
satishd
left a comment
There was a problem hiding this comment.
Thanks @abhijeetk88 for the PR, left a minor comment.
| private static final Logger log = LoggerFactory.getLogger(ConsumerTask.class); | ||
|
|
||
| private static final long POLL_INTERVAL_MS = 100L; | ||
| static long pollIntervalMs = 100L; |
There was a problem hiding this comment.
+1 on pollIntervalMs should not be a static field here. We can also have this as a config if needed later.
eff6247 to
fa93a24
Compare
da7aa6a to
6b84359
Compare
|
Thanks @showuon for your comments. I have addressed them inline/with the latest commits. Please take a look. Also, rebased the changes on top of the latest tunk. |
showuon
left a comment
There was a problem hiding this comment.
Overall LGTM! Just one comment. Thanks.
| @@ -0,0 +1,14 @@ | |||
| package org.apache.kafka.common.errors; | |||
|
|
|||
| public class ResourceNotReadyException extends RetriableException { | |||
There was a problem hiding this comment.
Sorry, org.apache.kafka.common.errors is a public API, we need a KIP to add it. That's why I suggested to use the existing ReplicaNotAvailableException instead. Could we change it and we can write a KIP for this new ResourceNotReadyException if you think it's more appropriate?
There was a problem hiding this comment.
+1 on not introducing a new error as it requires a KIP.
We can reuse the closest possible error for now. ReplicaNotAvailableException triggers refreshing a metadata update from the client as it is kind of an InvalidMetadataException which may generate a lot of metadata calls. It may be good to return an exception that does not call for metadata refresh.
We can introduce a new error as a separate KIP later and handle it appropriately.
There was a problem hiding this comment.
We couldn't find another exception that fits our case. We can use ReplicaNotAvailableException for now. I have created a jira to introduce a new retryable exception/error code.
https://issues.apache.org/jira/browse/KAFKA-15405
There was a problem hiding this comment.
That looks reasonable to me for now.
There was a problem hiding this comment.
Updated the PR.
showuon
left a comment
There was a problem hiding this comment.
LGTM! Just a minor comment. Thanks for the patch!
satishd
left a comment
There was a problem hiding this comment.
Thanks @abhijeetk88 for addressing the review comments. LGTM.
|
It seems test failures are unrelated to this PR. Merging it to trunk. |
|
Merging to 3.6 also. |
| private final Map<TopicIdPartition, Long> readOffsetsByUserTopicPartition = new HashMap<>(); | ||
|
|
||
| private final long committedOffsetSyncIntervalMs; | ||
| private CommittedOffsetsFile committedOffsetsFile; |
There was a problem hiding this comment.
@abhijeetk88 @satishd : Sorry to chime in late on this. It seems that we removed the usage of CommittedOffsetsFile.
- Does the consumer always start from the beginning offset after restart?
- Should we remove the CommittedOffsetsFile class?
- This actually changes the on-disk layout. Was this discussed in a KIP?
There was a problem hiding this comment.
@junrao , answering your question:
-
Does the consumer always start from the beginning offset after restart?
Yes, we always read from beginning each time we restart. -
Should we remove the CommittedOffsetsFile class?
Good point. Will remove it in another PR. -
This actually changes the on-disk layout. Was this discussed in a KIP?
I had a look, and yes, the committed offset file is mentioned in the KIP-405. @kamalcph @satishd , do we remove the offset committed file and read from beginning each time on purpose? To fix the original bug, I don't think we need to remove this offset committed file mechanism. WDYT?
There was a problem hiding this comment.
Sorry for the late reply. Now, we are always reading from the beginning of the topic to restore the state. And, the committed offsets design is unused and kept as-is for future improvement. Storing the snapshot of the remote log metadata under each partition requires some discussion.
How often to trigger the snapshot?
- If we configure to run the snapshots in a scheduled interval, then it can lead to flushing of the log segments which is async now.
- On broker shutdown: This can lead to two cases: clean and unclean shutdown. During unclean shutdown, we cannot depend on the snapshot stored in the local disk as it can be corrupted. Reading from the source-of-truth (__remote_log_metadata topic) is reliable (v0).
- On Partition reassignments/New-Topic: When a user-topic-partition (utp) is reassigned to another broker, it has to reload the context from the
__remote_log_metadatatopic.
For V0, we kept the design to read from the beginning of the respective metadata topic-partition.
There was a problem hiding this comment.
Sorry @junrao for missing this comment in my list of notifications.
Adding to what @showuon and @kamalcph mentioned above.
Should we remove the CommittedOffsetsFile class?
We may want to keep it in the repo and use it in the future or we can remove it for now and add it back when we work on this in future. I am fine with either way.
There was a problem hiding this comment.
@kamalcph @satishd @showuon : Thanks for the reply. Do we plan to use CommittedOffsetsFile anytime soon? If not, it's probably better to remove it for now and add it back when it's being used. Also, have we decided if CommittedOffsetsFile will be used in the same way as described in KIP-405? If not, it would be useful to update the KIP with the new design and send the update to the vote thread.
There was a problem hiding this comment.
@junrao We will remove the CommittedOffsetsFile for now as you suggested. The current plan is to use it as mentioned in KIP-405. We will follow KIP update process and notify in the vote thread if it is different from the existing proposal.
This PR adds the following changes to the
TopicBasedRemoteLogMetadataManagerand some other minor changes
Added Unit Tests for the changes
Co-authored-by: Kamal Chandraprakash kamal.chandraprakash@gmail.com
Committer Checklist (excluded from commit message)