Firejune님이 작성하신 Cluster를 이용한 Node.JS의 멀티-코어 서버 관리를 관련된 내용이 약간 나옵니다. 물론 지금은(Node.js v0.6.0 이상) Node.js에 Cluster 모듈이 기본모듈로 추가되었습니다. 서버는 보통 Stateless상태가 좋지만 Socket.IO는 그 특성상 많은 정보를 stateful하게 관리해야 하므로 내부적으로 MemoryStore를 사용해서 상태를 관리하고 있습니다. 이는 Socket.IO를 사용할 때 의식조차 못할 정도로 내부에서 처리가 됩니다. 하지만 스케일-아웃을 위해서 Cluster모듈을 사용하면 상황이 달라집니다. 다음은 간단히 작성한 Cluster와 Socket.IO를 함께 사용한 예제입니다.
// app.js
var express = require('express')
, routes = require('./routes')
, cluster = require('cluster')
, sio = require('socket.io');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', routes.index);
app.listen(3000);
io = sio.listen(app);
io.configure(function(){
io.set('log level', 1);
io.set('transports', [
'websocket'
, 'flashsocket'
, 'htmlfile'
, 'xhr-polling'
, 'jsonp-polling'
]);
});
io.on('connection', function(socket) {
console.log('connected by process #' + process.env.NODE_WORKER_ID);
socket.on('message', function(msg) {
console.log('message processed by process #' + process.env.NODE_WORKER_ID);
socket.send(msg);
socket.broadcast.send(msg);
});
});
}
express로 기본 생성한 템플릿코드에 Socket.IO를 연결하고 Cluster 모듈을 이용해서 워커에서 동작하도록 작성한 것입니다. Socket.IO와 Cluster의 사용법을 보신적이 있다면 그다지 어렵지 않은 코드이므로 기능 설명은 생략합니다.
// views/layout.jade
!!! 5
html
head
meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
title Socket.IO Test
link(rel='stylesheet', href='/stylesheets/style.css')
script(src='http://code.jquery.com/jquery-1.7.2.min.js')
script(src='/socket.io/socket.io.js')
body!= body
layout.jade파일은 HTML5로 변경하고 jquery와 socket.io.js를 불러왔습니다.
// views/index.jade
form
input(type='text', style='width:400px;')#message
input(type='submit', value='전송')
script
$(function() {
var socket = io.connect();
socket.on('connect', function() {
printMessage('connected');
});
socket.on('message', function(msg) {
printMessage(msg);
});
$('form').submit(function(e) {
e.preventDefault();
socket.send($('#message').val());
$('#message').val('');
});
var contents = $('#contents');
printMessage = function(msg) {
contents.append($('<p>').text(msg));
contents.scrollTop(contents.height());
}
});
index.jade는 기존의 코드는 지우고 채팅처럼 간단히 메시지를 표시할 DIV 영역과 input박스를 만들어서 페이지 접속시 Socket.IO 서버에 연결하고 메시지를 주고받을 수 있도록 하였습니다. 이 페이지에 접속한 사람들간에 메세지를 주고 받습니다.
Cluster를 사용하면 보통 Master프로세스를 띄우고 CPU 갯수만큼 워커 프로세스를 실행한 뒤 각 워커프로세스가 실제 웹서버같은 어플리케이션을 실행하게 됩니다. 이는 프로세스가 분리된 것이므로 서로간에 상태공유같은 것을 하지 않고 당연히 MemoryStore도 각 프로세스마다 생성되므로 서로간에 공유가 되지 않습니다. 즉, 워커프로세스를 2개 띄운 경우 처음 접속은 1번 프로세스가 처리했지만 메시지 통신을 했을 때는 2번 프로세스가 받았을 경우 2번 프로세스에는 접속정보가 없으므로 다시 연결을 시도하게 되고 이러한 상황이 계속해서 반복됩니다. 이러한 상황은 다음 화면에서 볼 수 있습니다.(접속했을 때와 메시지를 받았을 때 처리하는 워커프로세스의 번호를 표시하도록 했습니다. 제 맥이 듀얼코어이므로 2개의 워커로 동작합니다.)
프로세스가 분리되어 있으므로 당연한 얘기입니다. 여러번 테스트를 해보았지만 연결후 메시지통신을 할 때 워커프로세스가 바뀌어서 다시 연결되는 경우도 있고 한번 접속된 워커로 계속 연결되는 경우도 있었습니다. 같은 워커로 접속되었을 경우에 같은 워커에 붙은 다른 사용자와는 대화할 수가 있었지만 다른 워커프로세스의 사용자에게는 메시지가 전달되지 않았습니다.(어떤 워커프로세스에서 실행되는지 확인하기 위해서 로그 메시지로 현재 프로세스 ID를 출력했습니다.) 위의 Firejune님이 얘기하신 것과 마찬가지로 Socket.IO 개발팀은 이 문제를 Redis를 이용해서 외부 스토어로 해결했습니다. 소스는 동일하지만 다음과 같이 store로 RedisStore를 사용하도록 설정합니다.
// app.js
...
io.configure(function(){
io.set('log level', 1);
io.set('transports', [
'websocket'
, 'flashsocket'
, 'htmlfile'
, 'xhr-polling'
, 'jsonp-polling'
]);
io.set('store', new sio.RedisStore);
});
...
달라진 부분은 io.set('store', new sio.RedisStore); 부분 뿐입니다. 이렇게 Socket.IO가 내부에서 Redis클라이언트 모듈을 포함하고 있기 때문에 이렇게만 설정하면 스토어로 Redis를 사용합니다. 물론 접근할 수 있는 Redis가 있어야 하면 아무런 값도 지정하지 않았으므로 기본값인 127.0.0.1:6379로 접근합니다. 이 정보로 Redis에 접속하지 못하면 다음과 같은 오류가 발생합니다.
Redis를 스토어로 사용하면 다음과 같이 다른 워커에 사용자들이 접속했더라도 서로간에 메시지를 잘 주고 받을 수 있습니다.
Socket.IO 내부 소스를 보면 RedisStore는 다음과 같은 옵션을 사용할 수 있습니다.
- nodeId (fn) : 이 노드를 유일하게 식별할 수 있는 id를 리턴한다
- redis (fn) : redis 생성자로 기본값은 redis이다
- redisPub (object) : pub redis client에 전달할 옵션
- redisSub (object) : sub redis client에 전달할 옵션
- redisClient (object) : 일반적인 redis client에 전달할 옵션
- pack (fn) : 커스텀 패킹, 기본값은 메시지팩이 설지되어 있으면 메시지팩이고 그렇지 않으면 JSON이다
- unpack (fn) : 커스텀 패킹, 기본값은 메시지팩이 설지되어 있으면 메시지팩이고 그렇지 않으면 JSON이다
기본설정대신 다른 Redis에 접속하려면 Redis 클라이언트 객체를 만들어서 전달해야 합니다. 총 3가지가 필요한데 Pub 클라이언트, Sub 클라이언트, 일반용도의 클라이언트 이렇게 세 가지입니다. 제가 Redis는 이번에 처음 써봐서 세부사항은 잘 모르는데 Pub/Sub 클라이언트는 Redis에서 지원하는 리얼타임용 노티피케이션의 역할을 하는 것으로 보입니다. 어떻게 사용하는 지에 대해서는 Socket.IO에 문서화가 거의 되어 있지 않아서(Socket.IO는 문서화가 좀 더 되어야 된다고 생각합니다.) RedisStore and rooms with Socket.IO를 참고했습니다. 임의의 Redis 클라이언트를 사용하려면 다음과 같이 설정합니다.
// app.js
...
, sio = require('socket.io')
, redis = require('socket.io/node_modules/redis');
var numCPUs = require('os').cpus().length;
var pub = redis.createClient(8888, "127.0.0.1");
var sub = redis.createClient(8888, "127.0.0.1");
var store = redis.createClient(8888, "127.0.0.1");
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
...
io.configure(function(){
...
io.set('store', new sio.RedisStore({
redis: redis
, redisPub: pub
, redisSub: sub
, redisClient: store
}));
});
...
앞에서 얘기한대로 Pub 클라이언트, Sub 클라이언트, 일반용도의 클라이언트 세개의 Redis 클라이언트가 필요하기 때문에 먼저 redis 모듈을 불러옵니다. redis를 설치할 수 도 있지만 설치된 Socket.IO안에 이미 Redis모듈이 설치되어 있으므로 이를 불러옵니다. Redis서버가 로컬에 8888 포트로 실행되어 있다고 가정하면 위처럼 Redis클라이언트를 세개 생성합니다. 그리고 io.set('store')에서 옵션으로 각 클라이언트를 설정해서 전달해서 사용할 수 있습니다.
cluster 를 공부하다가 오게 되었습니다. :)
궁금한 것이 fork()는 새로운 프로세스를 생성하는 건데
테스트를 해보니 555 포트로 열었다고 가정 할 경우에
isten callback 안에서 로그를 찍으면cpu 수만큼 리스닝을 하는데
같은 포트로 여는게 불가능 한건데, 이런 현상이 어떻게 일어나는지 이해가 잘 되질 않네요^^:
설명 부탁드립니다~
의문을 가진것처럼 로그를 그렇게 출력해 줄 뿐이지 당연히 프로세스가 같은 포트를 리스닝하는 것은 아닙니다. 한 포트를 여럿이서 쓸 수는 없죠. 제가 C 개발자는 아니라서 코드레벨로 설명드리기는 어렵지만 포트는 마스터 프로세스가 열고 포트에 대한 핸들만 자식 프로세스로 전달하는 방식입니다.
http://nodejs.sideeffect.kr/docs/v0.8.15/api/cluster.html#cluster_how_it_works
위소스를 카피하여 실행했는데 계속 접속을 못합니다.
npm install socket.io 되어 있구요..
err : redis connect to 127.0.0.1:6379........................
error가 발생하면서 종료됩니다.
제가 해볼 수 해결 책??????? 부탁....
아래 두 부분에서 error가 발생됩니다.
제생각엔 redis가 접속이 잘 안되는 것 같습니다.
socket.io는 npm에서 install했고 따로 redis을 인스톨 했고 익스프레스도 install
----
var pub = redis.createClient(8888, "127.0.0.1");
var sub = redis.createClient(8888, "127.0.0.1");
var store = redis.createClient(8888, "127.0.0.1");
----
io.configure(function(){ ... io.set('store', new sio.RedisStore({ redis: redis , redisPub: pub , redisSub: sub , redisClient: store })); });
어떤 해결책이 있을까요??????
redis을 사용하지 않는 1번은 잘 실행됩니다....
제pc을 3대 바꿔 가면서 test을 해봐도 다른건 다
문제 없는데 redis연결이 안되네요
그래서 간단하게
var redis = require('socket.io/node_modules/redis'),
//var redis = require('redis'),
client = redis.createClient();
client.on("error", function (err) {
console.log("error event - " + client.host + ":" + client.port + " - " + err);
});
이렇게 연결해도 redis연결 error가 발생됩니다...
설치된 socket.io 버전이 어떻게 되시나요?
redis를 8888로 띄우신것 같은데 앞에서는 8888포트로 접속하시고 나중 소스에서는 기본포트를 사용하셔서 정확한 환경을 알기가 어렵습니다. 로컬에서 마지막에 주신 소스로 테스트해보았는데 레디스에 접속에 오류는 발생하지 않았습니다.
안녕하세요. cluster 관련 문서 찾다가 여기까지 왔습니다.
다름이 아니라...
socket-io.redis 모듈이 있더군요. cluster 적용하면서 해당 모듈을 적용해 봤습니다.
cluster 를 사용하지 않으면 redis 부분도 문제가 없는데 cluster 를 적용한 순간 룸에 연결된 소켓들은 자꾸 끊기게 됩니다.
룸에 대한 처리는 달리 가야 하는걸까요? ㅠㅠ
혹 가능하시다면 도움 좀 부탁 드려 봅니다. 한참을 찾아봐도 답이 보이질 않아 어렵게 질문 까지 드리게 되었습니다.
그럼 즐거운 하루 보내세요.
redis랑 클러스터를 많이 안써봐서 주신 내용만으로 답변 드리기는 어려운데 말씀하신 걸로 봐서는 레디스연결 코드에 문제가 있지 않나 합니다. redis를 안쓰면 내부 메모리에만 있으므로 재연결을 할 때 원래의 연결을 찾아올 수가 있는데 클러스터는 이렇게 동작하지 않으므로 이러한 정보를 레디스에서 얻어와야 하는데 말씀하신 부분으로는 여전히 redis는 연결되었지만 socket.io가 내부 메모리에서 정보를 찾는게 아닐까 합니다.