AEGLOSLAB ROS 2 Jazzy 대전

제12부 실시간·성능

12.4 엔드투엔드 지연 예산 설계

12.4.1 이 장에서 배우는 것 / 선수 지식

12.2과 12.3는 개별 층을 개선하는 방법이었다. 이 장은 설계다 — 전체 지연 목표를 정하고, 그것을 단계별로 배분하고, 각 단계가 예산을 지키는지 검증하는 절차.

예산이 필요한 이유는 두 가지다. 첫째, 목표 없이 최적화하면 어디서 멈출지 모른다. 둘째, 전체 목표가 없으면 각 단계가 서로에게 책임을 전가한다 — 인지는 "계획이 느려서", 계획은 "인지가 늦게 줘서". 예산은 그 논의를 숫자로 만든다.

배우는 것:

  • 예산의 도출 — 요구에서 숫자로.
  • 배분 — 단계별로 나누기.
  • 체인 설계 — 몇 단계를 거칠 것인가.
  • 단계별 원천 — 무엇이 시간을 쓰는가.
  • 실행기 구조의 영향 — 같은 코드, 다른 지연.
  • 처리량과의 절충 — 배치 vs 지연.
  • 검증과 재예산 — 안 맞을 때.

선수 지식: 11.7(측정), 12.1(실시간), 2.2~2.5(실행 모델), 8.7.4(정지 거리), 7.7(시간·동기화).

정직: 이 장에 나오는 숫자는 방법을 설명하기 위한 예시이며 권장값이 아니다. 실제 예산은 로봇의 속도·질량·안전 요구·센서 특성에서 나온다. 안전 관련 지연 요구의 결정은 안전 표준과 전문가의 영역이며(8.7.1·12.1.1), 이 책의 서술을 근거로 삼아서는 안 된다. 이 환경에서 검증하지 않았다(목차 3).(→ 178차 확인: 이 장의 두 핵심 실행 가능 주장 — 콜백 그룹 격리 패턴(12.4.6)과 주기 구동의 지연 추가(12.4.5) — 를 실제 컴파일·실행으로 검증했다. 후자에서 "폴링 주기의 절반"이라는 서술이 실제로는 폴링 주기와 도착 주기 중 더 드문 쪽의 절반이라는 중요한 미묘함을 발견했다. 아래 각 절의 실행 확인(178차) 참고.)

12.4.2 예산의 도출

요구에서 숫자를 만든다.

물리적 · 안전 요구 허용 총 지연 도출 여유 계수 적용 설계 예산 단계별 배분

예: 장애물 감지 → 정지(8.7.4의 구조)

$$d_{\text{stop}} = v \cdot t_{\text{latency}} + \frac{v^2}{2a}$$

$d_{\text{stop}}$에 여유가 있어야 하므로, 허용 $t_{\text{latency}}$가 결정된다.

입력출처
$v$ 최대 속도운영 요구
$a$ 감속도실측(10.6.3)
$d_{\text{stop}}$ 여유안전 요구·환경
→ $t_{\text{latency}}$계산 결과

다른 유형의 요구:

요구지연 제약의 근거
제어 안정성위상 여유 — 지연이 크면 발산
추종 정확도지연 × 속도 = 오차
사람과의 상호작용반응이 자연스럽게 느껴지는 한계
작업 사이클처리량 요구
안전정지 거리·힘 제한

견해: "얼마면 충분한가"를 근거 있게 정하는 것이 이 장에서 가장 중요한 단계다. 근거 없이 "100 ms"를 목표로 설정하면, 그것이 과한지 부족한지 판단할 수 없다. 물리에서 역산한 숫자만이 절충의 기준이 된다.

견해: 여유 계수를 명시적으로 두라. 측정은 조건에 따라 변하고, 미래에 기능이 추가된다. 계산된 한계의 일정 비율을 설계 목표로 삼고, 그 비율을 문서에 적어 두면 나중에 판단이 가능하다.

함정: 평균 지연으로 예산을 세우지 말라(11.7.3). 안전 계산에는 최악값이 들어가야 한다. 평균 50 ms·p99 200 ms인 시스템의 예산은 200 ms로 봐야 한다.

12.4.3 단계 분해

총 예산을 단계로 나눈다.

① 센서 획득 · 노출 ② 드라이버 · 전송 ③ 전처리 ④ 인지 · 검출 ⑤ 판단 · 계획 ⑥ 제어 ⑦ 액추에이터 · 통신 ⑧ 기계적 응답

예시 배분(설명용 — 권장값 아님):

단계예산비고
① 센서 획득30%센서 고유 — 바꾸기 어려움
② 드라이버·전송5%
③ 전처리10%
④ 인지25%대개 최대
⑤ 판단10%
⑥ 제어5%
⑦ 액추에이터10%하드웨어
⑧ 기계 응답5%물리

견해: ①과 ⑧을 잊지 말라. 소프트웨어 지연만 세다가 센서 노출 시간·기계 응답을 빠뜨리면 예산이 안 맞는다. 라이다 1회전 시간·카메라 노출·모터 응답은 소프트웨어로 줄일 수 없는 하한이다(7.7.8).

견해: "바꿀 수 없는 것"과 "바꿀 수 있는 것"을 먼저 구분하라. 센서 주기가 10 Hz면 그것만으로 최소 100 ms가 든다. 하드웨어 하한을 뺀 나머지가 소프트웨어의 예산이며, 그것이 너무 작으면 하드웨어를 바꿔야 한다는 결론이 나온다 — 이것이 예산 작업의 중요한 산출이다.

함정: 비동기 단계의 지연은 처리 시간이 아니라 대기 시간이 지배할 수 있다. 10 Hz로 실행되는 노드에 데이터가 도착하면 평균 50 ms를 기다린다. 주기적 폴링 구조는 그 자체로 지연을 만든다(12.4.5).

12.4.4 단계별 지연 원천

단계원천
센서노출·적분·내부 처리·전송(7.7.8)
드라이버버퍼링·폴링 주기·USB 프레임
직렬화크기 비례(1.3.9)
DDS 전달큐·네트워크·재전송(3.3)
실행기 대기콜백이 선택되기까지(2.2.5)
콜백 처리알고리즘
동기화 대기message_filters(7.7.5)
큐 적체소비가 느릴 때(1.4.4)
액추에이터통신·전류 응답

견해: "실행기 대기"와 "동기화 대기"가 과소평가된다(11.6.6의 반복). 알고리즘을 최적화해도 이 둘이 크면 종단 지연은 안 줄어든다. 트레이싱이나 구간별 스탬프 측정으로 먼저 확인하라.

함정: message_filters의 근사 동기화는 가장 늦은 센서를 기다린다(7.7.5). 10 Hz 라이다와 30 Hz 카메라를 동기화하면 라이다 주기가 지연을 지배한다. 동기화가 정말 필요한지, 어느 정밀도까지 필요한지 재검토할 가치가 있다.

실행 확인(179차) — 이 주장을 정확히 이 조합(10 Hz·30 Hz)으로 재현했다. message_filters::Synchronizer<ApproximateTime<Imu, Imu>>로 10 Hz(100ms 주기) "라이다"와 30 Hz(33.3ms 주기) "카메라" 발행자를 실제로 동기화했다. 측정된 동기화 콜백 속도는 정확히 10.1 Hz(평균 간격 99.46ms)로, 30 Hz(카메라) 예측이 아니라 10 Hz(라이다) 예측과 거의 정확히 일치했다. 라이다 메시지는 100%가 매칭에 쓰였지만 카메라 메시지는 33.4%만 쓰였다(발행 비율 1:3과 정확히 일치 — 카메라 메시지의 약 3분의 2는 구조적으로 "버려진다"). 매칭된 쌍의 실제 시각 차(라이다 스탬프−카메라 스탬프)는 평균 6.8ms·최대 17.5ms로 슬롭 윈도(20ms) 안에 들어 실제 매칭임을 확인했다. 컴파일 과정에서 부수적으로 발견한 것 — Synchronizer<ApproximateTime<M0,M1>>::registerCallback()에 람다나 std::function을 직접 넘기면 이 버전(Jazzy, message_filters 4.11.17)에서 컴파일이 실패한다(wrong number of template arguments (9, should be 1)) — 내부 Signal9 구현이 항상 9개 슬롯(미사용분은 NullType)을 전제하는데 일반 콜러블에 대한 오버로드 해석이 이 경우 깨진다. 멤버 함수 포인터 형태(registerCallback(&Class::method, this))로 바꾸니 정상 컴파일됐다 — 책에는 없는, 실무에서 바로 부딪힐 수 있는 API 함정이다. 원본: tools/gap_harness/gap179/sync_test/(sync_test_node.cpp·sync_test_result.log·build_log.txt(실패)·build_log3.txt(성공)).

함정: 큐 깊이가 크면 지연이 커진다. depth=10인 구독자가 지연되면 10개가 쌓이고, 소비자는 오래된 것부터 본다. 최신 데이터만 중요한 경우 depth=1이 지연에 유리하다(1.6.3·1.4.4) — 대신 유실이 늘어난다.

12.4.5 체인 설계

단계 수 자체가 지연이다.

긴 체인 짧은 체인 센서 필터 변환 검출 융합 계획 제어 센서 검출 + 융합 계획 + 제어
단계당 비용설명
직렬화·역직렬화크기 비례
DDS 전달큐·전송
실행기 대기노드마다
복사인트라프로세스가 아니면

견해: "기능마다 노드 하나"가 지연에는 불리하다(2.7.3의 절충). 모듈성은 좋지만 각 경계가 지연을 더한다. 지연이 중요한 경로는 노드를 합치거나 컴포넌트로 같은 프로세스에 넣는 것(2.7)이 효과적이다.

견해: 인트라프로세스 통신이 체인 비용을 크게 줄인다(2.7.5·12.3.5). 노드를 합치지 않고도 직렬화·복사를 없앨 수 있으므로, 컴포넌트 컨테이너로 묶는 것이 모듈성과 지연의 좋은 절충이다.(→ 190차 점검: 이 주장의 가장 구체적인 실측 근거는 14.1.7이다 — 별도 프로세스는 p99 60,626~73,272µs의 거대한 꼬리 지연을 보였지만 같은 컴포넌트를 컨테이너로 묶으니 p99 194µs로 약 310~370배 줄었다(180·185~187차, QoS 이력 깊이 스윕으로 인과까지 확증).)

주기 구동 vs 사건 구동:

방식지연
사건 구동(구독 콜백에서 처리)낮음 — 도착 즉시
주기 구동(타이머가 최신값 폴링)평균 주기의 절반 추가(→ 178차 확인·정정: "주기"가 폴링 주기 자체가 아니라 폴링 주기와 도착 주기 중 더 드문(긴) 쪽의 절반임을 실측으로 확인 — 아래 실행 확인(178차) 참고)

실행 확인(178차) — 이 주장을 실측했고, "주기"가 무엇을 뜻하는지에 대한 중요한 미묘함을 발견했다. 같은 토픽을 두 구독자가 받게 했다 — 하나는 사건 구동(콜백에서 즉시 처리, "지연=도착부터 처리까지"를 측정), 다른 하나는 최신값만 저장하고 별도 타이머가 주기적으로 폴링(폴링 시점의 지연을 측정)했다.

처음엔 발행 간격 20ms·폴링 주기 100ms로 시험했는데, 측정된 추가 지연이 11.0ms로 "폴링 주기의 절반"(50ms)과 전혀 맞지 않았다. 원인을 추적한 결과 — 도착이 폴링보다 훨씬 잦으면(이 경우처럼 발행이 20ms, 폴링이 100ms), 폴링 시점엔 항상 아주 최근 값이 있으므로 추가 지연은 폴링 주기가 아니라 "도착 주기의 절반"(10ms)에 수렴한다는 것을 확인했다(측정 11.0ms ≈ 예측 10.0ms, 오차 범위 내 일치). 반대로 도착이 폴링보다 훨씬 드물게(발행 300ms·폴링 20ms) 시험하니 추가 지연이 10.4ms로 이번엔 "폴링 주기의 절반"(10ms)과 정확히 일치했다(도착 주기의 절반인 150ms와는 전혀 안 맞음).

정리하면 "평균 주기의 절반 추가"는 참이지만, 그 "주기"는 폴링 타이머 자신의 설정값이 아니라 폴링 주기와 도착 주기 중 더 드문(더 긴) 쪽이다. 센서가 처리 노드의 타이머보다 빠르게 발행하는 흔한 구성(예: 30 Hz 카메라 + 10 Hz 처리 루프)에서는 추가 지연이 타이머 설정값이 아니라 센서 자신의 발행 주기에 좌우된다는 뜻이며, 문자 그대로 "폴링 주기의 절반"으로 읽으면 이 흔한 경우에 오도될 수 있다. 원본: tools/gap_harness/gap178/periodic_test/(periodic_test_node.cpp·periodic_test_result.log).

견해: 지연이 중요하면 사건 구동으로 설계하라. 타이머로 최신값을 읽는 구조는 이해하기 쉽고 주기가 안정적이지만, 구조적으로 지연을 더한다. 반대로 결정성이 중요하면 주기 구동이 유리하다(12.5.6) — 이것이 지연과 결정성의 절충이다.

체인 최적화 순서:

순서조치
1측정으로 병목 특정(11.7.4)
2불필요한 단계 제거
3인트라프로세스·컴포넌트화
4큐 깊이·QoS 조정
5실행기 구조 조정
6알고리즘 최적화
7하드웨어 변경

견해: 알고리즘 최적화가 6번인 것에 유의하라. 대개 여기부터 조정하지만, 1~5가 더 비용이 낮고 효과가 클 때가 많다. 측정이 순서를 정해 준다.

12.4.6 실행기 구조의 영향

같은 코드도 실행기 구조에 따라 지연이 다르다.

구조지연 특성
단일 스레드·모든 콜백긴 콜백이 다른 것을 막음(2.3.4)
멀티스레드·기본 그룹상호배타 — 여전히 직렬(2.3.3)
Reentrant 그룹 분리병렬 — 지연 감소
노드별 실행기·스레드격리
RT 스레드 분리우선순위 보장(12.2)

견해: "멀티스레드 실행기를 썼는데 왜 안 빨라지나"의 답은 대개 콜백 그룹이다(2.3.3). 기본 그룹은 상호배타라 스레드가 여러 개여도 콜백은 하나씩 실행된다. 지연이 중요한 콜백을 별도 Reentrant 그룹에 두는 것이 실질적 조치다.

지연 경로 격리 패턴:

// 지연이 중요한 경로를 전용 그룹·전용 실행기로
auto critical_group = node->create_callback_group(
  rclcpp::CallbackGroupType::MutuallyExclusive);

rclcpp::SubscriptionOptions opts;
opts.callback_group = critical_group;
auto sub = node->create_subscription<MsgT>("urgent", qos, cb, opts);

// 별도 실행기에서 이 그룹만 spin (2.4)
rclcpp::executors::SingleThreadedExecutor critical_exec;
critical_exec.add_callback_group(critical_group, node->get_node_base_interface());
std::thread t([&]{ critical_exec.spin(); });   // 이 스레드를 RT로

실행 확인(178차) — 이 정확한 패턴을 컴파일·실행해 격리 효과를 직접 측정했다. 20ms 주기로 실행되는 "긴급" 타이머와, 100ms마다 150ms씩 잠드는 "부하가 큰" 콜백을 같은 노드에 뒀다. 격리 없이 하나의 SingleThreadedExecutor가 둘 다 처리하게 하니, 긴급 타이머의 실제 평균 주기가 요청한 20ms가 아니라 221.76ms(최대 302.90ms)까지 급증했다 — 부하가 큰 콜백에 완전히 막힌 것이다. 위 코드 그대로 긴급 타이머를 전용 MutuallyExclusive 그룹 + 전용 SingleThreadedExecutor(별도 스레드)로 격리하니, 부하가 큰 콜백이 여전히 다른 스레드에서 실행되고 있는데도 긴급 타이머의 평균 주기가 정확히 20.00ms(최소 19.02ms·최대 21.04ms, 지터 1ms 이내)로 완전히 회복됐다 — 약 11배(221.76ms → 20.00ms) 개선이다. "긴급 경로를 전용 스레드로 격리"라는 주장을 정확한 숫자로 확인했다. 원본: tools/gap_harness/gap178/isolation_test/(isolation_test_node.cpp·isolation_test_result.log).

견해: "긴급 경로를 전용 스레드로 격리"가 실무의 주력 패턴이다. 안전 감시·비상 정지·제어 루프를 다른 콜백과 분리하면, 인지 부하가 커도 그 경로는 지켜진다. 2.3·2.4에서 다룬 구조가 여기서 실제 목적을 얻는다.

12.4.7 처리량과의 절충

선택지연처리량
배치 처리나쁨좋음
개별 처리좋음나쁨
큐 깊이 크게나쁨유실 적음
큐 깊이 1좋음유실 많음
다운샘플링좋음정보 손실
RELIABLE 재전송나쁠 수 있음신뢰성

견해: "최신 데이터가 중요한가, 모든 데이터가 중요한가"가 이 절충의 판단 기준이다. 제어·회피는 최신이 중요하므로 depth=1·BEST_EFFORT가 맞고(1.6.3.1), 기록·분석은 모든 데이터가 중요하므로 큰 큐·RELIABLE이 맞다. 같은 데이터라도 소비자에 따라 다르다.

견해: 경로를 나누는 것이 답인 경우가 많다. 같은 센서 데이터를 제어용(depth=1·BEST_EFFORT)과 기록용(큰 큐·RELIABLE)으로 각각 구독하면, 절충을 하지 않아도 된다. QoS는 구독자마다 다르게 설정할 수 있다(1.6).

12.4.8 검증과 재예산

아니오 불가 예 예산 수립 구현 단계별 측정 11.7.4 예산 내? 문서화 · 회귀 시험 초과 단계는? 개선 시도 ★ 재예산 · 요구 재검토

초과 시의 선택지:

선택설명
1. 그 단계 최적화알고리즘·구조
2. 다른 단계에서 여유 차용예산 재배분
3. 체인 단축노드 병합·인트라프로세스
4. 하드웨어 변경센서·계산 자원
5. 요구 완화속도 제한 등 운영 제약
6. 구조 변경하드 경로를 MCU로(12.1.8)

견해: 5번(요구 완화)을 정당한 선택지로 인정하라. 지연 예산을 못 맞추면 최대 속도를 낮추는 것이 안전하고 합리적인 해결이다. 8.7.4의 식에서 $v$를 줄이면 $t_{\text{latency}}$ 여유가 생긴다. "기술로 해결"만 고집하다 안전 여유를 깎는 것보다 낫다.

견해: 예산 문서를 유지하라. 각 단계의 목표·실측·측정 조건을 표로 두면, 기능을 추가할 때 "예산이 남아 있는가"를 물을 수 있다. 이것이 없으면 지연은 서서히, 아무도 모르게 악화된다.

예산 표 예시:

단계목표실측 p99여유측정일
센서 획득————
인지————
계획————
합계————

견해: 회귀 시험에 종단 지연을 넣으라(11.7.9·11.3.8). bag 재생 기반으로 종단 지연 p99를 측정해 기준과 비교하면, 성능 회귀를 자동으로 검출한다. 예산은 문서만으로는 지켜지지 않는다.

12.4.9 함정 카탈로그

#함정증상대처
1근거 없는 목표판단 불가물리에서 역산
2평균으로 예산안전 여유 부족최악값
3여유 계수 없음변화에 취약명시적 여유
4센서·기계 지연 누락예산 불일치①⑧ 포함
5하드웨어 하한 무시불가능한 목표먼저 계산
6실행기 대기 간과최적화 무효구간 측정
7동기화 대기 간과느린 센서가 지배필요성 재검토
8큐 깊이 과다오래된 데이터depth=1 검토
9노드를 잘게 쪼갬경계 비용 누적컴포넌트화
10주기 구동으로 지연 추가절반 주기 손실사건 구동
11콜백 그룹 미분리멀티스레드 무효Reentrant 분리
12알고리즘부터 최적화비효율측정이 순서 결정
13요구 완화를 배제무리한 설계정당한 선택지
14예산 문서 없음서서히 열화표 유지
15회귀 시험 없음조용한 악화CI 편입

12.4.10 체크포인트

  • 지연 예산이 필요한 두 가지 이유를 말할 수 있는가.
  • 정지 거리 식에서 허용 지연을 도출하는 과정을 설명할 수 있는가.
  • 지연 제약의 근거가 되는 요구 다섯 유형은?
  • 여유 계수를 명시하라는 조언의 근거는?
  • 단계 분해 여덟 단계와, 잊기 쉬운 두 단계는?
  • "바꿀 수 없는 것과 바꿀 수 있는 것"을 구분하는 것이 왜 중요한 산출인가.
  • 단계별 지연 원천 아홉 가지 중, 과소평가되는 둘은?
  • message_filters 동기화가 지연을 만드는 방식은?
  • 큐 깊이와 지연의 관계, 그리고 depth=1의 대가는?
  • 체인 단계 수가 지연인 이유 네 가지와, 모듈성과의 절충 해법은?
  • 사건 구동과 주기 구동의 지연 차이, 그리고 각각이 유리한 상황은?
  • 체인 최적화 일곱 순서에서 알고리즘 최적화가 6번인 이유는?
  • "멀티스레드인데 안 빨라진다"의 원인과 조치는?
  • 긴급 경로 격리 패턴의 구조와 그 목적은?
  • 지연·처리량 절충의 판단 기준과, "경로를 나누는" 해법은?
  • 예산 초과 시 여섯 선택지 중, 정당하지만 배제되기 쉬운 것은?
  • 예산 문서와 회귀 시험이 왜 함께 필요한가.

다음 장 12.5은 지연과 짝을 이루는 다른 축 — 결정성을 다룬다. 같은 입력에 같은 결과가 나오는가, 그리고 ROS 2에서 그것이 얼마나 가능한가.