Node verwendet das Modul async_hooks zur Anforderungsverfolgung

Node verwendet das Modul async_hooks zur Anforderungsverfolgung

Das Modul async_hooks ist eine experimentelle API, die in Version v8.0.0 offiziell zu Node.js hinzugefügt wurde. Wir haben es auch in der Produktionsumgebung unter Version v8.xx implementiert.

Was sind also async_hooks?

async_hooks bietet eine API zum Verfolgen asynchroner Ressourcen, bei denen es sich um Objekte mit zugehörigen Rückrufen handelt.

Kurz gesagt kann das Modul async_hooks zum Verfolgen asynchroner Rückrufe verwendet werden. Wie nutzen Sie also diese Tracking-Funktion und welche Probleme können bei ihrer Nutzung auftreten?

Grundlegendes zu async_hooks

async_hooks unter der Version v8.xx besteht hauptsächlich aus zwei Teilen, einer ist createHook zum Verfolgen des Lebenszyklus und der andere ist AsyncResource zum Erstellen asynchroner Ressourcen.

const { createHook, AsyncResource, executionAsyncId } = erforderlich('async_hooks')

const hook = erstelleHook({
 init (asyncId, Typ, triggerAsyncId, Ressource) {},
 vor (asyncId) {},
 nach (asyncId) {},
 zerstören (asyncId) {}
})
hook.aktivieren()

Funktion fn () {
 console.log(ausführungAsyncId())
}

const asyncResource = neue AsyncResource('demo').
asyncResource.run(fn)
asyncResource.run(fn)
asyncResource.emitDestroy()

Die Bedeutung und das Ausführungsergebnis des obigen Codes sind:

  1. Erstellt eine Hook-Instanz mit Hook-Funktionen, die während der Initialisierungs-, Vorher-, Nachher- und Destroy-Zyklen jeder asynchronen Operation ausgeführt werden.
  2. Aktivieren Sie diese Hook-Instanz.
  3. Erstellen Sie manuell eine asynchrone Ressource vom Typ „Demo“. Zu diesem Zeitpunkt wird der Init-Hook ausgelöst, die asynchrone Ressourcen-ID ist asyncId, der Typ ist Typ (dh Demo), die Erstellungskontext-ID der asynchronen Ressource ist triggerAsyncId und die asynchrone Ressource ist Ressource.
  4. Verwenden Sie diese asynchrone Ressource, um die fn-Funktion zweimal auszuführen. Dies wird zweimal vor und zweimal nach der Ausführung ausgelöst. Die asynchrone Ressourcen-ID ist asyncId, was dem Wert entspricht, der durch executionAsyncId in der fn-Funktion erhalten wird.
  5. Lösen Sie den Destroy-Lifecycle-Hook manuell aus.

Hinter den asynchronen Operationen wie Async, Await, Promise-Syntax oder Requests, die wir häufig verwenden, stehen asynchrone Ressourcen, die auch diese Lifecycle-Hook-Funktionen auslösen.

Anschließend können wir in der Init-Hook-Funktion eine Zeigerbeziehung vom Kontext der asynchronen Ressourcenerstellung triggerAsyncId (übergeordnet) zur aktuellen asynchronen Ressource asyncId (untergeordnet) erstellen, die asynchronen Aufrufe nacheinander verbinden, einen vollständigen Aufrufbaum erhalten und die asyncId der asynchronen Ressource abrufen, die den aktuellen Rückruf über executionAsyncId() in der Rückruffunktion ausführt (d. h. fn im obigen Code) und die Quelle des Aufrufs aus der Aufrufkette zurückverfolgen.

Gleichzeitig müssen wir auch beachten, dass init ein Hook für die asynchrone Ressourcenerstellung ist, kein Hook für die asynchrone Erstellung von Rückruffunktionen. Es wird nur einmal ausgeführt, wenn asynchrone Ressourcen erstellt werden. Welche Probleme bringt dies bei der tatsächlichen Verwendung mit sich?

Anfrageverfolgung

Zum Zwecke der Ausnahmebehebung und Datenanalyse hoffen wir, die Anforderungs-ID im Anforderungsheader der vom Client gesendeten Anforderung automatisch zum Anforderungsheader jeder Anforderung hinzufügen zu können, die an die Mid- und Back-End-Dienste in unserem Node.js-Dienst mit Ada-Architektur gesendet wird.

Der einfache Entwurf der Funktionsimplementierung sieht wie folgt aus:

  1. Der Init-Hook ermöglicht asynchronen Ressourcen in derselben Aufrufkette, ein Speicherobjekt gemeinsam zu nutzen.
  2. Analysieren Sie die Anforderungs-ID im Anforderungsheader und fügen Sie sie dem Speicher hinzu, der der aktuellen asynchronen Aufrufkette entspricht.
  3. Schreiben Sie die Anforderungsmethode von http- und https-Modulen neu, um die Anforderungs-ID zu erhalten, die in der aktuellen Aufrufkette gespeichert ist, wenn die Anforderung ausgeführt wird.

Der Beispielcode lautet wie folgt:

const http = erfordern('http').
const { createHook, executionAsyncId } = erforderlich('async_hooks')
const fs = erfordern('fs')

// Aufrufkette verfolgen und ein Speicherobjekt für die Aufrufkette erstellen const cache = {}
const hook = erstelleHook({
 init (asyncId, Typ, triggerAsyncId, Ressource) {
  wenn (Typ === 'TickObject') zurückgeben
  // Da console.log auch ein asynchrones Verhalten in Node.js ist, wird es den Init-Hook auslösen, sodass wir Protokolle nur über synchrone Methoden aufzeichnen können fs.appendFileSync('log.out', `init ${type}(${asyncId}: trigger: ${triggerAsyncId})\n`);
  // Bestimmen Sie, ob das Aufrufketten-Speicherobjekt initialisiert wurde, wenn (!cache[triggerAsyncId]) {
   cache[triggerAsyncId] = {}
  }
  // Teilen Sie den Speicher des übergeordneten Knotens mit der aktuellen asynchronen Ressource per Referenz cache[asyncId] = cache[triggerAsyncId]
 }
})
hook.aktivieren()

// http neu schreiben
const httpRequest = http.request
http.request = (Optionen, Rückruf) => {
 const client = httpRequest(Optionen, Rückruf)
 // Die in der asynchronen Ressource gespeicherte Request-ID zur aktuellen Anfrage abrufen und in den Header schreiben
 const requestId = cache[executionAsyncId()].requestId
 Konsole.log('Cache', Cache[Ausführungs-AsyncId()])
 client.setHeader('Anforderungs-ID', Anforderungs-ID)

 Stammkunde
}

Funktion Timeout () {
 returniere neues Promise((lösen, ablehnen) => {
  setTimeout(auflösen, Math.random() * 1000)
 })
}
// Dienst erstellen http
 .createServer(async (req, res) => {
  // Holen Sie die Anforderungs-ID der aktuellen Anforderung und schreiben Sie sie in den Speichercache [executionAsyncId()].requestId = req.headers['request-id']
  //Simulieren Sie einige andere zeitaufwändige Operationen await timeout()
  // Anfrage senden http.request('http://www.baidu.com', (res) => {})
  res.write('hallo\n')
  res.ende()
 })
 .listen(3000)

Führen Sie den Code aus und führen Sie einen Sendetest durch. Sie werden feststellen, dass die Anforderungs-ID korrekt abgerufen werden kann.

fangen

Gleichzeitig müssen wir auch beachten, dass init ein Hook für die asynchrone Ressourcenerstellung ist, kein Hook für die Erstellung asynchroner Rückruffunktionen, und dass er nur einmal ausgeführt wird, wenn asynchrone Ressourcen erstellt werden.

Der obige Code ist jedoch problematisch. Wie im Code des zuvor vorgestellten Moduls async_hooks gezeigt, kann eine asynchrone Ressource kontinuierlich verschiedene Funktionen ausführen, d. h. asynchrone Ressourcen können wiederverwendet werden. Insbesondere bei asynchronen Ressourcen wie TCP, die vom C/C++-Teil erstellt werden, können mehrere Anfragen dieselbe asynchrone TCP-Ressource verwenden. Infolgedessen wird in diesem Fall die anfängliche Init-Hook-Funktion nur einmal ausgeführt, wenn mehrere Anfragen beim Server eintreffen, was dazu führt, dass die Aufrufkettenverfolgung mehrerer Anfragen dieselbe triggerAsyncId verfolgt und somit auf denselben Speicher verweist.

Um eine Überprüfung durchzuführen, ändern wir den vorherigen Code wie folgt. Der Speicherinitialisierungsteil speichert triggerAsyncId, um die Beobachtung der Tracking-Beziehung asynchroner Aufrufe zu erleichtern:

  wenn (!cache[triggerAsyncId]) {
   cache[triggerAsyncId] = {
    ID: triggerAsyncId
   }
  }

Die Timeout-Funktion wird geändert, um zuerst einen Langzeitvorgang und dann einen Kurzzeitvorgang auszuführen:

Funktion Timeout () {
 returniere neues Promise((lösen, ablehnen) => {
  setTimeout(auflösen, [1000, 5000].pop())
 })
}

Nach dem Neustart des Dienstes verwenden Sie Postman (nicht Curl, da Curl die Verbindung nach jeder Anfrage schließt, was eine Reproduktion unmöglich macht), um zwei aufeinanderfolgende Anfragen zu senden. Sie können die folgende Ausgabe beobachten:

{ id: 1, requestId: 'Die ID der zweiten Anfrage' }
{ id: 1, requestId: 'Die ID der zweiten Anfrage' }

Es zeigt sich, dass bei mehreren gleichzeitigen Vorgängen mit anderen Vorgängen, die zwischen dem Schreiben und Lesen der Speicherung unterschiedlich viel Zeit in Anspruch nehmen, der in der Anfrage gespeicherte Wert, der zuerst beim Server eintrifft, durch die Anfrage überschrieben wird, die später beim Server eintrifft, wodurch die vorherige Anfrage den falschen Wert liest. Natürlich können Sie sicherstellen, dass zwischen dem Schreiben und Lesen keine anderen zeitaufwändigen Vorgänge eingefügt werden, aber bei komplexen Diensten ist diese Art der mentalen Wartungsmethode offensichtlich unzuverlässig. An diesem Punkt müssen wir JS dazu bringen, vor jedem Lesen und Schreiben in einen neuen asynchronen Ressourcenkontext zu wechseln, d. h. eine neue asynchrone ID abzurufen, um diese Wiederverwendung zu vermeiden. Die folgenden Änderungen müssen am Aufrufkettenspeicherteil vorgenommen werden:

const http = erfordern('http').
const { createHook, executionAsyncId } = erforderlich('async_hooks')
const fs = erfordern('fs')
const cache = {}

const httpRequest = http.request
http.request = (Optionen, Rückruf) => {
 const client = httpRequest(Optionen, Rückruf)
 const requestId = cache[executionAsyncId()].requestId
 Konsole.log('Cache', Cache[Ausführungs-AsyncId()])
 client.setHeader('Anforderungs-ID', Anforderungs-ID)

 Stammkunde
}

// Extrahiere die Speicherinitialisierung in eine unabhängige Methode async function cacheInit (callback) {
 // Verwenden Sie die await-Operation, damit der Code nach await in einen neuen asynchronen Kontext wechselt. await Promise.resolve()
 cache[executionAsyncId()] = {}
 //Verwende Callback-Ausführung, damit nachfolgende Vorgänge zu diesem neuen asynchronen Kontext gehören return callback()
}

const hook = erstelleHook({
 init (asyncId, Typ, triggerAsyncId, Ressource) {
  wenn (!cache[triggerAsyncId]) {
   // Init-Hook wird nicht mehr initialisiert return fs.appendFileSync('log.out', `Nicht mit der Methode cacheInit initialisiert`)
  }
  cache[asyncId] = cache[triggerAsyncId]
 }
})
hook.aktivieren()

Funktion Timeout () {
 returniere neues Promise((lösen, ablehnen) => {
  setTimeout(auflösen, [1000, 5000].pop())
 })
}

http
.createServer(async (req, res) => {
 // Nachfolgende Operationen als Callbacks an cacheInit übergeben
 warte auf CacheInit (asynchrone Funktion fn() {
  cache[executionAsyncId()].requestId = req.headers['Anforderungs-ID']
  warte auf Timeout()
  http.request('http://www.baidu.com', (res) => {})
  res.write('hallo\n')
  res.ende()
 })
})
.listen(3000)

Es ist erwähnenswert, dass diese Organisationsmethode mit Rückrufen sehr gut mit dem Middleware-Modell von Koajs übereinstimmt.

asynchrone Funktions-Middleware (ctx, next) {
 warte auf Promise.resolve()
 cache[executionAsyncId()] = {}
 returniere nächstes()
}

NodeJs v14

Diese Art der Erstellung eines neuen asynchronen Kontexts mit await Promise.resolve() erscheint immer ein wenig „ketzerisch“. Glücklicherweise bietet NodeJs v9.xx eine offizielle Implementierung zum Erstellen eines asynchronen Kontexts: asyncResource.runInAsyncScope. Noch besser: NodeJs v14.xx bietet direkt die offizielle Implementierung der asynchronen Aufrufkettendatenspeicherung, die Ihnen direkt dabei hilft, die drei Aufgaben der asynchronen Aufrufbeziehungsverfolgung, der Erstellung neuer asynchroner Startdokumente und der Datenverwaltung zu erledigen! Die API wird nicht im Detail vorgestellt. Wir werden die neue API direkt verwenden, um die vorherige Implementierung zu transformieren

const { AsyncLocalStorage } = erfordern('async_hooks')
// Erstellen Sie direkt eine asyncLocalStorage-Speicherinstanz, Sie müssen keine asynchronen Lebenszyklus-Hooks mehr verwalten const asyncLocalStorage = new AsyncLocalStorage()
const Speicher = {
 aktivieren (Rückruf) {
  // Verwende die Run-Methode, um einen neuen Speicher zu erstellen. Nachfolgende Vorgänge müssen als Rückrufe der Run-Methode ausgeführt werden, um einen neuen asynchronen Ressourcenkontext zu verwenden: asyncLocalStorage.run({}, callback)
 },
 get (Schlüssel) {
  returniere asyncLocalStorage.getStore()[Schlüssel]
 },
 set (Schlüssel, Wert) {
  asyncLocalStorage.getStore()[Schlüssel] = Wert
 }
}

// http neu schreiben
const httpRequest = http.request
http.request = (Optionen, Rückruf) => {
 const client = httpRequest(Optionen, Rückruf)
 // Die Request-ID des asynchronen Ressourcenspeichers abrufen und in den Header schreiben
 client.setHeader('Anforderungs-ID', storage.get('Anforderungs-ID'))

 Stammkunde
}

// Mit http
 .createServer((req, res) => {
  storage.enable(asynchrone Funktion () {
   // Holen Sie die Anforderungs-ID der aktuellen Anforderung und schreiben Sie sie in den Speicher storage.set('requestId', req.headers['request-id'])
   http.request('http://www.baidu.com', (res) => {})
   res.write('hallo\n')
   res.ende()
  })
 })
 .listen(3000)

Wie Sie sehen, stimmt die Struktur der offiziellen Implementierung der API asyncLocalStorage.run auch mit der Implementierung unserer zweiten Version überein.

Daher kann in Node.js v14.xx die Anforderungsverfolgungsfunktion einfach mithilfe des Moduls async_hooks implementiert werden.

Dies ist das Ende dieses Artikels über die Verwendung des Moduls async_hooks für die Knotenanforderungsverfolgung. Weitere Informationen zur Knotenanforderungsverfolgung async_hooks finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, Sie werden 123WORDPRESS.COM auch in Zukunft unterstützen!

Das könnte Sie auch interessieren:
  • Detaillierte Untersuchung der Verwendung des asynchronen Moduls in nodejs
  • So verwenden Sie asynchrone Funktionen in Node.js
  • Zusammenfassung gängiger asynchroner Node.js-Funktionen (empfohlen)
  • Node verwendet Async zur Steuerung der Parallelität
  • Nodejs asynchrone Prozess-Framework asynchrone Methode
  • Node kapselt MySQL basierend auf async/await
  • Eine kurze Diskussion über asynchrone Programmierung in node.js
  • Detaillierte Erklärung von Node Async/Await: eine bessere asynchrone Programmierlösung
  • So verwenden Sie asynchrone Funktionen in Node.js
  • So verwenden Sie async/await in Node.js für SQLite
  • Eine kurze Analyse des Anwendungsfalls des asynchronen Verarbeitungsmoduls Node Async und Einführung in gängige Methoden

<<:  Detaillierte Erläuterung des Nginx-Strombegrenzungsmoduls in der Nginx-Quellcode-Recherche

>>:  Eine kurze Erläuterung temporärer MySQL-Tabellen und abgeleiteter Tabellen

Artikel empfehlen

Welche Arten von MySQL-Verbindungsabfragen kennen Sie?

Vorwort Wenn die Abfrageinformationen aus mehrere...

Richtiger Einsatz von MySQL-Partitionstabellen

Übersicht über partitionierte MySQL-Tabellen Wir ...

Praktische Methode zum Löschen einer Zeile in einer MySql-Tabelle

Zunächst müssen Sie bestimmen, welche Felder oder...

Beispiele für häufige Nginx-Fehlkonfigurationen

Inhaltsverzeichnis Fehlender Stammspeicherort Off...

Einführung in CSS-Stileinführungsmethoden und ihre Vor- und Nachteile

Drei Möglichkeiten, CSS einzuführen 1. Inline-Sti...

Die letzten zwei Jahre mit User Experience

<br />Es ist nicht länger als zwei Jahre her...

Einführung in die Verwendung von Alt- und Titelattributen von HTML-Img-Tags

Wenn Browser-Anbieter die Standards umgehen und ei...

Der einfachste Weg zum Debuggen gespeicherter Prozeduren in MySQL

Ein Kollege hat mir einmal gesagt, ich solle eine...

Installieren Sie mysql5.7.17 mit RPM unter Linux

Die Installationsmethode von MySQL5.7 rpm unter L...

Optimieren Sie MySQL mit 3 einfachen Tricks

Ich erwarte nicht, ein erfahrener Datenbankadmini...

Implementierungsbeispiel für das Zurücksetzen des CSS-Reset-Stils

Einführung: Alle Browser verfügen über Standardst...