ABC omezení rychlosti serverů ExpressJS s Docker + Redis

V tomto tutoriálu využijeme sílu Dockeru ke spuštění instance Redis, která dokáže sledovat omezení rychlosti v jednoduché aplikaci ExpressJS, aby vám poskytla všechny informace o tom, jak to sami nastavit lokálně.

Pro tento výukový program musí být nainstalovány Docker a Redis, ale předchozí znalosti Dockeru a Redis nejsou vyžadovány (ani ExpressJS – můžeme to udělat!). Očekávají se také obvyklí podezřelí Nodejs.

Pokud jste nenainstalovali, můžete postupovat podle pokynů, jak to provést na webu Docker v rychlém startu Redis.

Nemáte dostatek času/péče? Podívejte se na dokončený projekt zde.

Nastavení Dockeru

Pojďme to nejprve odstranit z cesty! Chceme stáhnout obraz Redis a spustit jej s přesměrováním portů.

docker pull redis
docker run --name redis-test -p 6000:6379 -d redis
docker ps

Zde stahujeme obrázek, začínáme jej názvem „redis-test“ a předáváme výchozí port Redis 6379 na 6000. Děláme to, abychom zabránili kolizi s jinou instancí Redis, která může být spuštěna.

Spuštěno docker ps by měl ukázat něco podobného následujícímu:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
00fcae665347        redis               "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds        0.0.0.0:6000->6379/tcp   redis-test

Šťastné dny! Pojďme vpřed.

Nastavení projektu

Vytvoříme složku projektu, nainstalujeme všechny balíčky a pustíme se do práce.

mkdir express-redis-rate-limiting
cd express-redis-rate-limiting
yarn init -y
yarn add express express-rate-limit rate-limit-redis redis
yarn add --dev execa jest

Balíček express-rate-limit je způsob, jakým zavedeme omezení rychlosti, zatímco rate-limit-redis a redis nám umožní rozšířit možnosti omezení rychlosti, které budou použity na Redis a nebudou uloženy v paměti. Více o tom později.

Instalujeme vývojářské závislosti execa a jest pro testovací účely. Použijeme je jako svého druhu pomocníka pro kontrolu omezení rychlosti z CLI.

Nastavení expresního serveru

Přidejte to k index.js soubor v kořenovém adresáři projektu:

const express = require("express")
const app = express()
const port = 8080

app.get("/", (_, res) => res.send("RESPONSE_SUCCESS"))

app.listen(port, () => console.log("Server started"))

Toto je super základní aplikace Express, která má pouze požadavek GET na trase / .

Z terminálu spusťte node index.js a měli byste vidět server started .

Z jiného terminálu spusťte curl localhost:8000 a měli byste vidět naše RESPONSE_SUCCESS příkaz. Perfektní!

Přidáváme test, aby nám pomohl

Než se pustíme do omezování rychlosti, nastavíme test, který nám pomůže snadno zadat spoustu požadavků.

Nejprve v package.json , ujistěte se, že vaše vlastnost "scripts" vypadá takto:

"scripts": {
    "start": "node index.js",
    "test": "jest"
}

Dále vytvořte soubor __tests__/index.test.js a přidejte následující:

const execa = require("execa")

describe("rate limiter server", () => {
  // note: this will only succeed once in the 15min window designated
  test('expects GET / to return "RESPONSE_SUCCESS" the maximum number of times (100)', async () => {
    const { stdout } = await execa("ab", [
      "-n",
      "200",
      "-v",
      "3",
      "http://localhost:8080/",
    ])

    // expect only 100 successful responses
    const matches = stdout.match(/RESPONSE_SUCCESS/g)
    expect(matches.length).toEqual(100)
  })

  test("expects rate limit response after too many requests", async () => {
    const { stdout } = await execa("ab", [
      "-n",
      "1",
      "-v",
      "3",
      "http://localhost:8080/",
    ])

    expect(
      /Too many requests, please try again later./g.test(stdout)
    ).toBeTruthy()
  })
})

tak co se tady děje? "Testovací" popisy by vám snad měly přesně říct, co chceme, aby se stalo:

  1. Očekává, že GET / vrátí „RESPONSE_SUCCESS“ maximální počet opakování (100).
  2. Očekává odezvu na omezení rychlosti po příliš mnoha žádostech.

Pokud jde o execa , co to tady dělá? Execa v podstatě jen vezme počáteční příkaz terminálu a pole jakýchkoliv dalších "slov", která chceme předat (pro nedostatek lepšího termínu), takže to, co v prvním testu spustíme, je ab -n 200 -v 3 http://localhost:8080/ . Co je tedy ab ?

Spuštěn man ab , můžeme vidět, že manuál nám říká, že ab je "nástroj pro benchmarking serveru Apache HTTP".

Při pohledu do manuálu vidíme příznak -n je počet požadavků, které je třeba provést pro srovnávací relaci a -v je úroveň výřečnosti, kde "3" a vyšší tiskne kódy odpovědí, varování a informace. Ipso facto tento příkaz odesílá požadavek na http://localhost:8080/ 200krát a s dalšími informacemi. Neato!

Execa vrátí to, co je přihlášeno do stdout , takže následuje kontrola, kolikrát ve výstupu odpovídáme RESPONSE_SUCCESS :

const matches = stdout.match(/RESPONSE_SUCCESS/g)
expect(matches.length).toEqual(100)

Použijeme to, abychom zajistili, že během období omezení rychlosti povolíme maximálně 100 úspěšných odpovědí.

První spuštění testu

Spusťte yarn test dostat Jest nahoru a jít. Měli byste vidět "2 selhalo" - uh oh. Co se tady děje?

$ jest
 FAIL  __tests__/index.test.js
  rate limiter server
    ✕ expects GET / to return "Success" (133 ms)
    ✕ expects rate limit response after too many requests (18 ms)

  ● rate limiter server › expects GET / to return "Success"

    expect(received).toEqual(expected) // deep equality

    Expected: 100
    Received: 200

      14 |     // expect only 100 successful responses
      15 |     const matches = stdout.match(/RESPONSE_SUCCESS/g);
    > 16 |     expect(matches.length).toEqual(100);
         |                            ^
      17 |   });
      18 |
      19 |   test('expects rate limit response after too many requests', async () => {

      at Object.<anonymous> (__tests__/index.test.js:16:28)

  ● rate limiter server › expects rate limit response after too many requests

    expect(received).toBeTruthy()

    Received: false

      28 |     expect(
      29 |       /Too many requests, please try again later./g.test(stdout),
    > 30 |     ).toBeTruthy();
         |       ^
      31 |   });
      32 | });
      33 |

      at Object.<anonymous> (__tests__/index.test.js:30:7)

Test Suites: 1 failed, 1 total
Tests:       2 failed, 2 total
Snapshots:   0 total
Time:        1.366 s
Ran all test suites.
error Command failed with exit code 1.

U prvního testu jsme očekávali 100 případů RESPONSE_SUCCESS aby se objevilo, ne 200. Co se týče druhého, očekávali jsme, že se po dosažení limitu vrátí zpráva o příliš velkém počtu požadavků.

Otázka:Proč se to stalo?
Odpověď:Protože jsme nepřidali omezení rychlosti

Přidání omezení rychlosti paměti

Vraťte se na index.js a aktualizujte jej na následující:

const express = require("express")
const rateLimit = require("express-rate-limit")
const app = express()
const port = 8080

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes - only used for MemoryStore, ignored with RedisStore
  max: 100, // limit each IP to 100 requests per windowMs
})

// use limiter in the middleware
app.use(limiter)

app.get("/", (_, res) => res.send("RESPONSE_SUCCESS"))

app.listen(port, () => console.log("Server started"))

Zde přidáváme express-rate-limit knihovna. Na GitHubu je více informací o výchozích nastaveních, ale prozatím v podstatě říkáme, že „za 15 minut povolte, aby IP měla maximálně 100 požadavků“.

Znovu spusťte server pomocí yarn start a spusťte testy znovu s yarn test .

$ jest
 PASS  __tests__/index.test.js
  rate limiter server
    ✓ expects GET / to return "RESPONSE_SUCCESS" the maximum number of times (100) (188 ms)
    ✓ expects rate limit response after too many requests (18 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.254 s
Ran all test suites.
✨  Done in 9.21s.

Úspěch! Hurá!

Ale co se stane, když to spustíme znovu? Jeden test selže. Proč? Protože jsme již na limitu sazby, neočekáváme, že uvidíme 100 úspěšných požadavků! Řekl jsem, že tento test byl jen pomocník, že?

Zkusíme něco tady.

yarn start # In terminal one
yarn test # In terminal two - comes with a success
# restart the server again (within the 1 minute expiry) on terminal one
yarn start # In terminal one
yarn test # In terminal two

Počkej, teď uspějeme dvakrát? Co se stane s omezením sazby z našich 201 žádostí?

Bez dodání obchodu pro expresní omezovač rychlosti používáme úložiště v paměti. To znamená, že kdykoli se server vypne, ztratíme přehled o IP! Ještě horší je, že pokud máme nastavení s více servery, omezení rychlosti na jednom serveru nemusí nutně znamenat, že je omezeno na ostatní!

Redis to the Rescue

Aktualizujte index.js naposledy mít následující:

const express = require("express")
const rateLimit = require("express-rate-limit")
const RedisStore = require("rate-limit-redis")
const app = express()
const port = 8080

const limiter = rateLimit({
  store: new RedisStore({
    expiry: 60 * 15, // 15 minute expiring (in seconds)
    client: require("redis").createClient({
      // Exposing Docker port on 6000
      port: 6000,
    }),
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes - only used for MemoryStore, ignored with RedisStore
  max: 100, // limit each IP to 100 requests per windowMs
})

// use limiter in the middleware
app.use(limiter)

app.get("/", (_, res) => res.send("RESPONSE_SUCCESS"))

app.listen(port, () => console.log("Server started"))

S novým store konfigurace přidal omezovač rychlosti, nastavujeme RedisStore který nastavuje dobu expirace 15 minut a připojujeme se k portu 6000.

Znovu spusťte server a spusťte test znovu. Měli byste vidět stejný starý úspěch pro oba testy, které jsme viděli dříve. Tentokrát však běží Redis... takže zde můžeme udělat pár skvělých věcí.

V jiném terminálu spusťte redis-cli -p 6000 . To říká Redis CLI, aby se připojilo k databázi Redis na portu 6000.

Jakmile vstoupíte do Redis CLI, můžete spustit následující příkazy:

keys * # show all keys
# 1) "rl:::1" <- should be shown
get rl:::1
# "201"

Tak to je skvělé... nyní máme klíč, který ukládá hodnotu limitu rychlosti, a máme aktuální hodnotu 201!

Pokud zastavíme a restartujeme server, spustí se yarn test znovu uvidíme, že při prvním testu opět dojde k selhání, protože neměl 100 úspěšných odpovědí. Druhý test však prošel, takže musíme mít omezenou rychlost!

V rozhraní příkazového řádku Redis spusťte get rl:::1 znovu a uvidíte "402" jako počet požadavků, o které se tato IP pokusila v časovém limitu! Sladké vítězství!

Ve volné přírodě to nyní znamená, že instance Express, které připojují stejnou databázi Redis, se nyní mohou synchronizovat s limitem hodnot!

Závěr

Tím to ukončím, ale měli jsme velký úspěch.

Nezapomeňte poté své instance zrušit (při pohledu na váš Docker):

docker stop redis-test
docker rm redis-test

Jděte do toho a omezte ty otravné IP z vašich webů, které oceňují milé a milé psy, které vytváříte o víkendech, kamarádi.

Zdroje a další čtení

  1. Dokončený projekt
  2. Docker – Začínáme
  3. Rychlý start Redis
  4. execa
  5. rate-limit-redis
  6. expres-rate-limit
  7. Příkazy Redis
  8. DockerHub – Redis
  9. Express.js

Původně zveřejněno na mém blogu. Sledujte mě na Twitteru pro další skryté klenoty @dennisokeeffe92.