안녕하세요! NewCodes입니다!
어느덧 멤버십 11주가 지나갔네요.. 허허
이번에도 어김없이 회고해보겠습니다!
Week4 현재의 아키텍처
1️⃣ [멀티 프로세스] Nest 서버를 멀티 프로세스로 띄우고, NginX로 로드 밸런싱
더보기
- 개요
- 실시간 게임에서는 즉각적인 처리, 성능이 중요
- 한 서버 인스턴스에서 여러 프로세스를 돌리고자 함
- 멀티 프로세스를 통해 응답 시간을 최대한 줄이는 게 목적
- 첫 번째로 시도했던 방법
- 3000번, 3001번 포트 각각에서 Nest 프로세스 실행
- NginX에서 80번 포트로 요청을 받아 3000번, 3001번으로 로드 밸런싱 설정
- 만났던 문제
- 소켓 연결이 안 되고, polling 요청만 지속적으로 들어옴
- pm2 로그 열어보면 소켓 연결됐다는 로그가 안 찍힘
- NginX error 로그는 안 찍히고, access에서는 지속적으로 polling을 시도
- 시도해봤던 것들
- NginX 설정을 조금씩 계속해서 추가하거나 삭제해보며 설정 내에서 원인을 찾으려 함 → 결국 문제 해결X
- 소켓 연결이 아닌 API는 제대로 로드밸런싱이 되는지 테스트 → API는 로드 밸런싱이 제대로 되었음
- 로드 밸런싱을 우선 내려두고, NginX를 거쳐서 하나의 was에 소켓 연결하는 걸 시도함 (예전에는 was에 직접 소켓 연결하는 방식이었음) → 이것 조차도 되지 않음
- 처음부터 한다는 마음가짐으로 처음부터 NginX 다시 설정함 → 똑같은 문제 봉착
- chatGPT, claude에게 무한 질문 → 해답을 얻지 못함 (이미 시도해봤던 것들)
- 결국 백엔드 팀원들에게 도움 요청
- 팀원에게 도움을 요청해 원인을 파악할 수 있었음
- 주요 원인: 80번 포트로 모든 요청을 받고 있었어서 라우팅이 꼬임
- 소켓 연결이 80번 포트로 /game 으로 요청하는 것이었음
- 프론트엔드 내부에서도 80번 포트로 /game으로 라우팅하여 보여주는 페이지가 있었음
- // main.tsx
<Route path="/" element={<MainPage />} />
<Route path="/game/setup" element={<GameSetupPage />} />
<Route path="/game/:gameId" element={<GamePage />} />
<Route path="/quiz/setup" element={<QuizSetupPage />} />
<Route path="*" element={<div>not found</div>} /> - 같은 포트와 라우트를 다른 목적으로 쓰고 있어서 문제가 발생했던 것이었음
- 결국 해결했던 방법
- NginX에서 80번 포트로 오는 건 프론트엔드 라우팅, 백엔드 API 라우팅
- NginX에서 3333번 포트로 오는 건 소켓 연결로 3000번, 3001번로 로드밸런싱
- 네이버 클라우드 콘솔에서 ACG Inbound에 3333번 포트 새롭게 열어줌
- NginX 설정
# /etc/nginx/sites-available/quiz-ground upstream backend-socket { ip_hash; server localhost:3000; server localhost:3001; } upstream backend-api { #라운드 로빈 server localhost:3000; server localhost:3001; } server { listen 80; server_name _; root /home/ubuntu/nest-server/current/FE/dist; index index.html; # 프론트엔드 라우팅 location / { try_files $uri /index.html; add_header Cache-Control "no-cache"; } # 백엔드 라우팅 location /api { proxy_pass <http://backend-api>; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 정적 파일 처리 location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, no-transform"; } } server { listen 3333; server_name _; # 실제 도메인으로 변경 # Socket.io 프록시 설정 location / { proxy_pass <http://backend-socket>; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
- 문제 해결까지 오래 걸렸던 이유
- 문제가 일어났는데 어디가 원인일지 감이 전혀 안 잡혔어서
- NginX에 대해 잘 몰랐다보니 단순히 NginX 설정을 잘못해서 일어난 일인 줄 알았음
- 혼자 해결하려 해서
- 프론트엔드 라우팅 부분에 대해 인지를 못하고 있어서
- 문제가 일어났는데 어디가 원인일지 감이 전혀 안 잡혔어서
- 배운 점
- 프론트엔드 쪽을 블랙박스로 두면 안 되는 이유 깨달음 (풀스택의 중요성)
- NginX 설정 파일 읽고 쓰는 법 익숙
- 혼자 해결하기보다는 때로는 적극적으로 물어보자
- 그래도 문제 상황을 잘 정리해서 도움 구했어서 빨리 해결방안 찾은 듯
- 익숙하지 않은 새로운 작업을 한다면 기초 학습은 하고 시작하자
- 익숙하지 않은 작업에서 문제가 발생했다면, 팀원에게 물어보는 게 빨리 해결하는 방법일 수 있음
- 부차적인 것들
- 리눅스 CLI 명령어 익숙
- socket.io에서는 연결을 위해 내부적으로 polling을 시도함
2️⃣ [Race Condition] 멀티프로세스로 돌렸더니 퀴즈 채점이 되지 않는 문제 해결
더보기
- 문제 개요
- 멀티 프로세스로 돌렸더니 퀴즈 채점에 대한 결과 이벤트(endQuizTime)이 클라이언트에게 전송되지 않음
- endQuizTime이 오기 위한 조건
- if (this.scoringMap[gameId] >= playersCount)
- gameId에 해당하는 value가 현재 방 인원 수만큼 되어야 실행될 수 있음
- 즉, 해당 조건문이 false라는 건 scoringMap이 제대로 누적되지 않았을 가능성이 높음
- 원인
- scoringMap을 핸들링하는 함수가 비동기 함수로 구현되어 있으며, 이를 libuv가 스레드 풀을 이용해 동시적으로 실행하고 있습니다.
- 이 함수가 비동기적으로 호출되면서, 두 개의 비동기 작업이 별도의 스레드에서 동시에 scoringMap에 접근하게 됩니다.
- (3000번 WAS 기준, 본인의 채점 업데이트와 3001번 WAS의 채점 업데이트가 거의 동시에 실행됩니다.)
- 그러나 scoringMap은 thread-safe하지 않으며, 공유 자원 접근에 대한 동기화 처리가 부족하여 동시성 문제가 발생합니다.
- 이로 인해, 서로 다른 스레드가 동시에 값을 업데이트하면, 최종적으로 scoringMap의 상태가 예상과 다른 값으로 변경되는 문제가 나타납니다.
- 해결 방법
- Redis를 중앙 데이터 저장소로 활용
- scoringMap을 각 WAS의 로컬 메모리가 아닌 Redis에 저장하여, 모든 WAS가 중앙화된 데이터를 공유하도록 합니다.
- Redis는 단일 스레드로 동작하며, INCRBY 명령어를 통해 원자적(atomic) 연산을 보장합니다. 이를 통해 동시성 문제를 효과적으로 방지할 수 있습니다.
- Redis 구조 설계
- Redis의 키-값 구조를 사용하여 특정 게임방의 채점 상태를 관리합니다.
- Key: Room:${gameId}:CurrentScoring
- Value: 현재까지 해당 게임방에서 채점된 플레이어 수
- 새로운 플레이어의 채점 결과가 업데이트될 때, Redis의 INCRBY 명령어를 사용하여 안전하게 값을 증가시킵니다.
- Redis의 키-값 구조를 사용하여 특정 게임방의 채점 상태를 관리합니다.
- 구체적인 Redis 연산 흐름
- 채점 후 업데이트
- 각 WAS에서 특정 플레이어의 채점이 완료되면, Redis에 INCRBY 명령어로 해당 게임방의 채점 상태를 업데이트합니다.
- 이 연산은 원자적으로 실행되므로, 여러 WAS에서 동시에 실행해도 값의 충돌이 발생하지 않습니다.
- 최종 확인
- WAS는 채점 상태를 확인하기 위해 Redis에서 현재 채점된 플레이어 수를 조회합니다.
- 조회된 값이 게임방의 총 플레이어 수와 일치하면, endQuizTime 이벤트를 발송합니다.
- 채점 후 업데이트
- Redis를 중앙 데이터 저장소로 활용
- 기타 다른 해결 방법
- Lock
- Atomic 연산 (← 현재는 redis의 INCRBY로 해결)
- Semaphore
- 데이터 구조 변경 (불변 객체 같은)
🚀 위 경험 피드백
- 멘토님께 위 경험을 설명드리는 데 어려움을 겪었음
- 멀티 프로세스를 거의 처음 경험해보기도 하고, 동시성 이슈도 낯설어서 그랬었음
- 동시성 이슈, race condition, thread-safe 등 용어들이 어떤 의미인지 정확하게는 인지를 못하고 있어서 키워드로 설명을 하지 못했음 (머릿속에 구조가 잘 안 잡힌 느낌)
- 핵심부터 설명했어야 했는데, 주구절절 핵심까지 가는 과정이 너무 길었던 것 같음
- 동시성 이슈에는 구체적으로 어떤 이슈들이 있고, 여러 해결방안에 대해 더욱 분석해보고 싶음
- 지금은 redis로 하면 쉽게 될 것 같아서 redis를 우선적으로 고려했었음
- redis를 쓰기 힘든 상황도 분명 있기도 할 것이고, 다양한 해결방안을 미리 공부해두는 것이 좋다고 생각함
- 향후 블로그 글을 써보고자 함
- 향후 더 시도해볼 것
- pm2 cluster 모드 알아보기
- 물리적으로 다른 서버에 로드밸런싱 해보기
- socket.io redis adapter 쓰면 ip_hash 안 해도 되는지 알아보기
⭐️ 회고
- 다른 사람이 작성한 코드를 읽고 리팩토링하는 건 꽤 의미있는 작업인 듯
- 몰랐던 걸 더 공부하게 되는 계기
- 더 최적화하면서 공부하게 되는 계기 (’이 방식이 최선일까?’를 생각하게 되면서 개선하게 됨)
- ORM 제대로 쓰려면, 최적화하려면 은근 배울 게 많구나.
- Lazy Loading, Eager Loading
- N + 1 문제
- Dirty Checking
- Persistence Context
- 첫 설계에서의 실수가 나중에 큰 비용이 된다는 게 이런 경우일까?
- 1:n:n:n의 API가 과연 필요했을까?
- API 명세서 같은 건 시간 투자 더 해도 좋았을 듯
- 목요일에 급하게 PR을 올라가면서 나의 실수가 담긴 코드가 release 브랜치에 올라감
- 평소에 실수를 안 하려 항상 더블체크하는데, 급하다보니 실수가 나온 것 같음
- 이번 주도 운동을 소홀히 했다..
- 매번 다음 주는 운동을 해야지 다짐하지만 쉽지 않다…..
- 정신력이 약해져서 그런가? 디스인센티브를 좀 걸어봐야겠다.
🎯 다음 주의 나에게
- 다음 주는 새벽까지 달려보자! 그동안 체력 관리 잘했으니.. 마지막 스퍼트!
- 단 급하게는 x 집중하면서 차분하게 o
'회고 > 네이버 부스트캠프 9기' 카테고리의 다른 글
[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 최종 회고 (0) | 2024.12.16 |
---|---|
[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 13주 차 회고 (그룹프로젝트 week5) (0) | 2024.11.30 |
[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 11주 차 회고 (그룹프로젝트 week3) (0) | 2024.11.16 |
[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 10주 차 회고 (그룹프로젝트 week2) (0) | 2024.11.08 |
[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 9주 차 회고 (그룹프로젝트 시작!) (0) | 2024.11.03 |