제11부 도구·관측성
11.7 성능 측정 — 지연·지터·처리량
11.7.1 이 장에서 배우는 것 / 선수 지식
이 책은 3.6.1에서 방침을 세웠다 — 성능 수치를 인용하지 않고 측정 방법을 가르친다. 이 장이 그 약속을 지키는 자리다.
측정이 중요한 이유는 두 가지다. 첫째, 성능 문제는 추측으로 진단되지 않는다. "느린 것 같다"에서 출발한 최적화는 대개 잘못된 곳을 고친다. 둘째, 성능은 회귀한다. 측정 체계가 없으면 어느 변경이 성능을 떨어뜨렸는지 나중에 알 수 없다.
배우는 것:
- 무엇을 측정하는가 — 지표의 정의.
- 종단 지연 측정 — 가장 중요한 지표.
- 분포로 보기 — 평균이 거짓말하는 이유.
- 처리량과 대역 — 자원 관점.
- 자원 사용 — CPU·메모리.
- 측정 방법론 — 워밍업·부하·반복.
- 흔한 실수 — 잘못된 측정.
- 회귀 체계 — CI에 넣기.
- 내장 토픽 통계 — 코드를 안 고치고 구독별 주기·나이를 얻는 법과 그 관측 공백(11.7.12).
- 계측 도구 다섯(11.7.13) — 이 책의 실측을 자기 노드에서 재현하는 법.
선수 지식: 2.5.6(주기·지터), 11.6(트레이싱), 6.9(진단), 4.10(CI), 7.8(대역).
정직: 이 장은 방법론이며 절대 수치를 제시하지 않는다 — 하드웨어·구성·부하에 따라 자릿수가 달라지기 때문이다(BK-03·3.6.1). 도구 이름·옵션은 버전 의존이다
(확인 필요). 이 환경에서 검증하지 않았다(목차 3). 예외: 11.7.12(내장 토픽 통계)는 2026-08-16에 Jazzy 컨테이너에서 실행 확인했다.
11.7.2 무엇을 측정하는가
| 지표 | 정의 | 왜 중요 |
|---|---|---|
| 종단 지연 | 센서 획득 → 명령 출력 | 제어 품질·안전을 좌우 |
| 구간 지연 | 각 단계의 소요 | 병목 특정 |
| 주기 지터 | 실제 주기의 변동 | 제어 안정성(2.5.6) |
| 콜백 지속 | 개별 처리 시간 | 실행기 영향(2.2.7) |
| 처리량 | 초당 처리 건수 | 용량 |
| 대역 | 바이트/초 | 네트워크·디스크(7.8) |
| CPU·메모리 | 자원 점유 | 여유·안정성 |
| 장시간 거동 | 시간에 따른 변화 | 누수·열화 발견 |
견해: 종단 지연이 로봇에서 가장 중요한 지표다. 센서가 무언가를 본 순간부터 로봇이 반응하기까지의 시간이 곧 반응성이고, 이것이 안전 여유(8.7.4)와 제어 성능을 결정한다. 단계별 최적화보다 종단 지연을 먼저 측정하는 것이 옳다.
목표를 먼저 정한다:
| 항목 | 예 |
|---|---|
| 지표 | 장애물 감지 → 감속 명령 |
| 목표값 | 예: 100 ms 이내 |
| 백분위 | 99% 이상이 목표 이내 |
| 조건 | 정상 부하·목표 하드웨어 |
견해: 목표 없는 측정은 방향이 없다. "얼마면 충분한가"를 안전 요구·제어 요구에서 역산해 정하고(8.7.4의 정지 거리 계산이 그 예), 그 기준으로 측정 결과를 판정한다.
11.7.3 분포로 보기
평균은 로봇 성능 평가에서 거의 쓸모없다.
| 통계 | 의미 | 용도 |
|---|---|---|
| 평균 | 총량 | 자원 계획 |
| 중앙값 | 전형 | 참고 |
| p95·p99 | 실질 최악 | 판정 기준 |
| 최댓값 | 절대 최악 | 안전·실시간 |
| 표준편차 | 산포 | 참고 |
견해: 로봇에서는 꼬리가 사고를 만든다(2.5.6·11.6.5의 반복). 평균 지연 20 ms에 최댓값 300 ms인 시스템은, 그 300 ms 순간에 사람 앞에서 못 멈춘다. "평균은 좋은데 가끔 이상해요"가 곧 문제 있음을 뜻한다.
견해: 분포를 히스토그램·시계열로 함께 보라(11.5.7). 히스토그램은 형태(단봉·이봉)를, 시계열은 패턴(주기적·산발적)을 알려준다. 이봉 분포는 두 가지 경로가 있다는 신호이고, 주기적 스파이크는 다른 주기 작업과의 간섭을 시사한다.
11.7.4 종단 지연 측정
가장 실용적인 방법은 header.stamp 전파다.
// 최종 소비 지점에서
void control_callback(const my_msgs::msg::Target::SharedPtr msg)
{
auto latency = (now() - msg->header.stamp).seconds();
latency_stats_.add(latency); // 통계 누적
// 진단으로 발행 (6.9.5)
}
전제: 파이프라인 전체가 원본 스탬프를 유지해야 한다.
| 규칙 | 이유 |
|---|---|
| 파생 메시지는 원본 stamp 계승 | 지연 추적 가능 |
now()로 덮어쓰지 않는다 | 7.7.4의 원칙 |
| 융합 시 대표 stamp 규칙 | 일관되게 |
함정: 중간 노드가
header.stamp = now()로 덮어쓰면 종단 지연이 측정 불가능해진다. 7.7.4에서 "센서 스탬프를 보존하라"고 한 이유가 정합성만이 아니라 관측 가능성에도 있다.
함정: 분산 시스템이면 시계 동기가 전제다(7.7.6). 노드가 서로 다른 기기에 있고 시계가 안 맞으면, 측정된 "지연"에 시계 오차가 섞인다. PTP·NTP 상태를 먼저 확인한다.
구간 분해:
| 방법 | 설명 |
|---|---|
| 각 단계에서 stamp와 현재 시각 비교 | 누적 지연 |
| 차이로 구간 지연 계산 | 병목 특정 |
| 트레이싱 | 정밀(11.6.6) |
// 각 노드가 자기 시점의 누적 지연을 진단으로 발행
diag.add("cumulative_latency_ms", (now() - msg->header.stamp).seconds() * 1000);
견해: "각 노드가 자기 시점의 누적 지연을 진단으로 내보내는" 패턴이 비용이 낮고 강력하다. 코드 몇 줄이면 되고, 운영 중에도 병목을 볼 수 있으며, 트레이싱 없이 구간 분해가 된다. 이 책이 반복해 온 "진단으로 내보내라"(6.9)의 대표적 응용이다.
11.7.5 주기와 지터
// 콜백 간격 측정
void timer_callback()
{
auto t = now();
if (last_.nanoseconds() > 0) {
double interval = (t - last_).seconds();
interval_stats_.add(interval); // 분포 누적
}
last_ = t;
// ... 실제 작업 ...
}
// 콜백 지속 시간
void callback(...)
{
auto start = std::chrono::steady_clock::now(); // ★ steady_clock
// ... 작업 ...
auto dur = std::chrono::steady_clock::now() - start;
duration_stats_.add(std::chrono::duration<double>(dur).count());
}
함정: 지속 시간 측정에는
steady_clock(단조 시계)을 쓰라. ROS 시계(now())는 시뮬 시간이면 점프하고, 시스템 시계는 NTP 보정으로 뒤로 갈 수 있다(1.10.5). 경과 시간에는 단조 시계가 원칙이다.
CLI 확인:
ros2 topic hz /scan # 평균·최소·최대·표준편차
ros2 topic delay /scan # stamp 기준 지연 — 헤더 없는 타입은 거부한다(실행 확인)
ros2 topic bw /camera/image_raw # 대역
함정:
ros2 topic delay는header가 있는 메시지에만 쓸 수 있다(실행 확인 — 없으면msg does not have header로 거부한다).std_msgs/String처럼 헤더 없는 타입은 애초에 대상이 아니다. 이것이 1.2.7이 말한 "스탬프 없는 메시지는 나중에 시간을 못 따진다"의 구체적 대가다 — 진단 도구조차 쓸 수 없다.
함정:
ros2 topic hz는 그 자체로 구독자를 하나 추가한다. 대용량 토픽이면 측정 행위가 시스템에 부담을 준다(관찰자 효과 — 11.6.7). 용량이 작은 토픽에는 편리하지만, 이미지·포인트클라우드에서는 주의한다.
견해:
ros2 topic hz의 표준편차와 최소·최대를 보라. 평균만 보면 "10 Hz 정상"인데, 최솟값이 2 Hz면 가끔 크게 지연된 것이다. 11.7.3의 원칙이 CLI에서도 그대로다.
11.7.6 처리량·대역·자원
대역 계산(1.3.9·7.8.2·11.2.6에서 반복):
$$B = \sum_i S_{\text{msg},i} \cdot f_i \cdot n_{\text{sub},i}$$
$n_{\text{sub}}$가 곱해지는 것이 핵심이다 — 구독자가 늘면 대역도 는다(3.3.5의 멀티캐스트 예외 제외).
# 네트워크 실측
iftop -i wlan0
nload
sar -n DEV 1
# 프로세스별
nethogs
자원 측정:
# 노드별 CPU·메모리
top -H -p $(pgrep -d, -f my_node) # 스레드별
ps -o pid,%cpu,%mem,rss,comm -p PID
# 시간에 따른 추이 (누수 탐지)
while true; do
ps -o rss= -p PID
sleep 60
done
| 항목 | 확인 |
|---|---|
| CPU % | 코어 대비 여유 |
| RSS 추이 | 증가하면 누수 의심 |
| 스레드 수 | 예상과 일치(2.2.6) |
| 파일 디스크립터 | 누수 |
| 디스크 I/O | 기록 부하(11.2.6) |
견해: 메모리 추이를 장시간 관찰하는 것이 누수 탐지의 실용적 방법이다. 몇 시간 실행하며 RSS를 기록하면 증가 추세가 보인다. 운영 로봇은 며칠~몇 주 연속 동작하므로, 시간당 수 MB의 누수도 결국 문제가 된다.
견해: CPU 여유를 충분히 두라. 100%에 가까우면 지터가 급증하고(6.6.6), 예외 상황에서 대응할 여력이 없다. 평상시 여유를 확보하는 것이 안정성의 실질 조건이다 — 구체적 목표치는 시스템 특성에 따라 정한다.
11.7.7 측정 방법론
측정이 신뢰할 만하려면 절차가 있어야 한다.
| 단계 | 유의 |
|---|---|
| 2 환경 | 목표 하드웨어에서 — 개발 PC 아님 |
| 2 부하 | 실제와 유사한 부하 |
| 3 워밍업 | 초기 할당·캐시·디스커버리 제외 |
| 4 시간 | 꼬리를 포착할 만큼 길게 |
| 5 반복 | 회차 간 변동 확인 |
| 7 기록 | 커밋·설정·하드웨어 |
함정: 개발 PC에서 측정한 값은 임베디드 로봇을 대변하지 않는다(10.6.2의 ⑤ 실행 환경 간극). CPU·메모리 대역·캐시가 자릿수로 다를 수 있다. 목표 하드웨어에서 측정하는 것이 원칙이다.
함정: 부하 없이 측정한 값은 낙관적이다. 실제로는 항법·인지·기록이 동시에 실행되며 자원을 경쟁한다. 전체 시스템을 기동한 상태에서 측정해야 실효 값이 나온다.
견해: 워밍업 구간을 반드시 제외하라. 시작 직후에는 디스커버리(3.3), 메모리 할당, 캐시 미스, JIT 성격의 초기화가 겹쳐 값이 크게 악화된다. 수 초~수십 초를 버리고 정상 상태를 측정한다.
견해: 측정 조건을 기록하지 않으면 나중에 비교할 수 없다. 커밋 해시·설정 파일·하드웨어·부하 조건을 결과와 함께 남기면, 몇 달 뒤 "그때보다 느려졌다"를 판정할 수 있다.
11.7.8 흔한 실수
| 실수 | 왜 틀리나 |
|---|---|
| 평균만 본다 | 꼬리가 문제(11.7.3) |
| 개발 PC에서 측정 | 목표 환경과 다름 |
| 부하 없이 측정 | 낙관적 |
| 워밍업 포함 | 비관적·잡음 |
| 짧게 측정 | 드문 사건 놓침 |
| 한 번만 측정 | 변동 미확인 |
| 측정이 대상을 바꿈 | 관찰자 효과(11.6.7) |
now()로 경과 시간 | 시계 점프(1.10.5) |
| 시계 미동기 상태 비교 | 지연에 오차 섞임 |
| 조건 미기록 | 재현·비교 불가 |
| 최적화 후 재측정 안 함 | 효과 불명 |
| 병목 아닌 곳 최적화 | 노력 낭비 |
견해: "병목 아닌 곳 최적화"가 가장 흔한 낭비다. 종단 지연을 먼저 구간 분해하면(11.7.4), 어디를 고쳐야 하는지가 드러난다. 측정 없이 최적화하지 말라는 오래된 격언이 로봇에서도 그대로다.
견해: 측정 자체가 부하를 만든다는 점을 늘 의식하라.
ros2 topic hz·트레이싱·RViz2(11.4.5)·기록(11.2.6)이 모두 자원을 쓴다. 측정 도구를 끈 상태의 종단 지표와 비교해 영향을 확인한다.
11.7.9 회귀 체계
성능은 조용히 악화된다. 감시 체계가 필요하다.
| 계층 | 내용 |
|---|---|
| 단위 성능 | 알고리즘 함수 시간 |
| 통합 성능 | 시뮬·bag 기반 종단 지연 |
| 자원 | CPU·메모리 |
| 실기 정기 측정 | 실제 하드웨어 |
함정: CI 러너의 성능은 변동한다(4.10.6). 공유 러너에서 측정한 절대 시간은 신뢰도가 낮다. 여유 있는 임계로 큰 회귀만 검출하거나, 전용 하드웨어에서 측정한다.
견해: "큰 회귀만 검출하는" 것이 현실적이다. 5% 변동을 검출하려 하면 오탐이 대량 발생해 아무도 안 본다. 2배 느려짐 같은 명백한 회귀를 확실히 검출하는 것이 실질 가치가 크다.
견해: 운영 중 지표를 진단으로 상시 수집하는 것이 최종 방어선이다(6.9.5). 실기의 실제 부하·환경에서의 지연·주기·자원을 기록하면, CI가 검출하지 못한 회귀와 환경 변화를 발견할 수 있다. 11.3.2의 상시 기록에 성능 진단을 포함시키면 사후 분석도 가능해진다.
진단으로 내보낼 성능 지표:
| 지표 | 주기 |
|---|---|
| 종단 지연 p95 | 1 Hz |
| 주요 콜백 주기 지터 | 1 Hz |
| CPU·메모리 | 1 Hz |
| 큐 적체 | 1 Hz |
| 드롭 카운트 | 1 Hz |
11.7.12 내장 토픽 통계 — 코드를 안 고치고 측정하는 법
번호에 대하여: 이 절은 나중에 추가되었다. 기존 번호를 밀지 않는다는 규약(CLD-03)에 따라 번호는 끝에 붙이고, 읽는 흐름을 위해 함정 카탈로그 앞에 놓았다.
지금까지의 측정은 계측을 심는 방식이었다 — 스탬프를 기록하고, 로그를 남기고, 트레이싱을 켠다. rclcpp에는 그보다 앞서 시도해 볼 것이 하나 있다. 구독 하나하나에 대한 주기·나이 통계를 미들웨어 계층이 직접 계산해 토픽으로 내보내는 기능이다.
// ① 노드 전체에 켜기
auto node = std::make_shared<rclcpp::Node>(
"perf_probe", rclcpp::NodeOptions().enable_topic_statistics(true));
// ② 구독 하나만 켜기 (노드 설정과 무관하게)
rclcpp::SubscriptionOptions so;
so.topic_stats_options.state = rclcpp::TopicStatisticsState::Enable;
so.topic_stats_options.publish_topic = "/statistics"; // 기본값
so.topic_stats_options.publish_period = std::chrono::seconds(1); // 기본값
sub_ = create_subscription<SensorMsg>("scan", rclcpp::SensorDataQoS(), cb, so);
| 옵션 | 기본값 | 비고 |
|---|---|---|
NodeOptions::enable_topic_statistics | false | 노드의 모든 구독에 적용 |
topic_stats_options.state | NodeDefault | Enable·Disable로 구독별 재정의 |
topic_stats_options.publish_topic | /statistics | 바꿀 수 있다 |
topic_stats_options.publish_period | 1초 | 0보다 커야 한다 |
topic_stats_options.qos | SystemDefaultsQoS().keep_last(10) | 구독 QoS와 분리된 안정적 QoS |
실행 확인(2026-08-16): 위 표는
rclcpp/subscription_options.hpp·node_options.hpp원문이며, 노드 옵션만 켜고 구독 옵션은 변경하지 않아도/statistics가 나타나는 것을 실행으로 확인했다.
11.7.12.1 무엇이 나오는가
$ ros2 topic echo /statistics
measurement_source_name: probe # 측정한 노드
metrics_source: message_period # 또는 message_age
unit: ms
window_start: {sec: …}
window_stop: {sec: …}
statistics:
- {data_type: 1, data: 33.4} # 평균
- {data_type: 3, data: 51.2} # 최대
- {data_type: 2, data: 29.8} # 최소
- {data_type: 5, data: 30.0} # 표본 수
- {data_type: 4, data: 4.1} # 표준편차
메시지 타입은 statistics_msgs/msg/MetricsMessage이며, data_type은 statistics_msgs/msg/StatisticDataType의 상수다 — 실행 확인.
| 값 | 상수 | 의미 |
|---|---|---|
| 1 | STATISTICS_DATA_TYPE_AVERAGE | 평균 |
| 2 | STATISTICS_DATA_TYPE_MINIMUM | 최소 |
| 3 | STATISTICS_DATA_TYPE_MAXIMUM | 최대 |
| 4 | STATISTICS_DATA_TYPE_STDDEV | 표준편차 |
| 5 | STATISTICS_DATA_TYPE_SAMPLE_COUNT | 표본 수 |
두 지표의 뜻이 다르다 — 이 구분이 이 기능의 값어치다.
metrics_source | 측정하는 것 | 답해 주는 질문 |
|---|---|---|
message_period | 이 구독이 메시지를 받은 간격 | "센서가 약속한 주기로 오고 있나"(11.7.5) |
message_age | 메시지 헤더의 스탬프와 수신 시각의 차이 | "데이터가 얼마나 묵었나" — 종단 지연의 부분집합(11.7.4) |
11.7.12.2 한계 — 이것으로 안 되는 것
함정:
message_age는 헤더가 있는 메시지에만 의미가 있다.std_msgs/msg/String처럼std_msgs/msg/Header가 없는 타입으로 실행하면message_age의 모든 통계가NaN으로 나온다(실행 확인). 표본 수가 0인 창(window)에서도 마찬가지다./statistics에 NaN이 가득하다면 고장이 아니라 스탬프가 없다는 뜻이다(7.1.6).
그리고 이 기능이 측정하는 것은 구독 지점까지다.
| 재 준다 | 못 측정한다 |
|---|---|
| 수신 간격, 수신 시점의 메시지 나이 | 콜백 실행 시각(executor 대기 시간이 빠진다 — 2.2) |
| 구독별로 분리된 값 | 콜백 처리 시간, 체인 전체의 종단 지연(12.4) |
| 코드 수정 없이(노드 옵션만으로) | 발행 측 지표 |
견해: 성능 조사의 0단계로 쓰라. 노드 옵션 한 줄이면 켜지고,
/statistics를 기록해 두면(11.3.2) "언제부터 20 Hz가 아니었나"를 사후에 답할 수 있다. 그런데 "콜백이 지연된다"는 문제는 이것으로 안 보인다 — 수신은 제때 됐는데 executor가 늦게 부르는 경우가 정확히 그 관측 공백다. 거기서부터는 트레이싱(11.6)이 필요하다. 순서는/statistics→ 트레이싱 → 손계측이 경제적이다.
함정: 켜면 비용이 든다. 구독마다 계산과 1초 주기 발행이 추가된다. 고빈도 토픽이 수십 개인 노드에서 전부 켜면 그 자체가 부하이며, 특히 무선으로
/statistics가 나가면 대역을 소모한다(7.8.6). 조사할 때 켜고, 상시 수집은 대상을 골라서 하라. 비용의 크기는 환경 의존이므로 직접 측정해야 한다(확인 필요).
정직:
rclpy에서 같은 기능이 제공되는지는 확인하지 않았다(확인 필요). 위 서술과 실측은 모두rclcpp기준이다.
11.7.13 이 책의 실측을 자기 노드에서 재현하는 법 — 계측 도구 다섯
이 책의 여러 장에 붙은 실측 절은 전부 아래 다섯 가지 도구로 얻은 것이다. 어느 것도 특별한 패키지가 필요 없고, 대부분 스무 줄 안쪽이다. 독자가 자기 노드에서 같은 숫자를 얻을 수 있도록 한자리에 모은다.
각 도구에 "쓰면서 실제로 걸린 함정"을 함께 적는다 — 그것을 모르면 틀린 숫자를 얻고도 맞다고 믿게 된다.
11.7.13.1 할당 후크 — "이 경로에 할당이 있는가"
static std::atomic<bool> counting{false};
static std::atomic<size_t> n_new{0};
void * operator new(size_t sz) {
if (counting.load(std::memory_order_relaxed))
n_new.fetch_add(1, std::memory_order_relaxed);
void * p = std::malloc(sz);
if (!p) throw std::bad_alloc();
return p;
}
void operator delete(void * p) noexcept { std::free(p); }
void operator delete(void * p, size_t) noexcept { std::free(p); }
구간을 counting = true/false로 감싸고 반복 횟수로 나누면 회당 할당 수가 나온다.
| 답하는 질문 | 이 책의 결과 |
|---|---|
| 정상 운전 구간이 무할당인가 | 아니다 — 지속 executor로도 회당 7~9회(12.3.12.2) |
| 어떤 API가 비용이 큰가 | rclcpp::spin_some(node) 자유 함수는 회당 143회(12.3.12.1) |
| 발행 방식의 차이 | const& 재사용 0회 vs unique_ptr 1회(12.3.12.3) |
함정: 초기화 구간을 빼고 재라. 노드·발행자·구독자 생성만으로 13,088회가 나온다(12.3.12.4). 디스커버리가 끝난 뒤 예열을 거쳐 측정하지 않으면 그 값에 묻힌다.
11.7.13.2 겹침 계측 — "콜백이 정말 동시에 실행되는가"
static std::atomic<int> inflight{0}, max_inflight{0};
void on_callback() {
int cur = inflight.fetch_add(1) + 1;
int mx = max_inflight.load();
while (cur > mx && !max_inflight.compare_exchange_weak(mx, cur)) {}
/* … 실제 작업 … */
inflight.fetch_sub(1);
}
| 답하는 질문 | 이 책의 결과 |
|---|---|
| 콜백 그룹 설정이 의도대로인가 | 동시성 매트릭스 7행 전수 확인(2.4.12.1) |
Reentrant가 실제로 동시 실행하는가 | 최대 4(스레드 수만큼)(2.4.12.2) |
함정 — 이 책이 두 번 걸렸다: 동시성 실험은 "겹쳤는지"를 먼저 계측해야 한다. 콜백이 주기보다 짧으면
Reentrant그룹이어도 겹치지 않는다. 그 상태로 경쟁 조건을 측정하면 "안전하다"는 잘못된 결론이 나온다(2.9.10.4에서 실제로 그렇게 될 뻔했다).max_inflight가 2 이상인지 확인한 뒤에 결과를 읽으라.
11.7.13.3 주기 분포 — "드리프트인가 지터인가"
콜백 진입 시각을 벡터에 모으고 차분의 분위수와 누적 오차를 낸다.
std::vector<double> t; // 콜백마다 진입 시각(ms)
// 종료 후
std::vector<double> d; // 차분
for (size_t i = 1; i < t.size(); ++i) d.push_back(t[i] - t[i-1]);
std::sort(d.begin(), d.end());
double 누적오차 = (t.back() - t.front()) - T_ms * (t.size() - 1);
두 값을 함께 보는 것이 핵심이다 — 11.7.3의 분위수는 지터를, 누적 오차는 드리프트를 가른다.
| 답하는 질문 | 이 책의 결과 |
|---|---|
| 드리프트가 있는가 | 1500 주기에 0.000%(2.5.13.2) |
| executor 선택이 꼬리에 영향을 주는가 | Static +0.125 ms < Single +0.528 < Multi +1.161(2.5.13.3) |
함정:
-O2로 빌드하라. 디버그 빌드의 지터는 측정 대상이 아니라 빌드의 성질이다.
11.7.13.4 링크 확인 — "이 층이 정말 저 층만 부르는가"
readelf -d <라이브러리 또는 실행파일> | grep NEEDED # 직접 의존
nm -D --undefined-only <라이브러리> | grep <심볼> # 런타임 결합 여부
strings <라이브러리> | grep -E "^rmw_" # 하드코딩된 이름
코드를 읽지 않고 구조를 확정하는 가장 비용이 낮은 방법이다.
| 답하는 질문 | 이 책의 결과 |
|---|---|
| 사용자 코드가 벤더를 링크하는가 | 0회 — 재컴파일 없이 RMW를 바꿀 수 있는 근거(2.1.12.1) |
| 계층도에 빠진 층이 있는가 | rmw_implementation이 있었다(2.1.12.3) |
11.7.13.5 유실 주입 — "이 링크에서 견디는가"
tc qdisc add dev eth0 root netem loss 5% # NET_ADMIN 권한 필요
tc qdisc del dev eth0 root # 원복
| 답하는 질문 | 이 책의 결과 |
|---|---|
RELIABLE의 대가는 무엇인가 | 처리량이 아니라 꼬리 지연(p99가 100~200배)(3.9.13.1) |
| 메시지 크기가 얼마나 중요한가 | 유실 5%에서 1 KB 92% ↔ 256 KB 0%(3.9.13.3) |
함정: 유실이 있는 링크에서는 측정 자체가 불안정하다. 디스커버리도 같은 링크를 타므로 준비 시간이 불규칙하다. 한 번의 결과로 결론 내지 말고 반복해 범위로 보고하라(3.9.13.6).
11.7.13.6 공통 원칙 — 측정을 먼저 반증하라
다섯 도구 모두에서 같은 실수를 반복해 겪었다.
| 겪은 일 | 방어 |
|---|---|
| intra-process를 꺼도 버퍼 주소가 일치했다(할당자 재사용) | 대조군을 함께 실행한다(2.7.13.5) |
Reentrant인데 유실이 0이었다(겹치지 않았다) | 전제를 계측한다(11.7.13.2) |
유실 링크에서 RELIABLE 수신이 0이었다(재현 안 됨) | 반복하고 범위로 본다(3.9.13.6) |
tracemalloc 스냅숏 차이가 앞뒤 안 맞는 값을 냈다 | 도구가 무엇을 세는지 확인한다(아래) |
tracemalloc의 스냅숏 차이는 "할당 횟수"가 아니라 "잔존 블록"이다. 루프 안에서 만들고 버리는 객체는 차이가 0에 가깝게 나오므로, 그것으로 할당량을 측정하면 "메시지 재사용이 새 객체보다 비용이 크다" 같은 앞뒤 안 맞는 값이 나온다(81차에 겪었다). Python에서 할당 "양"을 측정하려면get_traced_memory()의 peak를, 비용을 측정하려면 시간을 보라.놀라운 결과가 나오면 측정 장치를 먼저 의심하라. 이 책의 실측 회차에서 틀린 결론에 가장 가까이 갔던 순간은 전부 "측정이 옳다고 가정했을 때"였다.
11.7.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 평균으로 판정 | 꼬리 놓침 | p95·p99·max |
| 2 | 개발 PC 측정 | 실기와 다름 | 목표 하드웨어 |
| 3 | 무부하 측정 | 낙관적 | 전체 시스템 |
| 4 | 워밍업 미제외 | 비관적 | 초기 버림 |
| 5 | now()로 경과 측정 | 점프 오류 | steady_clock |
| 6 | stamp 덮어쓰기 | 종단 지연 측정 불가 | 원본 유지(7.7.4) |
| 7 | 시계 미동기 | 오차 섞임 | PTP·NTP 확인 |
| 8 | 관찰자 효과 무시 | 오판 | 유무 비교 |
| 9 | 조건 미기록 | 비교 불가 | 커밋·설정 기록 |
| 10 | 측정 없이 최적화 | 낭비 | 구간 분해 먼저 |
| 11 | CI 절대 시간에 민감한 임계 | 오탐 | 큰 회귀만 |
| 12 | 메모리 추이 미관찰 | 장기 누수 | 장시간 기록 |
| 13 | CPU 여유 없음 | 지터 급증 | 여유 확보 |
| 14 | /statistics의 NaN을 고장으로 오해 | 무익한 작업 | 헤더 없는 타입은 message_age 계산 불가(11.7.12.2) |
| 15 | 토픽 통계를 상시 전부 켬 | 그 자체가 부하·대역 | 조사 시 대상만(11.7.12.2) |
| 16 | 동시성 실험에서 겹침을 계측 안 함 | "안전하다"는 오결론 | max_inflight 확인(11.7.13.2) |
| 17 | 놀라운 결과를 그대로 채택 | 측정 장치의 오류 | 대조군을 함께 실행한다(11.7.13.6) |
11.7.11 체크포인트
- 성능 지표 여덟 가지와 각각의 중요성을 말할 수 있는가. 로봇에서 가장 중요한 것은?
- 목표를 먼저 정하라는 조언에서, 목표에 포함되어야 할 네 요소는?
- 평균이 쓸모없는 이유와, 판정 기준으로 써야 할 통계는?
- 히스토그램과 시계열이 각각 알려주는 것은? 이봉 분포는 무엇을 시사하는가.
header.stamp전파로 종단 지연을 측정하는 방법과, 그 전제 셋은?- stamp를 덮어쓰면 안 되는 이유 둘(정합성 외)은?
- "각 노드가 누적 지연을 진단으로 내보내는" 패턴의 이점 셋은?
- 경과 시간 측정에
steady_clock을 써야 하는 이유는? ros2 topic hz의 주의점과, 무엇을 함께 봐야 하는가.- 대역 계산식에서 $n_{\text{sub}}$가 곱해지는 의미는?
- 메모리 추이 관찰이 중요한 이유는?
- 측정 방법론 일곱 단계와, 각 단계의 유의점을 말할 수 있는가.
- 흔한 측정 실수 열두 가지 중 "가장 흔한 낭비"는 무엇인가.
- 성능 회귀 체계의 네 계층과, CI에서 "큰 회귀만 검출하라"는 조언의 근거는?
- 운영 중 진단으로 내보낼 성능 지표 다섯 가지를 들 수 있는가.
- 코드를 고치지 않고 구독별 주기·나이를 얻는 방법은 무엇이며, 그 값이 콜백 실행 시각을 포함하지 않는다는 사실이 진단에 주는 한계는 무엇인가.
- 회당 할당 수를 측정하려면 무엇을 후킹하며, 어느 구간을 빼야 하는가(11.7.13.1).
- 동시성 실험에서 결과를 읽기 전에 반드시 확인할 값은 무엇인가(11.7.13.2).
다음 장 11.1은 제11부의 마지막 — 이 모든 도구를 현장에서 실제로 쓰는 순서로 구성한 디버깅 플레이북이다.