- 트랙픽 관리 서버는 클라이언트가 소켓 서버에 연결되기 전에 어떤 소켓 서버로 가면 좋을지 알려주는 서버입니다.
- 여러 소켓 서버로 나누면 트래픽을 분산은 가능한데 같은 룸에 조인할 수 없는 문제가 발생했습니다.
- 따라서 같은 룸에 조인할 클라이언트들을 같은 소켓 서버로 보내고 이를 중계 해주는 트래픽 관리 서버를 만들었습니다.
- 방향성은 크게 두가지였습니다.
- 트래픽 관리 서버에서 라운드 로빈 방식으로 소켓 URL을 제공해주는 방식입니다.
- 각 소켓 서버에서 자신의 소켓 상태를 주기적으로 업데이트하고 트래픽 관리 서버에서 최종적인 소켓 서버 URL 반환합니다.
- 라운드 로빈 방식은 각 소켓 서버에 대한 정보를 순차적으로 제공하는 것입니다.
- 다만 각 소켓 서버 인스턴스 성능 차이가 있고 어느 정도 부하가 있을지 정확한 지표를 모르기 때문에 트래픽을 관리한다는 측면에 문제가 있습니다.
- 소켓 서버고 메시지를 저장하거나 메모리를 사용하는 로직이 없습니다.
- 즉, 각 소켓 서버에 얼마나 많은 요청이 들어가고 나가는지가 중요하다고 생각했습니다.
- 따라서 CPU가 좀 더 정확한 지표라고 생각했습니다.
- 각 소켓 서버는 5초에 한번 서버의 CPU 사용량을 측정하여 이 정보를 종합해 5분에 한번 트래픽 관리 서버로 보냅니다.
- 이 과정에서 CPU를 측정하는 일이 메인 스레드의 작업을 방해 할 수 있었습니다.
- 따라서 CPU 측정 일을 워커 스레드에게 맡긴 후 소켓 서버가 트래픽 관리 서버에 정보를 보낼때만 메인 스레드를 활용했습니다.
😾 프로세스
- 클라이언트는 소켓 서버에 연결하기전 룸에 대한 정보와 함께 트래픽 관리 서버로 API 요청을 보냅니다.
- 만약 이미 생성된 룸에 대한 정보를 보내온다면 해당 룸과 소켓 서버 URL을 매칭 시켜 놓은 정보를 토대로 URL을 반환합니다.
- 만약 새로운 룸이라면 가장 부하가 적은 소켓 서버의 URL을 반환합니다.
- 이제 클라이언트는 적절한 소켓 URL을 받고 이를 통해 접속하게 됩니다.
- 같은 채팅방의 클라이언트 1,2,3,4,5가 채팅을 주고 받기 위해 소켓 서버로 연결합니다.
- 하지만 소켓 서버에 분산되어 배치되었고 서로 채팅을 주고받을 수 없게 되었습니다.
- 이제 트래픽 관리 서버를 만들어 클라이언트는 룸과 매핑된 소켓 서버 URL을 반환받습니다.
시그널링 서버와 트래픽 관리 서버에 대해 순차적으로 정리해보겠습니다.
private publishSocketInfo() {
const socketUrl = this.configService.get<string>('SOCKET_URL');
const message = {
url: socketUrl,
};
this.client.publish('register', JSON.stringify(message));
this.scheduling();
}
private subscribe() {
this.client.subscribe('register');
this.client.on('message', async (channel, message) => {
const data = JSON.parse(message);
if (channel === 'register') {
const { url } = data;
this.handleRegister(url);
}
});
}
private scheduling() {
cron.schedule('*/5 * * * *', () => {
const connectionCount = this.webRtcGateway.getConnectionCnt();
const message = {
url: this.configService.get<string>('SOCKET_URL'),
connections: connectionCount,
};
this.client.publish('signaling', JSON.stringify(message));
});
}
private subscribe() {
this.client.subscribe('signaling');
this.client.on('message', async (channel, message) => {
const data = JSON.parse(message);
if (channel === 'signaling') {
const { url, connections } = data;
this.handleSignaling(url, connections);
}
});
}
@Post('signaling/join')
async create(@Body() data: SignalingConnectionDto) {
const response: ReturnConnectionsDto =
this.eventService.findSignalingServer(data);
return response;
}
findSignalingServer(data: SignalingConnectionDto): ReturnConnectionsDto {
const { roomName } = data;
const isServer = this.roomToUrl.get(roomName);
if (isServer) {
const result: ReturnConnectionsDto = { url: isServer };
return result;
}
const server = this.serverToUrl.get('signaling');
this.roomToUrl.set(roomName, server);
const result: ReturnConnectionsDto = { url: server };
return result;
}
import { parentPort } from 'worker_threads';
import * as os from 'os';
function calculateCpuUsage() {
const cpus = os.cpus();
let totalIdleTime = 0;
let totalWorkTime = 0;
for (const cpu of cpus) {
for (const type in cpu.times) {
totalWorkTime += cpu.times[type];
}
totalIdleTime += cpu.times.idle;
}
const totalUsage = 100 - (100 * totalIdleTime) / totalWorkTime;
parentPort?.postMessage(totalUsage);
}
setInterval(calculateCpuUsage, 5000);