Skip to content
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

Rate Limit Quota #19793

Merged
merged 44 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
997dc2c
RLQS WIP
sergiitk Jan 27, 2022
4f70ae7
Unify rate limit strategies, add FallbackBehavior
sergiitk Feb 3, 2022
18e7271
feedback: simple field renames/deletions
sergiitk Feb 7, 2022
c0b2f07
BooleanChoice -> BlanketRule
sergiitk Feb 8, 2022
321cc71
feedback: move unsubsribe responsibility to the server
sergiitk Feb 8, 2022
94b0be5
feedback: remove BucketQuotaUsage.report_id
sergiitk Feb 8, 2022
4033ea9
feedback: rename RateLimitQuota -> RateLimitQuotaFilterConfig
sergiitk Feb 8, 2022
da9e37c
Update doc
yanavlasov Apr 1, 2022
bc4e19a
Fix envoy-presubmit
sergiitk Apr 13, 2022
96c94be
Fix doc build
sergiitk Apr 6, 2022
8776b2b
Finish DenyResponseSettings
sergiitk Apr 7, 2022
4b8dbd1
Move BucketId to common
sergiitk Apr 25, 2022
fe39450
Finish and document bucket_id
sergiitk Apr 26, 2022
8c20ec1
Use only BucketIdBuilder in filter configuration
sergiitk May 4, 2022
4b0d8d5
Update doc
yanavlasov May 24, 2022
0e3eaf3
Finish NoAssignmentBehavior and ExpiredAssignmentBehavior
sergiitk May 3, 2022
9b9fcb1
minor: ratelimit_strategy.proto lint fix
sergiitk May 25, 2022
679dfaf
Finish RateLimitQuotaService
sergiitk May 24, 2022
d6a2324
Finish RateLimitQuotaFilterConfig
sergiitk May 24, 2022
a8466df
Merge branch 'main' into rlqs
yanavlasov May 25, 2022
2a08a56
Remove not-implemented-hide tag
sergiitk May 25, 2022
f306840
Merge branch 'main' into rlqs
sergiitk May 26, 2022
4dfc740
Feedback: fix an incorrect statement in the example
sergiitk May 25, 2022
075f7a5
Doc clarification: RequestsPerTimeUnit, RateLimitQuotaBucketSettings
sergiitk May 26, 2022
6048279
Update docs
yanavlasov May 31, 2022
9d069c9
Add on_no_match example and clarify bucket unassigned behavior
sergiitk May 31, 2022
866945a
Fix rst lint
sergiitk Jun 2, 2022
f1209df
Update docs
yanavlasov Jun 2, 2022
83cfd3e
Merge branch 'main' into rlqs
yanavlasov Jun 2, 2022
4ff763e
Feedback: expired_assignment_behavior_timeout greater than zero
sergiitk Jun 2, 2022
8799cce
Feedback: split gRPC and HTTP DenyResponseSettings
sergiitk Jun 2, 2022
4b9d187
Feedback: empty deny_response_settings picks defaults for req types
sergiitk Jun 3, 2022
fa65605
Revert "Feedback: split gRPC and HTTP DenyResponseSettings"
sergiitk Jun 3, 2022
2bccd55
Feedback: Add a note on how Envoy distinguishes gRPC requests
sergiitk Jun 4, 2022
15a7622
Merge branch 'main' into rlqs
sergiitk Jun 14, 2022
b8667bf
Apply suggestions from code review
sergiitk Jun 14, 2022
6c449fb
Address comments
yanavlasov Jun 16, 2022
ca66552
Fix lint
sergiitk Jun 21, 2022
9306a59
Non-GitHub feedback: improve wording, add RequestsPerTimeUnit details
sergiitk Jun 23, 2022
f7665e1
Feedback: address API shepards' review feedback
sergiitk Jun 24, 2022
699957e
Address API shepards' feedback round 2; improve and expand wording
sergiitk Jun 24, 2022
39e9f7b
Fix lint: spelling (though disagreeable); remove unused import
sergiitk Jun 24, 2022
ad0fdd1
Feedback: filter_enforced defaults to 100%
sergiitk Jun 28, 2022
ab15657
Merge branch 'main' into rlqs
sergiitk Jun 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ proto_library(
"//envoy/extensions/filters/http/oauth2/v3:pkg",
"//envoy/extensions/filters/http/on_demand/v3:pkg",
"//envoy/extensions/filters/http/original_src/v3:pkg",
"//envoy/extensions/filters/http/rate_limit_quota/v3:pkg",
"//envoy/extensions/filters/http/ratelimit/v3:pkg",
"//envoy/extensions/filters/http/rbac/v3:pkg",
"//envoy/extensions/filters/http/router/v3:pkg",
Expand Down Expand Up @@ -283,6 +284,7 @@ proto_library(
"//envoy/service/listener/v3:pkg",
"//envoy/service/load_stats/v3:pkg",
"//envoy/service/metrics/v3:pkg",
"//envoy/service/rate_limit_quota/v3:pkg",
"//envoy/service/ratelimit/v3:pkg",
"//envoy/service/route/v3:pkg",
"//envoy/service/runtime/v3:pkg",
Expand Down
15 changes: 15 additions & 0 deletions api/envoy/extensions/filters/http/rate_limit_quota/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"//envoy/type/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
"@com_github_cncf_udpa//xds/annotations/v3:pkg",
"@com_github_cncf_udpa//xds/type/matcher/v3:pkg",
],
)

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions api/envoy/service/rate_limit_quota/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
has_services = True,
deps = [
"//envoy/type/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
"@com_github_cncf_udpa//xds/annotations/v3:pkg",
],
)
250 changes: 250 additions & 0 deletions api/envoy/service/rate_limit_quota/v3/rlqs.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
syntax = "proto3";

package envoy.service.rate_limit_quota.v3;

import "envoy/type/v3/ratelimit_strategy.proto";

import "google/protobuf/duration.proto";

import "xds/annotations/v3/status.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.service.rate_limit_quota.v3";
option java_outer_classname = "RlqsProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/rate_limit_quota/v3;rate_limit_quotav3";
option java_generic_services = true;
option (udpa.annotations.file_status).package_version_status = ACTIVE;
option (xds.annotations.v3.file_status).work_in_progress = true;

// [#protodoc-title: Rate Limit Quota Service (RLQS)]

// The Rate Limit Quota Service (RLQS) is a Envoy global rate limiting service that allows to
// delegate rate limit decisions to a remote service. The service will aggregate the usage reports
// from multiple data plane instances, and distribute Rate Limit Assignments to each instance
// based on its business logic. The logic is outside of the scope of the protocol API.
//
// The protocol is designed as a streaming-first API. It utilizes watch-like subscription model.
// The data plane groups requests into Quota Buckets as directed by the filter config,
// and periodically reports them to the RLQS server along with the Bucket identifier, :ref:`BucketId
// <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>`. Once RLQS server has collected enough
// reports to make a decision, it'll send back the assignment with the rate limiting instructions.
//
// The first report sent by the data plane is interpreted by the RLQS server as a "watch" request,
// indicating that the data plane instance is interested in receiving further updates for the
// ``BucketId``. From then on, RLQS server may push assignments to this instance at will, even if
// the instance is not sending usage reports. It's the responsibility of the RLQS server
// to determine when the data plane instance didn't send ``BucketId`` reports for too long,
// and to respond with the :ref:`AbandonAction
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`,
// indicating that the server has now stopped sending quota assignments for the ``BucketId`` bucket,
// and the data plane instance should :ref:`abandon
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.abandon_action>`
// it.
//
// Refer to Rate Limit Quota :ref:`configuration overview <config_http_filters_rate_limit_quota>`
// for further details.

// Defines the Rate Limit Quota Service (RLQS).
service RateLimitQuotaService {
// Main communication channel: the data plane sends usage reports to the RLQS server,
// and the server asynchronously responding with the assignments.
rpc StreamRateLimitQuotas(stream RateLimitQuotaUsageReports)
returns (stream RateLimitQuotaResponse) {
}
}

message RateLimitQuotaUsageReports {
// The usage report for a bucket.
//
// .. note::
// Note that the first report sent for a ``BucketId`` indicates to the RLQS server that
// the RLQS client is subscribing for the future assignments for this ``BucketId``.
message BucketQuotaUsage {
// ``BucketId`` for which request quota usage is reported.
BucketId bucket_id = 1 [(validate.rules).message = {required: true}];

// Time elapsed since the last report.
google.protobuf.Duration time_elapsed = 2 [(validate.rules).duration = {
required: true
gt {}
}];

// Requests the data plane has allowed through.
uint64 num_requests_allowed = 3;

// Requests throttled.
uint64 num_requests_denied = 4;
}

// All quota requests must specify the domain. This enables sharing the quota
// server between different applications without fear of overlap.
// E.g., "envoy".
//
// Should only be provided in the first report, all subsequent messages on the same
// stream are considered to be in the same domain. In case the domain needs to be
// changes, close the stream, and reopen a new one with the different domain.
string domain = 1 [(validate.rules).string = {min_len: 1}];

// A list of quota usage reports. The list is processed by the RLQS server in the same order
// it's provided by the client.
repeated BucketQuotaUsage bucket_quota_usages = 2 [(validate.rules).repeated = {min_items: 1}];
}

message RateLimitQuotaResponse {
// Commands the data plane to apply one of the actions to the bucket with the
// :ref:`bucket_id <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
message BucketAction {
// Quota assignment for the bucket. Configures the rate limiting strategy and the duration
// for the given :ref:`bucket_id
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
//
// **Applying the first assignment to the bucket**
//
// Once the data plane receives the ``QuotaAssignmentAction``, it must send the current usage
// report for the bucket, and start rate limiting requests matched into the bucket
// using the strategy configured in the :ref:`rate_limit_strategy
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.rate_limit_strategy>`
// field. The assignment becomes bucket's *active* assignment.
//
// **Expiring the assignment**
//
// The duration of the assignment defined in the :ref:`assignment_time_to_live
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.assignment_time_to_live>`
// field. When the duration runs off, the assignment is *expired*, and no longer *active*.
// The data plane should stop applying the rate limiting strategy to the bucket, and transition
// the bucket to the "expired assignment" state. This activates the behavior configured in the
// :ref:`expired_assignment_behavior <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.expired_assignment_behavior>`
// field.
//
// **Replacing the assignment**
//
// * If the rate limiting strategy is different from bucket's *active* assignment, or
// the current bucket assignment is *expired*, the data plane must immediately
// end the current assignment, report the bucket usage, and apply the new assignment.
// The new assignment becomes bucket's *active* assignment.
// * If the rate limiting strategy is the same as the bucket's *active* (not *expired*)
// assignment, the data plane should extend the duration of the *active* assignment
// for the duration of the new assignment provided in the :ref:`assignment_time_to_live
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.assignment_time_to_live>`
// field. The *active* assignment is considered unchanged.
message QuotaAssignmentAction {
// A duration after which the assignment is be considered *expired*. The process of the
// expiration is described :ref:`above
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`.
//
// * If unset, the assignment has no expiration date.
// * If set to ``0``, the assignment expires immediately, forcing the client into the
// :ref:`"expired assignment"
// <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior.expired_assignment_behavior_timeout>`
// state. This may be used by the RLQS server in cases when it needs clients to proactively
// fall back to the pre-configured :ref:`ExpiredAssignmentBehavior
// <envoy_v3_api_msg_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior>`,
// f.e. before the server going into restart.
//
// .. attention::
// Note that :ref:`expiring
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`
// the assignment is not the same as :ref:`abandoning
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
// the assignment. While expiring the assignment just transitions the bucket to
// the "expired assignment" state; abandoning the assignment completely erases
// the bucket from the data plane memory, and stops the usage reports.
google.protobuf.Duration assignment_time_to_live = 2 [(validate.rules).duration = {gte {}}];

// Configures the local rate limiter for the request matched to the bucket.
//
// If not set, allow all requests.
type.v3.RateLimitStrategy rate_limit_strategy = 3;
}

// Abandon action for the bucket. Indicates that the RLQS server will no longer be
// sending updates for the given :ref:`bucket_id
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
//
// If no requests are reported for a bucket, after some time the server considers the bucket
// inactive. The server stops tracking the bucket, and instructs the the data plane to abandon
// the bucket via this message.
//
// **Abandoning the assignment**
//
// The data plane is to erase the bucket (including its usage data) from the memory.
// It should stop tracking the bucket, and stop reporting its usage. This effectively resets
// the data plane to the state prior to matching the first request into the bucket.
//
// **Restarting the subscription**
//
// If a new request is matched into a bucket previously abandoned, the data plane must behave
// as if it has never tracked the bucket, and it's the first request matched into it:
//
// 1. The process of :ref:`subscription and reporting
// <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.reporting_interval>`
// starts from the beginning.
// 2. The bucket transitions to the :ref:`"no assignment"
// <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.no_assignment_behavior>`
// state.
// 3. Once the new assignment is received, it's applied per
// "Applying the first assignment to the bucket" section of the :ref:`QuotaAssignmentAction
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`.
message AbandonAction {
}

// ``BucketId`` for which request the action is applied.
BucketId bucket_id = 1 [(validate.rules).message = {required: true}];

oneof bucket_action {
option (validate.required) = true;

// Apply the quota assignment to the bucket.
//
// Commands the data plane to apply a rate limiting strategy to the bucket.
// The process of applying and expiring the rate limiting strategy is detailed in the
// :ref:`QuotaAssignmentAction
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`
// message.
QuotaAssignmentAction quota_assignment_action = 2;

// Abandon the bucket.
//
// Commands the data plane to abandon the bucket.
// The process of abandoning the bucket is described in the :ref:`AbandonAction
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
// message.
AbandonAction abandon_action = 3;
}
}

// An ordered list of actions to be applied to the buckets. The actions are applied in the
// given order, from top to bottom.
repeated BucketAction bucket_action = 1 [(validate.rules).repeated = {min_items: 1}];
}

// The identifier for the bucket. Used to match the bucket between the control plane (RLQS server),
// and the data plane (RLQS client), f.e.:
//
// * the data plane sends a usage report for requests matched into the bucket with ``BucketId``
// to the control plane
// * the control plane sends an assignment for the bucket with ``BucketId`` to the data plane
// Bucket ID.
//
// Example:
//
// .. validated-code-block:: yaml
// :type-name: envoy.service.rate_limit_quota.v3.BucketId
//
// bucket:
// name: my_bucket
// env: staging
//
// .. note::
// The order of ``BucketId`` keys do not matter. Buckets ``{ a: 'A', b: 'B' }`` and
// ``{ b: 'B', a: 'A' }`` are identical.
message BucketId {
map<string, string> bucket = 1 [(validate.rules).map = {
min_pairs: 1
keys {string {min_len: 1}}
values {string {min_len: 1}}
}];
}
5 changes: 4 additions & 1 deletion api/envoy/type/v3/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")
licenses(["notice"]) # Apache 2

api_proto_package(
deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"],
deps = [
"@com_github_cncf_udpa//udpa/annotations:pkg",
"@com_github_cncf_udpa//xds/annotations/v3:pkg",
],
)
79 changes: 79 additions & 0 deletions api/envoy/type/v3/ratelimit_strategy.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
syntax = "proto3";

package envoy.type.v3;

import "envoy/type/v3/ratelimit_unit.proto";
import "envoy/type/v3/token_bucket.proto";

import "xds/annotations/v3/status.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.type.v3";
option java_outer_classname = "RatelimitStrategyProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/type/v3;typev3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;
option (xds.annotations.v3.file_status).work_in_progress = true;

// [#protodoc-title: Rate Limit Strategies]

message RateLimitStrategy {
// Choose between allow all and deny all.
enum BlanketRule {
ALLOW_ALL = 0;
DENY_ALL = 1;
}

// Best-effort limit of the number of requests per time unit.
//
// Allows to specify the desired requests per second (RPS, QPS), requests per minute (QPM, RPM),
// etc., without specifying a rate limiting algorithm implementation.
//
// ``RequestsPerTimeUnit`` strategy does not demand any specific rate limiting algorithm to be
// used (in contrast to the :ref:`TokenBucket <envoy_v3_api_msg_type.v3.TokenBucket>`,
// for example). It implies that the implementation details of rate limiting algorithm are
// irrelevant as long as the configured number of "requests per time unit" is achieved.
//
// Note that the ``TokenBucket`` is still a valid implementation of the ``RequestsPerTimeUnit``
// strategy, and may be chosen to enforce the rate limit. However, there's no guarantee it will be
// the ``TokenBucket`` in particular, and not the Leaky Bucket, the Sliding Window, or any other
// rate limiting algorithm that fulfills the requirements.
message RequestsPerTimeUnit {
// The desired number of requests per :ref:`time_unit
// <envoy_v3_api_field_type.v3.RateLimitStrategy.RequestsPerTimeUnit.time_unit>` to allow.
// If set to ``0``, deny all (equivalent to ``BlanketRule.DENY_ALL``).
//
// .. note::
// Note that the algorithm implementation determines the course of action for the requests
// over the limit. As long as the ``requests_per_time_unit`` converges on the desired value,
// it's allowed to treat this field as a soft-limit: allow bursts, redistribute the allowance
// over time, etc.
//
uint64 requests_per_time_unit = 1;

// The unit of time. Ignored when :ref:`requests_per_time_unit
// <envoy_v3_api_field_type.v3.RateLimitStrategy.RequestsPerTimeUnit.requests_per_time_unit>`
// is ``0`` (deny all).
RateLimitUnit time_unit = 2 [(validate.rules).enum = {defined_only: true}];
}

oneof strategy {
option (validate.required) = true;

// Allow or Deny the requests.
// If unset, allow all.
BlanketRule blanket_rule = 1 [(validate.rules).enum = {defined_only: true}];

// Best-effort limit of the number of requests per time unit, f.e. requests per second.
// Does not prescribe any specific rate limiting algorithm, see :ref:`RequestsPerTimeUnit
// <envoy_v3_api_msg_type.v3.RateLimitStrategy.RequestsPerTimeUnit>` for details.
RequestsPerTimeUnit requests_per_time_unit = 2;

// Limit the requests by consuming tokens from the Token Bucket.
// Allow the same number of requests as the number of tokens available in
// the token bucket.
TokenBucket token_bucket = 3;
}
}
Loading