초기 우려사항:
이 시스템은 여러 컴포넌트가 HTTP로 연결된 분산 아키텍처입니다:
Robot → (ROS2) → malle_service → (HTTP) → AI Service → (HTTP) → Web Service
HTTP 통신의 잠재적 문제:
대안으로 고려했던 것:
"HTTP 통신이 병목이 될까? UDP를 필수적으로 적용해야 할까?"
이를 확인하기 위해 성능 측정을 진행했습니다.
전체 파이프라인 지연 시간 측정
구간별 병목 지점 파악
궁극적으론, UDP 적용 필요성 판단
요구사항:
가설:
아래 2개 상황일 경우, UDP 전환 없이도 충분히 실시간성을 확보할 수 있음:
검증 계획:
1분간 메시지를 연속 전송하며 다음 요소를 측정:
결과를 바탕으로 UDP 전환 필요 여부를 결정하고자 했습니다.
이번에 진행한 Malle 프로젝트는 다중 로봇 제어 시스템으로, 여러 대의 자율주행 로봇을 동시에 관리하고 모니터링합니다. 각 로봇의 센서 데이터를 수집하여 AI로 분석하고, 그 결과를 웹 대시보드에서 실시간으로 확인할 수 있습니다.
프로젝트 배경:
통신 흐름은 Sequence Diagram으로 미리 설계했습니다.
| 컴포넌트 | 기술 스택 | 포트 | 역할 |
|---|---|---|---|
| Robot Publisher (다이어그램 내 bot) | ROS2 Humble | - | 센서 데이터 발행 (다중 로봇) |
| malle_service (다이어그램 내 malle server) | ROS2 + FastAPI | 8000 | ROS Subscriber + 중앙 조정 |
| malle_ai_service (다이어그램 내 malle AI server) | Flask | 5000 | AI 데이터 분석 |
| malle_web_service (다이어그램 내 malle web server) | FastAPI | 8001 | WebSocket 브로드캐스트 |
| Web UI (다이어그램 내 malle UI server) | HTML/JS | 3000 | 실시간 모니터링 |
설계 배경:
다중 로봇 시스템에서는 각 메시지의 출처를 명확히 식별하는 것이 필수입니다. 단순히 센서 데이터만 보내면:
따라서 풍부한 메타데이터를 포함한 Header 구조를 설계했습니다:
# RobotMessage (ROS2 Custom Message)
Header header
string message_id # UUID - 메시지 고유 식별자 (중복 감지용)
int64 timestamp_sec # Unix timestamp (초) - 발행 시간
int64 timestamp_nsec # 나노초 - 고정밀 타임스탬프
string robot_id # 로봇 식별자 (예: "robot_001", "robot_002")
string message_type # 메시지 타입 (예: "status", "alert", "command")
int32 priority # 우선순위 (긴급 메시지 처리용)
int32 sequence # 시퀀스 번호 (메시지 순서 보장)
# Body (실제 센서 데이터)
float64 battery # 배터리 잔량 (%)
string robot_status # 상태 ("running", "idle", "error")
string command # 제어 명령
string error_message # 에러 메시지
각 필드의 역할:
| 필드 | 용도 | 예시 |
|---|---|---|
message_id | 메시지 중복 제거, 추적 | "7f939-3a0a-4742-91cc" |
robot_id | 로봇 식별 | "robot_001", "robot_A" |
timestamp_* | 메시지 발행 시간 (성능 측정) | 1770365590.123456789 |
message_type | 메시지 분류 | "status", "alert" |
priority | 긴급도 (높을수록 우선 처리) | 1 (일반), 5 (긴급) |
sequence | 순서 보장 (패킷 손실 감지) | 0, 1, 2, 3... |
문제 상황:
# 로봇에서 타임스탬프 설정
publish_time = time.time()
msg.header.timestamp_sec = int(publish_time)
# malle_service에서 수신
receive_time = datetime.now()
ros_latency = (receive_time - publish_time).total_seconds()
# 결과: 1.684초 (비정상적으로 큼)
원인:
time.time()과 datetime.now()의 미묘한 차이가 있었음.해결:
# 수신 시간을 기준점으로 통일
def listener_callback(self, msg):
start_time = datetime.now()
asyncio.run(self.process_message(msg, start_time))
ROS2는 DDS 기반으로, QoS 설정을 통해 통신 특성을 조정할 수 있습니다.
주요 QoS 파라미터:
| 파라미터 | 옵션 | 의미 |
|---|---|---|
| Reliability | RELIABLE | 모든 메시지 보장 (느림) |
| BEST_EFFORT | 최선 노력, 손실 가능 (빠름) | |
| History | KEEP_ALL | 모든 메시지 보관 |
| KEEP_LAST | 최신 N개만 보관 | |
| Depth | 1~N | 보관할 메시지 개수 |
| Durability | TRANSIENT_LOCAL | 구독자 연결 전 메시지 보관 |
| VOLATILE | 현재 구독자에게만 전송 |
RELIABLE (신뢰성 우선):
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10
)
특징:
BEST_EFFORT (속도 우선):
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1
)
특징:
Publisher (test_publisher.py):
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1,
durability=DurabilityPolicy.VOLATILE
)
self.publisher_ = self.create_publisher(
RobotMessage,
'robot_test_topic',
qos_profile
)
Subscriber (malle_service):
# Publisher와 동일한 QoS 설정 필요
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1,
durability=DurabilityPolicy.VOLATILE
)
self.subscription = self.create_subscription(
RobotMessage,
'robot_test_topic',
self.listener_callback,
qos_profile
)
효과:
1분간 측정 (58개 메시지):
============================================================
📊 통계 분석 결과 (수신 시간 기준)
============================================================
총 메시지 수: 58개
수집 기간: 60초
초당 평균 메시지: 0.97개/초
------------------------------------------------------------
단계 평균 최소 최대 중앙값
------------------------------------------------------------
AI 처리 1.006초 1.003초 1.007초 1.006초
웹 전송 0.003초 0.003초 0.003초 0.003초
오버헤드 0.028초 0.022초 0.067초 0.025초
전체 1.037초 1.029초 1.077초 1.034초
------------------------------------------------------------
📈 백분위수 분석 (전체 처리 시간)
------------------------------------------------------------
P50: 1.034초 (50%의 요청이 이 시간 이내 처리)
P75: 1.037초 (75%의 요청이 이 시간 이내 처리)
P90: 1.051초 (90%의 요청이 이 시간 이내 처리)
P95: 1.057초 (95%의 요청이 이 시간 이내 처리)
P99: 1.077초 (99%의 요청이 이 시간 이내 처리)
📊 각 단계별 시간 비율
------------------------------------------------------------
AI 처리: 97.00% (1.000초)
웹 전송: 0.29% (0.003초)
오버헤드: 2.71% (0.028초)
총합: 100.00% (1.037초)
✓ 합계 검증 통과 (차이: 0.000000초)
📉 표준편차 (안정성 지표)
------------------------------------------------------------
AI 처리 0.001초
웹 전송 0.000초
오버헤드 0.008초
전체 0.009초
============================================================
병목 순위:
AI 처리: 97.00% (1.000초)
오버헤드: 2.71% (0.028초)
웹 전송: 0.29% (0.003초)
결론: AI 처리 외 통신 오버헤드는 3% 미만으로 매우 효율적입니다.
측정 결과 기반 의사결정:
전체 통신 오버헤드 (웹 전송 + 오버헤드):
0.003초 + 0.028초 = 0.031초 (31ms)
판단:
| 항목 | 측정값 | 목표값 | 평가 |
|---|---|---|---|
| 총 지연 시간 | 1.037초 | < 2초 | ✅ 통과 |
| 통신 오버헤드 | 31ms | < 100ms | ✅ 통과 |
| 안정성 (σ) | 0.009초 | - | ✅ 매우 안정적 |
| P99 지연 | 1.077초 | < 2초 | ✅ 통과 |
결론:
HTTP 통신으로 충분하다. UDP 전환은 불필요.
나중에 리팩토링해도 되겠지만, 현재의 스펙에선 굳이 UDP로 바꾸지 않아도 실시간성이 유지된다.
의사결정:
현재 시스템에선 HTTP로 충분히 실시간성을 만족하므로, 불필요한 복잡도를 추가하지 않기로 결정했습니다.