yandrey321 opened a new pull request, #11937:
URL: https://github.com/apache/ozone/pull/11937

   ## What changes were proposed in this pull request?
   
   **Problem.** `ContainerReportQueue` is the work queue of SCM's FCR and ICR 
executors
   (`FixedThreadPoolWithAffinityExecutor`).
   - **Producers and consumer:** heartbeat handler threads add reports, and one 
worker thread per queue polls with a
     1 ms timeout.
   - **Locking:** every operation holds the queue's `synchronized` monitor, 
which guards `dataMap` and the capacity count.
     The order of datanodes is kept in an inner `LinkedBlockingQueue`, which 
has two more locks of its own.
   - **Effect:** when several handlers add reports at once, they all contend on 
the monitor together with the worker.
     Throughput then falls steeply as producers are added.
   
   **Change.**
   - **One lock:** the monitor and the inner `LinkedBlockingQueue` are replaced 
by one `ReentrantLock`, which guards
     everything. The ordering queue becomes a plain `ArrayDeque` under that 
lock.
   - **Waiting:** `take()` and `poll(timeout)` wait on a `Condition` of the 
lock, and each new entry in the ordering queue
     signals it.
   - **`isEmpty()`** now also takes the lock.
   
   **Key decisions.**
   - **A `ReentrantLock`, not just dropping the inner queue.**
     - The Jira proposed replacing `LinkedBlockingQueue` with 
`ConcurrentLinkedQueue`. But that has no blocking
       `poll(timeout)`, so the worker would need a sleep loop.
     - Measured first, I dropped the inner queue and kept the monitor, waiting 
with `wait`/`notify`. That was slower than
       master, 0.4x at 16 producers. The inner queue had kept the waiting 
worker off the monitor.
     - So what contends is the `synchronized` monitor itself. Only replacing it 
fixes that.
   - **The queue stays bounded as before.** The bound is still `maxCapacity`, 
from
     
`ozone.scm.event.<ContainerReport_OR_IncrementalContainerReport>.queue.size`, 
default 100,000.
     - The removed `LinkedBlockingQueue` was created without a capacity, so it 
never bounded anything.
     - `add()` still throws, and `offer()` still returns false, when the queue 
is full.
     - A newer FCR from a datanode still replaces its queued FCR (HDDS-7244).
   - **A non-fair lock**, the `ReentrantLock` default, which favours 
throughput. Its cost shows only with a single
     producer at rates far above what SCM sees; see the benchmark.
   
   **Side effects.**
   - **Behaviour is unchanged:** the same order, dedup, capacity and exceptions.
   - **`ReconContainerReportQueue`** overrides only `mergeIcr`, which still 
runs under the lock, so it needs no change.
   - **External synchronization:** no code synchronizes on a 
`ContainerReportQueue` instance from outside.
   
   
   Co-authored-by: Claude Opus 5.5 [email protected]
   
   ## What is the link to the Apache JIRA
   
   https://issues.apache.org/jira/browse/HDDS-13020
   
   ## How was this patch tested?
   
   - **Benchmark:** a JUnit micro-benchmark, not part of this PR.
     - Setup: producer threads add ICRs from 100 distinct datanodes each. One 
consumer calls `poll(1, MILLISECONDS)` in a
       loop, as the executor's worker does, and processing does nothing.
     - Runs: master and this PR, alternating, 2 rounds. JDK 21, macOS laptop, 
JaCoCo off.
     - **Queue full** (producers add as fast as they can, retrying when full). 
Median reports/ms, n = 10:
   
       | producers | master | this PR |
       |---|---|---|
       | 1 | 3,888 (2,414–7,690) | 3,436 (2,832–4,733) |
       | 4 | 861 (507–2,056) | 2,062 (1,723–2,454) (2.4x) |
       | 16 | 253 (105–401) | 599 (486–767) (2.4x) |
   
     - **Paced** (a fixed total rate, a burst every millisecond, so the queue 
never fills). CPU per report is thread CPU
       time divided by reports. Latency is from `offer` to `poll`. Medians, n = 
6:
   
       | producers | rate/s | CPU ns/report, master → this PR | p50 µs | p99 µs 
|
       |---|---|---|---|---|
       | 1 | 20,000 | 2,044 → 1,909 | 10 → 12 | 45 → 41 |
       | 1 | 200,000 | 1,490 → 900 | 39 → 43 | 124 → 181 |
       | 1 | 1,000,000 | 976 → 461 | 54 → 1,768 | 288 → 9,468 |
       | 4 | 20,000 | 5,952 → 4,994 | 11 → 12 | 70 → 64 |
       | 4 | 200,000 | 1,732 → 1,076 | 40 → 29 | 193 → 147 |
       | 4 | 1,000,000 | 1,046 → 651 | 186 → 126 | 931 → 973 |
   
     - **What the numbers show:**
       - **Several producers:** this PR has 2.4x the throughput, uses 16–38% 
less CPU per report, and latency is the
         same or better.
       - **One producer at 20,000/s:** the same. That is already a large report 
storm for SCM.
       - **One producer at 200,000/s and above:** less CPU but higher latency. 
With a non-fair lock, the producer
         re-takes the lock for each report of a 1,000-report burst before the 
woken worker gets it. So the worker drains
         mostly between bursts. On master the worker waits on the inner queue's 
own lock, which the producer touches
         only when the queue goes from empty to non-empty.
       - **In SCM:** processing a report takes far longer than either version 
of the queue operations. The difference
         matters when many heartbeat handlers add reports at the same time, 
such as after an SCM restart.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to