Skip to content

Commit 9f6bb6e

Browse files
authored
[server] Add new connection setup latency metric (linkedin#2554)
Track the time from channel initialization (including SSL factory creation) to SSL handshake completion as a Tehuti percentile sensor with P50/P95/P99, Avg, and Max. This enables measuring per-connection overhead during connection storms.
1 parent 41de460 commit 9f6bb6e

5 files changed

Lines changed: 85 additions & 0 deletions

File tree

services/venice-server/src/main/java/com/linkedin/venice/listener/HttpChannelInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ interface ChannelPipelineConsumer {
201201
@Override
202202
public void initChannel(SocketChannel ch) {
203203
if (sslFactory.isPresent()) {
204+
ch.attr(ServerConnectionStatsHandler.CHANNEL_INIT_START_TS).set(System.nanoTime());
204205
SslInitializer sslInitializer = new SslInitializer(SslUtils.toAlpiniSSLFactory(sslFactory.get()), false);
205206
if (sslHandshakeExecutor != null) {
206207
sslInitializer

services/venice-server/src/main/java/com/linkedin/venice/listener/ServerConnectionStatsHandler.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import com.linkedin.venice.authorization.IdentityParser;
44
import com.linkedin.venice.stats.ServerConnectionStats;
55
import com.linkedin.venice.utils.DaemonThreadFactory;
6+
import com.linkedin.venice.utils.LatencyUtils;
67
import io.netty.channel.ChannelHandler;
78
import io.netty.channel.ChannelHandlerContext;
89
import io.netty.channel.ChannelInboundHandlerAdapter;
910
import io.netty.handler.ssl.SslHandler;
11+
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
1012
import io.netty.util.Attribute;
1113
import io.netty.util.AttributeKey;
1214
import java.security.cert.Certificate;
@@ -27,6 +29,7 @@
2729
public class ServerConnectionStatsHandler extends ChannelInboundHandlerAdapter {
2830
private static final Logger LOGGER = LogManager.getLogger(ServerConnectionStatsHandler.class);
2931
public static final AttributeKey<Boolean> CHANNEL_ACTIVATED = AttributeKey.valueOf("channelActivated");
32+
public static final AttributeKey<Long> CHANNEL_INIT_START_TS = AttributeKey.valueOf("channelInitStartTs");
3033
private final IdentityParser identityParser;
3134
private final ServerConnectionStats serverConnectionStats;
3235
private final String routerPrincipalName;
@@ -97,6 +100,17 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception {
97100
super.channelActive(ctx);
98101
}
99102

103+
@Override
104+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
105+
if (evt instanceof SslHandshakeCompletionEvent && ((SslHandshakeCompletionEvent) evt).isSuccess()) {
106+
Long initStartTs = ctx.channel().attr(CHANNEL_INIT_START_TS).getAndSet(null);
107+
if (initStartTs != null) {
108+
serverConnectionStats.recordNewConnectionSetupLatency(LatencyUtils.getElapsedTimeFromNSToMS(initStartTs));
109+
}
110+
}
111+
super.userEventTriggered(ctx, evt);
112+
}
113+
100114
@Override
101115
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
102116
Attribute<Boolean> activated = ctx.channel().attr(CHANNEL_ACTIVATED);

services/venice-server/src/main/java/com/linkedin/venice/stats/ServerConnectionStats.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ public class ServerConnectionStats extends AbstractVeniceStats {
1212
public static final String ROUTER_CONNECTION_COUNT_GAUGE = "router_connection_count";
1313
public static final String CLIENT_CONNECTION_REQUEST = "client_connection_request";
1414
public static final String CLIENT_CONNECTION_COUNT_GAUGE = "client_connection_count";
15+
public static final String NEW_CONNECTION_SETUP_LATENCY = "new_connection_setup_latency";
1516

1617
private final Sensor routerConnectionRequestSensor;
1718
private final Sensor clientConnectionRequestSensor;
1819
private final Sensor connectionRequestSensor;
20+
private final Sensor newConnectionSetupLatencySensor;
1921

2022
private final AtomicLong routerConnectionCount = new AtomicLong();
2123
private final AtomicLong clientConnectionCount = new AtomicLong();
@@ -29,6 +31,9 @@ public ServerConnectionStats(MetricsRepository metricsRepository, String name) {
2931
new AsyncGauge((ignored, ignored2) -> clientConnectionCount.get(), CLIENT_CONNECTION_COUNT_GAUGE));
3032
clientConnectionRequestSensor = registerSensorIfAbsent(CLIENT_CONNECTION_REQUEST, new OccurrenceRate());
3133
connectionRequestSensor = registerSensorIfAbsent("connection_request", new OccurrenceRate());
34+
newConnectionSetupLatencySensor = registerSensorIfAbsent(
35+
NEW_CONNECTION_SETUP_LATENCY,
36+
TehutiUtils.getPercentileStatWithAvgAndMax(getName(), NEW_CONNECTION_SETUP_LATENCY));
3237
}
3338

3439
public void incrementRouterConnectionCount() {
@@ -52,4 +57,11 @@ public void decrementClientConnectionCount() {
5257
public void newConnectionRequest() {
5358
connectionRequestSensor.record();
5459
}
60+
61+
/**
62+
* Record the latency from the start of channel initialization to SSL handshake completion.
63+
*/
64+
public void recordNewConnectionSetupLatency(double latencyMs) {
65+
newConnectionSetupLatencySensor.record(latencyMs);
66+
}
5567
}

services/venice-server/src/test/java/com/linkedin/venice/listener/HttpChannelInitializerTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.grpc.ServerInterceptor;
1818
import io.netty.channel.ChannelPipeline;
1919
import io.netty.channel.socket.SocketChannel;
20+
import io.netty.util.Attribute;
2021
import io.tehuti.metrics.MetricsRepository;
2122
import java.util.List;
2223
import java.util.Optional;
@@ -102,6 +103,7 @@ public void testInitChannelWithSSLExecutor() {
102103
ChannelPipeline channelPipeline = mock(ChannelPipeline.class);
103104
SocketChannel ch = mock(SocketChannel.class);
104105
doReturn(channelPipeline).when(ch).pipeline();
106+
doReturn(mock(Attribute.class)).when(ch).attr(any());
105107
doReturn(channelPipeline).when(channelPipeline).addLast(any());
106108
HttpChannelInitializer initializer = new HttpChannelInitializer(
107109
storeMetadataRepository,

services/venice-server/src/test/java/com/linkedin/venice/listener/ServerConnectionStatsHandlerTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.linkedin.venice.listener;
22

3+
import static org.mockito.ArgumentMatchers.anyDouble;
34
import static org.mockito.ArgumentMatchers.eq;
45
import static org.mockito.Mockito.doReturn;
56
import static org.mockito.Mockito.mock;
@@ -8,18 +9,21 @@
89
import static org.mockito.Mockito.times;
910
import static org.mockito.Mockito.verify;
1011
import static org.mockito.Mockito.when;
12+
import static org.testng.Assert.assertTrue;
1113

1214
import com.linkedin.venice.stats.ServerConnectionStats;
1315
import io.netty.channel.Channel;
1416
import io.netty.channel.ChannelHandlerContext;
1517
import io.netty.channel.ChannelPipeline;
1618
import io.netty.handler.ssl.SslHandler;
19+
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
1720
import io.netty.util.Attribute;
1821
import java.security.cert.Certificate;
1922
import java.security.cert.X509Certificate;
2023
import javax.net.ssl.SSLEngine;
2124
import javax.net.ssl.SSLSession;
2225
import javax.security.auth.x500.X500Principal;
26+
import org.mockito.ArgumentCaptor;
2327
import org.testng.annotations.BeforeMethod;
2428
import org.testng.annotations.Test;
2529

@@ -97,4 +101,56 @@ public void testChannelRegisteredUnregisteredWithSslHandler() throws Exception {
97101
verify(serverConnectionStats, times(1)).decrementClientConnectionCount();
98102
verify(serverConnectionStats, times(2)).newConnectionRequest();
99103
}
104+
105+
@Test
106+
public void testConnectionSetupLatencyRecordedOnHandshakeSuccess() throws Exception {
107+
ServerConnectionStats serverConnectionStats = mock(ServerConnectionStats.class);
108+
ServerConnectionStatsHandler handler =
109+
new ServerConnectionStatsHandler(null, serverConnectionStats, "venice-router");
110+
111+
// Simulate initChannel setting the start timestamp
112+
Attribute<Long> initStartTsAttr = mock(Attribute.class);
113+
long startTs = System.nanoTime();
114+
when(channel.attr(ServerConnectionStatsHandler.CHANNEL_INIT_START_TS)).thenReturn(initStartTsAttr);
115+
when(initStartTsAttr.getAndSet(null)).thenReturn(startTs);
116+
117+
// Fire a successful handshake event
118+
handler.userEventTriggered(context, SslHandshakeCompletionEvent.SUCCESS);
119+
120+
// Verify latency was recorded with a non-negative value
121+
ArgumentCaptor<Double> latencyCaptor = ArgumentCaptor.forClass(Double.class);
122+
verify(serverConnectionStats, times(1)).recordNewConnectionSetupLatency(latencyCaptor.capture());
123+
assertTrue(latencyCaptor.getValue() >= 0, "Latency should be non-negative");
124+
}
125+
126+
@Test
127+
public void testConnectionSetupLatencyNotRecordedOnHandshakeFailure() throws Exception {
128+
ServerConnectionStats serverConnectionStats = mock(ServerConnectionStats.class);
129+
ServerConnectionStatsHandler handler =
130+
new ServerConnectionStatsHandler(null, serverConnectionStats, "venice-router");
131+
132+
// Fire a failed handshake event
133+
SslHandshakeCompletionEvent failedEvent =
134+
new SslHandshakeCompletionEvent(new javax.net.ssl.SSLHandshakeException("test failure"));
135+
handler.userEventTriggered(context, failedEvent);
136+
137+
// Verify latency was NOT recorded
138+
verify(serverConnectionStats, never()).recordNewConnectionSetupLatency(anyDouble());
139+
}
140+
141+
@Test
142+
public void testConnectionSetupLatencyNotRecordedWhenTimestampMissing() throws Exception {
143+
ServerConnectionStats serverConnectionStats = mock(ServerConnectionStats.class);
144+
ServerConnectionStatsHandler handler =
145+
new ServerConnectionStatsHandler(null, serverConnectionStats, "venice-router");
146+
147+
// No timestamp was set (e.g., non-SSL path)
148+
Attribute<Long> initStartTsAttr = mock(Attribute.class);
149+
when(channel.attr(ServerConnectionStatsHandler.CHANNEL_INIT_START_TS)).thenReturn(initStartTsAttr);
150+
when(initStartTsAttr.getAndSet(null)).thenReturn(null);
151+
152+
handler.userEventTriggered(context, SslHandshakeCompletionEvent.SUCCESS);
153+
154+
verify(serverConnectionStats, never()).recordNewConnectionSetupLatency(anyDouble());
155+
}
100156
}

0 commit comments

Comments
 (0)