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

chore(*) introduce rate limiter #2083

Merged
merged 33 commits into from
Jun 10, 2021
Merged

Conversation

nickolaev
Copy link
Contributor

@nickolaev nickolaev commented Jun 2, 2021

Summary

Adding the RateLimit policy as per #2039

  • only HTTP rate limites
  • no support for ExternalServices

Documentation

Testing

  • Unit tests
  • E2E tests
  • Manual testing on Universal
  • Manual testing on Kubernetes

@nickolaev nickolaev mentioned this pull request Jun 3, 2021
@nickolaev nickolaev changed the title Chore/introduce rate limiter chore(*) introduce rate limiter Jun 3, 2021
Nikolay Nikolaev added 5 commits June 4, 2021 16:59
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>
@nickolaev nickolaev force-pushed the chore/introduce_rate_limiter branch from 6476c26 to e0bc922 Compare June 4, 2021 13:59
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
@nickolaev nickolaev marked this pull request as ready for review June 7, 2021 07:07
@nickolaev nickolaev requested a review from a team as a code owner June 7, 2021 07:07
Nikolay Nikolaev added 2 commits June 7, 2021 10:19
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
api/mesh/v1alpha1/rate_limit.proto Show resolved Hide resolved
api/mesh/v1alpha1/rate_limit.proto Outdated Show resolved Hide resolved
pkg/core/ratelimits/matcher.go Outdated Show resolved Hide resolved
pkg/core/ratelimits/matcher.go Outdated Show resolved Hide resolved
pkg/core/ratelimits/matcher.go Outdated Show resolved Hide resolved
)

var _ = Describe("RateLimit", func() {
Describe("Validate()", func() {
Copy link
Contributor

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() {
Copy link
Contributor

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.

test/e2e/ratelimit/ratelimit_universal.go Outdated Show resolved Hide resolved
kuma.io/service: "demo-client"
destinations:
- match:
kuma.io/service: echo-server_kuma-test_svc_8080
Copy link
Contributor

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
Copy link
Contributor

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

  1. envoy_common.Route has field RateLimit *mesh_proto.RateLimit. It is "higher level" object, IMHO TypedPerFilterConfig is just too low level at this point
  2. HttpInboundRouteConfigurer takes Routes, not Route and it does not take RateLimit as an arguments
  3. When calling HttpInboundRoute (refactor to HttpInboundRoutes), first - build a Routes based on all the policies (in our case it's only RateLimit so something like func BuildInboundRoutes(rateLimit) Routes).
  4. Create a RateLimitConfigurer that configures envoy_hcm.HttpConnectionManager with envoy.filters.http.local_ratelimit
  5. In RoutesConfigurer check if Route has RateLimit, 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 on Route 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())
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Nikolay Nikolaev added 10 commits June 7, 2021 12:39
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>
@tharun208
Copy link
Contributor

tharun208 commented Jun 8, 2021

@nickolaev also can you update the tools/docs/kumactl/gen_help.sh ?

- name: envoy.filters.http.router
statPrefix: stats`,
}),
Entry("2 policy selectors", testCase{
Copy link
Contributor

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,
},
Copy link
Contributor

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) {
Copy link
Contributor

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.

pkg/xds/generator/inbound_proxy_generator.go Show resolved Hide resolved
}
}
return true
}, "60s", "5s").Should(BeTrue())
Copy link
Contributor

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())

Nikolay Nikolaev added 3 commits June 8, 2021 15:54
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>
@nickolaev nickolaev force-pushed the chore/introduce_rate_limiter branch from 2971628 to 3c5e2bb Compare June 9, 2021 10:04
Nikolay Nikolaev added 9 commits June 9, 2021 17:51
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
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// source?

Copy link
Contributor

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))
Copy link
Contributor

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?

Copy link
Contributor

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>
test/e2e/ratelimit/ratelimit_universal.go Outdated Show resolved Hide resolved
pkg/core/ratelimits/matcher.go Outdated Show resolved Hide resolved
pkg/core/policy/connection_matcher.go Outdated Show resolved Hide resolved
})

E2EAfterSuite(func() {
if ShouldSkipCleanup() {
Copy link
Contributor

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() {
Copy link
Contributor

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

Copy link
Contributor Author

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.

api/mesh/v1alpha1/rate_limit.proto Show resolved Hide resolved
api/mesh/v1alpha1/rate_limit.proto Outdated Show resolved Hide resolved
Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
@nickolaev nickolaev merged commit 31f79e0 into master Jun 10, 2021
@nickolaev nickolaev deleted the chore/introduce_rate_limiter branch June 10, 2021 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants