-
Notifications
You must be signed in to change notification settings - Fork 628
HDDS-13128. Expose per DN space utilisation as JMX metrics #10913
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
0459289
HDDS-13128. Expose per DN space utilisation as JMX metrics
anuragp010 40a0008
Address review comments
anuragp010 22bf5cd
Add Integration Test to verify DatanodeStorageMetrics with OzoneMiniC…
anuragp010 89131e7
Address additional review comments
anuragp010 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
...src/main/java/org/apache/hadoop/ozone/container/common/volume/DatanodeStorageMetrics.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.hadoop.ozone.container.common.volume; | ||
|
|
||
| import org.apache.hadoop.metrics2.MetricsCollector; | ||
| import org.apache.hadoop.metrics2.MetricsInfo; | ||
| import org.apache.hadoop.metrics2.MetricsRecordBuilder; | ||
| import org.apache.hadoop.metrics2.MetricsSource; | ||
| import org.apache.hadoop.metrics2.annotation.Metrics; | ||
| import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; | ||
| import org.apache.hadoop.metrics2.lib.Interns; | ||
| import org.apache.hadoop.metrics2.lib.MetricsRegistry; | ||
| import org.apache.hadoop.ozone.OzoneConsts; | ||
| import org.apache.hadoop.ozone.container.common.impl.StorageLocationReport; | ||
|
|
||
| /** | ||
| * Node-level storage totals for a DataNode, aggregated over its HDDS data volumes only | ||
| * ({@code VolumeType.DATA_VOLUME}) via {@link MutableVolumeSet#getStorageReport()}. | ||
| * This is the same scope as the {@code storageReport} entries produced by | ||
| * {@code OzoneContainer.getNodeReport()}; meta and DB volumes are excluded. | ||
| * Registered as {@code Hadoop:service=HddsDatanode,name=DatanodeStorageMetrics}. | ||
| */ | ||
| @Metrics(about = "Ozone DataNode node-level storage totals", | ||
| context = OzoneConsts.OZONE) | ||
| public final class DatanodeStorageMetrics implements MetricsSource { | ||
|
|
||
| public static final String SOURCE_NAME = DatanodeStorageMetrics.class.getSimpleName(); | ||
|
|
||
| private static final MetricsInfo CAPACITY = Interns.info("OzoneCapacity", | ||
| "Total Ozone usable capacity across the DataNode's data volumes (bytes," | ||
| + " post reserved-space adjustment)"); | ||
| private static final MetricsInfo USED = Interns.info("OzoneUsed", | ||
| "Total Ozone used space across the DataNode's data volumes (bytes)"); | ||
| private static final MetricsInfo USED_PERCENTAGE = | ||
| Interns.info("OzoneUsedPercentage", | ||
| "100 * OzoneUsed / OzoneCapacity across the DataNode's data volumes;" | ||
| + " 0 when OzoneCapacity is 0"); | ||
|
|
||
| private final MetricsRegistry registry; | ||
| private final MutableVolumeSet volumeSet; | ||
|
|
||
| private DatanodeStorageMetrics(MutableVolumeSet volumeSet) { | ||
| this.volumeSet = volumeSet; | ||
| this.registry = new MetricsRegistry(SOURCE_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new {@code DatanodeStorageMetrics} instance and registers it | ||
| * with the default Metrics2 system. | ||
| */ | ||
| public static DatanodeStorageMetrics create(MutableVolumeSet volumeSet) { | ||
| DatanodeStorageMetrics datanodeStorageMetrics = new DatanodeStorageMetrics(volumeSet); | ||
| DefaultMetricsSystem.instance().register( | ||
| SOURCE_NAME, "DataNode node-level storage totals", datanodeStorageMetrics); | ||
| return datanodeStorageMetrics; | ||
| } | ||
|
|
||
| /** | ||
| * Unregisters this source from the Metrics2 system. | ||
| */ | ||
| public void unregister() { | ||
| DefaultMetricsSystem.instance().unregisterSource(SOURCE_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * Metrics are computed on demand from the latest volume reports | ||
| * instead of maintaining cached counters. | ||
| */ | ||
| @Override | ||
| public void getMetrics(MetricsCollector collector, boolean all) { | ||
|
anuragp010 marked this conversation as resolved.
|
||
| MetricsRecordBuilder builder = collector.addRecord(SOURCE_NAME); | ||
| registry.snapshot(builder, all); | ||
|
|
||
| long capacity = 0L; | ||
| long used = 0L; | ||
| for (StorageLocationReport report : volumeSet.getStorageReport()) { | ||
| capacity = Math.addExact(capacity, report.getCapacity()); | ||
| used = Math.addExact(used, report.getScmUsed()); | ||
| } | ||
| double usedPercentage = capacity > 0 ? (100.0 * used / capacity) : 0.0; | ||
|
|
||
| builder | ||
| .addGauge(CAPACITY, capacity) | ||
| .addGauge(USED, used) | ||
| .addGauge(USED_PERCENTAGE, usedPercentage); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
...test/java/org/apache/hadoop/ozone/container/common/volume/TestDatanodeStorageMetrics.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.hadoop.ozone.container.common.volume; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import org.apache.hadoop.metrics2.AbstractMetric; | ||
| import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl; | ||
| import org.apache.hadoop.metrics2.impl.MetricsRecordImpl; | ||
| import org.apache.hadoop.ozone.container.common.impl.StorageLocationReport; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| /** | ||
| * Unit tests for {@link DatanodeStorageMetrics}. | ||
| * | ||
| * <p>Tests verify: | ||
| * <ul> | ||
| * <li>Correct aggregation of Capacity and Used across multiple volumes.</li> | ||
| * <li>OzoneUsedPercentage arithmetic (100 * OzoneUsed / OzoneCapacity).</li> | ||
| * <li>Zero-capacity guard: OzoneUsedPercentage returns 0 instead of NaN/divide-by-zero.</li> | ||
| * </ul> | ||
| */ | ||
| class TestDatanodeStorageMetrics { | ||
|
|
||
| @Test | ||
| void testAggregationAcrossTwoVolumes() { | ||
| // vol1: capacity=100, scmUsed=40 vol2: capacity=300, scmUsed=60 | ||
| // expected: OzoneCapacity=400, OzoneUsed=100, OzoneUsedPercentage=25.0 | ||
| StorageLocationReport vol1 = StorageLocationReport.newBuilder() | ||
| .setId("vol1").setCapacity(100L).setScmUsed(40L).setRemaining(60L) | ||
| .build(); | ||
| StorageLocationReport vol2 = StorageLocationReport.newBuilder() | ||
| .setId("vol2").setCapacity(300L).setScmUsed(60L).setRemaining(240L) | ||
| .build(); | ||
|
|
||
| MutableVolumeSet volumeSet = mock(MutableVolumeSet.class); | ||
| when(volumeSet.getStorageReport()) | ||
| .thenReturn(new StorageLocationReport[]{vol1, vol2}); | ||
|
|
||
| DatanodeStorageMetrics metrics = DatanodeStorageMetrics.create(volumeSet); | ||
| try { | ||
| MetricsCollectorImpl collector = new MetricsCollectorImpl(); | ||
| metrics.getMetrics(collector, true); | ||
|
|
||
| assertThat(collector.getRecords()).hasSize(1); | ||
| MetricsRecordImpl rec = collector.getRecords().get(0); | ||
|
|
||
| // Record name determines the JMX name= segment — must match verbatim. | ||
| assertThat(rec.name()).isEqualTo(DatanodeStorageMetrics.SOURCE_NAME); | ||
|
|
||
| Iterable<AbstractMetric> all = rec.metrics(); | ||
| assertThat(findLong(all, "OzoneCapacity")).isEqualTo(400L); | ||
| assertThat(findLong(all, "OzoneUsed")).isEqualTo(100L); | ||
| assertThat(findDouble(all, "OzoneUsedPercentage")).isEqualTo(25.0); | ||
| } finally { | ||
| metrics.unregister(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testZeroCapacityReturnsZeroPercentage() { | ||
| // No volumes → capacity=0, used=0; OzoneUsedPercentage must be 0.0, not NaN. | ||
| MutableVolumeSet volumeSet = mock(MutableVolumeSet.class); | ||
| when(volumeSet.getStorageReport()).thenReturn(new StorageLocationReport[0]); | ||
|
|
||
| DatanodeStorageMetrics metrics = DatanodeStorageMetrics.create(volumeSet); | ||
| try { | ||
| MetricsCollectorImpl collector = new MetricsCollectorImpl(); | ||
| metrics.getMetrics(collector, true); | ||
|
|
||
| Iterable<AbstractMetric> all = collector.getRecords().get(0).metrics(); | ||
| assertThat(findLong(all, "OzoneCapacity")).isEqualTo(0L); | ||
| assertThat(findLong(all, "OzoneUsed")).isEqualTo(0L); | ||
| assertThat(findDouble(all, "OzoneUsedPercentage")).isEqualTo(0.0); | ||
| } finally { | ||
| metrics.unregister(); | ||
| } | ||
| } | ||
|
|
||
| private static long findLong(Iterable<AbstractMetric> metrics, String name) { | ||
| for (AbstractMetric m : metrics) { | ||
| if (name.equals(m.name())) { | ||
| return m.value().longValue(); | ||
| } | ||
| } | ||
| throw new AssertionError("Missing metric: " + name); | ||
| } | ||
|
|
||
| private static double findDouble(Iterable<AbstractMetric> metrics, String name) { | ||
| for (AbstractMetric m : metrics) { | ||
| if (name.equals(m.name())) { | ||
| return m.value().doubleValue(); | ||
| } | ||
| } | ||
| throw new AssertionError("Missing metric: " + name); | ||
| } | ||
| } |
129 changes: 129 additions & 0 deletions
129
...-test/src/test/java/org/apache/hadoop/ozone/dn/TestDatanodeStorageMetricsIntegration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.hadoop.ozone.dn; | ||
|
|
||
| import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_CREATION; | ||
| import static org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory.Conf.configKeyForClassName; | ||
| import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; | ||
| import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CONTAINER_SIZE; | ||
| import static org.apache.ozone.test.MetricsAsserts.getDoubleGauge; | ||
| import static org.apache.ozone.test.MetricsAsserts.getLongGauge; | ||
| import static org.apache.ozone.test.MetricsAsserts.getMetrics; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.data.Offset.offset; | ||
|
|
||
| import java.util.HashMap; | ||
| import org.apache.hadoop.hdds.client.RatisReplicationConfig; | ||
| import org.apache.hadoop.hdds.conf.OzoneConfiguration; | ||
| import org.apache.hadoop.hdds.fs.DUFactory; | ||
| import org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory; | ||
| import org.apache.hadoop.metrics2.MetricsRecordBuilder; | ||
| import org.apache.hadoop.ozone.MiniOzoneCluster; | ||
| import org.apache.hadoop.ozone.client.OzoneClient; | ||
| import org.apache.hadoop.ozone.client.io.OzoneOutputStream; | ||
| import org.apache.hadoop.ozone.container.common.volume.DatanodeStorageMetrics; | ||
| import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet; | ||
| import org.apache.ozone.test.GenericTestUtils; | ||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.Timeout; | ||
|
|
||
| /** | ||
| * Integration tests for {@link DatanodeStorageMetrics}. | ||
| * | ||
| * <p>Verifies that the live registered metrics source on a real DataNode | ||
| * reflects actual storage usage: capacity is positive, used space increases | ||
| * after writing data, and the percentage arithmetic holds. | ||
| */ | ||
| @Timeout(300) | ||
| public class TestDatanodeStorageMetricsIntegration { | ||
|
|
||
| private MiniOzoneCluster cluster; | ||
|
|
||
| @BeforeEach | ||
| void startCluster() throws Exception { | ||
| OzoneConfiguration conf = new OzoneConfiguration(); | ||
| conf.set(OZONE_SCM_CONTAINER_SIZE, "1GB"); | ||
| conf.setBoolean(HDDS_SCM_SAFEMODE_PIPELINE_CREATION, false); | ||
| conf.setClass(configKeyForClassName(), DUFactory.class, SpaceUsageCheckFactory.class); | ||
| cluster = MiniOzoneCluster.newBuilder(conf) | ||
| .setNumDatanodes(1) | ||
| .build(); | ||
| cluster.waitForClusterToBeReady(); | ||
| cluster.waitForPipelineTobeReady(ONE, 30000); | ||
| } | ||
|
|
||
| @AfterEach | ||
| void stopCluster() { | ||
| if (cluster != null) { | ||
| cluster.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void storageMetricsReflectWrittenData() throws Exception { | ||
| // Baseline before any write. | ||
| long baselineUsed = getLongGauge("OzoneUsed", storageMetrics()); | ||
|
|
||
| // Write a key to generate real used space. | ||
| try (OzoneClient client = cluster.newClient()) { | ||
| client.getObjectStore().createVolume("vol"); | ||
| client.getObjectStore().getVolume("vol").createBucket("bucket"); | ||
| OzoneOutputStream key = client.getObjectStore().getVolume("vol") | ||
| .getBucket("bucket") | ||
| .createKey("key", 4096, | ||
| RatisReplicationConfig.getInstance(ONE), new HashMap<>()); | ||
| key.write(new byte[4096]); | ||
| key.close(); | ||
| } | ||
|
|
||
| // Force DU refresh so the in-memory usage cache reflects the write. | ||
| MutableVolumeSet volumeSet = cluster.getHddsDatanodes().get(0) | ||
| .getDatanodeStateMachine().getContainer().getVolumeSet(); | ||
| volumeSet.getVolumesList().get(0).getVolumeUsage().refreshNow(); | ||
|
|
||
| // Wait until OzoneUsed is reported as greater than the baseline. | ||
| GenericTestUtils.waitFor( | ||
| () -> getLongGauge("OzoneUsed", storageMetrics()) > baselineUsed, | ||
| 500, 10_000); | ||
|
|
||
| // Read all three gauges from one storageMetrics() call so they come from | ||
| // the same getStorageReport() iteration and are mutually consistent. | ||
| MetricsRecordBuilder rb = storageMetrics(); | ||
| long capacity = getLongGauge("OzoneCapacity", rb); | ||
| long used = getLongGauge("OzoneUsed", rb); | ||
| double usedPercentage = getDoubleGauge("OzoneUsedPercentage", rb); | ||
|
|
||
| assertThat(capacity).isGreaterThan(0L); | ||
| assertThat(used).isGreaterThan(baselineUsed); | ||
| assertThat(usedPercentage).isBetween(0.0, 100.0); | ||
|
|
||
| // Arithmetic invariant: usedPercentage == 100 * used / capacity. | ||
| assertThat(usedPercentage).isCloseTo(100.0 * used / capacity, offset(0.001)); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a fresh snapshot of the live {@link DatanodeStorageMetrics} source. | ||
| * Each call re-reads the underlying storage reports — do not mix values | ||
| * from different calls when checking invariants across gauges. | ||
| */ | ||
| private static MetricsRecordBuilder storageMetrics() { | ||
| return getMetrics(DatanodeStorageMetrics.SOURCE_NAME); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.