1kHz 실시간 로봇 제어 시스템 모니터링 아키텍처
1kHz 실시간 제어 루프에서 성능 데이터를 수집하려면 딜레마에 직면합니다:
| RT 루프 요구사항 | 모니터링 요구사항 |
|---|---|
| 결정론적 실행 | 데이터 저장 (메모리/디스크 I/O) |
| 메모리 할당 금지 | 네트워크 전송 (ROS2 퍼블리싱) |
| 블로킹 호출 금지 | 통계 계산 |
| 예외 처리 금지 | 시각화 |
이 글에서는 RT 결정성을 유지하면서 고해상도 성능 데이터를 수집하는 모니터링 아키텍처를 설계합니다.
아키텍처 개요
핵심 패턴: RT/Non-RT Producer-Consumer
- Producer: RT 스레드가 Lock-free 큐에 데이터 푸시 (블로킹 없음)
- Consumer: Non-RT 스레드가 큐에서 폴링하여 ROS2 토픽 퍼블리시
왜 스레드 분리인가? (프로세스 분리 대신)
| 측면 | 스레드 분리 | 프로세스 분리 |
|---|---|---|
| 메모리 공유 | 직접 힙 메모리 접근 | IPC 필요 |
| 레이턴시 | 마이크로초 수준 | IPC 오버헤드 추가 |
| 수명 관리 | 단일 프로세스 | 다중 프로세스 |
데이터 구조
RtSample (216 bytes)
매 1ms 주기의 완전한 RT 사이클 데이터:
| 카테고리 | 데이터 | 바이트 | 용도 |
|---|---|---|---|
| 타이밍 | monotonic_ns, sequence | 16 | 시퀀스 추적 |
| 루프 성능 | loop_exec_us, loop_period_us, loop_jitter_us, deadline_miss | 17 | RT 성능 분석 |
| 관절 상태 | 6축 actual/cmd (position, velocity, torque) | 144 | 제어 품질 평가 |
| 드라이브 상태 | CiA 402 status_word, control_word, op_mode | 30 | 서보 진단 |
| 필드버스 | wkc, wkc_mismatch, link_error | 4 | 통신 안정성 |
제약조건: Lock-free 큐 연산을 위해 trivially_copyable이어야 합니다.
Event (64 bytes, Cache-line 정렬)
이벤트 기반 알림용 구조체:
struct alignas(64) Event {
// 분류 (4 bytes)
EventType type;
uint8_t source_id;
EventSeverity severity;
uint8_t joint_id;
// 타이밍 (24 bytes)
uint64_t monotonic_ns;
uint64_t event_sequence;
uint64_t ref_sample_seq;
// 데이터 (24 bytes)
int32_t error_code;
uint8_t extra_len;
uint8_t extra[21];
// 수치 (4 bytes)
float value;
// 패딩 (8 bytes) - 64바이트 정렬
};
Lock-Free SPSC Queue
구현 선택: rigtorp::SPSCQueue
- Wait-free: 64비트 Linux에서 Producer가 최대 2개 atomic load만 수행
- False sharing 방지: 64바이트 캐시 라인 정렬
// 캐시 라인 정렬로 False Sharing 방지
static constexpr size_t kCacheLineSize = 64;
alignas(kCacheLineSize) std::atomic<size_t> writeIdx_ = {0}; // Producer만 쓰기
alignas(kCacheLineSize) size_t readIdxCache_ = 0; // Producer 로컬 캐시
alignas(kCacheLineSize) std::atomic<size_t> readIdx_ = {0}; // Consumer만 쓰기
alignas(kCacheLineSize) size_t writeIdxCache_ = 0; // Consumer 로컬 캐시
성능 벤치마크 (100,000 iterations, Release -O2)
| 연산 | 평균 | P99 | 비고 |
|---|---|---|---|
| RtSample 생성 + 큐 푸시 | 0.3 µs | 0.7 µs | 216B 구조체 |
| Event 생성 + 큐 푸시 | ~0.1 µs | 0.3 µs | 64B 구조체 |
| 메모리 사용량 (큐) | 1.8 MB | - | 8192 samples + 512 events |
총 모니터링 오버헤드: 1ms 주기의 < 0.05%
ROS2 토픽 설계 (3-Tier)
| 토픽 | 주파수 | QoS | 용도 |
|---|---|---|---|
/rt_raw | 1kHz | best_effort, depth=200 | 전체 기록, 사후 분석 |
/rt_events | 이벤트 발생 시 | reliable + transient_local, depth=50 | 이벤트 알림 |
/rt_monitor_stats | 10Hz | reliable, depth=20 | 실시간 헬스 대시보드 |
Decimation (1kHz → 10Hz)
// /rt_raw는 매 샘플 퍼블리시 (1kHz)
rt_raw_pub_->publish(to_rt_raw(sample, now));
sample_count_++;
// /rt_monitor_stats는 100:1 데시메이션 (10Hz)
if (sample_count_ % 100 == 0) {
publish_stats();
}
Edge Detection (이벤트 중복 방지)
// Rising edge 검출: current=true && previous=false일 때만 이벤트 발생
const bool faulted = servo.faulted;
if (faulted && !prev_faulted_) {
emit(EventType::SERVO_FAULT, ...);
}
prev_faulted_ = faulted;
Cooldown 메커니즘 (이벤트 폭주 방지)
데드라인 미스 같은 연속 발생 이벤트는 짧은 시간에 수천 개가 발생할 수 있습니다. 이를 방지하기 위해 100ms 쿨다운을 적용합니다:
// 같은 타입의 이벤트는 100ms 이내 재발생 시 무시
constexpr auto kEventCooldown = std::chrono::milliseconds(100);
if (now - last_event_time_[type] > kEventCooldown) {
emit(type, ...);
last_event_time_[type] = now;
}
저장 및 시각화
MCAP 포맷
- PlotJuggler 네이티브 지원
- 효율적인 시간 기반 인덱싱
- 압축 옵션 (zstd, lz4)
best_effort QoS로 퍼블리시되는 /rt_raw 토픽을 녹화할 때, rosbag2의 기본 QoS(reliable)와 충돌할 수 있습니다. QoS override 파일을 사용하세요:
# qos_override.yaml
/rt_raw:
reliability: best_effort
history: keep_last
depth: 200
ros2 bag record /rt_raw /rt_events --qos-profile-overrides-path qos_override.yaml
저장 용량 계산
| 토픽 | 계산 | 시간당 저장량 |
|---|---|---|
/rt_raw | 1kHz × ~250B × 3600s | ~1.0 GB |
/rt_events | 이벤트 빈도에 따라 가변 | 10-50 MB |
/rt_monitor_stats | 10Hz × 80B × 3600s | ~3 MB |
| 총계 | ~1.0-1.1 GB |
Rolling Retention (외부 스크립트)
무한 운영을 위한 2단계 정리 정책:
# 시간 기반: 60분 이상 된 파일 삭제
cleanup_old_files() {
find "$BAG_DIR" -name "*.mcap" -mmin +"$RETENTION_MIN" -delete
}
# 용량 기반: 디스크 사용량 70% 초과 시 FIFO 삭제
cleanup_disk_space() {
# 가장 오래된 파일부터 삭제
}
PlotJuggler 설정

PlotJuggler로 6축 관절의 actual_pos vs cmd_pos를 실시간 시각화
스트리밍 모드 설정:
- Mode: ROS2 Topic Subscriber
- Time Window: 180초 (3분 롤링)
- Max Points: 20,000
monotonic_ns, sequence 등 uint64 필드는 PlotJuggler에서 truncation 에러가 발생할 수 있습니다. 이 필드들은 시각화에서 제외하세요.
모니터링 헬스 메트릭
자기 모니터링
| 필드 | 의미 | 경고 임계값 |
|---|---|---|
rt_queue_fill_pct | SPSC 큐 사용률 | > 70% |
rt_overflow_delta | 큐 오버플로우 횟수 | > 0 |
publisher_lag_ms | RT → Non-RT 전파 지연 | > 50ms |
seq_gap_count_delta | 손실된 샘플 수 | > 0 |
운영 임계값
| 메트릭 | 정상 | 경고 | 위험 |
|---|---|---|---|
| rt_queue_fill_pct | < 50% | > 70% | > 90% |
| publisher_lag_ms | < 10ms | > 50ms | > 100ms |
| loop_jitter_us (P99) | < 50µs | > 100µs | > 200µs |
| seq_gap_count | 0 | > 0 | - |
운영 권장사항
이 모니터링 시스템은 개발 및 디버깅 목적으로 설계되었습니다.
| 상황 | 권장 |
|---|---|
| 개발 환경 | PlotJuggler 실시간 시각화 활성화 |
| 프로덕션 | 1kHz 토픽 퍼블리싱 비활성화 또는 필요시만 활성화 |
| 문제 발생 시 | 모니터링 활성화하여 근본 원인 분석 |
주의: PlotJuggler 실시간 시각화는 CPU 자원을 상당히 소모합니다. 1kHz 토픽 퍼블리싱도 시스템 부하를 유발할 수 있습니다.
핵심 정리
-
RT/Non-RT 분리: Lock-free SPSC Queue로 RT 스레드의 결정성을 유지하면서 데이터를 Non-RT 스레드로 전달합니다.
-
3-Tier 토픽 설계: 목적별로 QoS를 최적화합니다.
/rt_raw(1kHz): 전체 기록용/rt_events: 이벤트 알림용/rt_monitor_stats(10Hz): 대시보드용
-
Lock-free 큐 성능: RtSample 푸시에 0.3µs, 1ms 주기의 0.05% 미만 오버헤드.
-
MCAP Rolling Retention: 시간/용량 기반 외부 스크립트로 무한 운영 지원.
-
자기 모니터링: 큐 사용률, 지연, 손실 샘플을 추적하여 모니터링 시스템 자체의 건강 상태를 확인합니다.
-
개발용 도구: PlotJuggler와 1kHz 퍼블리싱은 개발 환경에서만 사용하고, 프로덕션에서는 필요시에만 활성화합니다.