This is an automated email from the ASF dual-hosted git repository.
jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 7d06fc7a2a6 HDDS-16304. Convert RpcMetrics latency counters to
lock-free ConcurrentMutableStat (#11425)
7d06fc7a2a6 is described below
commit 7d06fc7a2a681c45722483657aabcab8188dbf71
Author: Andrey Yarovoy <[email protected]>
AuthorDate: Wed Oct 7 16:47:59 2026 -0400
HDDS-16304. Convert RpcMetrics latency counters to lock-free
ConcurrentMutableStat (#11425)
Generated-by: Claude Code (claude-opus)
---
.../org/apache/hadoop/ipc_/metrics/RpcMetrics.java | 41 ++++-
.../apache/hadoop/ipc_/metrics/TestRpcMetrics.java | 195 +++++++++++++++++++++
2 files changed, 229 insertions(+), 7 deletions(-)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/metrics/RpcMetrics.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/metrics/RpcMetrics.java
index b4726d04bbd..83d831de142 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/metrics/RpcMetrics.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/metrics/RpcMetrics.java
@@ -22,6 +22,9 @@
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.ipc_.Server;
import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.metrics2.MetricsCollector;
+import org.apache.hadoop.metrics2.MetricsRecordBuilder;
+import org.apache.hadoop.metrics2.MetricsSource;
import org.apache.hadoop.metrics2.MetricsTag;
import org.apache.hadoop.metrics2.annotation.Metric;
import org.apache.hadoop.metrics2.annotation.Metrics;
@@ -29,16 +32,22 @@
import org.apache.hadoop.metrics2.lib.MetricsRegistry;
import org.apache.hadoop.metrics2.lib.MutableCounterLong;
import org.apache.hadoop.metrics2.lib.MutableQuantiles;
-import org.apache.hadoop.metrics2.lib.MutableRate;
+import org.apache.hadoop.ozone.util.ConcurrentMutableRate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is for maintaining the various RPC statistics
* and publishing them through the metrics interfaces.
+ *
+ * The three latency rates are kept outside the {@link MetricsRegistry} as
+ * lock-free {@link ConcurrentMutableRate}s, because they are updated on every
+ * RPC by every handler thread and {@code MutableStat.add} is synchronized.
+ * This class therefore implements {@link MetricsSource} itself and snapshots
+ * them next to the registry, keeping the emitted metric names unchanged.
*/
@Metrics(about="Aggregate RPC metrics", context="rpc")
-public class RpcMetrics {
+public class RpcMetrics implements MetricsSource {
static final Logger LOG = LoggerFactory.getLogger(RpcMetrics.class);
final Server server;
@@ -55,6 +64,11 @@ public class RpcMetrics {
registry = new MetricsRegistry("rpc")
.tag("port", "RPC port", port)
.tag("serverName", "Name of the RPC server", server.getServerName());
+ // Names match what MutableMetricsFactory derived from the field names, so
+ // the emitted metrics are unchanged
+ rpcQueueTime = new ConcurrentMutableRate("RpcQueueTime", "Queue time",
false);
+ rpcLockWaitTime = new ConcurrentMutableRate("RpcLockWaitTime", "Lock wait
time", false);
+ rpcProcessingTime = new ConcurrentMutableRate("RpcProcessingTime",
"Processing time", false);
int[] intervals = conf.getInts(
CommonConfigurationKeys.RPC_METRICS_PERCENTILES_INTERVALS_KEY);
rpcQuantileEnable = (intervals.length > 0) && conf.getBoolean(
@@ -94,11 +108,11 @@ public static RpcMetrics create(Server server,
Configuration conf) {
@Metric("Number of received bytes") MutableCounterLong receivedBytes;
@Metric("Number of sent bytes") MutableCounterLong sentBytes;
- @Metric("Queue time") MutableRate rpcQueueTime;
+ final ConcurrentMutableRate rpcQueueTime;
MutableQuantiles[] rpcQueueTimeQuantiles;
- @Metric("Lock wait time") MutableRate rpcLockWaitTime;
+ final ConcurrentMutableRate rpcLockWaitTime;
MutableQuantiles[] rpcLockWaitTimeQuantiles;
- @Metric("Processing time") MutableRate rpcProcessingTime;
+ final ConcurrentMutableRate rpcProcessingTime;
MutableQuantiles[] rpcProcessingTimeQuantiles;
@Metric("Number of authentication failures")
MutableCounterLong rpcAuthenticationFailures;
@@ -243,10 +257,10 @@ public void incrSlowRpc() {
rpcSlowCalls.incr();
}
/**
- * Returns a MutableRate Counter.
+ * Returns the processing time rate counter.
* @return Mutable Rate
*/
- public MutableRate getRpcProcessingTime() {
+ public ConcurrentMutableRate getRpcProcessingTime() {
return rpcProcessingTime;
}
@@ -285,4 +299,17 @@ public long getRpcSlowCalls() {
public MetricsTag getTag(String tagName) {
return registry.getTag(tagName);
}
+
+ /**
+ * Emits the registry (tags, counters, gauges and quantiles) and the three
+ * latency rates that are held outside of it
+ */
+ @Override
+ public void getMetrics(MetricsCollector collector, boolean all) {
+ MetricsRecordBuilder builder = collector.addRecord(registry.info());
+ registry.snapshot(builder, all);
+ rpcQueueTime.snapshot(builder, all);
+ rpcLockWaitTime.snapshot(builder, all);
+ rpcProcessingTime.snapshot(builder, all);
+ }
}
diff --git
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ipc_/metrics/TestRpcMetrics.java
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ipc_/metrics/TestRpcMetrics.java
new file mode 100644
index 00000000000..ae9856daf84
--- /dev/null
+++
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ipc_/metrics/TestRpcMetrics.java
@@ -0,0 +1,195 @@
+/*
+ * 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.ipc_.metrics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.ipc_.Server;
+import org.apache.hadoop.metrics2.AbstractMetric;
+import org.apache.hadoop.metrics2.MetricsRecord;
+import org.apache.hadoop.metrics2.MetricsSource;
+import org.apache.hadoop.metrics2.MetricsTag;
+import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl;
+import org.apache.hadoop.metrics2.lib.MetricsAnnotations;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link RpcMetrics}.
+ *
+ * The latency rates are held outside the {@link
org.apache.hadoop.metrics2.lib.MetricsRegistry}
+ * as lock-free {@link org.apache.hadoop.ozone.util.ConcurrentMutableRate}s,
so these tests pin the
+ * emitted metric names and the hybrid {@link MetricsSource} wiring that keeps
the registry-backed
+ * metrics in the same record.
+ */
+public class TestRpcMetrics {
+
+ private static final int PORT = 9999;
+ private static final String SERVER_NAME = "TestProtocol";
+
+ private RpcMetrics metrics;
+ private MetricsSource source;
+
+ @BeforeEach
+ public void setUp() {
+ Server server = mock(Server.class);
+ when(server.getListenerAddress()).thenReturn(new InetSocketAddress(PORT));
+ when(server.getServerName()).thenReturn(SERVER_NAME);
+ when(server.getNumOpenConnections()).thenReturn(7);
+ when(server.getNumOpenConnectionsPerUser()).thenReturn("{\"alice\":7}");
+ when(server.getCallQueueLen()).thenReturn(3);
+ when(server.getNumDroppedConnections()).thenReturn(5L);
+
+ metrics = new RpcMetrics(server, new Configuration());
+ // Same reflection DefaultMetricsSystem.register() performs: it populates
the
+ // @Metric counter fields and the @Metric methods into the registry
+ source = MetricsAnnotations.makeSource(metrics);
+ }
+
+ @Test
+ public void testRpcMetricsIsUsedAsItsOwnSource() {
+ assertSame(metrics, source);
+ }
+
+ @Test
+ public void testLatencyRateNamesAreUnchanged() {
+ metrics.addRpcQueueTime(10);
+ metrics.addRpcQueueTime(20);
+ metrics.addRpcLockWaitTime(4);
+ metrics.addRpcProcessingTime(100);
+ metrics.addRpcProcessingTime(300);
+
+ Map<String, Number> values = snapshotMetrics();
+
+ assertEquals(2L, values.get("RpcQueueTimeNumOps").longValue());
+ assertEquals(15.0, values.get("RpcQueueTimeAvgTime").doubleValue(), 0.001);
+ assertEquals(1L, values.get("RpcLockWaitTimeNumOps").longValue());
+ assertEquals(4.0, values.get("RpcLockWaitTimeAvgTime").doubleValue(),
0.001);
+ assertEquals(2L, values.get("RpcProcessingTimeNumOps").longValue());
+ assertEquals(200.0, values.get("RpcProcessingTimeAvgTime").doubleValue(),
0.001);
+
+ // the rates are not extended, matching @Metric.always() defaulting to
false
+ assertThat(values).doesNotContainKeys("RpcQueueTimeStdevTime",
+ "RpcProcessingTimeStdevTime");
+ }
+
+ @Test
+ public void testRegistryMetricsAndTagsStayInTheSameRecord() {
+ metrics.incrSentBytes(42);
+ metrics.incrReceivedBytes(24);
+ metrics.incrSlowRpc();
+
+ MetricsCollectorImpl collector = new MetricsCollectorImpl();
+ source.getMetrics(collector, true);
+ assertEquals(1, collector.getRecords().size());
+ MetricsRecord record = collector.getRecords().get(0);
+ assertEquals("rpc", record.name());
+
+ Map<String, String> tags = new HashMap<>();
+ for (MetricsTag tag : record.tags()) {
+ tags.put(tag.name(), tag.value());
+ }
+ assertEquals(String.valueOf(PORT), tags.get("port"));
+ assertEquals(SERVER_NAME, tags.get("serverName"));
+ assertEquals("rpc", tags.get("Context"));
+ // a String valued @Metric method is emitted as a tag, not as a gauge
+ assertEquals("{\"alice\":7}", tags.get("NumOpenConnectionsPerUser"));
+
+ Map<String, Number> values = new HashMap<>();
+ for (AbstractMetric metric : record.metrics()) {
+ values.put(metric.name(), metric.value());
+ }
+ assertEquals(42L, values.get("SentBytes").longValue());
+ assertEquals(24L, values.get("ReceivedBytes").longValue());
+ assertEquals(1L, values.get("RpcSlowCalls").longValue());
+ assertEquals(7, values.get("NumOpenConnections").intValue());
+ assertEquals(3, values.get("CallQueueLength").intValue());
+ assertEquals(5L, values.get("NumDroppedConnections").longValue());
+ assertThat(values).containsKey("RpcProcessingTimeNumOps");
+ }
+
+ @Test
+ public void testProcessingStatsFeedSlowCallDetection() {
+ metrics.addRpcProcessingTime(100);
+ metrics.addRpcProcessingTime(200);
+ metrics.addRpcProcessingTime(300);
+
+ assertEquals(3, metrics.getProcessingSampleCount());
+ assertEquals(200.0, metrics.getProcessingMean(), 0.001);
+ assertTrue(metrics.getProcessingStdDev() > 0.0,
+ "slow call detection needs a positive deviation");
+ }
+
+ @Test
+ public void testConcurrentProcessingTimeAddsAreCountedExactly()
+ throws InterruptedException {
+ int threads = 14;
+ int addsPerThread = 1000;
+ CountDownLatch start = new CountDownLatch(1);
+ CountDownLatch done = new CountDownLatch(threads);
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ try {
+ for (int t = 0; t < threads; t++) {
+ pool.submit(() -> {
+ try {
+ start.await();
+ for (int i = 0; i < addsPerThread; i++) {
+ metrics.addRpcProcessingTime(1);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+ start.countDown();
+ assertTrue(done.await(30, TimeUnit.SECONDS));
+ } finally {
+ pool.shutdown();
+ }
+
+ assertEquals((long) threads * addsPerThread,
+ snapshotMetrics().get("RpcProcessingTimeNumOps").longValue());
+ }
+
+ private Map<String, Number> snapshotMetrics() {
+ MetricsCollectorImpl collector = new MetricsCollectorImpl();
+ source.getMetrics(collector, true);
+ Map<String, Number> values = new HashMap<>();
+ for (MetricsRecord record : collector.getRecords()) {
+ for (AbstractMetric metric : record.metrics()) {
+ values.put(metric.name(), metric.value());
+ }
+ }
+ return values;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]