[ 
https://issues.apache.org/jira/browse/KAFKA-21243?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18128399#comment-18128399
 ] 

Viktor Klimin commented on KAFKA-21243:
---------------------------------------

Updated steps to reproduce - previous version had less concurrency and was 
harder t reproduce with short local RTT inside localhost

 
{code:java}
public static void main(String[] a) throws Exception {
    String topic = "probe";
    ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
    int probes = 5000;
    long pauseMs = 8_000, stallMs = 400;
    long estimateMs = 40;                // initial guess of the metadata round 
trip

    Properties p = new Properties();
    p.put("bootstrap.servers", "localhost:9092");
    p.put("key.serializer", ByteArraySerializer.class.getName());
    p.put("value.serializer", ByteArraySerializer.class.getName());
    p.put("metadata.max.idle.ms", "5000");     // minimum Kafka allows
    p.put("metadata.max.age.ms", "1000");      // refresh often so eviction 
happens inside the pause
    p.put("retry.backoff.ms", "750");
    // + security settings if needed

    KafkaProducer<byte[], byte[]> producer = new KafkaProducer<>(p);
    byte[] payload = "x".getBytes();

    for (int cycle = 0; cycle < 11; cycle++) {
        CountDownLatch startingGun = new CountDownLatch(1);

        List<Future<Long>> fs = new ArrayList<>();

        Future<Long> trigger = pool.submit(() -> {
            startingGun.await(); // Wait for the signal
            long t = System.nanoTime();
            producer.send(new ProducerRecord<>(topic, payload)).get();
            return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t);
        });

        for (int i = 0; i < probes; i++) {
            fs.add(pool.submit(() -> {
                startingGun.await();
                long t = System.nanoTime();
                producer.send(new ProducerRecord<>(topic, payload)).get();
                return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t);
            }));
        }

        startingGun.countDown();

        long triggerMs = trigger.get();
        List<Long> stalls = new ArrayList<>();
        for (Future<Long> f : fs) {
            long ms = f.get();
            if (ms >= stallMs) stalls.add(ms);
        }

        boolean evicted = triggerMs >= 5;
        System.out.printf("cycle=%d evicted=%b trigger=%dms stalledProbes=%d 
%s%n",
                cycle, evicted, triggerMs, stalls.size(), stalls);

        if (cycle > 0 && triggerMs >= 5 && triggerMs < stallMs)
            estimateMs = (estimateMs * 3 + triggerMs) / 4;
        Thread.sleep(pauseMs);
    }
    pool.shutdownNow();
    producer.close();
} {code}

> Producer: concurrent first sends can wait an extra retry.backoff.ms for a 
> redundant metadata refresh
> ----------------------------------------------------------------------------------------------------
>
>                 Key: KAFKA-21243
>                 URL: https://issues.apache.org/jira/browse/KAFKA-21243
>             Project: Kafka
>          Issue Type: Bug
>          Components: producer 
>    Affects Versions: 3.9.2, 4.4.1
>            Reporter: Viktor Klimin
>            Assignee: Edmond Abraham
>            Priority: Major
>
> h3. Issue
> We run a producer with metadata.max.idle.ms at its default (5 min) and 
> retry.backoff.ms=750. When a topic has been idle longer than 
> metadata.max.idle.ms and several threads call send() at about the same time, 
> some of them occasionally block for roughly 900 ms inside send(). That is 
> about one retry.backoff.ms, even though the brokers are healthy and the first 
> metadata response comes back in a couple of milliseconds. In our case two 
> threads that called send() within ~2 ms of each other were both stuck for 
> ~897 ms and were released together.
> h3. Description
>  # In KafkaProducer.{*}waitOnMetadata{*}() the thread first takes a snapshot 
> with metadata.fetch(), then calls metadata.requestUpdateForTopic(topic). 
> These are two separate lock acquisitions. If the sender thread applies the 
> first metadata response in between: ProducerMetadata.update() removes the 
> topic from newTopics, bumps updateVersion (V -> V+1), and sets lastRefreshMs 
> = now.
>  # The waiting thread still has the old snapshot (no partitions for the 
> topic), so it does not take the fast path.
>  # It calls {*}requestUpdateForTopic{*}(topic). The topic is no longer in 
> newTopics, so this goes to {*}requestUpdate{*}(false). That only sets 
> needFullUpdate and returns V+1.
>  # {*}awaitUpdate{*}(V+1) now needs a second metadata response, but a new 
> refresh is not allowed until *lastRefreshMs + retry.backoff.ms* (with 
> jitter), so the thread sits there for ~{*}0.6-0.9 s at 750{*} ms.
> The data the thread needs is already in the metadata at step 2, so the wait 
> is redundant. When the second response arrives, all the threads that hit this 
> window wake up together.
> If requestUpdateForTopic() happens before the response is applied, the topic 
> is still in newTopics, requestUpdateForNewTopics() is used (no backoff), and 
> the thread is released by the first response. So only the interleaving 
> decides.
> h3. Reproduction
> The race is also reproducible with a real KafkaProducer.
> Test setup: one producer, metadata.max.idle.ms=5000, 
> metadata.max.age.ms=1000, retry.backoff.ms=750, brokers ~35 ms away. Every 
> cycle: wait until the topic is evicted from the producer metadata, send from 
> one "trigger" thread, then send from 100 other threads at randomized instants 
> spread over a ~20 ms window around the time the first metadata response is 
> expected. Plain simultaneous start does not hit it, because all threads then 
> arrive before the first response is applied; the arrivals have to be spread 
> around the moment the response is applied.
> Since the window is the time between {*}fetch{*}() and 
> {*}requestUpdateForTopic{*}(), the example below shows the same.
> {code:java}
> public class MetadataRaceProbe {
>     public static void main(String[] a) throws Exception {
>         String topic = "probe";
>         int probes = 100;
>         long spreadMs = 20, pauseMs = 7_000, stallMs = 400;
>         long estimateMs = 40;                // initial guess of the metadata 
> round trip
>  
>         Properties p = new Properties();
>         p.put("bootstrap.servers", "broker:9092");
>         p.put("key.serializer", ByteArraySerializer.class.getName());
>         p.put("value.serializer", ByteArraySerializer.class.getName());
>         p.put("metadata.max.idle.ms", "5000");     // minimum Kafka allows
>         p.put("metadata.max.age.ms", "1000");      // refresh often so 
> eviction happens inside the pause
>         p.put("retry.backoff.ms", "750");
>         // + security settings if needed
>  
>         KafkaProducer<byte[], byte[]> producer = new KafkaProducer<>(p);
>         ExecutorService pool = Executors.newFixedThreadPool(probes + 1);
>         byte[] payload = "x".getBytes();
>  
>         for (int cycle = 0; cycle < 85; cycle++) {
>             long t0 = System.nanoTime() + 100_000_000L;          // common 
> start, 100 ms ahead
>             long est = estimateMs * 1_000_000L, spread = spreadMs * 
> 1_000_000L;
>  
>             Future<Long> trigger = pool.submit(timedSend(producer, topic, 
> payload, t0));
>             List<Future<Long>> fs = new ArrayList<>();
>             long slot = spread / probes;
>             for (int i = 0; i < probes; i++) {
>                 long offset = -spread / 2 + i * slot + 
> ThreadLocalRandom.current().nextLong(slot);
>                 fs.add(pool.submit(timedSend(producer, topic, payload, t0 + 
> Math.max(0, est + offset))));
>             }
>  
>             long triggerMs = trigger.get();
>             List<Long> stalls = new ArrayList<>();
>             for (Future<Long> f : fs) { long ms = f.get(); if (ms >= stallMs) 
> stalls.add(ms); }
>  
>             boolean evicted = triggerMs >= 5;            // trigger paid a 
> metadata round trip
>             System.out.printf("cycle=%d evicted=%b trigger=%dms 
> stalledProbes=%d %s%n",
>                     cycle, evicted, triggerMs, stalls.size(), stalls);
>  
>             if (cycle > 0 && triggerMs >= 5 && triggerMs < stallMs)
>                 estimateMs = (estimateMs * 3 + triggerMs) / 4;   // follow 
> the real RTT
>             Thread.sleep(pauseMs);                           // > idle + age, 
> so the topic gets evicted
>         }
>         pool.shutdownNow();
>         producer.close();
>     }
>     static Callable<Long> timedSend(KafkaProducer<byte[], byte[]> producer, 
> String topic, byte[] payload, long targetNanos) {
>         return () -> {
>             waitUntil(targetNanos);
>             long t = System.nanoTime();
>             Future<RecordMetadata> ack = producer.send(new 
> ProducerRecord<>(topic, payload));
>             long sendMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
> t);
>             ack.get();
>             return sendMs;
>         };
>     }
>     static void waitUntil(long target) {
>         long left;
>         while ((left = target - System.nanoTime()) > 0) {
>             if (left > 200_000) LockSupport.parkNanos(left - 150_000);
>             else Thread.onSpinWait();
>         }
>     }
> }{code}
> h3. Expected behavior
> A thread whose snapshot is older than an already-applied metadata update 
> should not schedule a new refresh. It should just re-read the metadata, find 
> the topic, and carry on.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to