실시간 센서 데이터 전송 시스템
선박에서 수집되는 센서 데이터를 외부 서버로 실시간 전송하는 데이터 연동 시스템
아키텍처
개요
- 선박의 온습도·화재·도어 센서 데이터를 외부 서버로 실시간 전송하는 시스템
- API Server가 수신한 센서 데이터를 MQTT 메시지로 변환해 발행
- WMS·API·ADMIN·SOCKET으로 분리된 네 애플리케이션과 MySQL·Redis가 8코어 단일 서버에서 함께 동작하는 국방 폐쇄망 환경
기술 선택 이유
-
HTTP Polling — 제외
- 주기적 요청으로 실시간성이 떨어지고, 작은 센서 메시지마다 요청·헤더 오버헤드 발생
-
Kafka — 제외
- 이벤트 영속성·재처리보다 단순 실시간 전달이 우선인 요구사항
- Broker·파티션·컨슈머 그룹까지 운영해야 해 요구사항 대비 복잡도 큼
-
MQTT — 채택
- 작은 센서 상태 메시지를 반복 전송하기에 적합한 경량 Pub/Sub 방식
- MQTT Broker를 사이에 두어 API Server와 외부 서버의 직접 연결 제거
- 온습도·화재·도어 센서의 평상값은 일부 누락돼도 다음 값으로 상태가 갱신되므로 QoS 0 적용
- 실제 화재 이벤트는 유실을 막기 위해 QoS 1 적용, 중복 메시지는 수신 측에서 멱등 처리
문제
- MQTT 전송을
@Async로 처리하며pool-size="100"인 공통 스레드풀 사용 - 400 req/s 부하가 이어지자 스레드가 최대 100개까지 늘고, 서버 CPU가 90~95%까지 상승하며 처리량이 유입량 아래로 감소
- 처리하지 못한 작업이 상한 없는 큐에 쌓이면서 대기시간이 최대 6–7초까지 증가
- 같은 서버의 위치 측위 요청도 함께 지연돼 서비스 운용에 영향
해결 전략
- MQTT 전송 전용 스레드풀을 분리하고 CorePoolSize 8, MaxPoolSize 16, Queue Capacity 200으로 제한
- Queue와 MaxPoolSize가 모두 차면 CallerRunsPolicy로 호출 스레드가 직접 처리해 추가 적체 억제
튜닝 기준
- CorePoolSize 8 · MaxPoolSize 16: 평상시 8개로 처리하고 순간적인 부하나 전송 지연이 겹칠 때만 16개까지 확장
- Queue Capacity 200: 운영 유입량의 약 0.5초분만 대기시켜 무제한 적체 방지
검증
테스트 설정
- Locust로 센서 400개의 데이터 수신 트래픽과 선원 300명의 위치 측위 트래픽을 함께 재현해 공통 풀과 MQTT 전용 풀을 30분씩 비교
- 완료 처리량·큐 적재량·대기시간을 측정하고 4·6·8개 스레드에서 큐 포화 여부 확인
- 8·16·32개 스레드의 처리량·서버 CPU·API 프로세스 Context Switching을 비교해 과도한 스레드의 비용 확인
동일 부하 전후 결과
| 지표 | 기존 공통 풀 | MQTT 전용 풀 |
|---|---|---|
| 최대 activeCount | 100 | 8 |
| 30분 시점 queueSize | 2,800 | 60 |
| 큐 대기시간 p95 | 5s | 150ms |
| 최대 큐 대기시간 | 7s | 500ms |
| 8코어 서버 CPU | 90–95% | 55–65% |
최소 스레드 수 검증
| 스레드 수 | 평균 완료 처리량 | 큐 상태 |
|---|---|---|
| 4 | 약 320 msg/s | 200건 도달 후 포화 지속 |
| 6 | 약 390 msg/s | 순간 전송 지연 시 200건 반복 도달 |
| 8 | 약 400 msg/s | 30분 시점 60건, 지속 증가 없음 |
- 8개부터 평균 400 msg/s를 유지하고 큐가 지속해서 늘지 않아 최소 스레드 수로 결정
8개 이상 스레드 수 비교
| 스레드 수 | 평균 처리량 | 서버 평균 CPU | API CS/s |
|---|---|---|---|
| 8 | 약 400 msg/s | 60% | 3.0K |
| 16 | 약 400 msg/s | 64% | 4.2K |
| 32 | 약 400 msg/s | 73% | 6.5K |
- 세 설정 모두 약 400 msg/s를 처리했지만, 스레드가 늘수록 CPU 사용량과 Context Switching 증가