Zpět na domů

Optimalizace Fastify: Bloomovy filtry a škálovatelné backendy

Prozkoumejte praktická řešení pro vytvoření vysoce zatíženého backendu na Fastify: vlastní ID, Bloomovy filtry pro uživatele a obrázky, autorizace bez přihlášení, asynchronní zápisy do DB, ochrana API a typizace TypeScript.

Fastify Backend Optimization: Bloom Filters & Scalable Architectures
Advertisement 728x90

Optimalizace Fastify backendu pro vysoce zatížené aplikace: Praktická řešení

Vytváření škálovatelných a výkonných backendů vyžaduje hluboké porozumění architektonickým kompromisům. V tomto materiálu se podíváme na zkušenosti s vývojem vysoce zatížené aplikace na Fastify, se zaměřením na metody minimalizace zátěže databáze, efektivní identifikaci uživatelů bez tradiční autorizace a ochranu proti zneužívajícím požadavkům, s využitím nástrojů jako jsou Bloomovy filtry a asynchronní operace.

Strategie generování identifikátorů a efektivní správa dat

Při navrhování systémů, které pracují s velkým množstvím entit, jako jsou uživatelé nebo obrázky, se výběr formátu identifikátoru (ID) stává kriticky důležitým. Standardní GUID nebo UUID, ačkoli zajišťují jedinečnost, mohou být pro indexaci v databázích a zpracování na backendu nadměrně náročné na zdroje. Pro snížení zátěže jsme použili hybridní formát ID: 19bc19e00ab-eh5kkeh99q7. První část představuje časovou značku (Unix timestamp) převedenou do šestnáctkového formátu a druhá je náhodná sekvence znaků. Pomlčka slouží výhradně ke zlepšení čitelnosti.

Příklad funkce pro generování takového ID:

Google AdInline article slot
function generateID(): string {
  const timestamp = Date.now().toString(16);
  const randomPart = generateRandomSequence( 11 );
  return timestamp + '-' + randomPart;
}

Tento přístup zajišťuje „podmíněně inkrementální“ ID, jelikož se jeho počáteční část mění s každou milisekundou, což přispívá k efektivnější práci indexů v databázích. Další výhodou je možnost dekódování data vytvoření entity přímo z ID, což umožňuje upustit od samostatného pole created_at v tabulkách. To snižuje objem uložených dat a počet operací zápisu, ačkoli to klade malá omezení na pohodlí při vytváření dotazů podle data.

Škálování pomocí Bloomových filtrů a bezpřihlašovací autorizace

Vývoj aplikace navržené pro tisíce souběžných relací a desítky tisíc obrázků vyžaduje přístupy, které minimalizují spotřebu procesoru a paměti. Tradiční SQL dotazy pro kontrolu zobrazených obrázků nebo existujících uživatelů se stávají úzkým hrdlem. V tomto kontextu hrají neocenitelnou roli Bloomovy filtry.

Bloomův filtr je pravděpodobnostní datová struktura, která umožňuje s vysokou účinností (například s 99,9999% pravděpodobností) určit, zda prvek v sadě chybí. Jeho klíčovou výhodou jsou extrémně nízké náklady na paměť a CPU ve srovnání s klasickými datovými strukturami při kontrole příslušnosti. Hlavní použití Bloomových filtrů v našem projektu:

Google AdInline article slot
  • Sledování zobrazených obrázků: Pro každého uživatele je udržován Bloomův filtr, který ukládá ID již zobrazených obrázků. To umožňuje rychle zkontrolovat, zda byl obrázek uživateli již nabídnut, a vyloučit duplikáty z nových sérií.
  • Identifikace existujících uživatelů: Při spuštění backendu se všechna ID registrovaných uživatelů načtou do společného Bloomova filtru. To zajišťuje okamžitou kontrolu, zda je příchozí device_id již systému znám.

Pro zvýšení uživatelského komfortu a zjednodušení moderace v obchodech s aplikacemi byl implementován systém identifikace bez explicitní autorizace. Při instalaci aplikace se vygeneruje unikátní device_id, který se následně používá k identifikaci uživatele na backendu. Pro ochranu proti generování falešných device_id a zneužití se používá vícestupňová kontrola:

  • Fastify JSON Schema Validation: Primární kontrola formátu device_id probíhá na úrovni Fastify s použitím JSON schématu, které definuje minLength, maxLength a pattern.

```javascript

fastify.post( '/backend', {

Google AdInline article slot

  schema: {

    body: {

      type: 'object',

      required: [ 'device_id' ],

      properties: {

        device_id: {

          type: 'string',

          minLength: 23,

          maxLength: 23,

          pattern: '^[a-z0-9]{11}-[a-z0-9]{11}$'

        }

      }

    }

  }

} );

```

  • Bloomův filtr uživatelů: Po projití schématem Fastify je device_id zkontrolován na přítomnost v Bloomově filtru existujících uživatelů.
  • Kontrola správnosti ID:

* Datum vytvoření: První část ID, která kóduje časovou značku, musí odpovídat období mezi spuštěním projektu a aktuálním datem.

* Entropie náhodné části: Druhá část ID, představující náhodnou sekvenci, je kontrolována na entropii. To zabraňuje použití jednoduchých, předvídatelných ID (například aaaaaaaaaaaa). Entropie by měla být vyšší než určitá prahová hodnota (například 2).

```javascript

function getEntropyString( str: string ): number {

  const frequencies = new Map();

  for (const char of str) {

    frequencies.set(char, (frequencies.get(char) || 0) + 1);   }

  return [...frequencies.values()].reduce((acc, count) => {

    const p = count / str.length;

    return acc - p * Math.log2(p);

  },

0);

}

```

  • Asynchronní vložení uživatele: Pokud je ID uznáno za nové a správné, je přidáno do Bloomova filtru existujících uživatelů a poté je asynchronně odeslán INSERT dotaz do databáze. Absence await umožňuje neblokovat zpracování požadavku, což výrazně snižuje zátěž backendu. Tento přístup připouští extrémně malou pravděpodobnost ztráty dat o novém uživateli při náhlém restartu backendu, což je přijatelný kompromis pro tento typ aplikace, jelikož „ztráta“ znamená pouze resetování historie swipů pro tohoto uživatele.

Optimalizace interakce s klientem a ochrana API

Efektivní interakce mezi mobilní aplikací a backendem je kriticky důležitá pro výkon. Pro doručování statického obsahu, jako jsou obrázky, se používá Nginx, běžící na samostatné doméně. To umožňuje maximálně odlehčit Fastify serveru, který se zaměřuje na obchodní logiku, a efektivně cachovat obsah.

Mobilní aplikace přijímá obrázky v sériích po 10 kusech. ID těchto obrázků jsou také přidána do Bloomova filtru „Zobrazené obrázky“ pro konkrétního uživatele. Tento přístup minimalizuje počet požadavků na API: aplikace načte dvě série při startu a poté si vyžádá novou sérii po každých 10 lajcích. To zajišťuje nepřetržitý tok obsahu, zatímco uživatel interaguje s aktuální sérií.

Pro ochranu API před nadměrnou zátěží a zneužitím se používá omezení počtu požadavků (rate limiting) pomocí pluginu rateLimit pro Fastify. V závislosti na architektuře může používat Redis pro distribuované ukládání dat nebo pracovat v paměti aplikace Node.js pro jednotlivé instance.

Důležitým aspektem je také kontrola uživatelských reakcí (lajků/dislajků), které jsou odesílány v dávkách po 10 kusech. Tento proces zahrnuje několik úrovní validace:

  • Fastify Schema Validation: ID uživatele, ID obrázku a samotná reakce (jako enum 'like' nebo 'dislike') jsou kontrolovány na shodu s JSON schématem.
  • Bloomův filtr uživatelů: Potvrzení existence user_id prostřednictvím Bloomova filtru „Existující uživatelé“.
  • Bloomův filtr zobrazených obrázků: Kontrola, zda image_id byl skutečně zobrazen aktuálnímu uživateli, pomocí jeho osobního Bloomova filtru „Zobrazené obrázky“.
  • Kontrola věrohodnosti dávky reakcí: Je implementována funkce trustLikeInBatch10, která analyzuje vzorce v dávce 10 reakcí. Pokud jsou všechny reakce stejné (všechny lajky nebo všechny dislajky) nebo je pozorováno příliš časté střídání (například 8 a více přepnutí), dávka je považována za nevěrohodnou a ignorována. To pomáhá filtrovat automatizované nebo nečestné akce.

```javascript

function trustLikeInBatch10( batch: string[] ): boolean {

  if( batch.length != feedLimit ) return false;

  const likes = batch.filter( v => v === 'like' ).length;

  if( likes === 0 || likes === feedLimit ) return false;

  const switches = batch.slice(1).reduce( ( acc, val, i ) => acc + ( val !== batch[ i ] ? 1 : 0 ), 0 );

  if( switches >= 8 ) return false;

  return true;

}

```

  • Asynchronní zápis reakcí: Stejně jako v případě nových uživatelů jsou reakce asynchronně zapisovány do databáze bez čekání na dokončení operace a bez použití omezení FOREIGN KEY. Tento přístup, ačkoli netypický, výrazně snižuje zátěž DB při vysoce intenzivních datových tocích, obětuje přísnou konzistenci za propustnost. V tomto kontextu potenciální ztráta několika reakcí není kritická pro celkovou statistiku a doporučovací systém.

Pro zvýšení bezpečnosti a prevenci exploitů souvisejících s detaily validace API jsou chyby generované Fastify při neshodě s JSON schématem maskovány. Místo podrobného popisu problému backend vrací zobecněnou odpověď. To ztěžuje útočníkům reverzní inženýrství datových schémat.

fastify.setErrorHandler((
  error: FastifyError,
  request: FastifyRequest,
  reply: FastifyReply ) =>
{
  console.error( 'setErrorHandler', error );
  countErrorStatistics();
  reply.code(200).send( 'Hello World!' );
});

Klientská aplikace musí být připravena zpracovávat takové zobecněné odpovědi, což je součástí celkového přístupu k bezpečnosti.

Typování v TypeScriptu pro spolehlivost kódu

Použití TypeScriptu výrazně zvyšuje spolehlivost a udržovatelnost kódu. Konkrétně pro práci s pevnými sadami řetězcových stavů (například stavy obrázků) lze elegantně vytvořit typ z pole řetězců. To zajišťuje striktní typování proměnných, eliminující chyby spojené s překlepy nebo použitím nepřípustných hodnot.

// Usually it's like this:
const imageStateArray1 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ];
type tpImageState1 = 'new' | 'broken' | 'gpt' | 'ready' | 'download' | 'done' | 'bad';

// or better yet:
const imageStateArray2 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ] as const;
type tpImageState2 = ( typeof imageStateArray2 )[ number ];

Druhý přístup s as const a (typeof array)[number] je preferovanější, jelikož automaticky odvozuje literální typ z pole, zajišťující synchronizaci mezi polem hodnot a jeho typem bez duplikace.

Co je důležité

  • Použití Bloomových filtrů výrazně snižuje zátěž databáze při kontrole existence prvků, což je kriticky důležité pro vysoce zatížené systémy.
  • Vlastní generování ID, zahrnující časovou značku, umožňuje vkládat metadata a optimalizovat indexaci, a také upustit od duplikujících polí created_at.
  • Asynchronní operace zápisu do DB a upuštění od FOREIGN KEY-ů zvyšují propustnost backendu za cenu potenciální, ale přijatelné ztráty dat při selhání v aplikacích s vysokou intenzitou operací.
  • Detailní validace příchozích požadavků na úrovni Fastify a kontrola entropie dat chrání API před zneužitím a nekorektními daty.
  • Maskování chyb validace Fastify zvyšuje bezpečnost API, skrývá vnitřní strukturu schémat a ztěžuje reverzní inženýrství potenciálním útočníkům.

— Editorial Team

Advertisement 728x90

Číst dál