Node 설계
* 참고 - 경험담의 개발 환경은 아래서버 환경에 따라 설계 되었음을 참고. 더 좋은 설계 방식이 있을 수 있음!
1. 물리서버 한 대에 노드, 엘라스틱 서버 사용
2. MongoDB 는 다른 서비스가 동작하는 물리서버에 올려서 사용
처음 노드 설계 했을 때 100명의 사용자 기준으로 소켓 서버 한 대면 충분하지 않을까 하고 생각했었다.
그래서 서버 한 대에 소켓, API 모두 사용하고 있었는데 가오픈 후에 서버가 버티지 못하는 것을 확인할 수 있었다.
대안은 두 가지가 있었는데,
1. 소켓 어뎁터로 서버를 증설
→ 소켓 통신은 백엔드와 프론트 1대 1 양방향 통신이지만 서버가 여러 대가 되면 n대 1이 되므로 어뎁터 필요.
→ 주어진 물리서버가 한 대 뿐이었기 때문에 노드, 엘라스틱, 레디스까지 들어가기엔 우려되어 몽고 어뎁터 테스트 진행.
→ 테스트 시 1대의 몽고디비에 너무 많은 부담을 주기 싫기도 했고,
몽고 어뎁터는 비교적 최근에 나왔기 때문에 이슈에 취약할 것 같아서 포기
2. Socket 서버, API 서버를 따로 두어 소켓 서버는 간단한 처리만 할 수 있도록 분리
→ 노드는 수평적으로 서버 크기를 늘리기 때문에 클러스터링 처리를 하여 요청을 분산시켜야한다.
하지만 소켓 서버는 어뎁터를 두지 않으면 늘릴 수 없기 때문에 클러스터링 처리도 불가능...
→ Socket 서버 1대 / API 서버 클러스터링 처리 하여 운영하기로 결정!
위와 같은 이유로 대안 2번이 채택되었다.
Socket 서버
[ 소켓 구조 ]
- "/" (기본 네임스페이스) : 채팅방 초대 등 모든 사용자에게 보내져야할 이벤트 수신/발신용으로 사용
- "/채팅방코드" : 채팅방 별로 각각 네임스페이스를 만들어서 채팅방 참여자가 채팅방을 선택했을 때 연결하여 사용
소켓 구조 설계 시 채팅방 하나 당 하나의 네임스페이스로 이루어지도록 설계 하였다.
( 사실, namespace vs room 으로 서칭해보아도 어떤 방식으로 설계하는 것이 옳다 그르다 하는 명쾌한 답변은 볼 수 없었다. 그래서 Socket IO 에 대해 무지한 상태로 여러 글을 읽어보고, 당시에 옳다고 생각한 방향으로 설계한 것이라 이것 보다 효율적인 방식이 있을 수 있다고 생각한다. )
[ 소켓 처리 방식 설계 ]
1. 클라이언트에서 처음 로그인 후에 기본 네임스페이스 연결
2. 30초마다 연결해야할 네임스페이스가 있는지 백에서 판단하여 채팅방 목록과 함께 리턴
( 채팅방 참여자가 채팅방을 선택했을 때 속해있는 모든 참여자에게 소켓으로 연결 필요 여부를 보내서
연결하는 것이 더 효율적. 하지만 소켓으로 통신 시 누락 가능성이 있어 30초마다 재조회로 변경 )
3. 클라이언트에서 리턴받은 연결할 네임스페이스 목록 연결
4. 연결 요청 들어왔을 때 네임스페이스 내부에 이미 연결된 소켓이 존재하는지 판단하여 연결 혹은 연결 안함 처리
( 네임스페이스 내부에 유저별 room 생성. 각 하나의 소켓만 존재하도록 처리 - 중복 연결 방지를 위해 )
[ 설계 시 주의점 및 발생 이슈 ]
1. 소켓의 이용을 최대한 줄이자.
처음에 한 대의 소켓서버로 알파 버전을 운용해보았는데, 100명의 통신을 감당하려니 서버가 지속적으로 죽었다.
모니터링을 해봤을 때 소켓 연결 및 이벤트 처리가 많아, CPU 점유율이 너무 높아서 죽은 것으로 판단되었다.
그래서 정말 필요한 기능을 제외하고는 소켓 이용을 최소화하려고 노력했고 그 외 기능 대부분을 API 서버로 이전했다.
( 사실 어뎁터로 클러스터링 처리 + API 튜닝 과정이 있었다면 더 양질의 서버를 운용할 수 있었을거라 생각한다. )
2. Namespace 신규 생성 후 room 생성 및 연결이 잘 되지 않음.
이건 Socket IO 문제로 판단되었던 이슈인데
새로 Namespace 생성 > 네임스페이스 연결 하면서 유저별 room 생성 > room 에 소켓 참여시킴
위 과정으로 소켓 연결 처리를 하려고 하니, room 에 소켓이 계속해서 참여되지 않는 오류가 있었다.
그래서 주기적으로 room 내부에 소켓이 하나도 없는 네임스페이스들을 정리해주는 스케줄러를 만들어서 돌렸다.
3. 서버에서 소켓 연결 해제 이후에도 여전히 남아있는 소켓
강제로 소켓 연결을 해제 시켰고 disconnect 이벤트가 발생된 것을 확인했으나,
여전히 남아있는 소켓들이 소켓 수 증가와 메모리 누수를 일으키고 있는 것을 발견했다.
강제 소켓 연결 해제 명령어로도 해결되지 않았지만 시간이 지나면 해제 되는 것으로 보아 연결 대기 상태로 추측했다.
(해결하지는 못했으나 유저 수가 적어 서버에 큰 문제가 없어 일단 보류했음...)
https://github.com/socketio/socket.io/issues/2427
Socket.io memory doesn't drop of after clients disconnect · Issue #2427 · socketio/socket.io
I use the most simple example var app = require('http').createServer(handler) var io = require('socket.io')(app); var fs = require('fs'); app.listen(1994); function handler(req, res) { res.end('soc...
github.com
4. 서버를 분리하고 최적화를 진행했음에도 소켓서버가 가끔 죽는 문제 발생
찾아보니 소켓 서버 파일 크기, 포트 범위 핸들링이 필요하다는 글이 있어서 파일을 추가하여 도커 실행파일에 추가 & 해결!
# /etc/sysctl.d/net.ipv4.ip_local_port_range.conf
# 포트 범위
net.ipv4.ip_local_port_range = 10000 65535
# /etc/security/limits.d/custom.conf
# 파일 최대 수
* soft nofile 102400
* hard nofile 102400
5. 참고
- 소켓 연결 시 transports "websocket" 권장
: polling 방식 등 여러 방식으로 소켓 연결이 가능하지만, 그 중에서 websocket 이 가장 권장되는 방식 - 많은 네임스페이스를 사용할 경우 "cleanupEmptyChildNamespaces: true" 권장
: 소켓이 연결되지 않은 자식 네임스페이스를 제거
( https://socket.io/docs/v4/server-options/#cleanupemptychildnamespaces )
이어서 다음 글에 API 서버 클러스터링 처리 및 몽고디비 트랜젝션 처리 방법을 게시해보겠다...
'📚 프로젝트' 카테고리의 다른 글
Flutter / Node 로 Window 메신저 개발기 - 몽고디비 도커 빌드하기 (2) (5) | 2024.09.04 |
---|---|
Flutter / Node 로 Window 메신저 개발기 - 들어가며 (1) (1) | 2024.08.27 |
KDX 유통·소비 데이터 분석 & 시각화 경진대회 소비트렌드 코리아 2020 (0) | 2021.12.29 |
화풍(웹툰, 명화) 예측 서비스 프로젝트 (0) | 2021.12.29 |
코로나 19 데이터 시각화 프로젝트 - 시각화 및 데이터 분석 (0) | 2021.12.29 |