Zurück zur Startseite

Fastify-Optimierung: Bloom Filters und Skalierbare Backends

Entdecken Sie praktische Lösungen für die Erstellung eines hochbelasteten Fastify-Backends: benutzerdefinierte IDs, Bloom Filters für Benutzer und Bilder, loginlose Autorisierung, asynchrone DB-Schreibvorgänge, API-Schutz und TypeScript-Typisierung.

Fastify Backend-Optimierung: Bloom Filters & Skalierbare Architekturen
Advertisement 728x90

Fastify-Backends für Hochlastanwendungen optimieren: Praktische Lösungen

Der Aufbau skalierbarer und hochperformanter Backends erfordert ein tiefes Verständnis architektonischer Kompromisse. In diesem Artikel teilen wir unsere Erfahrungen bei der Entwicklung einer Hochlastanwendung mit Fastify. Wir konzentrieren uns dabei auf Strategien zur Minimierung der Datenbanklast, zur effektiven Benutzeridentifikation ohne traditionelle Authentifizierung und zum Schutz vor bösartigen Anfragen, wobei wir Tools wie Bloom-Filter und asynchrone Operationen nutzen.

Strategien zur ID-Generierung und effizientes Datenmanagement

Beim Entwurf von Systemen, die eine große Anzahl von Entitäten wie Benutzer oder Bilder verwalten, ist die Wahl des Identifikatorformats (ID) von entscheidender Bedeutung. Standard-GUIDs oder UUIDs gewährleisten zwar die Einzigartigkeit, können aber für die Datenbankindizierung und Backend-Verarbeitung übermäßig ressourcenintensiv sein. Um diesen Overhead zu reduzieren, haben wir ein hybrides ID-Format implementiert: 19bc19e00ab-eh5kkeh99q7. Der erste Teil stellt einen in Hexadezimal umgewandelten Unix-Timestamp dar, der zweite ist eine zufällige Zeichenfolge. Der Bindestrich dient lediglich der besseren Lesbarkeit.

Hier ist eine Beispiel-Funktion zur Generierung einer solchen ID:

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

Dieser Ansatz liefert eine "bedingt inkrementelle" ID, da sich ihr anfänglicher Teil mit jeder Millisekunde ändert, was zu einer effizienteren Datenbankindizierung beiträgt. Ein weiterer Vorteil ist die Möglichkeit, das Erstellungsdatum der Entität direkt aus der ID zu dekodieren, wodurch ein separates created_at-Feld in Tabellen überflüssig wird. Dies reduziert das Volumen der gespeicherten Daten und die Anzahl der Schreiboperationen, auch wenn es geringfügige Einschränkungen bei der Bequemlichkeit datumsbasierter Abfragen mit sich bringt.

Skalierung mit Bloom-Filtern und passwortloser Authentifizierung

Die Entwicklung einer Anwendung, die für Tausende gleichzeitiger Sitzungen und Zehntausende von Bildern konzipiert ist, erfordert Ansätze, die den CPU- und Speicherverbrauch minimieren. Traditionelle SQL-Abfragen zur Überprüfung bereits angesehener Bilder oder existierender Benutzer werden schnell zu einem Engpass. In diesem Zusammenhang spielen Bloom-Filter eine unschätzbare Rolle.

Ein Bloom-Filter ist eine probabilistische Datenstruktur, die effizient (z. B. mit 99,9999%iger Wahrscheinlichkeit) feststellen kann, ob ein Element nicht in einer Menge enthalten ist. Sein Hauptvorteil ist der extrem geringe Speicher- und CPU-Overhead im Vergleich zu klassischen Datenstrukturen bei der Überprüfung der Mitgliedschaft. Die primären Anwendungen von Bloom-Filtern in unserem Projekt umfassen:

Google AdInline article slot
  • Verfolgung angesehener Bilder: Für jeden Benutzer wird ein Bloom-Filter geführt, der die IDs bereits angezeigter Bilder speichert. Dies ermöglicht schnelle Überprüfungen, ob ein Bild dem Benutzer bereits präsentiert wurde, und eliminiert Duplikate aus neuen Batches.
  • Identifizierung bestehender Benutzer: Beim Start des Backends werden alle registrierten Benutzer-IDs in einen globalen Bloom-Filter geladen. Dies ermöglicht eine sofortige Überprüfung, ob eine eingehende device_id dem System bereits bekannt ist.

Um den Benutzerkomfort zu erhöhen und die Moderation in App Stores zu vereinfachen, wurde ein System zur Identifikation ohne explizite Authentifizierung implementiert. Bei der Installation der Anwendung wird eine eindeutige device_id generiert, die dann zur Identifizierung des Benutzers im Backend verwendet wird. Zum Schutz vor der Generierung gefälschter device_ids und Missbrauch wird ein mehrstufiger Verifizierungsprozess eingesetzt:

  • Fastify JSON-Schema-Validierung: Die anfängliche Validierung des device_id-Formats erfolgt auf Fastify-Ebene mittels eines JSON-Schemas, das minLength, maxLength und ein pattern definiert.

```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}$'

        }

      }

    }

  }

} );

```

  • Benutzer-Bloom-Filter: Nach Bestehen des Fastify-Schemas wird die device_id mit dem Bloom-Filter der bestehenden Benutzer abgeglichen.
  • ID-Korrektheitsprüfung:

* Erstellungsdatum: Der erste Teil der ID, der den Zeitstempel kodiert, muss in den Zeitraum zwischen dem Projektstart und dem aktuellen Datum fallen.

* Entropie des Zufallsteils: Der zweite Teil der ID, der eine zufällige Sequenz darstellt, wird auf Entropie geprüft. Dies verhindert die Verwendung einfacher, vorhersagbarer IDs (z. B. aaaaaaaaaaaa). Die Entropie muss über einem bestimmten Schwellenwert liegen (z. B. 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);

}

```

  • Asynchrone Benutzer-Einfügung: Wird die ID als neu und gültig befunden, wird sie dem Bloom-Filter der bestehenden Benutzer hinzugefügt, und eine INSERT-Abfrage wird dann asynchron an die Datenbank gesendet. Das Weglassen von await verhindert das Blockieren der Anfragenverarbeitung und reduziert die Backend-Last erheblich. Dieser Ansatz akzeptiert eine extrem geringe Wahrscheinlichkeit, neue Benutzerdaten bei einem unerwarteten Backend-Neustart zu verlieren, was für diese Art von Anwendung ein akzeptabler Kompromiss ist, da "Verlust" lediglich das Zurücksetzen der Swipe-Historie für diesen Benutzer bedeutet.

Optimierung der Client-Interaktion und API-Schutz

Eine effiziente Interaktion zwischen der mobilen Anwendung und dem Backend ist für die Performance von entscheidender Bedeutung. Für die Bereitstellung statischer Inhalte wie Bilder wird Nginx verwendet, das auf einer separaten Domain läuft. Dies entlastet den Fastify-Server maximal, sodass er sich auf die Geschäftslogik konzentrieren kann, und ermöglicht effektives Content-Caching.

Die mobile Anwendung empfängt Bilder in Batches von 10. Die IDs dieser Bilder werden ebenfalls einem benutzerspezifischen "Angesehene Bilder"-Bloom-Filter hinzugefügt. Dieser Ansatz minimiert die Anzahl der API-Anfragen: Die Anwendung lädt beim Start zwei Batches und fordert dann nach jeweils 10 Likes einen neuen Batch an. Dies gewährleistet einen kontinuierlichen Inhaltsstrom, während der Benutzer mit dem aktuellen Batch interagiert.

Zum Schutz der API vor übermäßiger Last und Missbrauch wird Rate Limiting mittels des rateLimit-Plugins für Fastify implementiert. Je nach Architektur kann es Redis für die verteilte Datenspeicherung nutzen oder im Speicher der Node.js-Anwendung für Einzelinstanzen arbeiten.

Ein wichtiger Aspekt ist auch die Validierung von Benutzerreaktionen (Likes/Dislikes), die in Batches von 10 gesendet werden. Dieser Prozess umfasst mehrere Validierungsebenen:

  • Fastify-Schema-Validierung: Die Benutzer-ID, Bild-ID und die Reaktion selbst (als enum 'like' oder 'dislike') werden gegen ein JSON-Schema geprüft.
  • Benutzer-Bloom-Filter: Bestätigung der Existenz der user_id über den "Bestehende Benutzer"-Bloom-Filter.
  • Angesehene Bilder-Bloom-Filter: Überprüfung, ob die image_id dem aktuellen Benutzer tatsächlich angezeigt wurde, unter Verwendung seines persönlichen "Angesehene Bilder"-Bloom-Filters.
  • Authentizitätsprüfung von Reaktions-Batches: Eine Funktion, trustLikeInBatch10, wird implementiert, um Muster innerhalb eines Batches von 10 Reaktionen zu analysieren. Sind alle Reaktionen identisch (alle Likes oder alle Dislikes) oder gibt es einen übermäßig häufigen Wechsel (z. B. 8 oder mehr Wechsel), wird der Batch als unzuverlässig eingestuft und ignoriert. Dies hilft, automatisierte oder bösartige Aktionen herauszufiltern.

```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;

}

```

  • Asynchrone Reaktionsschreibvorgänge: Ähnlich wie bei neuen Benutzern werden Reaktionen asynchron in die Datenbank geschrieben, ohne auf den Abschluss der Operation zu warten und ohne FOREIGN KEY-Constraints zu verwenden. Dieser Ansatz, obwohl unkonventionell, reduziert die Datenbanklast bei hochintensiven Datenströmen erheblich, indem er strenge Konsistenz zugunsten des Durchsatzes opfert. In diesem Kontext ist der potenzielle Verlust einiger Reaktionen für die Gesamtstatistik und das Empfehlungssystem nicht kritisch.

Um die Sicherheit zu erhöhen und Exploits im Zusammenhang mit API-Validierungsdetails zu verhindern, werden von Fastify generierte Fehler aufgrund von JSON-Schema-Fehlern maskiert. Anstelle einer detaillierten Problembeschreibung gibt das Backend eine generische Antwort zurück. Dies erschwert es Angreifern, Datenschemata zu reverse-engineeren.

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

Die Client-Anwendung muss darauf vorbereitet sein, solche generischen Antworten zu verarbeiten, was Teil der gesamten Sicherheitsstrategie ist.

TypeScript für zuverlässigen Code nutzen

Der Einsatz von TypeScript erhöht die Code-Zuverlässigkeit und Wartbarkeit erheblich. Insbesondere bei der Arbeit mit festen Sätzen von String-Zuständen (z. B. Bildstatus) kann man elegant einen Typ aus einem String-Array erstellen. Dies gewährleistet eine strenge Typisierung für Variablen und eliminiert Fehler im Zusammenhang mit Tippfehlern oder der Verwendung ungültiger Werte.

// 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 ];

Der zweite Ansatz, die Verwendung von as const und (typeof array)[number], ist vorzuziehen, da er den Literal-Typ automatisch aus dem Array ableitet und so die Synchronisation zwischen den Array-Werten und ihrem Typ ohne Duplizierung gewährleistet.

Wichtige Erkenntnisse

  • Der Einsatz von Bloom-Filtern reduziert die Datenbanklast bei der Überprüfung der Existenz von Elementen erheblich, was für Hochlastsysteme von entscheidender Bedeutung ist.
  • Die benutzerdefinierte ID-Generierung, die einen Zeitstempel integriert, ermöglicht das Einbetten von Metadaten und die Optimierung der Indizierung sowie das Eliminieren redundanter created_at-Felder.
  • Asynchrone Datenbank-Schreiboperationen und das Weglassen von FOREIGN KEY-Constraints erhöhen den Backend-Durchsatz auf Kosten eines potenziellen, aber akzeptablen Datenverlusts bei Ausfällen in hochintensiven Anwendungen.
  • Detaillierte Validierung eingehender Anfragen auf Fastify-Ebene und Datenentropieprüfungen schützen die API vor Missbrauch und fehlerhaften Daten.
  • Das Maskieren von Fastify-Validierungsfehlern erhöht die API-Sicherheit, indem es die interne Struktur von Schemas verschleiert und das Reverse-Engineering für potenzielle Angreifer erschwert.

— Editorial Team

Advertisement 728x90

Weiterlesen