WORK EXPERIENCE

실시간 센서 데이터 전송 시스템

선박에서 수집되는 센서 데이터를 외부 서버로 실시간 전송하는 데이터 연동 시스템

Java, Spring, MQTT

아키텍처

온습도 센서화재 센서도어 센서API Server센서 데이터 수신 핸들러MQTT 전송 스레드풀MQTT PublisherMQTT Broker외부 서버

개요

  • 선박 내부의 온습도와 화재, 도어 센서 데이터를 외부 서버로 실시간 전송하는 시스템
  • API Server가 수신한 센서 데이터를 MQTT 메시지로 변환해 발행

전송 방식 선택

  • HTTP Polling - 제외

    • 주기적 요청으로 실시간성이 떨어지고, 작은 센서 메시지마다 요청과 헤더 오버헤드 발생
  • Kafka - 제외

    • 이벤트 영속성과 재처리보다 단순 실시간 전달이 우선인 요구사항
    • Broker와 파티션, 컨슈머 그룹까지 운영해야 해 요구사항 대비 복잡도 큼
  • MQTT - 채택

    • 작은 센서 상태 메시지를 반복 전송하기에 적합한 경량 Pub/Sub 방식
    • MQTT Broker를 사이에 두어 API Server와 외부 서버의 직접 연결 제거
    • 온습도와 화재, 도어 센서의 평상값은 일부 누락돼도 다음 값으로 상태가 갱신되므로 QoS 0 적용
    • 실제 화재 이벤트는 유실을 막기 위해 QoS 1 적용, 중복 메시지는 수신 측에서 멱등 처리

문제

  • WMS, API, ADMIN, SOCKET으로 나뉜 애플리케이션 네 개와 MySQL, Redis가 8코어 단일 서버를 공유하는 폐쇄망 환경
  • MQTT 전송 메서드에 @Async만 붙이고 전용 executor를 지정하지 않아 사내에 이미 등록된 공통 스레드풀(pool-size="100", queue-capacity 미설정)이 그대로 사용됨
  • 400 req/s 부하가 이어지자 스레드가 최대 100개로 늘어나면서 CPU 사용률이 95%까지 상승

해결 전략

  • MQTT 전송 전용 스레드풀을 분리하고 CorePoolSize 8, MaxPoolSize 16, Queue Capacity 200으로 제한
    • 평상시 8개로 처리하고 순간적인 부하나 전송 지연이 겹칠 때만 16개까지 확장
    • Queue는 운영 유입량의 약 0.5초분만 대기시켜 무제한 적체 방지
  • Queue와 MaxPoolSize가 모두 차면 CallerRunsPolicy로 호출 스레드가 직접 처리해 추가 적체 억제

기술 선택 이유

  • 공통 스레드풀 크기 축소 - 제외

    • 크기를 줄여도 공통 풀을 쓰는 구조는 바뀌지 않아 용도에 맞는 스레드풀을 지정하는 편이 나음
  • MQTT 전송 전용 스레드풀 - 채택

    • 공통 풀을 그대로 두고 MQTT 경로의 스레드 수만 제한

검증

  • Locust로 400 req/s 수준의 MQTT 전송과 기존 서비스 트래픽인 선원 300명의 위치 측위를 함께 재현
  • 같은 부하에서 공통 풀과 MQTT 전용 풀을 각각 5분간 실행해 비교

동일 부하 전후 결과

지표기존 공통 풀MQTT 전용 풀
최대 activeCount1008
5분 시점 queueSize90020
8코어 서버 CPU 사용률90~95%55~60%

회고

  • 운영 서버가 8코어 16스레드이고 기존 트래픽도 함께 처리하고 있어 스레드 수를 8개로 설정. 스레드 수별 성능 비교 없이 정한 값임
  • 8코어에 스레드가 100개까지 늘면서 발생한 Context Switching이 CPU 사용률 상승의 주된 요인으로 보임. 다만 따로 측정하지 않아 원인으로 단정하기 어려움
  • 다시 진행한다면 스레드 수에 따른 처리량, CPU 사용률, queueSize, Context Switching을 함께 측정해 필요한 최소 스레드 수를 정해야 함