I have 3 nodejs apps running on a GCP compute engine instance(2cpu, 2GB ram, ubuntu 20.04) with Nginx reverse proxy. One of them is a socket.io chat server. The socket.io app uses #socket.io/cluster-adapter to utilize all available CPU cores.
I followed this tutorial to update the Linux settings to get maximum number of connections. Here is the output of ulimit command,
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7856
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 500000
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7856
virtual memory (kbytes, -v) unlimited
cat /proc/sys/fs/file-max
2097152
/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 30000;
# multi_accept on;
}
...
/etc/nginx/sites-available/default
...
//socket.io part
location /socket.io/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy false;
proxy_pass http://localhost:3001/socket.io/;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
...
My chat server code,
const os = require("os");
const cluster = require("cluster");
const http = require("http");
const { Server } = require("socket.io");
const { setupMaster, setupWorker } = require("#socket.io/sticky");
const { createAdapter, setupPrimary } = require("#socket.io/cluster-adapter");
const { response } = require("express");
const PORT = process.env.PORT || 3001;
const numberOfCPUs = os.cpus().length || 2;
if (cluster.isPrimary) {
const httpServer = http.createServer();
// setup sticky sessions
setupMaster(httpServer, {
loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection"
});
// setup connections between the workers
setupPrimary();
cluster.setupPrimary({
serialization: "advanced",
});
httpServer.listen(PORT);
for (let i = 0; i < numberOfCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
}
//worker process
else {
const express = require("express");
const app = express();
const Chat = require("./models/chat");
const mongoose = require("mongoose");
const request = require("request"); //todo remove
var admin = require("firebase-admin");
var serviceAccount = require("./serviceAccountKey.json");
const httpServer = http.createServer(app);
const io = require("socket.io")(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
transports: "websocket",
});
mongoose.connect(process.env.DB_URL, {
authSource: "admin",
user: process.env.DB_USERNAME,
pass: process.env.DB_PASSWORD,
});
app.use(express.json());
app.get("/", (req, res) => {
res
.status(200)
.json({ status: "success", message: "Hello, I'm your chat server.." });
});
// use the cluster adapter
io.adapter(createAdapter());
// setup connection with the primary process
setupWorker(io);
io.on("connection", (socket) => {
activityLog(
"Num of connected users: " + io.engine.clientsCount + " (per CPU)"
);
...
//chat implementations
});
}
Load test client code,
const { io } = require("socket.io-client");
const URL = //"https://myserver.com/";
const MAX_CLIENTS = 6000;
const CLIENT_CREATION_INTERVAL_IN_MS = 100;
const EMIT_INTERVAL_IN_MS = 300; //1000;
let clientCount = 0;
let lastReport = new Date().getTime();
let packetsSinceLastReport = 0;
const createClient = () => {
const transports = ["websocket"];
const socket = io(URL, {
transports,
});
setInterval(() => {
socket.emit("chat_event", {});
}, EMIT_INTERVAL_IN_MS);
socket.on("chat_event", (e) => {
packetsSinceLastReport++;
});
socket.on("disconnect", (reason) => {
console.log(`disconnect due to ${reason}`);
});
if (++clientCount < MAX_CLIENTS) {
setTimeout(createClient, CLIENT_CREATION_INTERVAL_IN_MS);
}
};
createClient();
const printReport = () => {
const now = new Date().getTime();
const durationSinceLastReport = (now - lastReport) / 1000;
const packetsPerSeconds = (
packetsSinceLastReport / durationSinceLastReport
).toFixed(2);
console.log(
`client count: ${clientCount} ; average packets received per second: ${packetsPerSeconds}`
);
packetsSinceLastReport = 0;
lastReport = now;
};
setInterval(printReport, 5000);
As you can see from the code, I'm only using websocket for transports. So, it should be able to serve up to 8000 connections as per this StackOverflow answer. But when I run the load test, the server becomes unstable after 1600 connections. And CPU usage goes up to 90% and memory usage up to 70%. I couldn’t find anything in the Nginx error log. How can increase the number of connections to at least 8000? Should I upgrade the instance or change any Linux settings? Any help would be appreciated.
UPDATE
I removed everything related to clustering and ran it again as a regular single-threaded nodejs app. This time, the result was a little better, 2800 stable connections (CPU usage 40%., memory usage 50%). Please note that I'm not performing any disk I/O during the test.
You are using the cluster adapter, which is not meant to be used with sticky sessions. You should use the Redis adapter instead. Each worker will connect to Redis and will be able to communicate with each other. You can also use the Redis adapter with sticky sessions, but you will need to use the Redis adapter on the primary process as well.
To answer your another question:
"if I remove sticky session and use only websocket, will the workers be able to communicate to each other?"
Yes, the workers will be able to communicate to each other. I don't think it's a good idea to use sticky sessions for a chat application. You should use a pub/sub system like Redis or NATS to communicate between the workers. For example, you can use Redis to publish a message to a channel and the other workers will receive the message and send it to the client.
When you use sticky sessions, each worker will be connected to a single client. So, if you have 4 workers, you will be able to serve 4 clients at the same time. If you use the cluster adapter, each worker will be connected to all the clients. So, if you have 4 workers, you will be able to serve 4 * 4 clients at the same time. So, you will be able to serve more clients with the cluster adapter.
Example of using the Redis adapter:
const { createAdapter } = require("socket.io-redis");
const io = require("socket.io")(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
transports: "websocket",
});
io.adapter(createAdapter("redis://localhost:6379"));
Example of using the NATS adapter:
const { createAdapter } = require("socket.io-nats");
const io = require("socket.io")(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
transports: "websocket",
});
io.adapter(createAdapter("nats://localhost:4222"));
Try both options and see which works best for you.
Related
I'm using a Fastify server to send SSE events to a React front-end.
While everything worked well locally, I'm having issues once deployed behind Nginx. The front-end and the server aren't on the same domain and although I set the cors origin to be "*" on the server, and the other call resolve without issue, for the server-sent-events endpoint only I get
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/events. (Reason: CORS request did not succeed). Status code: (null).
Here's how Fastify is configured, using #fastify/cors and fastify-sse-v2
import fastifyCors from "#fastify/cors";
import FastifySSEPlugin from "fastify-sse-v2";
// ...
await this.instance.register(FastifySSEPlugin, { logLevel: "debug" });
await this.instance.after();
await this.instance.register(fastifyCors, { origin: "*" });
Then sending events based on postgres pubsub with:
await pubsub.addChannel(TxUpdateChannel);
reply.sse(
(async function* () {
for await (const [event] of on(pubsub, TxUpdateChannel)) {
yield {
event: event.name,
data: JSON.stringify(event.data),
};
}
})()
);
On the front-end I use eventsource so that I can add Authorization headers:
import EventSource from "eventsource";
// ...
const source = new EventSource(`${SERVER_URL}/transaction/events`, {
headers: {
'Authorization': `Bearer ${jwt}`,
},
});
source.onmessage = (event) => {
console.log('got message', event)
getUserData()
}
source.onopen = (event) => {
console.log('---> open', event)
}
source.onerror = (event) => {
console.error('Event error', event)
}
The problem was in the nginx configuration. Thanks to EventSource / Server-Sent Events through Nginx I solved it using the following placed into the location of the nginx conf :
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
i have a self signed grpc service on server, and got it working for dart server with dart client.
But i could not figure how to bypass or allow self signed certificate for node client..
I've tried this:
const sslCreds = await grpc.credentials.createSsl(
fs.readFileSync('./ssl/client.crt'),
null, // privatekey
null, // certChain
{
checkServerIdentity: function(host, info) {
console.log('verify?', host, info);
if (
host.startsWith('127.0.0.1') ||
host.startsWith('logs.example.com')
) {
return true;
}
console.log('verify other?', host);
return true;
},
},
);
// sslCreds.options.checkServerIdentity = checkCert;
const gLogClient = new synagieLogGrpc.LoggerClient(
'host:port',
sslCreds,
);
but when i call, my validation checkServerIdentity did not call.
anyone have any clue?
after checking out multiple github issues, and testing for 2 days,
this code below works.
critical point is, actual host:port is the destination, which could be localhost. but we will need to override the ssl target name with the actual generated ssl domain.
for sample of tls generation:
https://github.com/grpc/grpc-node/issues/1451
const host = 'localhost';
const port = 8088;;
const hostPort = `${host}:${port}`;
const gLogClient = new synagieLogGrpc.LoggerClient(hostPort, sslCreds, {
'grpc.ssl_target_name_override': 'actual_tlsdomain.example.com',
});
This is the code I used to connect http server.
var app = require('http').createServer(require('express')),
io = require('socket.io').listen(app),
util = require('util'),
connectionsArray = [], // maintain active connected client details
connectionStatistics = {'summary': {'instance_count': 0, 'user_count': 0, 'customer_count': 0}, 'customers': {}}, // for debugging purpose
server_port = 3000, // port on which nodejs engine to run
POLLING_INTERVAL = 10 * 1000, // 10 sec
pollingTimer = [], // timeouts for connected sockets
fs = require('fs'), // lib for file related operations
log_file = {
'error': fs.createWriteStream(__dirname + '/debug.log', {flags: 'a'}), // file to log error messages
'info': fs.createWriteStream(__dirname + '/info.log', {flags: 'a'}) // file to log info messages
};
var server = app.listen(server_port, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Please use your browser to navigate to http://%s:%s', host, port);
});
I want to include https connection in the above code.
I tried to connect https using SSLCertificateFile and SSLCertificateKeyFile.
But it didn't work for me.
install https module ( yarn add https / npm i https )
change options (ssl file path) as below :
const options = {
key: fs.readFileSync('./ssl/private.key'),
cert: fs.readFileSync('./ssl/certificate.crt'),
ca:fs.readFileSync('./ssl/ca_bundle.crt')
}
https.createServer(options, app).listen(port);
Try this snippet using express and https module instead of http
let fs = require('fs');
let https = require('https');
let express = require('express');
let app = express();
let options = {
key: fs.readFileSync('./file.pem'),
cert: fs.readFileSync('./file.crt')
};
let serverPort = 3000;
let server = https.createServer(options, app);
let io = require('socket.io')(server);
io.on('connection', function(socket) {
console.log('new connection');
});
server.listen(serverPort, function() {
console.log('server up and running at %s port', serverPort);
});
I'm quite confused. I was testing this application on localhost with Mamp and everything was working fine but when moved to the development server the client stop receiving messages from server. I'm using it inside a Vuejs component.
On the client I've logged socket.on('connect') and the second check is returning true.
This is my code:
Server
var server = require ('http').Server();
var io = require ('socket.io')(server);
var Redis = require ('ioredis');
var redis = new Redis();
redis.subscribe('chat');
redis.on('message', (channel, message) => {
message = JSON.parse(message);
// channel:event:to_id:to_type - message.data
io.emit(channel + ':' + message.event + ':' + message.to_id + ':' + message.to_type, message.data);
console.log(message +' '+ channel);
});
server.listen('6001');
Client
var io = require('socket.io-client')
var socket = io.connect('http://localhost:6001', {reconnect: true});
...
mounted() {
console.log('check 1', socket.connected);
socket.on('connect', function() {
console.log('check 2', socket.connected);
});
socket.on('chat:newMessage:'+this.fromid+':'+this.fromtype, (data) => {
console.log('new message');
var message = {
'msg': data.message,
'type': 'received',
'color': 'green',
'pos': 'justify-content-start',
}
this.messages.push(message);
});
}
Nginx conf
upstream node1 {
server 127.0.0.1:6001;
}
server {
listen 80;
server_name localhost;
location / {
#Configure proxy to pass data to upstream node1
proxy_pass http://node1/socket.io/;
#HTTP version 1.1 is needed for sockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
thanks a lot!
Ok I've found a solution.
I was using localhost
I've replaced
var socket = io.connect('http://localhost:6001', {reconnect: true});
With:
var socket = io.connect('http://'+ window.location.hostname +':6001', {reconnect: true});
And now everything is working fine.
I was wondering if anyone could tell me what the default HTTP request timeout is when using express.
What I mean by this is: after how many seconds of dealing with a http request will the Express / Node.js server close the connection, when the browser nor server closed the connection manually?
How do I alter this timeout for a single route? I would like to set it to about 15 minutes for a special audio conversion route.
Thanks a lot.
Tom
req.connection.setTimeout(ms); appears to set the request timeout for a HTTP server in Node.js.
req.connection.setTimeout(ms); might be a bad idea since multiple requests can be sent over the same socket.
Try connect-timeout or use this:
var errors = require('./errors');
const DEFAULT_TIMEOUT = 10000;
const DEFAULT_UPLOAD_TIMEOUT = 2 * 60 * 1000;
/*
Throws an error after the specified request timeout elapses.
Options include:
- timeout
- uploadTimeout
- errorPrototype (the type of Error to throw)
*/
module.exports = function(options) {
//Set options
options = options || {};
if(options.timeout == null)
options.timeout = DEFAULT_TIMEOUT;
if(options.uploadTimeout == null)
options.uploadTimeout = DEFAULT_UPLOAD_TIMEOUT;
return function(req, res, next) {
//timeout is the timeout timeout for this request
var tid, timeout = req.is('multipart/form-data') ? options.uploadTimeout : options.timeout;
//Add setTimeout and clearTimeout functions
req.setTimeout = function(newTimeout) {
if(newTimeout != null)
timeout = newTimeout; //Reset the timeout for this request
req.clearTimeout();
tid = setTimeout(function() {
if(options.throwError && !res.finished)
{
//throw the error
var proto = options.error == null ? Error : options.error;
next(new proto("Timeout " + req.method + " " + req.url) );
}
}, timeout);
};
req.clearTimeout = function() {
clearTimeout(tid);
};
req.getTimeout = function() {
return timeout;
};
//proxy end to clear the timeout
var oldEnd = res.end;
res.end = function() {
req.clearTimeout();
res.end = oldEnd;
return res.end.apply(res, arguments);
}
//start the timer
req.setTimeout();
next();
};
}
The default request timeout in Node v0.9+ is 2 minutes. That is what express uses.
You can increase it for a single route using:
app.get('/longendpoint', function (req, res) {
req.setTimeout(360000); // 5 minutes
...
});
connect-timeout or any other generally don't work.
What always works is setting the timeout in nginx domain/subdomain level like this:
location / {
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_pass http://xxxx:3009;
}