-
Notifications
You must be signed in to change notification settings - Fork 327
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
chore(*) introduce rate limiter #2083
Conversation
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
6476c26
to
e0bc922
Compare
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
) | ||
|
||
var _ = Describe("RateLimit", func() { | ||
Describe("Validate()", func() { |
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.
I'd like to have examples of both "positive" and "negative" test cases
value: "true" | ||
` | ||
|
||
BeforeEach(func() { |
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.
Please write this test with BeforeSuite / AfterSuite. We already learned that writing it this way is a huge win for test performance and writing new test cases.
kuma.io/service: "demo-client" | ||
destinations: | ||
- match: | ||
kuma.io/service: echo-server_kuma-test_svc_8080 |
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.
would be great to test many tags in sources in this test or another
Route envoy_common.Route | ||
Service string | ||
Route envoy_common.Route | ||
RateLimit *mesh_proto.RateLimit |
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.
Ok, I feel like here we are doing mix of a lot of things. The whole point of having configureres is a separation of concerns while building Envoy config. Here what I would do
envoy_common.Route
has fieldRateLimit *mesh_proto.RateLimit
. It is "higher level" object, IMHOTypedPerFilterConfig
is just too low level at this pointHttpInboundRouteConfigurer
takesRoutes
, notRoute
and it does not takeRateLimit
as an arguments- When calling
HttpInboundRoute
(refactor toHttpInboundRoutes
), first - build aRoutes
based on all the policies (in our case it's onlyRateLimit
so something likefunc BuildInboundRoutes(rateLimit) Routes
). - Create a
RateLimitConfigurer
that configuresenvoy_hcm.HttpConnectionManager
withenvoy.filters.http.local_ratelimit
- In
RoutesConfigurer
check ifRoute
hasRateLimit
, if so, then build a config for this route.
Here I'd consider refactoring RoutesConfigurer into Configurer pattern so you can configure individual elements on the Route, but we can skip it for now and do it if we were ever to configure more stuff.
The result of this is:
HttpInboundRoute
has 0 knowledge about RateLimit - separation of concerns- You now have
RateLimitConfigurer
and an easy way to add RateLimit onRoute
so you can much easier reuse this on the outbound side
Eventually(func() bool { | ||
_, _, err := cluster.Exec("", "", "demo-client", "curl", "-v", "--fail", "echo-server_kuma-test_svc_8080.mesh") | ||
return err == nil | ||
}, "30s", "3s").Should(BeTrue()) |
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.
You cannot assume that this policy is automatically applied in 0ms latency. I think this test will be flaky.
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.
That is the point of Eventually, to make sure that in 30s we'll get at least one pass.
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.
I might misread the code, but I think the flow right now is like this with the second It()
- we apply
rateLimitPolicy
in BeforeSuite - You apply new policy
- We now have this eventually block that immediately will pass regardless if new policy is propagated to envoys or not
- You go ahead and do the X requests. It might be the case that those requests are still using the old policy
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
…e_limiter # Conflicts: # pkg/xds/envoy/route.go # pkg/xds/envoy/routes/v2/routes_configurer.go # pkg/xds/envoy/routes/v3/routes_configurer.go Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
…e_limiter # Conflicts: # pkg/xds/envoy/listeners/configurers.go # pkg/xds/envoy/listeners/v2/http_inbound_routes_configurer.go # pkg/xds/envoy/routes/v2/routes_configurer.go Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
@nickolaev also can you update the |
- name: envoy.filters.http.router | ||
statPrefix: stats`, | ||
}), | ||
Entry("2 policy selectors", testCase{ |
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.
I think this test case is no longer valuable.
MaxTokens: rlHttp.GetRequests().GetValue(), | ||
TokensPerFill: &wrappers.UInt32Value{ | ||
Value: 1, | ||
}, |
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.
I don't think this is correct. Let's say you have limit of 10 requests per 5 second.
You first have 10 tokens. Then you send 10 requests in 1s and you have 0 tokens. Then you fill 1 token per 5 seconds allowing 1req/5s.
Additionaly, I think E2E does not catch this because we are only using initial max tokens.
The Value should be GetRequests().GetValue
.
I might be wrong though, let me know.
return typedPerFilterConfig, nil | ||
} | ||
|
||
func (c *RoutesConfigurer) createRateLimit(route *envoy_common.Route) (*any.Any, error) { |
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.
optional nit: you don't need the whole route here, I'd pass *RateLimit_Conf_Http
.
It helps me to read the code if the scope is limited, here I had to double-check what else you are using from the Route.
} | ||
} | ||
return true | ||
}, "60s", "5s").Should(BeTrue()) |
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.
Let's check if rate limiting is consistently applied
requestsRateLimitted := func() bool...
Eventually(requestsRateLimitted, "60s", "6s").Should(BeTrue())
time.Wait(6s)
Expect(requestsRateLimitted).To(BeTrue())
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
2971628
to
3c5e2bb
Compare
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
…e_limiter # Conflicts: # pkg/xds/sync/components.go Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
|
||
return false | ||
return tagSelectorI.Rank().CompareTo(tagSelectorJ.Rank()) > 0 |
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.
One more thing here. If ranks are equal, we need to compare GetCreationTime()
@@ -54,3 +56,21 @@ func BuildRateLimitMap( | |||
} | |||
return result, nil | |||
} | |||
|
|||
func splitPoliciesBySourceMatch(rateLimits []*mesh_core.RateLimitResource) []*mesh_core.RateLimitResource { |
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.
would be nice to have a comment why we are doing this
for _, rateLimit := range rateLimits { | ||
if rateLimit.GetConf().GetHttp() != nil { | ||
route := envoy_common.NewRouteFromCluster(cluster) | ||
// Source |
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.
// source
?
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.
I'd leave a comment somewhere around this function that we rely on the fact that rateLimits are in order from most specific to most general
|
||
It("should limit to 2 requests per 5 sec", func() { | ||
Eventually(verifyRateLimit("demo-client", 5), "60s", "5s").Should(Equal(2)) | ||
Eventually(verifyRateLimit("demo-client", 5), "30s", "1s").Should(Equal(2)) |
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 there a reason for intervals to be different?
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.
(also leave a comment that we check a second time to see if the limit is deterministic)
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
}) | ||
|
||
E2EAfterSuite(func() { | ||
if ShouldSkipCleanup() { |
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.
if ShouldSkipCleanup() {
is in E2EAfterSuite
no need to do that again
Eventually(verifyRateLimit("web", 5), "30s", "1s").Should(Equal(2)) | ||
}) | ||
|
||
It("should limit multiple source", func() { |
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.
are It()
executed in the order defined in the code? I hope so, otherwise will fail. In TrafficPermission and TrafficRoute test we clean resources in AfterEach
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.
I see, well form my experience they are run in the order they appear.
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
Summary
Adding the
RateLimit
policy as per #2039Documentation
Testing