Multi-Instance Node.js App in PaaS met Redis Pub / Sub

Als u PaaS als hosting voor uw toepassing hebt gekozen, heeft u waarschijnlijk dit probleem gehad of zal dit zich voordoen: uw app wordt geïmplementeerd in kleine "containers" (bekend als dynos in Heroku, of versnellingen in OpenShift) en u wilt het schalen. 

Om dit te doen, verhoogt u het aantal containers - en elk exemplaar van uw app wordt vrijwel uitgevoerd op een andere virtuele machine. Dit is goed om een ​​aantal redenen, maar het betekent ook dat de instanties geen geheugen delen. 

In deze tutorial zal ik je laten zien hoe je dit kleine ongemak kunt overwinnen.

Toen je PaaS-hosting koos, neem ik aan dat je rekening had gehouden met schalen. Misschien is uw site al getuige geweest van het Slashdot-effect of wilt u zich erop voorbereiden. Hoe dan ook, het maken van de instanties om met elkaar te communiceren is vrij eenvoudig.

Houd er rekening mee dat ik in het artikel ga ervan uit dat je al een Node.js-app hebt geschreven en gebruikt.


Stap 1: Opnieuw instellen

Eerst moet je je Redis-database voorbereiden. Ik gebruik Redis To Go graag, omdat de instelling erg snel is en als je Heroku gebruikt, is er een add-on (hoewel je account aan een creditcard moet zijn toegewezen). Er is ook Redis Cloud, die meer opslag en back-ups bevat.

Vanaf daar is de Heroku-setup vrij eenvoudig: selecteer de add-on op de Heroku-add-onspagina en selecteer Redis Cloud of Redis To Go of gebruik een van de volgende opdrachten (merk op dat de eerste is voor Redis To Go , en de tweede is voor Redis Cloud):

$ heroku addons: add redistogo $ heroku addons: voeg rediscloud toe

Stap 2: node_redis instellen

Op dit punt moeten we de vereiste knooppuntmodule toevoegen aan de package.json het dossier. We gebruiken de aanbevolen module node_redis. Voeg deze regel toe aan uw package.json bestand, in de sectie afhankelijkheden:

"node_redis": "0.11.x"

Als je wilt, kun je ook opnemen hiredis, een high-performance bibliotheek geschreven in C, die node_redis zal gebruiken als het beschikbaar is:

"hiredis": "0.1.x"

Afhankelijk van hoe je je Redis-database hebt gemaakt en welke PaaS-provider je gebruikt, zal de verbindingsinstellingen er iets anders uitzien. Jij hebt nodig gastheer, haven, gebruikersnaam, en wachtwoord voor uw verbinding.

Heroku

Heroku slaat alles in de config-variabelen op als URL's. Je moet de informatie die je nodig hebt extraheren met behulp van Node's url module (config var voor Redis To Go is process.env.REDISTOGO_URL en voor Redis Cloud process.env.REDISCLOUD_URL). Deze code staat bovenaan uw hoofdtoepassingsbestand:

var redis = require ('redis'); var url = require ('url'); var redisURL = url.parse (YOUR_CONFIG_VAR_HERE); var client = redis.createClient (redisURL.host, redisURL.port); client.auth (redisURL.auth.split ( ':') [1]); 

anderen

Als u de database handmatig hebt gemaakt of een andere aanbieder dan Heroku hebt gebruikt, moet u al over de verbindingsopties en legitimatiegegevens beschikken, dus gebruik ze gewoon:

var redis = require ('redis'); var client = redis.createClient (YOUR_HOST, YOUR_PORT); client.auth (your_password);

Daarna kunnen we gaan werken aan communicatie tussen instanties.


Stap 3: Gegevens verzenden en ontvangen

Het eenvoudigste voorbeeld zal alleen informatie verzenden naar andere instanties die u net bent gestart. U kunt deze informatie bijvoorbeeld weergeven in het beheerdersdashboard.

Maak voordat we iets doen een nieuwe verbinding met de naam client2. Ik zal uitleggen waarom we het later nodig hebben.

Laten we beginnen met het verzenden van het bericht dat we zijn gestart. Het is gedaan met behulp van de publiceren() methode van de klant. Er zijn twee argumenten voor nodig: het kanaal waarnaar we het bericht willen verzenden en de tekst van het bericht:

client.publish ('instanties', 'start'); 

Dat is alles wat u nodig hebt om het bericht te verzenden. We kunnen luisteren naar berichten in de bericht event handler (merk op dat we dit op onze tweede client noemen):

client2.on ('bericht', functie (kanaal, bericht) 

De callback krijgt dezelfde argumenten die we doorgeven aan de publiceren() methode. Laten we deze informatie nu in de console weergeven:

if ((channel == 'instanties') en (message == 'start')) console.log ('Nieuw exemplaar gestart!'); );

Het laatste wat je moet doen is je daadwerkelijk abonneren op het kanaal dat we gaan gebruiken:

client2.subscribe (instances);

We hebben hiervoor twee clients gebruikt, omdat je belt abonneren () op de client wordt de verbinding omgeschakeld naar de abonnee modus. Vanaf dat moment zijn de enige methoden die u op de Redis-server kunt gebruiken INSCHRIJVEN en AFMELDEN. Dus als we in de abonnee modus die we kunnen publiceren() berichten.

Als u wilt, kunt u ook een bericht verzenden wanneer het exemplaar wordt afgesloten. U kunt naar de SIGTERM evenement en stuur het bericht naar hetzelfde kanaal:

process.on ('SIGTERM', functie () client.publish ('instanties', 'stop'); process.exit ();); 

Om die zaak in de bericht handler voeg dit toe anders als daarin:

else if ((channel == 'instances') en (message == 'stop')) console.log ('Exemplaar gestopt!');

Dus het ziet er na afloop als volgt uit:

client2.on ('message', function (channel, message) if ((channel == 'instances') en (message == 'start')) console.log ('New instance started!'); else if ( (kanaal == 'instanties') en (bericht == 'stop')) console.log ('Instance stopped!'););

Merk op dat als u aan het testen bent op Windows, dit niet de SIGTERM signaal.

Als u het lokaal wilt testen, start u uw app een paar keer en ziet u wat er in de console gebeurt. Als u het beëindigingsbericht wilt testen, geeft u het bericht niet uit Ctrl + C commando in de terminal - gebruik in plaats daarvan de doden commando. Merk op dat dit niet wordt ondersteund in Windows, dus u kunt het niet controleren.

Gebruik eerst de ps opdracht om te controleren naar welk id uw proces heeft - leid het naar grep om het makkelijker te maken:

$ ps -aux | grep your_apps_name 

De tweede kolom van de uitvoer is de ID waarnaar u op zoek bent. Houd er rekening mee dat er ook een regel is voor het commando dat u net hebt uitgevoerd. Voer nu de doden commando gebruiken 15 voor het signaal-het is SIGTERM:

$ kill -15 PID

PID is uw proces-ID.


Real-World voorbeelden

Nu u weet hoe u het Redis Pub / Sub-protocol kunt gebruiken, kunt u verder gaan dan het eenvoudige voorbeeld dat eerder is gepresenteerd. Hier zijn een paar gebruiksgevallen die nuttig kunnen zijn.

Express Sessions

Deze is erg handig als je Express.js als kader gebruikt. Als uw toepassing gebruikersaanmeldingen of vrijwel alles met sessies ondersteunt, moet u ervoor zorgen dat de gebruikerssessies worden behouden, ongeacht of het exemplaar opnieuw wordt opgestart, de gebruiker naar een locatie gaat die door een ander wordt behandeld, of de gebruiker is overgeschakeld naar een andere instantie omdat de oorspronkelijke is verdwenen.

Een paar dingen om te onthouden:

  • De gratis Redis-instanties zijn niet voldoende: u hebt meer geheugen nodig dan de 5 MB / 25 MB die ze bieden.
  • Hiervoor heeft u een andere verbinding nodig.

We hebben de module connect-redis nodig. De versie is afhankelijk van de versie van Express die u gebruikt. Deze is voor Express 3.x:

"connect-redis": "1.4.7"

En dit voor Express 4.x:

"connect-redis": "2.x"

Maak nu een andere Redis-verbinding met de naam client_sessions. Het gebruik van de module is weer afhankelijk van de Express-versie. Voor 3.x maak je de RedisStore zoals dit:

var RedisStore = vereisen ('connect-redis') (express)

En in 4.x moet je de express-sessie als de parameter:

var session = require ('express-sessie'); var RedisStore = vereisen ('connect-redis') (sessie);

Daarna is de setup in beide versies hetzelfde:

app.use (sessie (store: new RedisStore (client: client_sessions), geheim: 'your secret string'));

Zoals je kunt zien, passeren we onze Redis-client als de cliënt eigenschap van het object doorgegeven aan RedisStore's constructor, en dan passeren we de winkel naar de sessie bouwer.

Als u nu uw app start, inlogt of een sessie start en de instance herstart, blijft uw sessie behouden. Hetzelfde gebeurt wanneer de instantie wordt omgeschakeld naar de gebruiker.

Gegevens uitwisselen met WebSockets

Laten we zeggen dat je een volledig gescheiden instantie hebt (werknemer dyno op Heroku) voor het doen van meer middelen-etend werk zoals ingewikkelde berekeningen, het verwerken van gegevens in de database of het uitwisselen van veel gegevens met een externe dienst. U wilt dat de "normale" instanties (en dus de gebruikers) het resultaat van dit werk weten wanneer het klaar is.

Afhankelijk van of u wilt dat de webexemplaren gegevens naar de werknemer verzenden, hebt u een of twee verbindingen nodig (laten we ze een naam geven) client_sub en client_pub ook op de werknemer). U kunt ook elke verbinding opnieuw gebruiken die niet op iets is geabonneerd (zoals degene die u gebruikt voor Express-sessies) in plaats van de client_pub.

Wanneer de gebruiker de actie wil uitvoeren, publiceert u het bericht op het kanaal dat alleen voor deze gebruiker en voor deze specifieke taak is gereserveerd:

// dit gaat in uw request handler client_pub.publish ('JOB: USERID: JOBNAME: START', JSON.stringify (THEDATAYOUWANTTOSEND)); client_sub.subscribe ( 'JOB: USERID: TAAKNAAM: PROGRESS');

Natuurlijk moet je het vervangen GEBRUIKERSNAAM en TAAKNAAM met geschikte waarden. Je zou ook de bericht manager voorbereid op de client_sub verbinding:

client_sub.on ('bericht', functie (kanaal, bericht) var USERID = channel.split (':') [1]; if (message == 'DONE') client_sub.unsubscribe (channel); sockets [USERID] .emit (kanaal, bericht););

Dit haalt de GEBRUIKERSNAAM van de kanaalnaam (dus zorg ervoor dat u zich niet abonneert op kanalen die geen verband houden met gebruikerstaken op deze verbinding) en verzend het bericht naar de juiste client. Afhankelijk van welke WebSocket-bibliotheek u gebruikt, is er een manier om toegang te krijgen tot een socket via zijn ID.

U kunt zich afvragen hoe de worker-instantie zich op al deze kanalen kan abonneren. Natuurlijk wil je niet alleen een paar loops doen op alle mogelijke manieren GEBRUIKERSNAAMs en TAAKNAAMs. De psubscribe () methode accepteert een patroon als het argument, zodat het zich voor iedereen kan abonneren JOB: * kanalen:

// deze code gaat naar de worker instance // en je noemt het ONCE client_sub.psubscribe ('JOB: *')

Veel voorkomende problemen

Er zijn een paar problemen die u kunt tegenkomen bij het gebruik van Pub / Sub:

  • Uw verbinding met de Redis-server wordt geweigerd. Als dit gebeurt, zorgt u ervoor dat u de juiste verbindingsopties en legitimatiegegevens verstrekt en dat het maximale aantal verbindingen niet is bereikt.
  • Uw berichten worden niet afgeleverd. Als dit gebeurt, controleer dan of je bent geabonneerd op hetzelfde kanaal waarop je berichten verstuurt (lijkt gek, maar gebeurt soms). Zorg er ook voor dat u de bericht handler voor het bellen abonneren (), en dat je belt abonneren () in één instantie voordat u belt publiceren() op de andere.