[ 
https://issues.apache.org/jira/browse/HDDS-16119?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-16119:
------------------------------
    Component/s: Ozone Datanode
    Description: 
{{DatanodeStorageMetrics}} (added in HDDS-13128) has two defects. The first is 
a process-wide deadlock that is the root cause of the recent master 
integration-job 90-minute timeouts; the second is a metrics-source and 
volume-set leak in mini-cluster JVMs. Both are in the same class and are fixed 
together.

h2. Defect 1 (Critical): metrics-system deadlock against the volume-set lock

{{getMetrics()}} reads storage totals by calling 
{{MutableVolumeSet.getStorageReport()}}, which acquires the volume-set read 
lock. That creates a lock-ordering inversion against the global 
{{DefaultMetricsSystem}} monitor. Every HDDS service registers 
{{PrometheusMetricsSink}} by default ({{HDDS_PROMETHEUS_ENABLED}} defaults to 
true, via {{BaseHttpServer}}), so {{MetricsSystemImpl}} has at least one sink 
and its periodic timer samples all sources. The two lock orders are opposite:

# Metrics sampler timer: {{MetricsSystemImpl.onTimerEvent()}} is synchronized 
on the metrics-system monitor, then calls {{sampleMetrics()}} to 
{{DatanodeStorageMetrics.getMetrics()}} to 
{{MutableVolumeSet.getStorageReport()}} to the volume-set read lock. Holds the 
monitor, wants the volume-set lock.
# Volume-failure handler: {{MutableVolumeSet.failVolume()}} (and 
{{handleVolumeFailures()}}) takes the volume-set write lock, then calls 
{{HddsVolume.failVolume()}} to {{VolumeIOStats.unregister()}} to 
{{DefaultMetricsSystem.unregisterSource()}}, which is synchronized on the 
metrics-system monitor. Holds the volume-set lock, wants the monitor.

When the sampler tick lands inside a {{failVolume}} critical section, the two 
threads deadlock. The sampler then holds the global metrics monitor forever, so 
every subsequent metrics register, unregister, or sample across the whole 
process blocks. On a real datanode this hangs the metrics thread and the 
Prometheus endpoint precisely when a data volume fails. In a mini-cluster (SCM, 
OM, and datanodes share one {{DefaultMetricsSystem}} per JVM) it freezes the 
entire cluster, which is the observed CI failure: an intermittent, silent hang 
of a long-running test with a random cross-subsystem victim (SCM HA, OM HA, 
snapshot, client, none of which changed), no slowdown on runs that miss the 
race, and a sharp onset at the first integration coverage of HDDS-13128. Before 
HDDS-13128 no sampled metrics source acquired the shared {{MutableVolumeSet}} 
lock ({{VolumeInfoMetrics.getMetrics()}} reads only its own volume), so the 
cycle did not exist.

h2. Defect 2 (Major, mini-cluster/test scope): source and volume-set leak

{{DatanodeStorageMetrics}} registers under a constant source name and keeps a 
{{final MutableVolumeSet}}. Integration tests run many datanodes in one JVM 
with {{DefaultMetricsSystem.setMiniClusterMode(true)}}, so the second and later 
datanodes register as {{DatanodeStorageMetrics-1}}, {{-2}}, and so on, but 
{{unregister()}} removes the constant base name. Every datanode past the first 
leaks its source, and each leaked source pins a shut-down datanode's whole 
{{MutableVolumeSet}}. This is heap and registry growth within a test class's 
JVM. It is mini-cluster scoped (a production datanode has a single instance 
created once and unregistered on stop). It does not cause the CI timeouts 
(Surefire uses {{reuseForks=false}}, so it cannot accumulate across a split, 
and completed-split durations are flat across the onset); it is a correctness 
and hygiene bug fixed alongside Defect 1.

h2. Proposed fix

For Defect 1, make the sampling path lock-free so it never blocks on the 
volume-set lock while the metrics monitor is held. Add 
{{MutableVolumeSet.getStorageReportSnapshot()}}, which builds the report from 
the existing {{ConcurrentHashMap}}s without taking the volume-set lock (a 
weakly-consistent snapshot, the same guarantee already used by 
{{getVolumesList()}}), and have {{getMetrics()}} call it instead of 
{{getStorageReport()}}. The locking {{getStorageReport()}} is unchanged for the 
node-report path. A transient one-sample miscount during a rare 
volume-membership change is acceptable for a gauge and far preferable to a 
process-wide deadlock.

For Defect 2, register and unregister under a per-instance unique source name 
following the existing {{VolumeInfoMetrics}} pattern ({{SOURCE_BASENAME + '-' + 
identifier}}), keeping register and unregister symmetric so no source or volume 
set is leaked.

h2. Testing

* {{TestVolumeSet.testStorageReportSnapshotDoesNotBlockOnWriteLock}}: hold the 
volume-set write lock, assert {{getStorageReportSnapshot()}} returns promptly 
from another thread while the locking {{getStorageReport()}} blocks (times out).
* Leak test: in mini-cluster mode, repeated create plus unregister cycles leave 
no residual {{DatanodeStorageMetrics}} sources, and shut-down 
{{MutableVolumeSet}} instances become unreachable after GC (measured: 30 of 45 
volume sets survived GC before the fix, 0 of 45 after).
* {{TestDatanodeStorageMetrics}} aggregation and zero-capacity behavior 
unchanged. Container-service unit tests pass and checkstyle is clean.

  was:
{{DatanodeStorageMetrics}} (added in HDDS-13128) registers with the metrics 
system under a constant source name and keeps a {{final MutableVolumeSet}} 
reference. {{OzoneContainer}} creates one instance per datanode.

Integration tests run many datanodes in a single JVM and enable 
{{DefaultMetricsSystem.setMiniClusterMode(true)}} (see {{MiniOzoneClusterImpl}} 
and {{MiniOzoneHAClusterImpl}}). In mini-cluster mode the metrics system 
uniquifies duplicate source names, so the second and later datanodes register 
as {{DatanodeStorageMetrics-1}}, {{DatanodeStorageMetrics-2}}, and so on. 
However {{DatanodeStorageMetrics.unregister()}} calls 
{{unregisterSource("DatanodeStorageMetrics")}} using the constant base name, so 
only the first datanode's source is ever removed. Every datanode beyond the 
first leaks its metrics source, and because each leaked source holds a 
{{MutableVolumeSet}}, the entire volume set graph of the shut-down datanode 
stays reachable and cannot be collected.

Within any single test class that stands up multiple datanodes (or builds 
clusters back to back), the leaked sources and their pinned volume sets 
accumulate in that class's JVM until the class finishes. This is a correctness 
and heap-hygiene bug: shut-down datanode state that should be collectable stays 
reachable, and stale {{DatanodeStorageMetrics-N}} sources pile up in the 
metrics registry.

The older {{ContainerMetrics}} uses a similar constant-name register and 
unregister and also leaks its source in mini-cluster mode, but it retains only 
lightweight counters, so it was tolerated. {{DatanodeStorageMetrics}} is 
qualitatively worse because it pins a full datanode volume set.

h3. Reproduction (unit level, no cluster)

With {{DefaultMetricsSystem.setMiniClusterMode(true)}}, create N 
{{DatanodeStorageMetrics}} over real {{MutableVolumeSet}} instances, shut the 
volume sets down, then call {{unregister()}} on each (as 
{{OzoneContainer.stop()}} does), and force GC. Volume sets held only through 
the leaked metrics sources remain reachable. Measured with 15 clusters of 3 
datanodes: 30 of 45 volume sets survived GC with {{DatanodeStorageMetrics}}, 
versus 0 of 45 for the pre-change baseline.

h3. Proposed fix

Register and unregister under a per-instance unique source name, following the 
existing {{VolumeInfoMetrics}} pattern ({{SOURCE_BASENAME + '-' + 
identifier}}). This keeps register and unregister symmetric so no source or 
volume set is leaked. With the fix the same reproduction retains 0 of 45 volume 
sets.

h3. Testing

Unit test that asserts, in mini-cluster mode, that repeated create plus 
unregister cycles leave no residual {{DatanodeStorageMetrics}} sources and that 
shut-down {{MutableVolumeSet}} instances become unreachable after GC.

       Priority: Critical  (was: Major)
        Summary: DatanodeStorageMetrics can deadlock the metrics system on 
volume failure, and leaks its source in mini-cluster mode  (was: 
DatanodeStorageMetrics leaks a metrics source and its MutableVolumeSet per 
datanode in mini-cluster JVMs)

> DatanodeStorageMetrics can deadlock the metrics system on volume failure, and 
> leaks its source in mini-cluster mode
> -------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16119
>                 URL: https://issues.apache.org/jira/browse/HDDS-16119
>             Project: Apache Ozone
>          Issue Type: Bug
>          Components: Ozone Datanode
>            Reporter: Siyao Meng
>            Priority: Critical
>
> {{DatanodeStorageMetrics}} (added in HDDS-13128) has two defects. The first 
> is a process-wide deadlock that is the root cause of the recent master 
> integration-job 90-minute timeouts; the second is a metrics-source and 
> volume-set leak in mini-cluster JVMs. Both are in the same class and are 
> fixed together.
> h2. Defect 1 (Critical): metrics-system deadlock against the volume-set lock
> {{getMetrics()}} reads storage totals by calling 
> {{MutableVolumeSet.getStorageReport()}}, which acquires the volume-set read 
> lock. That creates a lock-ordering inversion against the global 
> {{DefaultMetricsSystem}} monitor. Every HDDS service registers 
> {{PrometheusMetricsSink}} by default ({{HDDS_PROMETHEUS_ENABLED}} defaults to 
> true, via {{BaseHttpServer}}), so {{MetricsSystemImpl}} has at least one sink 
> and its periodic timer samples all sources. The two lock orders are opposite:
> # Metrics sampler timer: {{MetricsSystemImpl.onTimerEvent()}} is synchronized 
> on the metrics-system monitor, then calls {{sampleMetrics()}} to 
> {{DatanodeStorageMetrics.getMetrics()}} to 
> {{MutableVolumeSet.getStorageReport()}} to the volume-set read lock. Holds 
> the monitor, wants the volume-set lock.
> # Volume-failure handler: {{MutableVolumeSet.failVolume()}} (and 
> {{handleVolumeFailures()}}) takes the volume-set write lock, then calls 
> {{HddsVolume.failVolume()}} to {{VolumeIOStats.unregister()}} to 
> {{DefaultMetricsSystem.unregisterSource()}}, which is synchronized on the 
> metrics-system monitor. Holds the volume-set lock, wants the monitor.
> When the sampler tick lands inside a {{failVolume}} critical section, the two 
> threads deadlock. The sampler then holds the global metrics monitor forever, 
> so every subsequent metrics register, unregister, or sample across the whole 
> process blocks. On a real datanode this hangs the metrics thread and the 
> Prometheus endpoint precisely when a data volume fails. In a mini-cluster 
> (SCM, OM, and datanodes share one {{DefaultMetricsSystem}} per JVM) it 
> freezes the entire cluster, which is the observed CI failure: an 
> intermittent, silent hang of a long-running test with a random 
> cross-subsystem victim (SCM HA, OM HA, snapshot, client, none of which 
> changed), no slowdown on runs that miss the race, and a sharp onset at the 
> first integration coverage of HDDS-13128. Before HDDS-13128 no sampled 
> metrics source acquired the shared {{MutableVolumeSet}} lock 
> ({{VolumeInfoMetrics.getMetrics()}} reads only its own volume), so the cycle 
> did not exist.
> h2. Defect 2 (Major, mini-cluster/test scope): source and volume-set leak
> {{DatanodeStorageMetrics}} registers under a constant source name and keeps a 
> {{final MutableVolumeSet}}. Integration tests run many datanodes in one JVM 
> with {{DefaultMetricsSystem.setMiniClusterMode(true)}}, so the second and 
> later datanodes register as {{DatanodeStorageMetrics-1}}, {{-2}}, and so on, 
> but {{unregister()}} removes the constant base name. Every datanode past the 
> first leaks its source, and each leaked source pins a shut-down datanode's 
> whole {{MutableVolumeSet}}. This is heap and registry growth within a test 
> class's JVM. It is mini-cluster scoped (a production datanode has a single 
> instance created once and unregistered on stop). It does not cause the CI 
> timeouts (Surefire uses {{reuseForks=false}}, so it cannot accumulate across 
> a split, and completed-split durations are flat across the onset); it is a 
> correctness and hygiene bug fixed alongside Defect 1.
> h2. Proposed fix
> For Defect 1, make the sampling path lock-free so it never blocks on the 
> volume-set lock while the metrics monitor is held. Add 
> {{MutableVolumeSet.getStorageReportSnapshot()}}, which builds the report from 
> the existing {{ConcurrentHashMap}}s without taking the volume-set lock (a 
> weakly-consistent snapshot, the same guarantee already used by 
> {{getVolumesList()}}), and have {{getMetrics()}} call it instead of 
> {{getStorageReport()}}. The locking {{getStorageReport()}} is unchanged for 
> the node-report path. A transient one-sample miscount during a rare 
> volume-membership change is acceptable for a gauge and far preferable to a 
> process-wide deadlock.
> For Defect 2, register and unregister under a per-instance unique source name 
> following the existing {{VolumeInfoMetrics}} pattern ({{SOURCE_BASENAME + '-' 
> + identifier}}), keeping register and unregister symmetric so no source or 
> volume set is leaked.
> h2. Testing
> * {{TestVolumeSet.testStorageReportSnapshotDoesNotBlockOnWriteLock}}: hold 
> the volume-set write lock, assert {{getStorageReportSnapshot()}} returns 
> promptly from another thread while the locking {{getStorageReport()}} blocks 
> (times out).
> * Leak test: in mini-cluster mode, repeated create plus unregister cycles 
> leave no residual {{DatanodeStorageMetrics}} sources, and shut-down 
> {{MutableVolumeSet}} instances become unreachable after GC (measured: 30 of 
> 45 volume sets survived GC before the fix, 0 of 45 after).
> * {{TestDatanodeStorageMetrics}} aggregation and zero-capacity behavior 
> unchanged. Container-service unit tests pass and checkstyle is clean.



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

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

Reply via email to