-
Notifications
You must be signed in to change notification settings - Fork 14k
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
KAFKA-10271 Performance regression while fetching a key from a single partition #9020
Changes from 13 commits
2fb966f
839379d
a15d28f
c180275
4209ce2
f5f2e46
b7d38c6
89ac008
158db40
b22417f
da8554c
fca67e4
0fb7fe2
9838391
5d7c37f
73def50
f4f9355
702cc06
274617c
049ec22
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 |
---|---|---|
|
@@ -20,62 +20,53 @@ | |
import org.apache.kafka.streams.errors.InvalidStateStoreException; | ||
import org.apache.kafka.streams.processor.StateStore; | ||
import org.apache.kafka.streams.processor.TaskId; | ||
import org.apache.kafka.streams.processor.internals.InternalTopologyBuilder; | ||
import org.apache.kafka.streams.processor.internals.StreamThread; | ||
import org.apache.kafka.streams.processor.internals.Task; | ||
import org.apache.kafka.streams.state.QueryableStoreType; | ||
import org.apache.kafka.streams.state.QueryableStoreTypes; | ||
import org.apache.kafka.streams.state.TimestampedKeyValueStore; | ||
import org.apache.kafka.streams.state.TimestampedWindowStore; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.Collection; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.stream.Collectors; | ||
|
||
public class StreamThreadStateStoreProvider { | ||
|
||
private final StreamThread streamThread; | ||
private final InternalTopologyBuilder internalTopologyBuilder; | ||
|
||
public StreamThreadStateStoreProvider(final StreamThread streamThread, | ||
final InternalTopologyBuilder internalTopologyBuilder) { | ||
public StreamThreadStateStoreProvider(final StreamThread streamThread) { | ||
this.streamThread = streamThread; | ||
this.internalTopologyBuilder = internalTopologyBuilder; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
public <T> List<T> stores(final StoreQueryParameters storeQueryParams) { | ||
final String storeName = storeQueryParams.storeName(); | ||
final QueryableStoreType<T> queryableStoreType = storeQueryParams.queryableStoreType(); | ||
final TaskId keyTaskId = createKeyTaskId(storeName, storeQueryParams.partition()); | ||
if (streamThread.state() == StreamThread.State.DEAD) { | ||
return Collections.emptyList(); | ||
} | ||
final StreamThread.State state = streamThread.state(); | ||
if (storeQueryParams.staleStoresEnabled() ? state.isAlive() : state == StreamThread.State.RUNNING) { | ||
final Map<TaskId, ? extends Task> tasks = storeQueryParams.staleStoresEnabled() ? streamThread.allTasks() : streamThread.activeTaskMap(); | ||
final List<T> stores = new ArrayList<>(); | ||
if (keyTaskId != null) { | ||
final Task task = tasks.get(keyTaskId); | ||
if (task == null) { | ||
return Collections.emptyList(); | ||
} | ||
final T store = validateAndListStores(task.getStore(storeName), queryableStoreType, storeName, keyTaskId); | ||
if (store != null) { | ||
return Collections.singletonList(store); | ||
} | ||
final Collection<Task> tasks = storeQueryParams.staleStoresEnabled() ? | ||
streamThread.allTasks().values() : streamThread.activeTasks(); | ||
|
||
if (storeQueryParams.partition() != null) { | ||
return findStreamTask(tasks, storeName, storeQueryParams.partition()). | ||
map(streamTask -> | ||
validateAndListStores(streamTask.getStore(storeName), queryableStoreType, storeName, streamTask.id())). | ||
map(Collections::singletonList). | ||
orElse(Collections.emptyList()); | ||
} else { | ||
for (final Task streamTask : tasks.values()) { | ||
final T store = validateAndListStores(streamTask.getStore(storeName), queryableStoreType, storeName, streamTask.id()); | ||
if (store != null) { | ||
stores.add(store); | ||
} | ||
} | ||
return tasks.stream(). | ||
map(streamTask -> | ||
validateAndListStores(streamTask.getStore(storeName), queryableStoreType, storeName, streamTask.id())). | ||
filter(Objects::nonNull). | ||
collect(Collectors.toList()); | ||
} | ||
return stores; | ||
} else { | ||
throw new InvalidStateStoreException("Cannot get state store " + storeName + " because the stream thread is " + | ||
state + ", not RUNNING" + | ||
|
@@ -104,19 +95,11 @@ private <T> T validateAndListStores(final StateStore store, final QueryableStore | |
} | ||
} | ||
|
||
private TaskId createKeyTaskId(final String storeName, final Integer partition) { | ||
if (partition == null) { | ||
return null; | ||
} | ||
final List<String> sourceTopics = internalTopologyBuilder.stateStoreNameToSourceTopics().get(storeName); | ||
final Set<String> sourceTopicsSet = new HashSet<>(sourceTopics); | ||
final Map<Integer, InternalTopologyBuilder.TopicsInfo> topicGroups = internalTopologyBuilder.topicGroups(); | ||
for (final Map.Entry<Integer, InternalTopologyBuilder.TopicsInfo> topicGroup : topicGroups.entrySet()) { | ||
if (topicGroup.getValue().sourceTopics.containsAll(sourceTopicsSet)) { | ||
return new TaskId(topicGroup.getKey(), partition); | ||
} | ||
} | ||
throw new InvalidStateStoreException("Cannot get state store " + storeName + " because the requested partition " + | ||
partition + " is not available on this instance"); | ||
private Optional<Task> findStreamTask(final Collection<Task> tasks, final String storeName, final int partition) { | ||
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. This is a great find, thanks! |
||
return tasks.stream(). | ||
filter(streamTask -> streamTask.id().partition == partition && | ||
streamTask.getStore(storeName) != null && | ||
storeName.equals(streamTask.getStore(storeName).name())). | ||
findFirst(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,11 +46,22 @@ public void setStoreQueryParameters(final StoreQueryParameters storeQueryParamet | |
public <T> List<T> stores(final String storeName, | ||
final QueryableStoreType<T> queryableStoreType) { | ||
final List<T> allStores = new ArrayList<>(); | ||
for (final StreamThreadStateStoreProvider provider : storeProviders) { | ||
final List<T> stores = provider.stores(storeQueryParameters); | ||
allStores.addAll(stores); | ||
for (final StreamThreadStateStoreProvider storeProvider : storeProviders) { | ||
final List<T> stores = storeProvider.stores(storeQueryParameters); | ||
if (!stores.isEmpty()) { | ||
allStores.addAll(stores); | ||
if (storeQueryParameters.partition() != null) { | ||
break; | ||
} | ||
} | ||
} | ||
if (allStores.isEmpty()) { | ||
if (storeQueryParameters.partition() != null) { | ||
throw new InvalidStateStoreException( | ||
String.format("The specified partition %d for store %s does not exist.", | ||
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. Is this really a different condition than the one on L65? It seems like the failure is still probably that the store "migrated" instead of "doesn't exist", right? 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. L65 catches on rebalancing, while L60 is parameter validation for incorrect partition case. 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. Could you elaborate a bit more about this? If 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. Hey @dima5rr , I think Guozhang's question was hidden because the conversation was already "resolved". Do you mind answering this concern? 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. Hey @guozhangwang, you're right, this check is ambiguous, it's more likely parameter sanity validation when user explicitly specify a single partition. 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. Got it, in that case how about we just encode the partition in the thrown's message so that upon throwing, people can still check if the partition is Otherwise, this PR all LGTM :) 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. Hey @guozhangwang, I am just care that in case of partition is null, the error message is referenced in official FAQ. 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. That's a fair point, let's just merge it as is then. |
||
storeQueryParameters.partition(), | ||
storeName)); | ||
} | ||
throw new InvalidStateStoreException("The state store, " + storeName + ", may have migrated to another instance."); | ||
} | ||
return allStores; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,34 @@ public void prepareTopology() throws InterruptedException { | |
rightStream = builder.stream(INPUT_TOPIC_RIGHT); | ||
} | ||
|
||
@Test | ||
public void shouldNotAccessJoinStoresWhenGivingName() throws InterruptedException { | ||
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. A good coverage improvement! Thanks. |
||
STREAMS_CONFIG.put(StreamsConfig.APPLICATION_ID_CONFIG, appID + "-no-store-access"); | ||
final StreamsBuilder builder = new StreamsBuilder(); | ||
|
||
final KStream<String, Integer> left = builder.stream(INPUT_TOPIC_LEFT, Consumed.with(Serdes.String(), Serdes.Integer())); | ||
final KStream<String, Integer> right = builder.stream(INPUT_TOPIC_RIGHT, Consumed.with(Serdes.String(), Serdes.Integer())); | ||
final CountDownLatch latch = new CountDownLatch(1); | ||
|
||
left.join( | ||
right, | ||
(value1, value2) -> value1 + value2, | ||
JoinWindows.of(ofMillis(100)), | ||
StreamJoined.with(Serdes.String(), Serdes.Integer(), Serdes.Integer()).withStoreName("join-store")); | ||
|
||
try (final KafkaStreams kafkaStreams = new KafkaStreams(builder.build(), STREAMS_CONFIG)) { | ||
kafkaStreams.setStateListener((newState, oldState) -> { | ||
if (newState == KafkaStreams.State.RUNNING) { | ||
latch.countDown(); | ||
} | ||
}); | ||
|
||
kafkaStreams.start(); | ||
latch.await(); | ||
assertThrows(InvalidStateStoreException.class, () -> kafkaStreams.store(StoreQueryParameters.fromNameAndType("join-store", QueryableStoreTypes.keyValueStore())).get("1")); | ||
} | ||
} | ||
|
||
@Test | ||
public void testInner() { | ||
STREAMS_CONFIG.put(StreamsConfig.APPLICATION_ID_CONFIG, appID + "-inner"); | ||
|
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.
LGTM.