AEGLOSLAB ROS 2 Jazzy 대전

제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 구조

ROS 토픽 rosbag2 기록기 제네릭 구독 (1.3.8) 직렬화된 메시지 (역직렬화 없음) 저장 플러그인 MCAP 파일 SQLite3 파일 메시지 정의 수집

핵심: 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

축SQLite3MCAP
형식관계형 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이 메시지 정의를 함께 저장한다.

기록 시점 메시지 타입 이름 메시지 정의(IDL· msg 텍스트) type hash (1.3.5) 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 기록에서도 메시지가 유실된다.

토픽 발행 ① DDS 전달 QoS · 버퍼 (1.4.4) ② rosbag2 수신 큐 ③ 직렬화 버퍼 ④ 디스크 쓰기 파일
지점유실 원인
①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 함정 카탈로그

#함정증상대처
1QoS 불일치아무것도 안 담김오버라이드·확인
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)
11TRANSIENT_LOCAL 토픽 다량 유입시작 시 부하인지·필요 시 제외
12옛 SQLite3 bag 읽기 가정플러그인 없음읽기 가능성 확인
13QoS 오버라이드 후 bag이 빔비호환으로 구독 미매칭11.2.13.4
14metadata.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은 이 도구를 어떻게 운용할 것인가 — 무엇을 언제 기록하고, 어떻게 재생하며, 데이터를 어떻게 관리하는가를 다룬다.