{{announcement.body}}
{{announcement.title}}

HTTP Throttling Using Lyft Global Ratelimiting

DZone 's Guide to

HTTP Throttling Using Lyft Global Ratelimiting

The aim for this blog is help you get started with the rate-limiting service and configure various combinations of rate-limiting scenarios.

· DevOps Zone ·
Free Resource

Sometime ago, for a project of mine, I was looking for a good rate-limiting service. For the scope of that project, the service would run along a front proxy and would rate-limit requests to third-party applications.

Nginx Plus and Kong certainly have rate-limiting features but are not OSS; while I am a bigger fan of OSS. Using Istio service mesh would have been a overkill. Therefore, I decided to use Envoy Proxy + Lyft Ratelimiting.

The aim for this blog is help you get started with the rate-limiting service and configure various combinations of rate-limiting scenarios.

Let’s dive in…

Understanding Lyft Ratelimiter

Ratelimit configuration consists of

  1. Domain: A domain is a container for a set of rate limits. All domains known to the Ratelimit service must be globally unique. They serve as a way for different teams/projects to have rate limit configurations that don’t conflict.
  2. Descriptor: A descriptor is a list of key/value pairs owned by a domain that the Ratelimit service uses to select the correct rate limit. Descriptors are case-sensitive. Examples of descriptors are:
    • (“database”, “users”)
    • (“message_type”, “marketing”),(“to_number”,”2061234567")
    • (“to_cluster”, “service_a”)
    • (“to_cluster”, “service_a”),(“from_cluster”, “service_b”)

Descriptors can also be nested to achieve more complex rate-limiting scenarios.

We will be performing rate limiting based on various HTTP headers. Let’s have a look at the configuration file.

Shell


In the configuration above, it can be clearly seen

  1. There are various different keys with different ratelimit values.
  2. We can use them globally for the entire vhost in envoy or even locally for a particular path.
  3. We can also have nested values of descriptors.

Envoy Front Proxy

Let’s see how we can use them in the envoy config now. Have a look at the configuration below:

Shell


Here's is how it works:

  1. We have defined a single vhost named nginx which matches all domains.
  2. There is global rate-limit defined for this vhost. The descriptor value is global.
  3. Next, we have 2 clusters under this vhost. Namely, nginx1 and nginx2. Routes for path /nginx1 are routed to nginx1 cluster and similarly for nginx2.
  4. For nginx1, there is generic rate limit defined by descriptor value local and then we have rate-limits for different values of standard HTTP headers such as method, path etc., and some custom HTTP headers such as X-CustomHeader.
  5. We have similar rate-limiting set for nginx2 cluster.
  6. These 2 nginx clusters defined here actually refer to 2 different nginx containers running as part of docker-compose stack.

The overall architecture can be visualized as:

Simulation Architecture

Simulation Architecture

All configuration files for this set-up can be found here. Since they are running on the same network as the ratelimiter and envoy proxy, they can be accessed easily using the container name.

In order to run the set-up just clone the repo and do

Shell


Once the stack is up, you will have

  1. 2 nginx containers running on port 9090 and 9091 of localhost.
  2. Envoy proxy to intercept and relay requests to nginx servers. Envoy admin console can be reached at localhost:9901.
  3. Envoy will be listening as localhost:10000.
  4. Ratelimiting service container with configured rate limits which will be used to envoy.
  5. Redis container which is used by the Ratelimiting service.

It is important to understand that all the applicable actions for a particular path in a cluster are aggregated by the ratelimiter for the result i.e.,

Logical OR of all the applicable limits

Let’s Test Our Setup

Firstly we need to install Vegeta, a load testing framework. It can be done by

Shell


Test Scenario 1:
Case: GET request on /nginx_1/ at 100 requests per second
Expected Result: 10% requests successful. ( Logical OR of "descriptor_value": "global" , "descriptor_value": "local" and "descriptor_value": "get" )
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
Actual Result

Shell

— — —

Test Scenario 2:
Case:POST request on /nginx_1/ at 100 requests per second.
ExpectedResult: 50% requests successful. ( Logical OR of "descriptor_value": "global" and "descriptor_value": "local" )
Command: echo "POST http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 | vegeta report
ActualResult:

Shell

— — —

Test Scenario 3:
Case: GET request on /nginx_2/ at 100 requests per second with X-MyHeader: 123
Expected Result: 5% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "123", and "descriptor_value": "path")
Command: echo "GET http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header "X-MyHeader: 123" | vegeta report
Actual Result:

Shell

— — —

Test Scenario 4:
Case: POST request on /nginx_2/ at 100 requests per second with X-MyHeader: 456
Expected Result: 5% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "post", "descriptor_value": "456", and "descriptor_value": "path")
Command: echo "POST http://localhost:10000/nginx_2/" | vegeta attack -rate=100 -duration=0 -header "X-MyHeader: 456" | vegeta report
Actual Result:

Shell

— — —

Test Scenario 5:
Case: GET request on /nginx_1/ at 100 requests per second with X-CustomHeader: XYZ and X-CustomPlan: PLUS
Expected Result: 3% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "get", "descriptor_key": "custom_header", and "descriptor_key": "plan")
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header "X-CustomHeader: XYZ" -header "X-CustomPlan: PLUS" | vegeta report
Actual Result:

Shell

— — —

Test Scenario 6:
Case: GET request on /nginx_1/ at 100 requests per second with X-Header: XYZ and X-CustomPlan: PLUS
Expected Result: 10% requests successful ( Logical OR of "descriptor_value": "global", "descriptor_value": "local", "descriptor_value": "get")
Command: echo "GET http://localhost:10000/nginx_1/" | vegeta attack -rate=100 -duration=0 -header "X-Header: XYZ" -header "X-CustomPlan: PLUS" | vegeta report
Actual Result:

Shell


Scalability

In a production scenario, you might want to run multiple instances of your proxy which can refer to the same ratelimit cluster. The front proxy is basically stateless.

As far as the ratelimiting service is concerned, I would recommend scaling it horizontally and moving out Redis Cache to a cloud-based service like RedisLabs or AWS Elastic-cache.

Also, using separate Redis for Per Second Limits is highly recommended. All you need to do is:

  1. Set the env var, REDIS_PERSECOND: "true"
  2. Set Redis endpoint, REDIS_PERSECOND_URL

Conclusion

We can clearly see that the actual results are pretty close to expected results. We can accomplish all kinds of complex rate limiting scenarios using this and perform request throttling. Feel free to reach out should you have any questions around this.

Topics:
istio, microservice, ratelimiter, service mesh, tutorial

Published at DZone with permission of Sudip Sengupta . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}