-
Notifications
You must be signed in to change notification settings - Fork 91
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
[common][router][WIP] cache dimensions for otel #1532
base: main
Are you sure you want to change the base?
Conversation
String dimensionValue = reusableDimensionsMap.get(dimension); | ||
if (dimensionValue == null) { | ||
// TODO: this is not a comprehensive check as this thread local map is not cleared after use | ||
throw new VeniceException("Dimension value cannot be null for " + dimension); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we can assert dimensionValue
is always in reusableDimensionsMap
? Is it because we always filling in the content before calling checkCacheAndGetDimensions
or something else? Would it be intuitive to move the codes inside this function, I mean the codes that inserts dimension values into the cache. If we could do that, then probably we don't need to expose the getThreadLocalReusableDimensionsMap
to public.
If TODO comments is still valid and we never clear the cache after use, is there any reasonable limit value that we can cap the size of this cache?
private final String baseMetricDimensionsKey; | ||
|
||
/** used to pass in the dimension and its values to create {@link Attributes} and avoid creating temp maps/arrays */ | ||
private static final ThreadLocal<Map<VeniceMetricsDimensions, String>> threadLocalReusableDimensionsMap = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct that, ThreadLocal requires every router thread servicing a client query have to create a copy of this map? If it is the case, do we know the upper limit of how large this number could be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a high level comment... Please take a look and LMK what you think.
dimensions = Attributes.builder() | ||
.putAll(commonMetricDimensions) | ||
.put(getDimensionName(VENICE_REQUEST_RETRY_TYPE), retryType.getRetryType()) | ||
.build(); | ||
Map<VeniceMetricsDimensions, String> reusableDimensionsMap = getThreadLocalReusableDimensionsMap(); | ||
reusableDimensionsMap.put(VENICE_REQUEST_RETRY_TYPE, retryType.getRetryType()); | ||
dimensions = otelDimensionsCache.checkCacheAndGetDimensions(retryCountMetric.getMetricEntity()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic looks extremely complex to me, both in terms of my ability to understand it, but also in terms of time complexity (so many map operations, string building, etc...).
Since the MetricEntityState retryCountMetric
is a private property of this class, and this class has the per-store scope that we're interested in, I don't understand, why not cache the Attributes dimensions
inside of the MetricEntityState
object itself? Then this whole function can become simply retryCountMetric.record(1);
and we let the dimension-passing be completely handled on the inside, by simply taking it from some private final Attributes dimensions
property of the MetricEntityState
... No map lookups, no string building, no threadlocal state... all of that disappears completely. And by hiding the OTel complexity in this way, we should greatly simplify the migration from Tehuti to OTel.
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @FelixGV for the comment. Packing the cache inside MetricEntityState
by relying on the existing class with the per-store scope sounds good as well, but it doesn't eliminate the string building (for cache key) completely as each state can have multiple Attributes
keyed by one/more of the varying dimensions. For instance,
retryCountMetric
can have multiple Attributes based on values ofRequestRetryType
healthyRequestMetric
can have multiple Attributes based on values ofHttpResponseStatus
andHttpResponseStatusCodeCategory
.
Also, when we move away fromtehuti
we will regroup some of the currentMetricEntityState
s into 1 (for instance:healthy_request
,unhealthy_request
,tardy_request
, etc to just 1) which is going to increase this cardinality further. We can continue keeping each of these combinations to be a separateMetricEntityState
, but I feel like its too much denormalizing and we have to further denormalize it to keep things 1:1. In other alternative routes, we will need some form of key to access the cache (global
or perRouterHttpRequestStats
or perMetricEntityState
). The content of the key gets smaller as we move further away from global cache, the easier being just 1 dimension like inretryCountMetric
where a cache perRouterHttpRequestStats
like below would work.
VeniceConcurrentHashMap<RequestRetryType, Attributes> otelDimensionCacheForRequestRetryType;
If not, if we have combinations of two or more dimensions as keys, we need to construct some form of key for the cache. What do you think?
Summary
Problem:
Currently, every
metric.record()
call creates a newAttributes
object with all the dimensions needed for that metric. As it also happens on the happy path, it will lead to high rate of object churn and potentially affecting GC.Solution:
This PR aims at reducing this object churn.
Considered 2 approaches and end up choosing approach 2 to cache the dimensions:
Attributes
object and we need to craft a key during runtime to get to the precreated dimensions. Precreating everything is not possible as there can be new stores coming into the picture after bootstrap.Implementation details:
ThreadLocal<Map<VeniceMetricsDimensions, String>>
to pass in the dimension and its values rather than building an object everytime or pass using varargs or writing custom methods for each metricsVeniceConcurrentHashMap<String, Attributes>
to cache the uniqueAttributes
String
)to access this cache is the combination of all dimension names and values. Eg: "DIMENSION1NAMEdimension1valueDIMENSION2NAMEdimension2value...
"dimensionsList
inMetricEntity
fromSet
to aSortedSet
to help in creating consistent keys.RouterHttpRequestStats
(or potentially any stats object class) will create its ownVeniceOpenTelemetryDimensionsCache
to take advantage of the base dimensions.MetricEntity
like "DIMENSION1NAME%sDIMENSION2NAME%s...
" but that needs usingString.format()
, so ended up using aStringBuilder
and creating the full string during runtime instead.How was this PR tested?
NA
Does this PR introduce any user-facing changes?