포도가게의 개발일지
[Node] Chat Server 부하줄이기(분산처리)[1] 본문
Why?
- 서비스를 발표 후 질의 중에 서버에 부하를 줄이기 위해 비즈니스로직 리팩토링이나 로드밸런싱을 한다하였는데 로드 밸런싱을 하게 되면 채팅방에 있는 유저가 서로 다른 서버에 존재할 수 있게된다. 이것을 어떻게 연결시켜줄 것인가?
How?
- 첫번째 Node에서 지원해주는 클러스터기능을 이용하는 것입니다. 이 기능은 서버의 CPU코어의 수 만큼 Worker를 만들어 여러개의 서버를 띄우는 것과 비슷한 성능을 하도록 만들어 줍니다. 이유는 node는 기본적으로 싱글쓰레드 방식으로 프로세서를 하나만 쓰게 된다. 때문에 서버 cpu가 2개이상일 경우 나머지 프로세서들이 놀기 때문에 모든 cpu가 노드서버를 하나씩 맡음으로써 요청을 분산 시킬 수 있다.(포트를 공유하는 노드 프로세스를 여러 개 둘 수 있으므로 요청이 많이 들어올 시 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있다.) -> 하지만 이번에는 이 방식을 사용하지 않는 이유로 기본적으로 지금 쓰고 있는 서버는 aws micro t2 free tier서버로 cpu가 1개 뿐이기 때문이다.
- 두번째 서버내의 포트를 여러개로 열고 포트마다 노드서버를 실행시켜서 사용자가 들어 올때마다 서버를 순차적으로 나눠주어서 서버의 부하를 줄이는 방법입니다. -> 현재는 포트를 나눠 두개의 노드 서버를 띄우고 통신이 가능한지 확인 후 auto scaling을 이용하여 채팅 부하 분산을 진행할 예정입니다.
- 세번째로, AWS auto scaling을 이용하여 요청에 따라 ec2를 띄어주어 서버 분산을 해줄겁니다. -> 최종 목표입니다.
첫번째 목표
Redis(REmote DIctionary Server)란?
레디스는 고성능 키-값 저장소로서 문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL이다.
- 레디스는 모든 데이터를 메모리에 저장하고 조회합니다. 즉, 인메모리 데이터베이스 입니다
- 데이터베이스로 사용될 수 있으며 Cache로도 사용될 수 있는 기술이다.
- 성능은 서버에 따라 다르나 초당 2만 ~ 10만회 수행한다
Redis 설치(Mac)
mac은 brew를 이용하여 쉽게 설치할 수 있습니다.
$ brew install redis
$ brew services start redis
$ brew services stop redis
$ brew services restart redis
brew services start 명령어를 통해 Redis를 실행시켜 줍니다.
$ redis-cli
위의 명령어를 통해 CLI를 사용할 수 있습니다.
Redis pub/sub 기능
- pub/sub이란 채널을 구독한 subscribe에게 모든 메세지를 전송 하는것을 의미합니다.
- pub/sub의 경우 바로 subscribe한 구독자들에게 메세지를 전송 하므로 , 메세지를 보관하지 않습니다.
- pub/sub은 주로 채팅 기능이나, 푸시 알림등에서 사용 합니다.
why
- 동일한 채널을 구독한 subscribe에게 모든 메세지를 전송하기 위해(서로 다른 서버에 있는 같은 채널 구독자에게 message를 보낼 수 있다.)
How?
redis-cli
redis-cli에 subscribe Channel(채널 이름) 을 입력합니다.
subscribe Test(채널명)
다른 콘솔을 하나 더 켠 후 redis-cli를 입력합니다. 그 후 Publish Channel(채널 이름) Message(메세지)를 입력합니다.
publish test(채널명) "hello world"(message)
- 확인해보면 "test" channel를 구독하고 있는 console에 message를 보내주는 것을 볼 수 있다. 아래 두명의 구독자 모두 message를 받는 것을 확인 할 수 있다.
Socket.io adapter Redis pub/sub 기능 활용
- 서로 다른 서버에 연결된 client가 서버로 message를 emit하고 server는 redis adapter를 이용하여 같은 channel에 있는 다른 client들에게 message를 보내게 된다.
Socket.io Adapter
- Socket.io 에서는 멀티 서버 환경에서 소켓 통신을 가능하게 하고자 Adapter 라는 것을 뒀다. Adapter는 서버 쪽에 있는 컴포넌트로 모든 클라이언트나 일부 클라이언트에게 메시지를 보내는 역할을 한다. 따로 Adapter를 선언 하지 않으면 In Memory Adapter를 사용하나 이처럼 멀티 프로세서 환경에서 클라이언트 소켓 세션을 관리하기 위해선 별도의 Custom Adapter를 사용해야 한다.
Redis Adapter를 이용하면 Redis에게 이벤트 정보를 구독하고 이벤트를 전달 받으면 이를 서버와 연결된 클라이언트 소켓에 전달하게 된다.
// socket.io-redis adpater on message 부분
// adapter가 Redis를 구독하고 신호가 오게되면 호출된다
onmessage(pattern, channel, msg) {
channel = channel.toString();
const channelMatches = channel.startsWith(this.channel);
// 이때 채널이 일치않으면 패스
if (!channelMatches) {
return debug("ignore different channel");
}
// 해당 소켓 서버에 room(channel)이 없으면 패스된다.
const room = channel.slice(this.channel.length, -1);
if (room !== "" && !this.rooms.has(room)) {
return debug("ignore unknown room %s", room);
}
const args = msgpack.decode(msg);
const [uid, packet, opts] = args;
if (this.uid === uid)
return debug("ignore same uid");
if (packet && packet.nsp === undefined) {
packet.nsp = "/";
}
if (!packet || packet.nsp !== this.nsp.name) {
return debug("ignore different namespace");
}
opts.rooms = new Set(opts.rooms);
opts.except = new Set(opts.except);
super.broadcast(packet, opts);
}
// front는 Vue를 이용
// 서로 다른 port접속을 위해 나눠 놓음
login1(){
this.conneted == false;
this.socket = io("http://localhost:8000",{
query: {
"channel": this.channel,
"userId": this.user,
}
});
this.title = "8000 port";
if(this.conneted==false){
this.connectSocket();
this.conneted=true;
}
},
login2(){
this.conneted == false;
this.socket = io("http://localhost:8001",{
query: {
"channel": this.channel,
"userId": this.user
}
});
this.title = "8001 port";
if(this.conneted==false){
this.connectSocket();
this.conneted=true;
}
}
//send message 부분
sendMessage(){
this.socket.emit("message", {userId:this.user, channel:this.channel, body:this.body});
this.message.push({userId:this.user, body:this.body});
this.body="";
},
// back-end는 nest js를 이용하였습니다.
// chat.gateway.ts 부분
@SubscribeMessage('message')
message(client:Socket, payload:object){
client.to(client.handshake.query['channel']).emit("chat", payload);
}
handleConnection(client:Socket, args:object){
client.join(client.handshake.query['channel']);
console.log(client.handshake.query);
client.to(client.handshake.query['channel']).emit("login", client.handshake.query);
}
handleDisconnect(client:Socket){
const channel:string = client.handshake.query['channel'].toString();
client.leave(channel);
}
// redis <-> server1, server2
// redis.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { RedisClient } from 'redis';
import { ServerOptions } from 'socket.io';
import { createAdapter } from 'socket.io-redis';
const pubClient = new RedisClient({ host: 'localhost', port: 6379 });
const subClient = pubClient.duplicate();
const redisAdapter = createAdapter({ pubClient, subClient });
export class RedisIoAdapter extends IoAdapter {
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.adapter(redisAdapter);
return server;
}
}
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis.adapter';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
app.useWebSocketAdapter(new RedisIoAdapter(app));
await app.listen(process.argv[2]);
}
bootstrap();
'프로젝트' 카테고리의 다른 글
[Node] Chat Server 부하줄이기(분산처리)[2] (0) | 2022.02.02 |
---|---|
Node js profiling (0) | 2021.12.07 |
heartalk stress test (0) | 2021.12.04 |
heartalk 개발 (0) | 2021.11.19 |
NestJS 초기 명령어(CLI) & 기초 (0) | 2021.11.06 |