Eswarar Siva created KAFKA-21364:
------------------------------------
Summary: Streams EOS: input records skipped after a commit
timeout, the offset reset of the revived task is lost when the enforced
rebalance closes it
Key: KAFKA-21364
URL: https://issues.apache.org/jira/browse/KAFKA-21364
Project: Kafka
Issue Type: Bug
Components: streams
Affects Versions: 4.4.0, 4.2.1
Reporter: Eswarar Siva
With exactly_once_v2 and the classic group protocol, a commit that times out
against the transaction coordinator can make Streams skip input records. The
input offsets get committed past records that were never processed, so they
never reach the store or the changelog.
When it happens: the commit has to fail with a TimeoutException while the
transaction is still open at the coordinator. So transaction.timeout.ms has to
be above the producer max.block.ms, and the transaction coordinator has to be
unreachable for longer than max.block.ms. With the Streams EOS defaults
(transaction.timeout.ms 10000, max.block.ms 60000) the coordinator aborts
first, the next commit fails with InvalidTxnStateException and the thread dies,
and I saw no loss. With transaction.timeout.ms=120000 and max.block.ms left at
60000, a 70 s outage was enough.
Reproduced on 4.2.1 and 4.4.0. Line numbers at 4.4.0, the same code is on trunk
(not run on trunk). The streams group protocol was not tested; StreamThread
only enforces the rebalance below when streamsRebalanceData is empty.
What happens:
-> The commit fails with "Timeout expired after 60000ms while awaiting
EndTxn(true)" and TaskExecutor marks the task corrupted (188-200).
-> TaskManager.handleCorruption closes the task dirty, which drops the records
it fetched but has not processed yet, adds the input partitions for an offset
reset (TaskManager 320), revives the task and puts it in the tasks pending
initialization (325).
-> StreamThread enforces a rebalance, "Active task(s) got corrupted. Triggering
a rebalance." (1025-1027).
-> On the new assignment handleTasksPendingInitialization closes every task
pending initialization (TaskManager 514-525), the revived one included, and a
new StreamTask is created for the same task id. Its resetOffsetsForPartitions
starts empty (StreamTask 198).
-> The seek back to the committed offset only runs for partitions in that set
(StreamTask 1054-1069), so there is no seek. The new task starts at the
consumer position and the next commit moves the input offset past the dropped
records.
Setup:
# Three KRaft brokers. __transaction_state on broker 1, __consumer_offsets on
broker 2, input topic (1 partition) and changelog on broker 3.
# One instance, one thread, exactly_once_v2, commit.interval.ms=100, producer
transaction.timeout.ms=120000, max.block.ms default. A processor that puts
every input record into a persistent KeyValueStore and sleeps 2 ms per record,
so a commit lands in the middle of a fetched batch.
# Produce 24,000 records with unique keys f-0 to f-23999 at 200 per second.
# 40 s into the feed blackhole the route from the app to broker 1 only, for 70
s, then remove it.
# Compare the distinct keys in the changelog (read_committed) with the input,
and the group's committed offset with the log end offset.
Results:
||Version||Old task last processed offset||New task first processed
offset||Seek on the input after startup||Keys missing from the changelog||Group
committed||
|4.4.0|8102|8200|none|97, f-8103 to f-8199|24,000 of 24,000|
|4.2.1|8055|8200|none|144, f-8056 to f-8199|24,000 of 24,000|
The same happened in 9 more runs with max.block.ms=3000,
transaction.timeout.ms=60000 and two 6 s outages, on both versions, with one
instance and with three instances on six partitions, and also without the
processing delay. Those runs lost 39 to 365 records each. Runs without an
outage lost nothing.
Two things that point at the lost reset rather than at the transaction:
-> In one run the revived task instance was initialized again instead of being
replaced. The consumer logged "Seeking to offset 8223" and nothing was lost.
-> In a lossy run the changelog has the transaction of producer epoch 53 ending
at f-8222 and the next one, epoch 54, starting at f-8400. f-8223 to f-8399 are
in no batch at all and there is no abort marker, so they were never written.
They were fetched, dropped by the dirty close, and skipped.
KAFKA-21159 on trunk clears the next offsets to consume on close so a revived
task cannot commit a stale offset after the consumer is seeked back. Here the
seek back never happens, so it does not cover this.
What I think could be done: keep the pending offset reset when
handleTasksPendingInitialization closes a revived task, for example by carrying
it into the new task, or seek those input partitions to their committed offsets
when the revived task is closed.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)