Giter Club home page Giter Club logo

nginx-lua-redis-rate-measuring's Introduction

Build Status license Coverage Status

Resty Redis Rate

A Lua library providing rate measurement using nginx + Redis. This lib was inspired on Cloudflare's post How we built rate limiting capable of scaling to millions of domains.

You can found more about why and when this library was created here..

Use case: distributed throttling

Nginx has already a rate limiting feature but it is restricted by the local node. Once you have more than one server behind a load balancer this won't work as expected, so you can use redis as a distributed storage to keep the rating data.

local redis_client = redis_cluster:new(config)
-- let's say we'll use the ?token=<value> as the key to rate limit
local rate, err = redis_rate.measure(redis_client, ngx.var.arg_token)
if err then
    ngx.log(ngx.ERR, "err: ", err)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

-- once we hit more than 10 reqs/m we'll reply 403
if rate > 10 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
end

ngx.say(rate)

Tests result

We ran three different experiments constrained by a rate limit of 10 req/minute:

  1. Experiment1: 1 reqs/second
  2. Experiment2: 1/6 reqs/second
  3. Experiment3: 1/5 reqs/second

nginx redis throttling exprimentes graph result

All the data points above the rate limit (the red line) resulted in forbidden responses.

You can run the throttling example locally, open up a terminal tab to run the servers.

Make sure you have docker and docker-compose installed.

make up

Open another terminal tab and perform the experiments:

# Experiment 1
for i in {1..120}; do curl "http://localhost:8080/lua_content?token=Experiment1" && sleep 1; done

# Experiment 2
for i in {1..20}; do curl "http://localhost:8080/lua_content?token=Experiment2" && sleep 6; done

# Experiment 3
for i in {1..24}; do curl "http://localhost:8080/lua_content?token=Experiment3" && sleep 5; done

Pipeline and hash tag

We're using the combination of pipeline and hash tag to perform all the commands in a single connection to redis cluster. You can see the tcpdump output showing the three-way handshake followed by the three commands requests $get, $inc and $expire and the redis response.

22:20:10.515457 IP (tos 0x0, ttl 64, id 38199, offset 0, flags [DF], proto TCP (6), length 60)
    172.31.0.3.49824 > 172.31.0.2.7000: Flags [S], cksum 0x5872 (incorrect -> 0xb9b9), seq 1010830934, win 29200, options [mss 1460,sackOK,TS val 170849 ecr 0,nop,wscale 7], length 0

22:20:10.515505 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.31.0.2.7000 > 172.31.0.3.49824: Flags [S.], cksum 0x5872 (incorrect -> 0xfcda), seq 1496303914, ack 1010830935, win 28960, options [mss 1460,sackOK,TS val 170849 ecr 170849,nop,wscale 7], length 0

22:20:10.515518 IP (tos 0x0, ttl 64, id 38200, offset 0, flags [DF], proto TCP (6), length 52)
    172.31.0.3.49824 > 172.31.0.2.7000: Flags [.], cksum 0x586a (incorrect -> 0x9be2), seq 1, ack 1, win 229, options [nop,nop,TS val 170849 ecr 170849], length 0

22:20:10.515648 IP (tos 0x0, ttl 64, id 38201, offset 0, flags [DF], proto TCP (6), length 212)
    172.31.0.3.49824 > 172.31.0.2.7000: Flags [P.], cksum 0x590a (incorrect -> 0x7954), seq 1:161, ack 1, win 229, options [nop,nop,TS val 170849 ecr 170849], length 160
	0x0000:  4500 00d4 9539 4000 4006 4ca7 ac1f 0003  E....9@[email protected].....
	0x0010:  ac1f 0002 c2a0 1b58 3c40 0e57 592f c92b  .......X<@.WY/.+
	0x0020:  8018 00e5 590a 0000 0101 080a 0002 9b61  ....Y..........a
	0x0030:  0002 9b61 2a32 0d0a 2433 0d0a 6765 740d  ...a*2..$3..get.
	0x0040:  0a24 3239 0d0a 6e67 785f 7261 7465 5f6d  .$29..ngx_rate_m
	0x0050:  6561 7375 7269 6e67 5f7b 6c75 6967 697d  easuring_{luigi}
	0x0060:  5f31 390d 0a2a 320d 0a24 340d 0a69 6e63  _19..*2..$4..inc
	0x0070:  720d 0a24 3239 0d0a 6e67 785f 7261 7465  r..$29..ngx_rate
	0x0080:  5f6d 6561 7375 7269 6e67 5f7b 6c75 6967  _measuring_{luig
	0x0090:  697d 5f32 300d 0a2a 330d 0a24 360d 0a65  i}_20..*3..$6..e
	0x00a0:  7870 6972 650d 0a24 3239 0d0a 6e67 785f  xpire..$29..ngx_
	0x00b0:  7261 7465 5f6d 6561 7375 7269 6e67 5f7b  rate_measuring_{
	0x00c0:  6c75 6967 697d 5f32 300d 0a24 330d 0a31  luigi}_20..$3..1
	0x00d0:  3230 0d0a                                20..
22:20:10.517337 IP (tos 0x0, ttl 64, id 21067, offset 0, flags [DF], proto TCP (6), length 65)
    172.31.0.2.7000 > 172.31.0.3.49824: Flags [P.], cksum 0x5877 (incorrect -> 0xc55e), seq 1:14, ack 161, win 235, options [nop,nop,TS val 170849 ecr 170849], length 13
	0x0000:  4500 0041 524b 4000 4006 9028 ac1f 0002  E..ARK@.@..(....
	0x0010:  ac1f 0003 1b58 c2a0 592f c92b 3c40 0ef7  .....X..Y/.+<@..
	0x0020:  8018 00eb 5877 0000 0101 080a 0002 9b61  ....Xw.........a
	0x0030:  0002 9b61 242d 310d 0a3a 310d 0a3a 310d  ...a$-1..:1..:1.
	0x0040:  0a                                       .

nginx-lua-redis-rate-measuring's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

nginx-lua-redis-rate-measuring's Issues

To use pipeline

Although it does not provide MULTI it works with pipeline:

red_c:init_pipeline()
red_c:get("name")
red_c:get("name1")
red_c:get("name2")

local res, err = red_c:commit_pipeline()

attempt to index local 'slot_item' (a nil value)

Hi
I have setup everything as described.
I call the website f.ex. like this: www.foo.de/ratechecker?token=foo
Inside nginx i have defined the location /ratechecker with redis like your example:

        location /ratechecker {
          default_type 'text/plain';
          content_by_lua_block {
            local redis_client = redis_cluster:new(config)
            local rate, err = redis_rate.measure(redis_client, ngx.var.arg_token)
            if err then
              ngx.log(ngx.ERR, "err: ", err)
              ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            end
            if rate > 10 then
              ngx.exit(ngx.HTTP_FORBIDDEN)
            end
            ngx.say(rate)
          }
        }

Now I get the error

019/05/29 16:42:34 [error] 10383#10383: *26 lua entry thread aborted: runtime error: /usr/local/openresty/lualib/resty-redis-cluster.lua:506: attempt to index local 'slot_item' (a nil value)
stack traceback:
coroutine 0:
        /usr/local/openresty/lualib/resty-redis-cluster.lua: in function 'commit_pipeline'
        /usr/local/openresty/lualib/resty-redis-rate.lua:19: in function 'measure'
        content_by_lua(nginx.conf:160):3: in main chunk, client: 10.51.21.110, server: foo.de, request: "GET /ratechecker?token=foo HTTP/1.1", host: "foo.de:8111"

Line 506 is this last line of this code passage:

        _reqs[i].origin_index = i
        local key = _reqs[i].key
        local slot = redis_slot(tostring(key))
        local slot_item = slots[slot]

        local ip, port, slave, err = pick_node(self, slot_item.serv_list, slot, magicRandomPickupSeed)

Between the empty line I have inserted this:

        ngx.say("ok " .. slot .. " ok " .. key)
        ngx.exit(200)

It outputs:
ok 12182 ok ngx_rate_measuring_{foo}_20
But slot_item is nil...

Can someone help me?
Whats wrong?

Error when running local test

Hi! ๐Ÿ‘‹

Thanks for the project. I'm trying to test things locally, but am receiving failed to load the resty.core module when running make up

โฏ  make up
docker-compose down -v
WARNING: The COVERALLS_REPO_TOKEN variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_JOB_ID variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_BRANCH variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_REPO_SLUG variable is not set. Defaulting to a blank string.
Stopping nginx-lua-redis-rate-measuring_redis_cluster_1 ... done
Removing nginx-lua-redis-rate-measuring_nginx_1         ... done
Removing nginx-lua-redis-rate-measuring_redis_cluster_1 ... done
Removing network nginx-lua-redis-rate-measuring_default
docker-compose up nginx
WARNING: The COVERALLS_REPO_TOKEN variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_JOB_ID variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_BRANCH variable is not set. Defaulting to a blank string.
WARNING: The TRAVIS_REPO_SLUG variable is not set. Defaulting to a blank string.
Creating network "nginx-lua-redis-rate-measuring_default" with the default driver
Creating nginx-lua-redis-rate-measuring_redis_cluster_1 ... done
Creating nginx-lua-redis-rate-measuring_nginx_1         ... done
Attaching to nginx-lua-redis-rate-measuring_nginx_1
nginx_1          | 2019/08/02 17:10:23 [error] 1#1: lua_load_resty_core failed to load the resty.core module from https://github.com/openresty/lua-resty-core; ensure you are using an OpenResty release from https://openresty.org/en/download.html (rc: 2, reason: module 'resty.core' not found:
nginx_1          | 	no field package.preload['resty.core']
nginx_1          | 	no file '/usr/local/openresty/luajit/share/lua/5.1/resty/core.lua'
nginx_1          | 	no file '/lua/src/resty/core.lua'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty/core.so'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty.so')
nginx_1          | nginx: [error] lua_load_resty_core failed to load the resty.core module from https://github.com/openresty/lua-resty-core; ensure you are using an OpenResty release from https://openresty.org/en/download.html (rc: 2, reason: module 'resty.core' not found:
nginx_1          | 	no field package.preload['resty.core']
nginx_1          | 	no file '/usr/local/openresty/luajit/share/lua/5.1/resty/core.lua'
nginx_1          | 	no file '/lua/src/resty/core.lua'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty/core.so'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty.so')
nginx_1          | 2019/08/02 17:10:23 [error] 1#1: init_by_lua error: /usr/local/openresty/luajit/share/lua/5.1/resty/lock.lua:4: module 'resty.core.shdict' not found:
nginx_1          | 	no field package.preload['resty.core.shdict']
nginx_1          | 	no file '/usr/local/openresty/luajit/share/lua/5.1/resty/core/shdict.lua'
nginx_1          | 	no file '/lua/src/resty/core/shdict.lua'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty/core/shdict.so'
nginx_1          | 	no file '/usr/local/openresty/luajit/lib/lua/5.1/resty.so'
nginx_1          | stack traceback:
nginx_1          | 	[C]: in function 'require'
nginx_1          | 	/usr/local/openresty/luajit/share/lua/5.1/resty/lock.lua:4: in main chunk
nginx_1          | 	[C]: in function 'require'
nginx_1          | 	...l/openresty/luajit/share/lua/5.1/resty-redis-cluster.lua:3: in main chunk
nginx_1          | 	[C]: in function 'require'
nginx_1          | 	init_by_lua:13: in main chunk
nginx-lua-redis-rate-measuring_nginx_1 exited with code 1

Any ideas? Thanks!

Add unit tests

  • rate
  • rate with no previous key (ngx.null)
  • rate with previsou key
  • logic for error handling (when redis commit pipeline doesn't work)
  • logic for current counter (local current_counter = tonumber(resp[2]) - 1)
  • logic for expire drift
  • logic for past minute

To use hash tags

To force a single token (with its two key counters pre_token_current_minute and pre_token_past_minute) to belongs to a single host slot.

General improvements

Shorten the key_prefix

Let's say we want to count 200K users or tokens, it implies the double of the counters (past and current minute) multiply that by 18 (key_prefix length), maybe we can use something like nrm.

  • (4e5 * 18)/1024^2 6MB vs 1MB (4e5 * 3)/1024^2

Uncouple ngx from measure function

It can be useful outside of nginx, we can change the function signature to also receive the current_time in seconds.

Generate docs

http://stevedonovan.github.io/ldoc/

How i can you with docker-gen

I try use this with docker-gen https://github.com/jwilder/docker-gen

version: '3'
services:
  nginx-web:
    build:
      context: .
    labels:
        com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
    container_name: ${NGINX_WEB:-nginx-web}
    restart: always
    ports:
      - "${IP:-0.0.0.0}:${DOCKER_HTTP:-80}:80"
      - "${IP:-0.0.0.0}:${DOCKER_HTTPS:-443}:443"
    volumes:
      - ${NGINX_FILES_PATH:-./data}/conf.d:/etc/nginx/conf.d
      - ${NGINX_FILES_PATH:-./data}/vhost.d:/usr/local/openresty/nginx/vhost.d
      - ${NGINX_FILES_PATH:-./data}/html:/usr/share/nginx/html
      - ${NGINX_FILES_PATH:-./data}/certs:/usr/local/openresty/nginx/certs:ro
      - ${NGINX_FILES_PATH:-./data}/htpasswd:/usr/local/openresty/nginx/htpasswd:ro
      - ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      - ./lua:/lua/src
    logging:
      driver: ${NGINX_WEB_LOG_DRIVER:-json-file}
      options:
        max-size: ${NGINX_WEB_LOG_MAX_SIZE:-4m}
        max-file: ${NGINX_WEB_LOG_MAX_FILE:-10}
    depends_on:
      - redis_cluster
    links:
      - redis_cluster

  nginx-gen:
    image: jwilder/docker-gen
    command: -notify-sighup ${NGINX_WEB:-nginx-web} -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    container_name: ${DOCKER_GEN:-nginx-gen}
    restart: always
    environment:
      SSL_POLICY: ${SSL_POLICY:-Mozilla-Intermediate}
    volumes:
      - ${NGINX_FILES_PATH:-./data}/conf.d:/etc/nginx/conf.d
      - ${NGINX_FILES_PATH:-./data}/vhost.d:/etc/nginx/vhost.d
      - ${NGINX_FILES_PATH:-./data}/html:/usr/share/nginx/html
      - ${NGINX_FILES_PATH:-./data}/certs:/etc/nginx/certs:ro
      - ${NGINX_FILES_PATH:-./data}/htpasswd:/etc/nginx/htpasswd:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
    logging:
      driver: ${NGINX_GEN_LOG_DRIVER:-json-file}
      options:
        max-size: ${NGINX_GEN_LOG_MAX_SIZE:-2m}
        max-file: ${NGINX_GEN_LOG_MAX_FILE:-10}

But all domain alway response to default openresty
Any body have any solution ?

Bug of last minute equals to -1

Our current implementation works but it has a silly bug.

-- at current_minute 0 the past_minute becames -1
local current_minute = math.floor(current_time / 60) % 60
local past_minute = current_minute - 1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.