Multi-Instance Node.js App i PaaS med Redis Pub / Sub

Om du valde PaaS som värd för din ansökan har du förmodligen eller kommer att få detta problem: Din app är distribuerad till små "behållare" (känd som dynos i Heroku, eller kugghjul i OpenShift) och du vill skala den. 

För att göra så ökar du antalet behållare, och varje instans av din app körs ganska mycket i en annan virtuell maskin. Det här är bra av ett antal skäl, men det betyder också att instanserna inte delar minnet. 

I denna handledning kommer jag att visa dig hur du kan övervinna detta lilla besvär.

När du valde PaaS-värd antar jag att du hade skalning i åtanke. Kanske har din webbplats redan sett Slashdot-effekten eller du vill förbereda dig för det. Hur som helst, vilket gör instanser att kommunicera med varandra är ganska enkelt.

Tänk på att i artikeln antar jag att du redan har en Node.js-app som skrivs och körs.


Steg 1: Redis Setup

Först måste du förbereda din Redis-databas. Jag gillar att använda Redis To Go, eftersom installationen är väldigt snabb, och om du använder Heroku finns det ett tillägg (även om ditt konto måste ha ett kreditkort tilldelat det). Det finns också Redis Cloud, som innehåller mer lagring och säkerhetskopiering.

Därifrån är Heroku-installationen ganska enkel: Välj tillägget på sidan Heroku tillägg och välj Redis Cloud eller Redis To Go, eller använd en av följande kommandon (observera att den första är för Redis To Go , och den andra är för Redis Cloud):

$ heroku addons: lägg till redistogo $ heroku addons: lägg till rediscloud

Steg 2: Ställa in node_redis

Vid denna tidpunkt måste vi lägga till den obligatoriska nodmodulen till package.json fil. Vi använder den rekommenderade node_redis-modulen. Lägg till den här raden i din package.json fil, i avhängighetssektionen:

"node_redis": "0.11.x"

Om du vill kan du också inkludera hiredis, ett högpresterande bibliotek skrivet i C, vilket node_redis kommer att använda om det är tillgängligt:

"hiredis": "0.1.x"

Beroende på hur du skapade din Redis-databas och vilken PaaS-leverantör du använder kommer anslutningsinställningen att se lite annorlunda ut. Du behöver värd, hamn, Användarnamn, och Lösenord för din anslutning.

Heroku

Heroku lagrar allt i config-variablerna som URL-adresser. Du måste extrahera den information du behöver från dem med Node url modulen (config var för Redis To Go är process.env.REDISTOGO_URL och för Redis Cloud process.env.REDISCLOUD_URL). Den här koden går överst på din huvudapplikationsfil:

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

Övriga

Om du skapade databasen för hand eller använd en annan leverantör än Heroku, bör du ha anslutningsalternativ och behörighetsuppgifter redan så använd bara dem:

var redis = kräver ("redis"); var klient = redis.createClient (YOUR_HOST, YOUR_PORT); client.auth (ditt_lösenord);

Därefter kan vi börja arbeta med kommunikation mellan instanser.


Steg 3: Skicka och ta emot data

Det enklaste exemplet kommer bara att skicka information till andra instanser som du just har börjat. Du kan till exempel visa denna information i adminpanelen.

Innan vi gör någonting, skapa en annan ansluten namn client2. Jag kommer att förklara varför vi behöver det senare.

Låt oss börja med att bara skicka meddelandet som vi började. Det är gjort med hjälp av publicera() metod för kunden. Det tar två argument: Kanalen vi vill skicka meddelandet till och meddelandets text:

client.publish ("instanser", "start"); 

Det är allt du behöver för att skicka meddelandet. Vi kan lyssna på meddelanden i meddelande händelsehanterare (observera att vi kallar detta på vår andra klient):

client2.on ("message", funktion (kanal, meddelande) 

Återuppringningen passeras samma argument som vi överför till publicera() metod. Låt oss nu visa denna information i konsolen:

om ((kanal == 'instanser') och (meddelande == 'start')) console.log ('Ny instans startad!'); );

Det sista att göra är att faktiskt prenumerera på den kanal vi ska använda:

client2.subscribe ( 'fall');

Vi använde två klienter för detta eftersom när du ringer prenumerera() På klienten byts anslutningen till abonnent läge. Från den tidpunkten är de enda metoderna du kan ringa på Redis-servern PRENUMERERA och SÄGA UPP. Så om vi är i abonnent läge vi kan publicera() meddelanden.

Om du vill kan du också skicka ett meddelande när instansen stängs av - du kan lyssna på SIGTERM händelse och skicka meddelandet till samma kanal:

process.on ('SIGTERM', funktion () client.publish ('instances', 'stop'); process.exit ();); 

För att hantera det fallet i meddelande handlare lägg till detta annars om där inne:

annars om ((kanal == 'instanser') och (meddelande == 'stopp')) console.log ('Instance stopped!');

Så ser det ut så här efteråt:

client2.on ("message", funktion (kanal, meddelande) if ((channel == 'instances') och (message == 'start')) console.log ('Ny instans startad!'), annars om (kanal == 'instanser') och (meddelande == 'stopp')) console.log ('Instance stopped!'););

Observera att om du testar på Windows, stöder den inte SIGTERM signal.

För att testa den lokalt, starta programmet några gånger och se vad som händer i konsolen. Om du vill testa uppsägningsmeddelandet ska du inte utfärda Ctrl + C kommando i terminalen-istället, använd döda kommando. Observera att detta inte stöds i Windows, så du kan inte kontrollera det.

Använd först ps kommando för att kontrollera vilken id din process har-rör den till grep för att underlätta:

$ ps -aux | grep your_apps_name 

Den andra kolumnen i utgången är det ID som du tittar på. Tänk på att det också kommer att finnas en rad för det kommando du bara sprang. Utför nu döda kommando med 15 för signalen-det är det SIGTERM:

$ kill -15 PID

PID är ditt process-ID.


Real-World Exempel

Nu när du vet hur du använder Redis Pub / Sub-protokollet kan du gå utöver det enkla exemplet som presenterades tidigare. Här är några användarfall som kan vara till hjälp.

Express sessioner

Det här är mycket användbart om du använder Express.js som ramverk. Om din ansökan stöder användarinloggningar eller nästan allt som använder sessioner, vill du se till att användarnas sessioner bevaras, oavsett om instansen startas om, flyttar användaren till en plats som hanteras av en annan eller användaren Växlas till en annan instans eftersom den ursprungliga gick ner.

Några saker att komma ihåg:

  • De fria Redis-instanserna kommer inte att räcka: du behöver mer minne än de 5 MB / 25 MB som de tillhandahåller.
  • Du behöver en annan anslutning för detta.

Vi behöver connect-redis-modulen. Versionen beror på vilken version av Express du använder. Den här är för Express 3.x:

"connect-redis": "1.4.7"

Och detta för Express 4.x:

"connect-redis": "2.x"

Skapa nu en annan Redis-anslutning som heter client_sessions. Modulens användning beror igen på Express-versionen. För 3.x skapar du RedisStore så här:

var RedisStore = kräver ('connect-redis') (express)

Och i 4.x måste du passera express-session som parameter:

var session = kräver ("express-session"); var RedisStore = kräver ('connect-redis') (session);

Därefter är inställningen densamma i båda versionerna:

app.use (session (store: new RedisStore (client: client_sessions), hemlighet: "din hemliga sträng"));

Som du kan se passerar vi vår Redis-klient som klient egenskapen till objektet som passerade till RedisStores konstruktör, och sedan passerar vi affären till session konstruktör.

Nu om du startar din app, loggar du in eller startar en session och startar om instansen, kommer din session att bevaras. Detsamma händer när förekomsten byts till användaren.

Utbyte av data med WebSockets

Låt oss säga att du har en helt separerad instans (arbetare dyno på Heroku) för att göra mer resursätande arbete som komplicerade beräkningar, bearbeta data i databasen eller utbyta mycket data med en extern tjänst. Du kommer att vilja ha de "normala" fallen (och därmed användarna) att veta resultatet av det här arbetet när det är klart.

Beroende på om du vill att webinstanserna ska skicka data till arbetstagaren behöver du en eller två anslutningar (låt oss namnge dem client_sub och client_pub på arbetaren också). Du kan också återanvända alla anslutningar som inte prenumererar på något (som det du använder för Express-sessioner) istället för client_pub.

Nu när användaren vill utföra åtgärden publicerar du meddelandet på den kanal som är reserverad bara för den här användaren och för det här specifika jobbet:

// detta går in i din förfrågan handler client_pub.publish ("JOB: USERID: JOBNAME: START", JSON.stringify (THEDATAYOUWANTTOSEND)); client_sub.subscribe ( 'JOBB: USERID: JOBB: PROGRESS');

Självklart måste du ersätta ANVÄNDAR ID och JOBB NAMN med lämpliga värden. Du borde också ha meddelande handlare förberedd för client_sub förbindelse:

klient_sub.on ("meddelande", funktion (kanal, meddelande) var USERID = channel.split (':') [1], om (meddelande == 'DONE') client_sub.unsubscribe (kanal); uttag [USERID] .emit (kanal, meddelande););

Detta extraherar ANVÄNDAR ID från kanalnamnet (så se till att du inte prenumererar på kanaler som inte är relaterade till användarjobb på den här anslutningen) och skickar meddelandet till lämplig klient. Beroende på vilket WebSocket-bibliotek du använder kommer det att finnas något sätt att komma åt ett uttag med sitt ID.

Du kanske undrar hur arbetstagarens förekomst kan prenumerera på alla dessa kanaler. Självklart vill du inte bara göra några loopar på allt som möjligt ANVÄNDAR IDs och JOBB NAMNs. De psubscribe () Metoden accepterar ett mönster som argumentet, så det kan prenumerera på alla JOBB:* kanaler:

// den här koden går till arbetstagarens instans // och du kallar det en gång client_sub.psubscribe ('JOB: *')

Vanliga problem

Det finns några problem du kan stöta på när du använder Pub / Sub:

  • Din anslutning till Redis-servern avvisas. Om detta händer, se till att du anger lämpliga anslutningsalternativ och referenser, och att det maximala antalet anslutningar inte har uppnåtts.
  • Dina meddelanden levereras inte. Om detta händer, kontrollera att du prenumererar på samma kanal som du skickar meddelanden på (verkar dumt, men händer ibland). Se också till att du bifogar meddelande handler innan du ringer prenumerera(), och det du ringer till prenumerera() i ett fall innan du ringer publicera() på den andra.