제11부 도구·관측성
11.2 rosbag2 심층 — MCAP·저장소·성능
11.2.1 이 장에서 배우는 것 / 선수 지식
이 책은 여러 장에서 "bag으로 기록하라" 고 반복했다 — 매핑 세션(8.2.10), 항법 디버깅(8.8.6), 인지 회귀 데이터셋(10.6.6), 사고 분석(8.7.9). 이 장이 그 도구의 내부다.
rosbag2를 제대로 아는 것이 중요한 이유는 명확하다. 기록에 실패하면 그 순간은 다시 오지 않는다. 현장에서 어렵게 재현한 문제를 기록하려 했는데 디스크가 가득 찼거나, 메시지가 유실됐거나, QoS가 안 맞아 아무것도 안 담겼다면 그 기회를 잃는다.
배우는 것:
- 구조 — 기록기·저장 플러그인·직렬화.
- MCAP과 SQLite3 — Jazzy의 기본과 그 이유.
- 자기 기술성 — bag이 메시지 정의를 담는다는 것.
- 압축 — 파일 단위와 메시지 단위.
- 성능 — 유실이 발생하는 지점.
- 분할과 메타데이터 — 파일 관리.
- 프로그램 접근 — 코드에서 읽고 쓰기.
선수 지식: 1.3(직렬화·type hash), 1.6(QoS), 1.4.4(유실 지점), 1.10(시간).
정직: rosbag2의 저장 플러그인·CLI 옵션은 배포판마다 변해 왔다. BK-05·CHG-01에서 확인했듯 Jazzy에서 MCAP이 기본 저장소로 전환된 것으로 알려져 있으나, 정확한 기본값·옵션 이름은
ros2 bag record --help와 rosbag2 문서가 정본이다(확인 필요). 성능 수치는 하드웨어 의존이므로 제시하지 않는다(3.6.1). 이 환경에서 실행 검증하지 않았다(목차 3). Jazzy 기준의 기본 저장소·기록 옵션·변환·복구·QoS 오버라이드는 실제로 실행해 확인했다(실행 확인 — 11.2.13). 성능 수치는 여전히 하드웨어 의존이라 제시하지 않는다.검증 상태(추가): 이 장의 서술 가운데 1곳은 이후 회차에서 Jazzy 실물과 대조해 확인했다 — 본문의 (실행 확인) 표기가 그것이다. 표기가 없는 서술은 위 고지가 그대로 적용된다(목차 3의 검증 범위 표).
11.2.2 구조
핵심: rosbag2는 메시지를 역직렬화하지 않는다.
1.3.8에서 본 제네릭 구독을 쓴다 — 토픽의 타입 이름만 알면 바이트를 그대로 받아 저장한다. 이것이 임의의 타입을 기록할 수 있는 이유이자 빠른 이유다.
| 결과 | 설명 |
|---|---|
| 임의 타입 기록 | 커스텀 메시지도 문제없다 |
| CPU 효율 | 역직렬화·재직렬화 없음 |
| 타입 정의가 필요 없음 | 기록 시점에는 |
| 재생 시에도 그대로 | 바이트를 그대로 발행 |
함정: 기록은 타입 정의 없이 되지만, 재생 후 구독해서 내용을 보려면 정의가 필요하다(Jazzy의
ros2 bag서브커맨드는record·play·info·list·convert·reindex·burst이며echo는 없다 — 실행 확인)(확인 필요: 자기 기술 bag의 지원 범위 — 11.2.4). 커스텀 메시지 패키지를 소싱하지 않으면 내용을 볼 수 없다(1.3.10의 1번과 같은 성질).
11.2.3 MCAP과 SQLite3
| 축 | SQLite3 | MCAP |
|---|---|---|
| 형식 | 관계형 DB | 로그 전용 컨테이너 |
| 쓰기 성능 | 보통 | 좋음 |
| 스트리밍 읽기 | 제한적 | 최적화됨 |
| 자기 기술성 | 부분적 | 메시지 정의 내장 |
| 도구 생태계 | ROS 중심 | 다양(외부 도구 지원) |
| 압축 | 별도 | 내장 |
| 파일 구조 | 단일 DB | 청크 기반 |
MCAP이 로그 데이터에 맞춰 설계된 형식이다. SQLite3는 범용 데이터베이스라 임의 접근에는 좋지만, 순차 쓰기·읽기가 지배적인 로깅에는 최적이 아니다.
# 저장소 지정 (Jazzy 기본은 mcap — 실행 확인, 11.2.13)
ros2 bag record -s mcap -a
ros2 bag record -s sqlite3 -a
견해: MCAP의 실무적 이점 중 "외부 도구 지원"이 크다. ROS 밖의 시각화·분석 도구가 MCAP을 읽을 수 있어, 데이터 파이프라인을 ROS에 가두지 않아도 된다. 장기 데이터 자산(10.6.6)을 다룰 때 특히 유리하다.
정직: 기존 SQLite3 bag은 계속 읽을 수 있어야 하지만, 배포판·버전에 따라 플러그인 설치가 필요할 수 있다
(확인 필요). 과거 데이터를 보관한다면 읽기 가능성을 정기적으로 확인하는 것이 좋다.
11.2.4 자기 기술성
bag이 메시지 정의를 함께 저장한다.
이것이 중요한 이유:
| 이유 | 설명 |
|---|---|
| 미래 재생 | 그 패키지가 없어도 구조를 안다 |
| 외부 도구 | ROS 없이 읽기 |
| 버전 추적 | 그때의 정의가 무엇이었나 |
| type hash 검증 | 정의가 바뀌었는지 확인(1.3.5) |
견해: 자기 기술성이 장기 데이터 보관의 핵심이다. 1.2.8에서 "메시지 정의를 바꾸면 옛 bag이 새 코드로 안 읽힌다"고 했는데, 정의가 bag에 있으면 최소한 그때의 구조는 복원할 수 있다. 완전한 해결은 아니지만 큰 차이다.
정직: "정의가 있으면 임의의 도구가 읽을 수 있다"의 실제 지원 범위는 확인이 필요하다
(확인 필요). ROS 2 도구가 bag의 정의를 이용해 미설치 타입을 다룰 수 있는지는 버전 의존이다.
11.2.5 압축
# 파일 단위 압축
ros2 bag record -a --compression-mode file --compression-format zstd
# 메시지 단위 압축
ros2 bag record -a --compression-mode message --compression-format zstd
| 방식 | 특성 |
|---|---|
| file | 파일을 다 쓴 뒤 압축 — 압축률 좋음, 재생 시 전체 해제 |
| message | 메시지마다 압축 — 임의 접근 가능, 압축률 낮음 |
| 없음 | 가장 빠름 |
| 데이터 | 압축 효과 |
|---|---|
| 이미지(비압축) | 큼 |
| 이미 압축된 이미지 | 거의 없음 |
| 포인트클라우드 | 중간~큼 |
| 소형 메시지(tf·odom) | 중간 |
견해: CPU 여유가 없으면 압축하지 말라. 기록 중 압축은 CPU를 쓰고, 그것이 메시지 유실을 유발할 수 있다(11.2.6). 기록은 압축 없이 하고 나중에 후처리로 압축하는 것이 안전한 경우가 많다.
함정: 이미 압축된 데이터(
CompressedImage— 7.2.7)를 다시 압축하면 CPU만 쓰고 효과가 없다. 무엇을 기록하는지에 따라 판단한다.
11.2.6 성능과 유실
bag 기록에서도 메시지가 유실된다.
| 지점 | 유실 원인 |
|---|---|
| ① | QoS 불일치·네트워크·큐(1.4.4) |
| ② 수신 큐 | 기록기가 못 따라감 |
| ③ 버퍼 | 메모리 |
| ④ 디스크 | 쓰기 속도 부족·디스크 가득 |
진단:
# 기록 중 경고 확인 (드롭 메시지가 로그에 나온다)
ros2 bag record -a 2>&1 | grep -i drop
# 기록 후 메시지 수 확인
ros2 bag info my_bag
# 예상 수와 비교: 주기 × 기간
함정: 디스크 쓰기 속도가 가장 흔한 병목이다. SD 카드는 특히 느리고, 대용량 토픽(이미지·포인트클라우드 — 7.8.2)을 기록하면 금방 한계에 닿는다. NVMe·SSD를 쓰거나 기록량을 줄여야 한다.
대역 계산(1.3.9):
$$B_{\text{disk}} = \sum_i S_{\text{msg},i} \cdot f_i$$
이 값이 디스크 쓰기 속도보다 작아야 한다.
| 구성 | 대역 |
|---|---|
| tf + odom + scan | 수백 KB/s |
| + 압축 이미지 5 Hz | 수 MB/s |
| + 원본 이미지 30 Hz | 수백 MB/s — 대부분 불가 |
| + 포인트클라우드 10 Hz | 수십 MB/s |
견해: 기록 대역 예산을 7.8.2의 표에서 계산하라. 무엇을 기록할지 정하기 전에 디스크가 감당할 수 있는지 확인하면, 현장에서 "기록이 안 됐다"를 피할 수 있다.
성능 옵션:
# 버퍼 크기·쓰기 방식 (실행 확인 — 옵션 이름은 아래 그대로다)
ros2 bag record -a --max-cache-size 500000000
ros2 bag record -a --storage-preset-profile fastwrite
옵션 이름은 실행 확인했다(11.2.13.2에 전수).
--storage-preset-profile이 받는 값은none·fastwrite·zstd_fast·zstd_small넷이다. 효과는 여전히 하드웨어·플러그인 의존이므로 자신의 환경에서 측정하라.
디스크 여유 확인:
df -h /path/to/bags
# 기록 전에 확인하고, 장시간 기록이면 감시
함정: 디스크가 가득 차면 기록이 조용히 멈추거나 파일이 손상될 수 있다. 장시간 기록에서는 여유 공간 감시와 자동 정리가 필요하다(11.3.7).
11.2.7 분할과 메타데이터
# 크기로 분할
ros2 bag record -a --max-bag-size 1000000000 # 1 GB
# 시간으로 분할
ros2 bag record -a --max-bag-duration 300 # 300초
# 크기로 분할 (실행 확인)
ros2 bag record -a --max-bag-size 1000000000
# 주의: "오래된 조각을 지우는 순환 기록" 옵션은 없다 — 11.2.13.2
분할의 이점:
| 이점 | 설명 |
|---|---|
| 파일 크기 관리 | 전송·복사가 쉬움 |
| 부분 손실 방지 | 하나가 손상돼도 나머지는 |
| 순환 기록 | 오래된 것 삭제(11.3.3) |
| 병렬 처리 | 분석 시 |
메타데이터:
# metadata.yaml (개념)
rosbag2_bagfile_information:
version: 9
storage_identifier: mcap
duration: {nanoseconds: 120000000000}
starting_time: {nanoseconds_since_epoch: ...}
message_count: 45231
topics_with_message_count:
- topic_metadata:
name: /scan
type: sensor_msgs/msg/LaserScan
serialization_format: cdr
offered_qos_profiles: "..." # ★ QoS 기록
message_count: 1200
files: [...]
offered_qos_profiles가 중요하다 — 원본 발행자의 QoS가 기록되어, 재생 시 같은 QoS로 발행할 수 있다(11.3.4).
ros2 bag info my_bag # 요약
cat my_bag/metadata.yaml # 상세
함정:
metadata.yaml이 없거나 손상되면 bag을 읽기 어렵다. 기록이 비정상 종료되면 이것이 안 써질 수 있다(확인 필요: 복구 방법). Ctrl-C로 정상 종료하는 것이 안전하며, 강제 종료는 피한다.
11.2.8 프로그램 접근
코드에서 bag을 읽고 쓴다.
#include <rosbag2_cpp/reader.hpp>
#include <rosbag2_cpp/writer.hpp>
// 읽기
rosbag2_cpp::Reader reader;
reader.open("my_bag");
while (reader.has_next()) {
auto msg = reader.read_next();
// msg->topic_name, msg->time_stamp, msg->serialized_data
if (msg->topic_name == "/scan") {
sensor_msgs::msg::LaserScan scan;
rclcpp::Serialization<sensor_msgs::msg::LaserScan> ser;
rclcpp::SerializedMessage serialized(*msg->serialized_data);
ser.deserialize_message(&serialized, &scan); // 1.3.8
// 처리
}
}
import rosbag2_py
from rclpy.serialization import deserialize_message
from rosidl_runtime_py.utilities import get_message
reader = rosbag2_py.SequentialReader()
reader.open(
rosbag2_py.StorageOptions(uri='my_bag', storage_id='mcap'),
rosbag2_py.ConverterOptions('', ''))
type_map = {t.name: t.type for t in reader.get_all_topics_and_types()}
while reader.has_next():
topic, data, timestamp = reader.read_next()
msg_type = get_message(type_map[topic])
msg = deserialize_message(data, msg_type)
# 처리
쓰기:
writer = rosbag2_py.SequentialWriter()
writer.open(
rosbag2_py.StorageOptions(uri='out_bag', storage_id='mcap'),
rosbag2_py.ConverterOptions('', ''))
writer.create_topic(rosbag2_py.TopicMetadata(
name='/result', type='std_msgs/msg/String', serialization_format='cdr'))
writer.write('/result', serialize_message(msg), timestamp_ns)
정직: API 세부(클래스명·인자)는 버전 의존이다
(확인 필요).
활용:
| 용도 | 설명 |
|---|---|
| 오프라인 분석 | 지표 추출·통계 |
| 필터링 | 필요한 토픽·구간만 추출 |
| 형식 변환 | CSV·다른 형식으로 |
| 회귀 데이터셋 | 알고리즘에 입력(10.6.6) |
| 후처리 | 압축·익명화 |
견해: "bag에서 지표를 추출하는 스크립트"를 만들어 두면 반복 분석이 쉬워진다. 항법 성공률(8.8.5)·파지 성공률(9.5.8)·주기 분포(2.5.6)를 bag에서 계산하면, 현장 데이터를 일관되게 평가할 수 있다.
CLI 도구:
ros2 bag info my_bag
ros2 bag convert -i my_bag -o convert.yaml # 형식·토픽 변환(11.2.13.3)
ros2 bag reindex my_bag # 인덱스 복구 — 실행 확인(11.2.13.3)
11.2.9 QoS와 기록
1.6의 QoS가 기록에도 적용된다.
| 문제 | 설명 |
|---|---|
| QoS 불일치 | 기록기가 구독을 못 함 → 아무것도 안 담김(1.5.4) |
TRANSIENT_LOCAL 토픽 | 기록 시작 시 과거 메시지가 한꺼번에(1.11.9) |
BEST_EFFORT 센서 | 기록기 부하 시 유실 |
# QoS 오버라이드 (실행 확인 — record·play 둘 다 같은 옵션을 받는다)
ros2 bag record --qos-profile-overrides-path qos_override.yaml /scan
# qos_override.yaml
/scan:
reliability: best_effort
durability: volatile
history: keep_last
depth: 10
함정: 센서 토픽이
BEST_EFFORT인데 기록기가 기본RELIABLE로 구독하면 아무것도 안 담긴다(1.6.3.1의 최다 사고). 기록 시작 후ros2 bag info나 메시지 수로 확인하는 습관이 필요하다.
견해: rosbag2가 발행자 QoS에 맞춰 자동 조정하는 경우가 있지만
(확인 필요: 동작 범위), 믿지 말고 확인하라. 현장에서 기록이 비어 있는 것을 나중에 발견하면 그 기회는 사라진 뒤다.
11.2.10 진단·검증
기록이 제대로 됐는지 확인하는 절차다.
# 1. 요약 — 토픽·메시지 수·기간
ros2 bag info my_bag
# 2. 예상 수와 비교
# 10 Hz × 120초 = 1200개여야 함
# 3. 내용 확인 (재생하며)
ros2 bag play my_bag &
ros2 topic echo /scan --once --no-arr
# 4. 파일 크기·개수
du -sh my_bag
ls -lh my_bag/
확인 항목:
| 항목 | 확인 |
|---|---|
| 기대 토픽이 전부 있는가 | 누락 없음 |
| 메시지 수가 합리적인가 | 주기 × 기간 |
| 기간이 맞는가 | 시작·종료 |
metadata.yaml 존재 | 정상 종료 |
| 파일 크기 | 예상 대비 |
견해: 현장에서 기록 직후
ros2 bag info를 확인하는 것을 절차로 만들라. 5초면 되고, "돌아와서 보니 비어 있었다"를 막는다. 특히 한 번뿐인 기회(현장 방문·재현된 문제)에서는 필수다.
11.2.13 저장소·옵션·복구 — 실측
이 장의 (확인 필요)는 대부분 명령 한 번이면 답이 나오는 것들이었다. Jazzy에서 실제로 기록·변환·복구까지 실행한 결과다(실행 확인).
11.2.13.1 기본 저장소는 mcap이다
아무 옵션 없이 기록하면 이렇게 나온다:
ros2 bag record -o b1 /chatter
ls b1/ # → b1_0.mcap metadata.yaml
metadata.yaml이 확인해 준다:
version: 9
storage_identifier: mcap
compression_format: ""
compression_mode: ""
설치된 저장 플러그인은 둘이다 — ros2 bag list storage가 mcap과 sqlite3를 준다. -s sqlite3을 주면 .db3로 기록되므로, 옛 bag과의 호환은 유지된다(11.2.3).
11.2.13.2 기록 옵션의 실제 이름
ros2 bag record --help가 내놓는 옵션 중 이 장이 다루는 것들이다(실행 확인):
| 목적 | 옵션 |
|---|---|
| 쓰기 버퍼 | --max-cache-size |
| 분할(시간) | --max-bag-duration |
| 분할(크기) | --max-bag-size |
| 성능 프리셋 | --storage-preset-profile {none, fastwrite, zstd_fast, zstd_small} |
| 압축 방식 | --compression-mode {none, file, message} · --compression-format {zstd} |
| 압축 병렬 | --compression-threads · --compression-queue-size · --compression-threads-priority |
| QoS 오버라이드 | --qos-profile-overrides-path |
| 저장소 세부 설정 | --storage-config-file |
| 스냅숏 | --snapshot-mode · --start-paused |
"오래된 조각을 지우는 순환 기록"은 없다. --max-bag-size·--max-bag-duration은 분할만 하고 지우지는 않는다. 디스크가 차면 기록이 멈추거나 시스템이 문제를 일으키므로(14.8.11), 오래된 bag을 지우는 일은 밖에서 해야 한다 — logrotate나 별도 정리 스크립트가 실무적 해법이다.
--compression-format이 받는 값은zstd하나뿐이다. 형식을 고르는 것이 아니라 켜고 끄는 스위치에 가깝다. 대신--compression-mode로 파일 단위(file)와 메시지 단위(message)를 고르는 것이 실제 선택지다.
11.2.13.3 convert와 reindex는 실제로 동작한다
둘 다 (확인 필요)로 남아 있었는데, 실행해 보니 정상 동작한다.
형식 변환 — sqlite3로 기록한 bag을 mcap으로 옮겨 봤다(실행 확인):
# convert.yaml
output_bags:
- uri: /tmp/b3
storage_id: mcap
all_topics: true
all_services: true
ros2 bag convert -i b2 -o convert.yaml
# → b3_0.mcap metadata.yaml
인덱스 복구 — metadata.yaml을 일부러 지우고 복구시켰다(실행 확인):
rm b2/metadata.yaml
ros2 bag reindex b2
# [INFO] [rosbag2_cpp]: Reindexing complete.
# → metadata.yaml 이 다시 생긴다
이것이 11.2.8의 함정("
metadata.yaml이 없거나 손상되면 bag을 읽기 어렵다")에 대한 해법이다. 기록이 비정상 종료돼 메타데이터가 안 써졌더라도 데이터 파일만 남아 있으면 되살릴 수 있다. 현장에서 전원이 끊긴 bag을 포기하기 전에 시도할 것.
11.2.13.4 QoS 오버라이드를 잘못 주면 조용히 빈 bag이 된다
이 장은 "rosbag2가 발행자 QoS에 맞춰 자동 조정하는 경우가 있지만 믿지 말라"고 적어 두었다. 실측이 그 경고를 정확히 뒷받침한다.
/chatter(기본 VOLATILE 발행자)를 상대로 오버라이드에 durability: transient_local을 요구해 봤다:
/chatter:
history: keep_last
depth: 10
reliability: reliable
durability: transient_local
[WARN] [rosbag2_recorder]: New publisher discovered on topic '/chatter',
offering incompatible QoS. No messages will be sent to it.
Last incompatible policy: DURABILITY_QOS_POLICY
기록은 "성공"한다 — 명령이 실패하지 않고 bag 파일도 생긴다. 다만 그 토픽의 메시지가 하나도 안 들어간다.
함정: 오버라이드는 요구 조건이지 협상이 아니다. 구독자(=기록기)가 발행자보다 강한 조건을 요구하면 1.5.4의 호환성 규칙 그대로 매칭이 안 된다. 자동 조정에 기대지 말고, 오버라이드를 쓸 때는 반드시
ros2 bag info로 메시지 수를 확인하라 — 0이면 이 경우다.
견해: 오버라이드는 "낮추는 방향"으로만 쓰는 편이 안전하다. 발행자가
RELIABLE인데 기록기만BEST_EFFORT로 낮추는 것은 매칭되지만(1.5.4), 그 반대는 위처럼 조용히 실패한다.--qos-profile-overrides-path는 센서 토픽의 과도한 신뢰성 요구를 덜어 기록 부하를 낮추는 용도가 본령이다.
11.2.11 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | QoS 불일치 | 아무것도 안 담김 | 오버라이드·확인 |
| 2 | 기록 후 확인 안 함 | 나중에 발견 | ros2 bag info 즉시 |
| 3 | 디스크 쓰기 속도 부족 | 메시지 유실 | SSD·기록량 감소 |
| 4 | 디스크 가득 | 중단·손상 | 사전 확인·감시 |
| 5 | 기록 중 압축 | CPU 부족 → 유실 | 후처리 압축 |
| 6 | 이미 압축된 데이터 재압축 | CPU 낭비 | 판단 |
| 7 | 강제 종료 | metadata.yaml 손실 | Ctrl-C 정상 종료 |
| 8 | 커스텀 타입 미소싱 | 내용을 못 봄 | 워크스페이스 소싱 |
| 9 | 분할 없이 장시간 | 거대 파일 | --max-bag-size |
| 10 | 대역 계산 없이 기록 | 유실·디스크 용량 급증 | 예산 계산(7.8.2) |
| 11 | TRANSIENT_LOCAL 토픽 다량 유입 | 시작 시 부하 | 인지·필요 시 제외 |
| 12 | 옛 SQLite3 bag 읽기 가정 | 플러그인 없음 | 읽기 가능성 확인 |
| 13 | QoS 오버라이드 후 bag이 빔 | 비호환으로 구독 미매칭 | 11.2.13.4 |
| 14 | metadata.yaml 손상 | ros2 bag reindex로 복구 가능 | 11.2.13.3 |
11.2.12 체크포인트
- rosbag2가 메시지를 역직렬화하지 않는다는 사실의 결과 네 가지를 말할 수 있는가.
- MCAP과 SQLite3의 차이 일곱 가지와, MCAP의 실무적 이점은?
- 자기 기술성이 무엇이며, 장기 데이터 보관에서 왜 중요한가.
- 압축 두 방식의 특성과, 기록 중 압축을 피하라는 조언의 근거는?
- bag 기록에서 유실이 발생하는 네 지점과, 가장 흔한 병목은?
- 기록 대역을 계산하는 식과, 원본 이미지 30 Hz가 왜 대부분 불가능한가.
- 분할의 이점 네 가지와,
metadata.yaml의offered_qos_profiles가 왜 중요한가. - 프로그램 접근의 활용 다섯 가지와, "지표 추출 스크립트"의 가치는?
- QoS 불일치가 기록에서 만드는 최악의 결과는? 어떻게 예방하는가.
- 기록 검증 항목 다섯 가지와, 현장에서 절차로 만들어야 하는 것은?
- rosbag2의 기본 저장소는 무엇인가. 순환 기록은 지원되는가?
metadata.yaml이 손상됐을 때의 복구 수단은(11.2.13).
다음 장 11.3은 이 도구를 어떻게 운용할 것인가 — 무엇을 언제 기록하고, 어떻게 재생하며, 데이터를 어떻게 관리하는가를 다룬다.