In the previous section, we learned how to use Docker Compose to manage the Notes application stack. Looking ahead, we can see the potential need to use multiple instances of the Notes container when we deploy to Docker Swarm on AWS EC2. In this section, we will make a small modification to the Docker Compose file for an ad hoc test with multiple Notes containers. This test will show us a couple of problems. Among the available solutions are two packages that fix both problems by installing a Redis instance.
A common tactic for handling high traffic loads is to deploy multiple service instances as needed. This is called horizontal scaling, where we deploy multiple instances of a service to multiple servers. What we’ll do in this section is learn a little about horizontal scaling in Docker by starting two Notes instances to see how it behaves.
As it currently exists, Notes stores some data—the session data—on the local disk space. As orchestrators such as Docker Swarm, ECS, and Kubernetes scale containers up and down, containers are constantly created and destroyed or moved from one host to another. This is done in the name of handling the traffic while optimizing the load on the available servers. In this case, whatever active data we’re storing on a local disk will be lost. Losing the session data means users will be randomly logged out. The users will be rightfully upset and will then send us support requests asking what’s wrong and whether we have even tested this thing!
In this section, we will learn that Notes does not behave well when we have multiple instances of svc-notes. To address this problem, we will add a Redis container to the Docker Compose setup and configure Notes to use Redis to solve the two problems that we have discovered. This will ensure that the session data is shared between multiple Notes instances via a Redis server.
Let’s get started by performing a little ad hoc testing to better understand the problem.
1. Testing session management with multiple Notes service instances
We can easily verify whether Notes properly handles session data if there are multiple svc-notes instances. With a small modification to compose- local/docker-compose.yml, we can start two svc-notes instances, or more. They’ll be on separate TCP ports, but it will let us see how Notes behaves with multiple instances of the Notes service.
Create a new service, svc-notes-2, by duplicating the svc-notes declaration. The only thing to change is the container name, which should be svc-notes-2, and the published port, which should be port 3020.
For example, add the following to compose-local/docker-compose.yml:
svc-notes-2:
build: ../notes
container_name: svc-notes-2
depends_on:
– db-notes
networks:
– frontnet
– svcnet
ports:
– “3020:3020”
restart: always environment:
PORT: “3020”
This is the service definition for the svc-notes-2 container we just described. Because we set the PORT variable, the container will listen on port 3020, which is what is advertised in the ports attribute.
As before, when we quickly reconfigured the network configuration, notice that a simple edit to the Docker Compose file was all that was required to change things around.
Then, relaunch the Notes stack, as follows:
$ docker-compose up
In this case, there was no source code change, only a configuration change. Therefore, the containers do not need to be rebuilt, and we can simply relaunch with the new configuration.
That will give us two Notes containers on different ports. Each is configured as normal; for example, they connect to the same user authentication service. Using two browser windows, visit both at their respective port numbers. You’ll be able to log in with one browser window, but you’ll encounter the following situation:
The browser window on port 3020 is logged out, while the window open to port 3000 is logged in. Remember that port 3020 is svc-notes-2, while port 3000 is svc-notes. However, as you use the two windows, you’ll observe some flaky behavior with regard to staying logged in.
The issue is that the session data is not shared between svc-notes and svc- notes-2. Instead, the session data is in files stored within each container.
We’ve identified a problem whereby keeping the session data inside the container makes it impossible to share session data across all instances of the Notes service. To fix this, we need a session store that shares the session data across processes.
2. Storing Express/Passport session data in a Redis server
Looking back, we saw that we might have multiple instances of svc-notes deployed on Docker Swarm. To test this, we created a second instance, svc-notes-2, and found that user sessions were not maintained between the two Notes instances. This told us that we must store session data in a shared data storage system.
There are several choices when it comes to storing sessions. While it is tempting to use the express-session-sequelize package, because we’re already using Sequelize to manage a database, we have another issue to solve that requires the use of Redis. We’ll discuss this other issue later.
Redis is a widely used key-value data store that is known for being very fast. It is also very easy to install and use. We won’t have to learn anything about Redis, either.
Several steps are required in order to set up Redis:
- In compose-local/docker-compose.yml, add the following definition to the services section:
redis:
image: “redis:5.0”
networks:
– frontnet
container_name: redis
This sets up a Redis server in a container named redis. This means that other services wanting to use Redis will access it at the host named redis.
For any svc-notes services you’ve defined (svc-notes and svc- notes-2), we must now tell the Notes application where to find the Redis server. We can do this by using an environment variable.
- In compose-local/docker-compose.yml, add the following environment variable declaration to any such services:
svc-notes: # Also do this for svc-notes-2
…
environment:
REDIS_ENDPOINT: “redis”
Add this to both the svc-notes and svc-notes-2 service declarations. This passes the Redis hostname to the Notes service.
- Next, install the package:
$ cd notes
$ npm install redis connect-redis –save
This installs the required packages. The redis package is a client for using Redis from Node.js and the connect-redis package is the Express session store for Redis.
- We need to change the initialization in mjs to use the connect- redis package in order to store session data:
import session from ‘express-session’;
import sessionFileStore from ‘session-file-store’;
import ConnectRedis from ‘connect-redis’;
const RedisStore = ConnectRedis(session);
import redis from ‘redis’;
var sessionStore;
if (typeof process.env.REDIS_ENDPOINT !== ‘undefined’
&& process.env.REDIS_ENDPOINT !== ”) {
const RedisStore = ConnectRedis(session);
const redisClient = redis.createClient({
host: process.env.REDIS_ENDPOINT
});
sessionStore = new RedisStore({ client: redisClient });
} else {
const FileStore = sessionFileStore(session);
sessionStore = new FileStore({ path: “sessions” });
}
export const sessionCookieName = ‘notescookie.sid’;
const sessionSecret = ‘keyboard mouse’;
This brings in the Redis-based session store provided by connect-redis.
This imports the two packages and then configures the connect-redis package to use the redis package. We consulted the REDIS_ENDPOINT environment variable to configure the redis client object. The result landed in the same sessionStore variable we used previously. Therefore, no other change is required in app.mjs.
If no Redis endpoint is specified, we instead revert to the file-based session store. We might not always deploy Notes in a context where we can run Redis; for example, while developing on our laptop. Therefore, we require the option of not using Redis, and, at the moment, the choice looks to be between using Redis or the filesystem to store session data.
With these changes, we can relaunch the Notes application stack. It might help to relaunch the stack using the following command:
$ docker-compose up –build –force-recreate
Because source file changes were made, the containers need to be rebuilt. These options ensure that this happens.
We’ll now be able to connect to both the Notes service
on http://localhost:3000 (svc-notes) and the service
on http://localhost:3020 (svc-notes-2), and it will handle the login session on both services.
Another issue should be noted, however, and this is the fact that real-time notifications are not sent between the two servers. To see this, set up four browser windows, two for each of the servers. Navigate all of them to the same note. Then, add and delete some comments. Only the browser windows connected to the same server will dynamically show changes to the comments. Browser windows connected to the other server will not.
This is the second horizontal scaling issue. Fortunately, its solution also involves the use of Redis.
3. Distributing Socket.IO messages using Redis
While testing what happens when we have multiple svc-notes containers, we found that login/logout was not reliable. We fixed this by installing a Redis-based session store to keep session data in a place that is accessible by multiple containers. But we also noticed another issue: the fact that the Socket.IO-based messaging did not reliably cause updates in all browser windows.
Remember that the updates we want to happen in the browser are triggered by updates to the SQNotes or SQMessages tables. The events emitted by updating either table are emitted by the server making the update. An update happening in one service container (say, svc-notes-2) will emit an event from that container, but not from the other one (say, svc-notes). There is no mechanism for the other containers to know that they should emit such events.
The Socket.IO team provides the socket.io-redis package as the solution to this problem. It ensures that events emitted through Socket.IO by any server will be passed along to other servers so that they can also emit those events.
Since we already have the Redis server installed, we simply need to install the package and configure it as per the instructions. Again, we will not need to learn anything about Redis:
$ npm install socket.io-redis –save
This installs the socket.io-redis package. Then, we configure it in app.mjs, as follows:
export const io = socketio(server);
io.use(passportSocketIo.authorize({
…
}));
import redisIO from ‘socket.io-redis’;
if (typeof process.env.REDIS_ENDPOINT !== ‘undefined’
&& process.env.REDIS_ENDPOINT !== ”) {
io.adapter(redisIO({ host: process.env.REDIS_ENDPOINT, port: 6379
}));
}
The only change is to add the lines in bold. The socket.io-redis package is what the Socket.IO team calls an adapter. Adapters are added to Socket.IO by using the io.adapter call.
We only connect this adapter if a Redis endpoint has been specified. As before, this is so that Notes can be run without Redis as needed.
Nothing else is required. If you relaunch the Notes application stack, you will now receive updates in every browser window connected to every instance of the Notes service.
In this section, we thought ahead about deployment to a cloud-hosting service. Knowing that we might want to implement multiple Notes containers, we tested this scenario on our laptop and found a couple of issues. They were easily fixed by installing a Redis server and adding a couple of packages.
We’re getting ready to finish this chapter, and there’s one task to take care of before we do. The svc-notes-2 container was useful for ad hoc testing, but it is not the correct way to deploy multiple Notes instances. Therefore, in compose- local/docker-compose.yml, comment out the svc-notes-2 definition.
This gave us some valuable exposure to a new tool that’s widely used—Redis. Our application now also appears to be ready for deployment. We’ll take care of that in the next chapter.
Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.