실시간 센서 데이터 전송 시스템
CPU 사용률 90%→60%선박 센서(온·습도, 화재, 도어) 데이터를 수집해 외부 서버로 실시간 전송하는 데이터 연동 시스템
JavaSpringMQTT
아키텍처
개요
- 선박 내 온습도, 화재, 도어 센서 데이터를 외부 서버로 실시간 전송하는 시스템
- 여러 전송 방식을 비교한 후 MQTT 채택
- Worker Server에서 센서 데이터를 수신해 MQTT 메시지로 변환·발행
문제
- MQTT 전송을
@Async로 처리하면서 전용 스레드풀을 지정하지 않아, 기존 공통 코드의 기본 스레드풀(CorePoolSize 100 + 무제한 큐)을 그대로 사용 - 트래픽 버스트 시 Context Switching 증가로 처리량이 저하되고, 그로 인해 큐 적체와 Queueing Delay 증가
- 피크 시점에 CPU 사용률이 90%까지 상승
해결 전략
- MQTT 전송 전용 스레드풀을 별도로 정의해 기존 스레드풀과 분리
- CorePoolSize 100에서 8로 축소하고 Queue Capacity 50으로 제한
- Queue 포화 시 호출 스레드가 직접 전송하는 CallerRunsPolicy로 전환해 유입 속도 조절
기술 선택 이유
-
HTTP Polling 제외
- 주기적 요청 구조라 실시간성이 떨어지고 Worker–외부 서버 간 결합도가 높음
- 요청마다 연결·헤더 비용이 붙어 작은 센서 메시지 대비 HTTP 오버헤드가 큼
-
Kafka 제외
- 이벤트 영속성, 재처리, 대규모 스트림 처리보다 단순 실시간 전달 요구가 우선
- Broker 운영, 파티션, 컨슈머 그룹 관리까지 포함하면 요구사항 대비 운영 복잡도 과도
-
MQTT
- 작고 반복적인 센서 상태 메시지를 낮은 오버헤드로 지속 전송하는 요구에 적합
- Pub/Sub 구조로 Worker Server와 외부 서버 간 결합도 감소
- 온습도 데이터는 최신 상태 중심이라 QoS 0 적용 가능
- 화재·도어 이벤트는 유실 허용도가 낮아 QoS 1 기준으로 재전송 보장
- QoS 1 중복 수신은 센서 ID와 이벤트 시각 기준 멱등 처리로 흡수
튜닝 기준
-
CorePoolSize 8
- 기존 공통 풀(CorePoolSize 100)은 8코어 환경 대비 스레드가 과도해, 버스트 시 다수 스레드가 코어를 두고 경합하며 Context Switching·스케줄링 비용만 키움
- 전송 작업이 짧아 8개로도 피크 초당 100건을 적체 없이 소화 가능함을 부하 테스트로 확인하고 코어 수에 맞춰 축소
- 스레드를 더 늘려도 처리량은 늘지 않고 스케줄링·메모리 비용만 증가하는 구간이라 판단
-
Queue Capacity 50
- 순간 버스트를 흡수하되 Queueing Delay가 커지지 않도록 코어 수의 수 배 수준(50)으로 설정
- 피크 초당 100건 기준 약 0.5초분 버퍼로, 짧은 적체는 흡수하고 그 이상은 거절 정책으로 처리
-
거절 정책 (CallerRunsPolicy)
- 큐가 가득 차면 호출 스레드가 직접 전송을 수행해 유입 속도 조절
- 스레드를 무한정 늘리지 않으면서 버스트를 흡수
검증
테스트 설정
- Worker Server 8코어 환경
- Locust로 센서 100개 동시 접속 재현
- 부하 산정 기준: 선박 1대 기준 센서 100개 모델
- 센서별 초당 1회 전송, 전체 피크 초당 100건
- 2분 ramp-up 후 10분간 지속 부하 수행
- 부하 구간 동안
top으로 Worker Server의 피크 CPU 사용률을 측정해 전후 비교 - 전송 스레드풀을 기존 기본 풀(100 + 무제한 큐)에서 전용 풀(8 + Queue 50 + CallerRuns)로 변경하며 전후 비교
측정 결과
- 전용 스레드풀 적용 후 피크 CPU 사용률 90%에서 60%로 감소
- 큐 적체로 인한 MQTT 전송 지연 감소
배운 점
- MQTT는 작고 잦은 센서 메시지를 낮은 오버헤드로 전송하고 QoS로 유실 허용도를 차등할 수 있어 적합
- 비동기 작업은 작업 특성에 맞는 전용 스레드풀·큐·거절 정책을 지정해야 기본 풀의 스레드 폭증과 큐 적체를 막을 수 있음