[
https://issues.apache.org/jira/browse/HDDS-16741?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Andrey Yarovoy updated HDDS-16741:
----------------------------------
Description:
The `GrpcQueueTime*` and `GrpcProcessingTime*` metrics published by
`GrpcMetrics` do not report the
latency of any individual gRPC call. Three independent causes, all in
`GrpcMetricsServerRequestInterceptor`:
*1. The timestamps are server-wide, not per-call.*
`receivedTime`, `startTime` and `endTime` are plain instance fields on the
interceptor:
{code:java}
public class GrpcMetricsServerRequestInterceptor implements ServerInterceptor {
private final GrpcMetrics grpcMetrics;
private long receivedTime;
private long startTime;
private long endTime;
{code}
`GrpcOzoneManagerServer` constructs exactly one instance and hands it to
`ServerInterceptors.intercept(...)`, so that one object serves every call on
the server. `interceptCall`
writes `receivedTime`, the returned listener's `onMessage` writes `startTime`,
and `onComplete` writes
`endTime`. With concurrent calls these writes interleave, so the deltas
{code:java}
int queueTime = (int) (startTime - receivedTime);
int processingTime = (int) (endTime - startTime);
{code}
routinely subtract timestamps taken from *different* calls. The results are not
noisy versions of the
real latency, they are arbitrary — including negative, when the later call's
timestamp lands first.
The timestamps belong in per-call scope: `receivedTime` as a local in
`interceptCall`, captured by the
listener, and `startTime` as a field on the listener instance, which gRPC
creates per call and whose
callbacks it invokes serially.
*2. A nanosecond delta is truncated to `int`.*
Both timestamps come from `System.nanoTime()`, and the delta is cast to `int`
before being passed to
`addGrpcQueueTime(int)` / `addGrpcProcessingTime(int)`. An `int` of nanoseconds
overflows at ~2.147 s,
so even a correctly-paired measurement wraps to a negative or small value for
any call slower than
that — precisely the calls the metric exists to show. The accessors should take
`long`.
*3. Nanoseconds are published under a millisecond label.*
No conversion happens anywhere between `System.nanoTime()` and the metric, yet
the quantiles are
created as
{code:java}
registry.newQuantiles("grpcQueueTime" + interval + "s", "grpc queue time in
millisecond", ...)
{code}
and held in fields named `grpcQueueTimeMillisQuantiles` /
`grpcProcessingTimeMillisQuantiles`. So the
emitted values are nanoseconds described as milliseconds — a factor of
1,000,000. This also makes the
gRPC numbers incomparable with the IPC ones, where `RpcMetrics.TIMEUNIT` is
`TimeUnit.MILLISECONDS` and `Server` passes milliseconds.
Fixing any one of the three in isolation still leaves the metric wrong, so they
belong together.
Note on compatibility: this changes the *values* of
`GrpcQueueTime\{NumOps,AvgTime}`,
`GrpcProcessingTime\{NumOps,AvgTime}` and the two quantile families —
necessarily, since the current
values are wrong. Metric names stay unchanged. If the millisecond conversion is
considered a visible
change rather than a correction, the alternative is to keep nanoseconds and fix
the descriptions and
field names instead; worth deciding on the Jira.
*Testing*
Unit test on the interceptor alone, following the mock-based idiom in
`hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/protocolPB/grpc/TestClientAddressServerInterceptor.java`
(mock `ServerCall` / `ServerCallHandler`, drive `interceptCall` then the
returned listener): interleave
two calls' `onMessage` / `onComplete` callbacks and assert both recorded
durations are non-negative and
attributable to their own call, which fails on the current code. Plus a case
asserting a duration above
2.147 s is not truncated.
was:
The `GrpcQueueTime*` and `GrpcProcessingTime*` metrics published by
`GrpcMetrics` do not report the
latency of any individual gRPC call. Three independent causes, all in
`GrpcMetricsServerRequestInterceptor`:
*1. The timestamps are server-wide, not per-call.*
`receivedTime`, `startTime` and `endTime` are plain instance fields on the
interceptor:
{code}
public class GrpcMetricsServerRequestInterceptor implements ServerInterceptor {
private final GrpcMetrics grpcMetrics;
private long receivedTime;
private long startTime;
private long endTime;
{code}
`GrpcOzoneManagerServer` constructs exactly one instance and hands it to
`ServerInterceptors.intercept(...)`, so that one object serves every call on
the server. `interceptCall`
writes `receivedTime`, the returned listener's `onMessage` writes `startTime`,
and `onComplete` writes
`endTime`. With concurrent calls these writes interleave, so the deltas
{code}
int queueTime = (int) (startTime - receivedTime);
int processingTime = (int) (endTime - startTime);
{code}
routinely subtract timestamps taken from *different* calls. The results are not
noisy versions of the
real latency, they are arbitrary — including negative, when the later call's
timestamp lands first.
The timestamps belong in per-call scope: `receivedTime` as a local in
`interceptCall`, captured by the
listener, and `startTime` as a field on the listener instance, which gRPC
creates per call and whose
callbacks it invokes serially.
*2. A nanosecond delta is truncated to `int`.*
Both timestamps come from `System.nanoTime()`, and the delta is cast to `int`
before being passed to
`addGrpcQueueTime(int)` / `addGrpcProcessingTime(int)`. An `int` of nanoseconds
overflows at ~2.147 s,
so even a correctly-paired measurement wraps to a negative or small value for
any call slower than
that — precisely the calls the metric exists to show. The accessors should take
`long`.
*3. Nanoseconds are published under a millisecond label.*
No conversion happens anywhere between `System.nanoTime()` and the metric, yet
the quantiles are
created as
{code}
registry.newQuantiles("grpcQueueTime" + interval + "s", "grpc queue time in
millisecond", ...)
{code}
and held in fields named `grpcQueueTimeMillisQuantiles` /
`grpcProcessingTimeMillisQuantiles`. So the
emitted values are nanoseconds described as milliseconds — a factor of
1,000,000. This also makes the
gRPC numbers incomparable with the IPC ones, where `RpcMetrics.TIMEUNIT` is
`TimeUnit.MILLISECONDS` and `Server` passes milliseconds.
Fixing any one of the three in isolation still leaves the metric wrong, so they
belong together.
Note on compatibility: this changes the *values* of
`GrpcQueueTime\{NumOps,AvgTime}`,
`GrpcProcessingTime\{NumOps,AvgTime}` and the two quantile families —
necessarily, since the current
values are wrong. Metric names stay unchanged. If the millisecond conversion is
considered a visible
change rather than a correction, the alternative is to keep nanoseconds and fix
the descriptions and
field names instead; worth deciding on the Jira.
### Testing
Unit test on the interceptor alone, following the mock-based idiom in
`hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/protocolPB/grpc/TestClientAddressServerInterceptor.java`
(mock `ServerCall` / `ServerCallHandler`, drive `interceptCall` then the
returned listener): interleave
two calls' `onMessage` / `onComplete` callbacks and assert both recorded
durations are non-negative and
attributable to their own call, which fails on the current code. Plus a case
asserting a duration above
2.147 s is not truncated.
> GrpcMetrics queue and processing times are recorded from timestamps shared
> across concurrent calls
> --------------------------------------------------------------------------------------------------
>
> Key: HDDS-16741
> URL: https://issues.apache.org/jira/browse/HDDS-16741
> Project: Apache Ozone
> Issue Type: Bug
> Components: OM
> Reporter: Andrey Yarovoy
> Priority: Major
>
> The `GrpcQueueTime*` and `GrpcProcessingTime*` metrics published by
> `GrpcMetrics` do not report the
> latency of any individual gRPC call. Three independent causes, all in
> `GrpcMetricsServerRequestInterceptor`:
> *1. The timestamps are server-wide, not per-call.*
> `receivedTime`, `startTime` and `endTime` are plain instance fields on the
> interceptor:
> {code:java}
> public class GrpcMetricsServerRequestInterceptor implements ServerInterceptor
> {
> private final GrpcMetrics grpcMetrics;
> private long receivedTime;
> private long startTime;
> private long endTime;
> {code}
> `GrpcOzoneManagerServer` constructs exactly one instance and hands it to
> `ServerInterceptors.intercept(...)`, so that one object serves every call on
> the server. `interceptCall`
> writes `receivedTime`, the returned listener's `onMessage` writes
> `startTime`, and `onComplete` writes
> `endTime`. With concurrent calls these writes interleave, so the deltas
> {code:java}
> int queueTime = (int) (startTime - receivedTime);
> int processingTime = (int) (endTime - startTime);
> {code}
> routinely subtract timestamps taken from *different* calls. The results are
> not noisy versions of the
> real latency, they are arbitrary — including negative, when the later call's
> timestamp lands first.
> The timestamps belong in per-call scope: `receivedTime` as a local in
> `interceptCall`, captured by the
> listener, and `startTime` as a field on the listener instance, which gRPC
> creates per call and whose
> callbacks it invokes serially.
> *2. A nanosecond delta is truncated to `int`.*
> Both timestamps come from `System.nanoTime()`, and the delta is cast to `int`
> before being passed to
> `addGrpcQueueTime(int)` / `addGrpcProcessingTime(int)`. An `int` of
> nanoseconds overflows at ~2.147 s,
> so even a correctly-paired measurement wraps to a negative or small value for
> any call slower than
> that — precisely the calls the metric exists to show. The accessors should
> take `long`.
> *3. Nanoseconds are published under a millisecond label.*
> No conversion happens anywhere between `System.nanoTime()` and the metric,
> yet the quantiles are
> created as
> {code:java}
> registry.newQuantiles("grpcQueueTime" + interval + "s", "grpc queue time in
> millisecond", ...)
> {code}
> and held in fields named `grpcQueueTimeMillisQuantiles` /
> `grpcProcessingTimeMillisQuantiles`. So the
> emitted values are nanoseconds described as milliseconds — a factor of
> 1,000,000. This also makes the
> gRPC numbers incomparable with the IPC ones, where `RpcMetrics.TIMEUNIT` is
> `TimeUnit.MILLISECONDS` and `Server` passes milliseconds.
> Fixing any one of the three in isolation still leaves the metric wrong, so
> they belong together.
> Note on compatibility: this changes the *values* of
> `GrpcQueueTime\{NumOps,AvgTime}`,
> `GrpcProcessingTime\{NumOps,AvgTime}` and the two quantile families —
> necessarily, since the current
> values are wrong. Metric names stay unchanged. If the millisecond conversion
> is considered a visible
> change rather than a correction, the alternative is to keep nanoseconds and
> fix the descriptions and
> field names instead; worth deciding on the Jira.
> *Testing*
> Unit test on the interceptor alone, following the mock-based idiom in
> `hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/protocolPB/grpc/TestClientAddressServerInterceptor.java`
> (mock `ServerCall` / `ServerCallHandler`, drive `interceptCall` then the
> returned listener): interleave
> two calls' `onMessage` / `onComplete` callbacks and assert both recorded
> durations are non-negative and
> attributable to their own call, which fails on the current code. Plus a case
> asserting a duration above
> 2.147 s is not truncated.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]