feat: support per policy overriden stream limits on limits service - #19403
Conversation
7c5cd05 to
40895ee
Compare
40895ee to
5f1990f
Compare
| uint64 streamHash = 1; | ||
| uint64 totalSize = 2; | ||
| // The resolved ingestion policy for this stream if any. May be used to override some ingestion limits for this stream such as the max streams allowed. | ||
| string ingestionPolicy = 3; |
There was a problem hiding this comment.
Is this an optional field? If so, I think it should have optional, this means the field is not serialized to the wire if there is no value.
There was a problem hiding this comment.
Doesn't look like our current proto version supports optionals.
$ make protos
...
pkg/limits/proto/limits.proto: is a proto3 file that contains optional fields, but code generator protoc-gen-gogoslick hasn't been updated to support optional fields in proto3. Please ask the owner of this code generator to support proto3 optional.--gogoslick_out:
| client kafkaConsumer | ||
| partitionManager *partitionManager | ||
| usage *usageStore | ||
| limits Limits |
There was a problem hiding this comment.
Not used, can be removed?
| // tenantUsage contains the per-partition stream usage for a tenant. | ||
| type tenantUsage map[int32]map[uint64]streamUsage | ||
| // The structure is: partition -> policy -> streamHash -> streamUsage | ||
| // Policy "" represents streams that don't match any specific policy. |
There was a problem hiding this comment.
Normally I wouldn't bother, but let's define a const NoPolicy as empty string?
There was a problem hiding this comment.
Agree, I think it makes it easier to read and reason about
| // implementing rate limits to a later date in the future. | ||
| totalSize uint64 | ||
| rateBuckets []rateBucket | ||
| policy string // The ingestion policy for this stream if any |
There was a problem hiding this comment.
I did some quick napkin math and if we assume 1 million streams, an average of ~100 bytes per string, this would consume an extra 95MB of memory. If we use a uint64 hash, we consume an extra 7.5MB of memory. Not sure this is worth optimizing for now.
There was a problem hiding this comment.
I think this is a valid concern, but on the other hand the amount of streams that will have this field populated will be smaller. Only the streams of policies (i.e. products in our usecase) we override the stream limits for.
My vote goes to leave is as it now: simpler to reason about and less compute to get the hash of the policy. I will leave a comment on how to optimize this if necessary.
On a related note, I fixed this line to use the policyBucket instead. That way we only populate it for streams for which we have a stream limit override.
Lines 388 to 390 in 40d8600
| Rate float64 `json:"rate"` | ||
| Tenant string `json:"tenant"` | ||
| Streams uint64 `json:"streams"` | ||
| PerPolicyStreams map[string]uint64 `json:"per_policy_streams"` |
There was a problem hiding this comment.
| PerPolicyStreams map[string]uint64 `json:"per_policy_streams"` | |
| StreamsByPolicy map[string]uint64 `json:"streams_by_policy"` |
| Streams uint64 `json:"streams"` | ||
| Rate float64 `json:"rate"` | ||
| Tenant string `json:"tenant"` | ||
| Streams uint64 `json:"streams"` |
There was a problem hiding this comment.
| Streams uint64 `json:"streams"` | |
| StreamsTotal uint64 `json:"streams_total"` |
| // for a given tenant and policy. Returns the policy bucket name and the max streams limit. | ||
| // The policy bucket will be the input policy name only if the max streams limit is overriden for the policy. | ||
| func (s *usageStore) getPolicyBucketAndLimit(tenant, policy string) (policyBucket string, maxStreams uint64) { | ||
| defaultMaxStreams := uint64(math.Max(float64(s.limits.MaxGlobalStreamsPerUser(tenant)/s.numPartitions), 1)) |
There was a problem hiding this comment.
I think the use of math.Max is incorrect. 0 should disable it (which I think is also missing from the current implementation – 0 is interpreted as a literal 0).
There was a problem hiding this comment.
I added this because if the stream limit is small (e.g. 50) and the numParitions is bigger (e.g. 64) this would return 0.
That's not a reasonable stream limit tho 😂 but it's what I configured when I was testing this and I found the edge-case. This will not happen on the real world where stream limits are >thousands.
Still I wonder if a ceil would make more sense here. 10000/64=156.25, should we ceil it to 157?
Tbf I don't think this will solve any actual problem, so I think we can skip the ceiling compute.
There was a problem hiding this comment.
So there are two problems here:
- Limit is set to 0
- Limit < number of partitions, such that
limit / num_partitionsis less than 1
The first one (limit is set to 0) is broken in both main and this PR. Looking at how these limits work in the ingesters, limit set to 0 is supposed to mean unlimited.
I suggest we remove the math.Max for now, and we solve both 1 and 2 in a future PR.
There was a problem hiding this comment.
Agree. My latest commit reverted that:
Lines 44 to 53 in 5133697
| streams := s.stripes[i][tenant][partition] | ||
|
|
||
| // Determine which policy bucket to use and the max streams limit | ||
| policyBucket, maxStreamsForStream := s.getPolicyBucketAndLimit(tenant, m.IngestionPolicy) |
There was a problem hiding this comment.
| policyBucket, maxStreamsForStream := s.getPolicyBucketAndLimit(tenant, m.IngestionPolicy) | |
| policyBucket, maxStreamsForPolicy := s.getPolicyBucketAndLimit(tenant, m.IngestionPolicy) |
I assume?
There was a problem hiding this comment.
Actually I meant it to be like that. I'm changing it back to maxStreams
| // Search across all policies for this stream | ||
| // Note that in most cases, there will only be one item on this list (empty policy, ""). | ||
| // and at most just a few items. One idea to speed this up would be to keep a cache of tenant-partition-streamHash -> policy for those streams that have a matching policy. | ||
| for _, streams := range policies { |
There was a problem hiding this comment.
I think the issue here is that if a stream is present in two policies at the same time, this is non-deterministic now because iterating maps is non-deterministic in Go.
There was a problem hiding this comment.
I think we can just remove the Get method for now, it isn't used.
There was a problem hiding this comment.
I removed the Get method.
In the future if we add this back, I think the stream should be in both the noPolicy ("") bucket and a named policy bucket when we strat configuring those streams to be assigned to a particular policy. In which case I think we could remove the stream form the noPolicy bucket when it is added for the first time.


What this PR does / why we need it:
This is a followup for #18994. Here we update the new limits service to support the policy-overridable stream limits.
We modified the
tenantUsageto add a new map of policies so we can track the number of streams per policy independently. This way, the stream of a policy with a stream limit override does not account for the global stream limit and vice-versa.Special notes for reviewer:
I see two ways to get the policy for each stream on the stream service:
Option 1 - What's implemented in this PR:
Update: @grobinson-grafana and I agreed on this approach
We resolve the policy for each stream on the distributor, and pass the policy (if any) on the
proto.StreamMetadatathat is part of theproto.ExceedsLimitsRequestdistributor sends to the limits frontend and this to the limits service.Resolving the policies for a stream can be somewhat expensive as we need to check the stream matchers from the mappings against the stream labels. This way we only resolve the policies once on the distributors which is already done anyways.
Option 2
Resolve the policies again on the limits service. This would make a bit of more work, but will reduce the request size. Plus we can decide to only resolve the policy for those policies that are overriding the stream limits.
We are considering allowing to set the policy via a header on the gateway, so this option may end up being a bit more complicated since we'd need to pass the header from the distributors down to the limits service.
Checklist
CONTRIBUTING.mdguide (required)featPRs are unlikely to be accepted unless a case can be made for the feature actually being a bug fix to existing behavior.docs/sources/setup/upgrade/_index.mddeprecated-config.yamlanddeleted-config.yamlfiles respectively in thetools/deprecated-config-checkerdirectory. Example PR