제3부 미들웨어: DDS·RTPS·RMW
3.7 Fast DDS 튜닝 — XML 프로파일·전송
3.7.1 이 장에서 배우는 것 / 선수 지식
1.5부터 계속 "그것은 벤더 XML로 내려가야 한다"고 미뤄 왔다. 이 장이 그 XML이다. Fast DDS는 Jazzy의 기본 RMW이므로(BK-05), 대부분의 독자에게 이 장이 실제로 쓰게 될 튜닝 문서다.
목표는 XML 문법을 외우는 것이 아니다 — 그것은 eProsima 문서가 정본이다. 이 장의 목표는 어떤 문제에 어떤 손잡이가 대응하는지의 지도를 세우고, 튜닝이 실제로 적용됐는지 확인하는 방법을 익히는 것이다.
배우는 것:
- XML이 개입하는 지점 — 코드의 QoS와 XML이 충돌하면 무엇이 우선하는가.
- 프로파일 구조 — participant·datawriter·datareader·transport.
- 전송 설정 — UDP·TCP·공유메모리, 그리고 언제 무엇을.
- 메모리 정책 — 사전 할당과 실시간(2.6의 벤더 쪽 절반).
- 흐름 제어 — 대용량 메시지의 필수 설정(3.2.7).
- 실전 레시피 — 무선·대용량·저지연·대규모 각각의 출발점.
- 적용 확인과 디버깅 — 설정이 적용됐는지 어떻게 아는가.
- XML을 실제로 먹인 결과(3.7.12) — 우선순위의 예외 하나와, 네 가지 조용한 실패.
선수 지식: 1.5·1.6(QoS), 3.2(RTPS·조각화), 3.3(디스커버리), 2.6(메모리), 3.5(RMW 계층).
정직: XML 요소 이름·구조·기본값은 Fast DDS 버전에 따라 변한다. 아래 XML 예시는 공개 문서 기준으로 작성했으며, 적용 여부와 우선순위는 3.7.12에서 실제로 적용해 확인했다(실행 확인) — 다만 개별 요소 하나하나를 다 적용해 본 것은 아니다
(확인 필요)(목차 3). 다만 환경변수 이름은 실측으로 확정했다 — Jazzy(Fast DDS 2.14.x)에서는FASTRTPS_DEFAULT_PROFILES_FILE이 맞고FASTDDS_DEFAULT_PROFILES_FILE은 라이브러리에 존재하지 않는다(3.7.3). 로그 레벨 환경변수도 없음을 확인했다(3.7.9). eProsima Fast DDS 문서의 해당 버전 페이지가 정본이며, 반드시 적용 확인(3.7.9)을 거치라.
3.7.2 XML이 개입하는 지점 — 무엇이 우선하는가
가장 먼저 이해해야 할 것은 우선순위다.
| 상황 | 결정 주체 |
|---|---|
코드에서 reliable() 명시 | 코드 |
코드에서 SystemDefaultsQoS() | XML(1.5.5) |
RESOURCE_LIMITS·OWNERSHIP 등 미노출 정책 | XML만(3.1.7) |
| 전송(transport)·메모리 정책·흐름 제어 | XML만 |
| 디스커버리 파라미터 | XML 또는 환경변수 |
정직: "코드가 우선한다"는 일반 원칙이지만 예외가 있을 수 있다. 벤더가 XML 우선 적용 경로를 제공하는 경우가 있고, rmw 구현이 어떻게 매핑하는지에 달렸다
(확인 필요). 그래서 3.7.9의 적용 확인이 선택이 아니라 필수다. "설정했으니 됐겠지"가 이 장에서 가장 위험한 태도다.
SystemDefaultsQoS()의 전략적 가치가 여기서 분명해진다(1.5.5). 코드에 QoS를 박지 않고 XML에 위임하면, 현장에서 재빌드 없이 튜닝할 수 있다. 대가는 코드만 읽어서는 실제 QoS를 알 수 없다는 것이다.
3.7.3 파일 지정과 기본 구조
export FASTRTPS_DEFAULT_PROFILES_FILE=/etc/robot/fastdds.xml
ros2 launch my_robot bringup.launch.py
Jazzy에서는
FASTRTPS_접두사가 맞다(실행 확인). Jazzy가 싣는 Fast DDS는 2.14.x이며, 이 라이브러리에 실재하는 프로파일 관련 환경변수는FASTRTPS_DEFAULT_PROFILES_FILE·FASTDDS_ENVIRONMENT_FILE·SKIP_DEFAULT_XML_FILE이다.FASTDDS_DEFAULT_PROFILES_FILE이라는 문자열은 라이브러리 안에 없다 — Fast DDS 3.x 계열의 이름이므로 Jazzy에 그대로 쓰면 조용히 무시된다. 다른 배포판·직접 빌드로 Fast DDS를 업그레이드했다면 그 버전의 문서를 확인하라.
최소 구조:
<?xml version="1.0" encoding="UTF-8" ?>
<dds xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<profiles>
<!-- 참가자 기본 프로파일 -->
<participant profile_name="default_participant" is_default_profile="true">
<rtps>
<name>robot_participant</name>
</rtps>
</participant>
<!-- 발행자 기본 프로파일 -->
<data_writer profile_name="default_writer" is_default_profile="true">
<qos>
<reliability><kind>RELIABLE</kind></reliability>
</qos>
</data_writer>
<!-- 구독자 기본 프로파일 -->
<data_reader profile_name="default_reader" is_default_profile="true">
<qos>
<reliability><kind>BEST_EFFORT</kind></reliability>
</qos>
</data_reader>
</profiles>
</dds>
is_default_profile="true"가 핵심이다 — 이름으로 지정하지 않은 모든 엔티티에 적용된다.
토픽별 프로파일: ROS 2에서는 토픽 이름으로 프로파일을 매칭할 수 있다.
<data_writer profile_name="rt/camera/image_raw">
<qos>
<reliability><kind>BEST_EFFORT</kind></reliability>
</qos>
</data_writer>
프로파일 이름에 ROS 토픽의 DDS 이름(rt/ 접두어 포함 — 2.1.4)을 쓴다. 이것을 모르면 토픽별 설정이 먹지 않는다.
함정:
rt/접두어를 빠뜨리는 것이 토픽별 프로파일 실패의 최다 원인이다. ROS 2에서/camera/image_raw는 DDS 상에서rt/camera/image_raw다. 서비스는rq/·rr/.
정직: 토픽 이름 기반 프로파일 매칭이 rmw 구현에서 활성화되어 있어야 동작하며, 별도 설정이 필요할 수 있다
(확인 필요).
3.7.4 전송(transport) 설정
전송은 XML로만 바꿀 수 있다. ROS 2 API에는 없다.
<profiles>
<!-- 전송 서술자 정의 -->
<transport_descriptors>
<transport_descriptor>
<transport_id>custom_udp</transport_id>
<type>UDPv4</type>
<sendBufferSize>2097152</sendBufferSize> <!-- 2 MB -->
<receiveBufferSize>2097152</receiveBufferSize>
<maxMessageSize>65500</maxMessageSize>
<maxInitialPeersRange>10</maxInitialPeersRange>
</transport_descriptor>
<transport_descriptor>
<transport_id>shm_transport</transport_id>
<type>SHM</type>
<segment_size>16777216</segment_size> <!-- 16 MB -->
</transport_descriptor>
</transport_descriptors>
<participant profile_name="tuned" is_default_profile="true">
<rtps>
<userTransports>
<transport_id>shm_transport</transport_id>
<transport_id>custom_udp</transport_id>
</userTransports>
<useBuiltinTransports>false</useBuiltinTransports>
</rtps>
</participant>
</profiles>
| 전송 | 언제 | 관련 장 |
|---|---|---|
| UDPv4(기본) | 일반 LAN | 3.2.9 |
| SHM | 같은 호스트 프로세스 간 — zero-copy 기반 | 2.7.7 |
| TCPv4 | 방화벽·NAT·WAN | 3.2.9 |
| UDPv6 | IPv6 환경 | — |
useBuiltinTransports=false에 주의: 이것을 끄면 명시한 전송만 쓴다. SHM만 지정하고 이 값을 false로 두면 다른 호스트와 통신이 안 된다. 실수하기 쉬운 지점이다.
버퍼 크기가 특히 중요하다. 1.4.4에서 본 커널 버퍼 문제와 짝을 이룬다 — DDS 쪽 버퍼를 키워도 OS 한계를 넘을 수 없다.
# OS 한계를 먼저 상향해야 한다
sudo sysctl -w net.core.rmem_max=8388608
sudo sysctl -w net.core.wmem_max=8388608
# 영구 적용
echo "net.core.rmem_max=8388608" | sudo tee -a /etc/sysctl.d/10-ros2.conf
견해: 버퍼 튜닝은 XML보다 sysctl을 먼저 확인하라. DDS 설정에서 2 MB를 요청해도
rmem_max가 208 KB면 그 값으로 잘린다. 대용량 토픽 유실 문제의 상당수가 이 한 줄로 해결된다.
3.7.5 메모리 정책 — 실시간의 벤더 쪽 절반
2.6에서 "allocator만으로는 부족하고 벤더 사전 할당이 필요하다"고 했다. 그것이 이 절이다.
<data_writer profile_name="rt_writer">
<historyMemoryPolicy>PREALLOCATED_WITH_REALLOC</historyMemoryPolicy>
<qos>
<reliability><kind>BEST_EFFORT</kind></reliability>
<durability><kind>VOLATILE</kind></durability>
</qos>
<topic>
<historyQos>
<kind>KEEP_LAST</kind>
<depth>1</depth>
</historyQos>
<resourceLimitsQos>
<max_samples>8</max_samples>
<max_instances>1</max_instances>
<max_samples_per_instance>8</max_samples_per_instance>
<allocated_samples>8</allocated_samples>
</resourceLimitsQos>
</topic>
</data_writer>
historyMemoryPolicy의 값들:
| 값 | 동작 | 실시간 적합성 |
|---|---|---|
PREALLOCATED | 최대 크기로 미리 할당 | 최적 — 런타임 할당 없음 |
PREALLOCATED_WITH_REALLOC | 미리 할당하되 부족하면 재할당 | 좋음(재할당은 예외적) |
DYNAMIC_RESERVE | 필요할 때 할당 | 나쁨 — 지터 |
DYNAMIC_REUSABLE | 동적이되 재사용 | 중간 |
견해: 실시간 경로에는
PREALLOCATED계열을 쓰되,max_samples·allocated_samples를 실제 필요보다 넉넉히 설정해야 한다.PREALLOCATED는 최대 크기 기준으로 메모리를 확보하므로, 무한 배열 타입에서는 사실상 못 쓴다 — 최대 크기를 알 수 없기 때문이다. 1.2.3에서 "유한 배열로 설계하라"고 한 것이 여기서 다시 값을 한다.
resourceLimitsQos가 ROS 2 API에 없던 RESOURCE_LIMITS다(3.1.7). KEEP_ALL의 실제 상한을 여기서 정한다.
3.7.6 흐름 제어 — 대용량 메시지의 필수 설정
3.2.7에서 "조각을 한꺼번에 쏟으면 오히려 처리량이 떨어진다"고 했다. 흐름 제어가 그 해법이다.
<participant profile_name="camera_participant">
<rtps>
<flow_controller_descriptors>
<flow_controller_descriptor>
<name>image_limiter</name>
<scheduler>FIFO</scheduler>
<max_bytes_per_period>1048576</max_bytes_per_period> <!-- 1 MB -->
<period_ms>50</period_ms> <!-- 50 ms -->
</flow_controller_descriptor>
</flow_controller_descriptors>
</rtps>
</participant>
<data_writer profile_name="rt/camera/image_raw">
<rtps>
<flow_controller_name>image_limiter</flow_controller_name>
</rtps>
</data_writer>
위 설정은 초당 약 20 MB로 제한한다:
$$B_{\max} = \frac{1\,\text{MB}}{50\,\text{ms}} = 20\,\text{MB/s} = 160\,\text{Mbps}$$
설정값 산정:
$$\text{max\_bytes\_per\_period} \geq S_{\text{msg}} \cdot f \cdot \text{period}$$
30 Hz로 2 MB 이미지를 보내면 초당 60 MB가 필요하다. period 50 ms면 max_bytes_per_period ≥ 3 MB. 여유를 두되 링크 용량을 넘지 않게 설정한다.
견해: 흐름 제어는 "느리게 만드는 설정"이 아니라 "버스트를 평탄화하는 설정"이다. 링크 용량을 넘는 순간 폭증하는 유실을 막아 실효 처리량을 높인다. 대용량 토픽이 있는 시스템에서는 기본으로 검토할 항목이다.
정직: 요소 이름·스케줄러 종류·기본 동작은 버전 의존이다
(확인 필요). 개념(주기당 바이트 상한)은 안정적이다.
3.7.7 디스커버리 설정
3.3·3.4의 손잡이들이다.
<participant profile_name="disc_tuned">
<rtps>
<builtin>
<discovery_config>
<!-- 알림 주기 — 늘리면 트래픽 감소, 발견 지연 -->
<leaseDuration>
<sec>20</sec>
</leaseDuration>
<leaseAnnouncement>
<sec>3</sec>
</leaseAnnouncement>
<!-- Discovery Server 클라이언트로 (3.4) -->
<discoveryProtocol>CLIENT</discoveryProtocol>
<discoveryServersList>
<RemoteServer prefix="44.53.00.5f.45.50.52.4f.53.49.4d.41">
<metatrafficUnicastLocatorList>
<locator>
<udpv4>
<address>192.168.1.100</address>
<port>11811</port>
</udpv4>
</locator>
</metatrafficUnicastLocatorList>
</RemoteServer>
</discoveryServersList>
</discovery_config>
<!-- 멀티캐스트 없이 지정 피어만 (3.3.7) -->
<initialPeersList>
<locator>
<udpv4><address>192.168.1.10</address></udpv4>
</locator>
</initialPeersList>
</builtin>
</rtps>
</participant>
| 손잡이 | 효과 | 대가 |
|---|---|---|
leaseAnnouncement ↑ | SPDP 트래픽 감소 | 발견이 느려짐 |
leaseDuration ↑ | 트래픽 감소 | 종료된 노드 감지 지연(3.3.3) |
discoveryProtocol=CLIENT | $O(N)$ 디스커버리 | 서버 의존(3.4) |
initialPeersList | 멀티캐스트 불필요 | 수동 관리 |
함정:
leaseDuration을 크게 늘리면 유령 노드가 훨씬 오래 남는다(0.7.3). 트래픽 절감과 장애 감지 속도의 트레이드오프를 의식적으로 선택해야 한다.
3.7.8 실전 레시피
각 레시피는 출발점이며, 반드시 자기 환경에서 측정해 조정해야 한다.
3.7.8.1 무선 링크 (1.6.5)
<data_writer profile_name="wireless_stream">
<qos>
<reliability><kind>BEST_EFFORT</kind></reliability>
<durability><kind>VOLATILE</kind></durability>
<lifespan><duration><sec>1</sec></duration></lifespan>
</qos>
<topic>
<historyQos><kind>KEEP_LAST</kind><depth>1</depth></historyQos>
</topic>
<rtps>
<flow_controller_name>wireless_limiter</flow_controller_name>
</rtps>
</data_writer>
BEST_EFFORT로 재전송 악순환 차단.lifespan으로 묵은 데이터 폐기.- 흐름 제어로 버스트 억제.
- 디스커버리:
leaseAnnouncement를 늘려 상시 트래픽 감소.
3.7.8.2 대용량 (이미지·포인트클라우드)
<transport_descriptor>
<transport_id>big_udp</transport_id>
<type>UDPv4</type>
<sendBufferSize>8388608</sendBufferSize>
<receiveBufferSize>8388608</receiveBufferSize>
<maxMessageSize>65000</maxMessageSize>
</transport_descriptor>
- OS 버퍼를 먼저 키운다(3.7.4).
- 흐름 제어 필수(3.7.6).
- 같은 호스트면 SHM + 컴포지션 검토(2.7).
- 근본 해법은 압축·다운샘플링(7.2·7.8).
3.7.8.3 저지연 제어 루프
<data_writer profile_name="control_cmd">
<historyMemoryPolicy>PREALLOCATED</historyMemoryPolicy>
<qos>
<reliability><kind>RELIABLE</kind></reliability>
<durability><kind>VOLATILE</kind></durability>
</qos>
<topic>
<historyQos><kind>KEEP_LAST</kind><depth>1</depth></historyQos>
<resourceLimitsQos>
<max_samples>4</max_samples>
<allocated_samples>4</allocated_samples>
</resourceLimitsQos>
</topic>
</data_writer>
PREALLOCATED로 런타임 할당 제거(2.6).- depth 1로 최신 명령만(1.6.3.2).
- 같은 호스트면 SHM.
- XML만으로는 부족하다 — 애플리케이션 쪽 무할당·RT 우선순위가 함께 가야 한다(2.6.8·12.2).
3.7.8.4 대규모 그래프
discoveryProtocol=CLIENT+ Discovery Server(3.4).leaseAnnouncement증가.- 컴포지션으로 참가자 수 감소(3.3.5).
3.7.9 적용 확인과 디버깅 — 이 절이 가장 중요하다
"XML을 썼다"와 "설정이 적용됐다"는 다르다. 파일 경로가 틀렸거나, 프로파일 이름이 안 맞거나, 문법 오류가 있으면 조용히 무시된다.
① 파일이 읽혔는지
echo $FASTRTPS_DEFAULT_PROFILES_FILE
ls -l $FASTRTPS_DEFAULT_PROFILES_FILE
xmllint --noout $FASTRTPS_DEFAULT_PROFILES_FILE # 문법 검사
② QoS가 실제로 적용됐는지
ros2 topic info -v /camera/image_raw
이것이 가장 확실한 확인이다. rmw가 보고하는 실제 값이며, 코드·XML 어느 쪽이 우선했는지 여기서 드러난다.
// 코드에서도 확인 (3.5.5)
const auto & q = pub_->get_actual_qos();
RCLCPP_INFO(get_logger(), "실제 depth=%zu reliability=%d",
q.depth(), static_cast<int>(q.reliability()));
③ 전송이 실제로 쓰이는지
# SHM을 설정했는데 UDP 패킷이 보이면 SHM이 안 쓰인 것
sudo tcpdump -i lo -c 20 'udp portrange 7400-7700'
# 공유메모리 세그먼트 확인
ls -l /dev/shm/ | grep -i fastdds
ipcs -m
④ Fast DDS 로그 활성화
로그 레벨을 켜는 환경변수는 없다(실행 확인). Jazzy의 Fast DDS 2.14.6에는 FASTDDS_LOG_LEVEL 같은 문자열이 존재하지 않는다 — ROS 2 설치 전체를 뒤져도 *LOG_LEVEL* 환경변수는 하나도 없다. Fast DDS 로그는 XML의 <log> 섹션이나 Log::SetVerbosity() API로 켠다.
<?xml version="1.0" encoding="UTF-8"?>
<dds xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<log>
<use_default>false</use_default>
<consumer>
<class>StdoutErrConsumer</class>
<property>
<name>stderr_threshold</name>
<value>Log::Kind::Warning</value>
</property>
</consumer>
</log>
</dds>
정직:
<log>섹션의 존재는 라이브러리 문자열(use_default·consumer·thread_settings)로 확인했으나, 위 XML을 실제로 적용해 로그가 켜지는 것까지 실행 검증하지는 않았다(확인 필요). 소비자 클래스명·프로퍼티 키는 Fast DDS 버전에 따라 다르므로 eProsima 문서의 해당 버전 페이지를 정본으로 삼으라. 로그가 켜지면 프로파일 로딩 성공·실패 메시지가 나오므로, XML이 적용되지 않을 때 가장 빠른 진단 수단이다.
⑤ 성능이 실제로 바뀌었는지
측정 없이는 튜닝이 아니다(2.7.8·3.6.5).
# 튜닝 전후 종단 지연 p50 · p99 · max 비교
흔한 실패 원인:
| 원인 | 증상 |
|---|---|
| 환경변수 이름 오류 | 파일 무시 |
| 파일 경로가 상대 경로 | 실행 위치에 따라 실패 |
| XML 문법 오류 | 전체 무시 |
rt/ 접두어 누락 | 토픽별 프로파일 미적용 |
is_default_profile 누락 | 이름 지정 안 한 엔티티에 미적용 |
| 코드가 QoS를 명시 | XML이 무시됨(3.7.2) |
| launch에서 환경변수 미전파 | 노드마다 다른 설정 |
마지막 항목이 자주 발생한다.
# launch에서 명시적으로 전파
SetEnvironmentVariable('FASTRTPS_DEFAULT_PROFILES_FILE', '/etc/robot/fastdds.xml'),
3.7.12 XML 프로파일을 실제로 적용해 봤다 — 실측
이 장의 서두는 "XML 예시는 공개 문서 기준으로 작성했으며 적용해 보지 않았다"고 적어 두었다. 적용해 봤다(실행 확인, Jazzy, Fast DDS 2.14.x). 확인 방법은 Publisher::get_actual_qos() — 실제로 적용된 QoS를 그대로 돌려준다.
auto pub = node->create_publisher<std_msgs::msg::String>("t", rclcpp::QoS(10).reliable());
auto q = pub->get_actual_qos(); // ★ 이것이 적용 확인의 정본이다
3.7.12.1 우선순위 — 표가 대체로 맞았고, 예외가 하나 있다
코드는 rclcpp::QoS(10).reliable()(depth 10·RELIABLE·durability 미지정), XML은 BEST_EFFORT · TRANSIENT_LOCAL · depth 20으로 두고 측정한 결과.
| 정책 | 코드 | XML | 실제 적용 | 누가 우선했나 |
|---|---|---|---|---|
| reliability | RELIABLE(명시) | BEST_EFFORT | RELIABLE | 코드 |
| durability | (미지정 → 기본 VOLATILE) | TRANSIENT_LOCAL | VOLATILE | 코드의 기본값 |
| history depth | 10 | 20 | 20 | XML |
두 가지가 드러난다.
*① 코드가 명시하지 않은 정책도 XML을 덮는다. durability를 코드에서 설정하지 않았는데도 rclcpp의 기본값 VOLATILE이 XML의 TRANSIENT_LOCAL에 우선했다. rmw가 ROS QoS 구조체를 빈칸 없이 채워서* DDS로 넘기기 때문이다.
*그러므로 3.7.2의 "코드가 우선한다"는 더 강하게 읽어야 한다 — "코드에 적은 정책만 코드가 우선한다"*가 아니라 "rclcpp가 아는 정책은 전부 코드 쪽이 우선한다"이다.
SystemDefaultsQoS()(1.5.5)를 쓰지 않는 한 XML로는 QoS를 못 바꾼다. 이 장이 그 프로파일의 전략적 가치를 강조한 이유가 실측으로 확인된다.
② 예외는 depth다 — 큰 쪽이 우선한다.
| XML depth | 코드 depth | 실제 |
|---|---|---|
| 3 | 10 | 10 |
| 20 | 10 | 20 |
max(코드, XML)이다. depth는 "정책의 종류"가 아니라 자원 요구량이므로, 양쪽 요구를 모두 만족하는 값으로 합쳐지는 것으로 보인다(확인 필요: 구현 근거).
함정: XML로 depth를 키우면 코드를 안 고쳐도 큐가 커진다. 메모리 계획(3.7.5)에서 이 경로를 잊기 쉽다 — "코드에 10이라고 적혀 있으니 10이겠지"가 틀린다.
3.7.12.2 SystemDefaultsQoS()는 XML이 온전히 가져간다
같은 XML에서 rclcpp::SystemDefaultsQoS()로 만든 발행자는 이렇게 됐다.
| XML 없음 | XML 있음 | |
|---|---|---|
| reliability | RELIABLE | BEST_EFFORT |
| durability | TRANSIENT_LOCAL | TRANSIENT_LOCAL |
| depth | 1 | 20 |
XML이 있으면 세 정책 모두 XML이 정한다. 그리고 XML이 없을 때의 SystemDefaultsQoS()는 Fast DDS에서 RELIABLE·TRANSIENT_LOCAL·depth 1이다 — 1.6.11이 측정한 구성 시점의 값(SYSTEM_DEFAULT·depth 0)이 실제로는 이렇게 해석된다.
TRANSIENT_LOCAL에 주목하라.SystemDefaultsQoS()로 만든 발행자는 Fast DDS에서 아무 설정 없이 래치된다 — 늦게 참여한 구독자가 마지막 메시지를 받는다. 의도한 것이 아니라면 놀랄 일이고, 벤더를 바꾸면 이 동작이 사라진다(3.8.12.2).
현장 튜닝을 하려면 토픽을
SystemDefaultsQoS()로 열어 두어야 한다. 그러지 않은 토픽은 XML을 아무리 써도 depth 말고는 바뀌지 않는다.
3.7.12.3 실패는 전부 조용하다 — 네 가지를 확인했다
3.7.9가 "이 절이 가장 중요하다"고 한 이유를 실측으로 뒷받침한다. 일부러 틀린 XML을 적용했다.
| 시나리오 | 적용됐나 | stderr 오류 |
|---|---|---|
| 정상 XML | ✅ 적용 | 0줄 |
is_default_profile 표시 없음 | ❌ 무시 | 0줄 |
| 존재하지 않는 파일 경로 | ❌ 무시 | 0줄 |
| 깨진 XML(닫는 태그 누락) | ❌ 무시 | 0줄 |
| 모르는 요소 포함 | ❌ 무시 | 0줄 |
네 가지 실패 모두 오류 한 줄 없이 기본값으로 실행됐다. 파일 경로 오타조차 조용하다.
그래서 확인 방법이 필요하다. 로그를 봐도 알 수 없으므로
get_actual_qos()를 출력하는 것이 유일하게 확실한 절차다. 노드 시작 시 한 번 출력해 두면 "XML이 적용됐는가"가 즉시 판별된다.
// 시작 로그에 한 줄 — XML 적용 확인용
auto q = pub->get_actual_qos();
RCLCPP_INFO(get_logger(), "적용 QoS: depth=%zu reliability=%d",
q.get_rmw_qos_profile().depth, (int)q.reliability());
is_default_profile="true"를 빠뜨리는 것이 가장 흔한 실수일 것이다. XML 문법은 완벽하고 파일도 읽히는데 아무 일도 일어나지 않는다. 위 확인 절차가 없으면 "XML이 원래 안 되나 보다"로 끝난다.
3.7.12.4 확인하지 못한 것
- 전송(transport)·메모리 정책·흐름 제어(3.7.4~3.7.6)는
get_actual_qos()로 보이지 않는다 — 적용 확인 방법이 QoS와 다르다(확인 필요). 패킷 캡처나 벤더 로그가 필요하다. - Cyclone DDS의 XML은 형식도 확인 경로도 다르다(3.8)
(확인 필요). - 이 실험의 XML 요소 이름은 Fast DDS 2.14.x 기준이며, 다른 버전에서 같다는 보장은 없다.
3.7.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | XML 적용 확인 생략 | 튜닝했다고 믿는데 미적용 | topic info -v(3.7.9) |
| 2 | 토픽 프로파일에 rt/ 누락 | 토픽별 설정 미적용 | DDS 이름 사용 |
| 3 | useBuiltinTransports=false + SHM만 | 다른 호스트와 통신 불가 | UDP 함께 지정 |
| 4 | DDS 버퍼만 키우고 sysctl 방치 | 값이 잘림 | rmem_max 먼저 |
| 5 | 흐름 제어 없이 대용량 | 유실 폭증·처리량 감소 | 흐름 제어(3.7.6) |
| 6 | PREALLOCATED + 무한 배열 타입 | 적용 불가·메모리 폭증 | 유한 타입(1.2.3) |
| 7 | leaseDuration 과대 | 유령 노드 장기 잔존 | 트레이드오프 판단 |
| 8 | launch에서 환경변수 미전파 | 노드마다 설정 불일치 | SetEnvironmentVariable |
| 9 | XML 문법 오류 | 전체 무시(조용히) | xmllint 검사 |
| 10 | 코드 QoS와 XML 의도 충돌 | 예상과 다른 동작 | SystemDefaultsQoS 검토 |
| 11 | 측정 없이 튜닝 | 개선 여부 불명 | 전후 비교 |
| 12 | 벤더 교체 계획 없이 XML 축적 | 교체 시 재작성 | 의존 문서화(3.5.6) |
| 13 | is_default_profile 누락 | 문법 정상·파일 읽힘·아무 변화 없음 | get_actual_qos()로 확인(3.7.12.3) |
| 14 | 파일 경로 오타 | 오류 0줄 — 조용히 무시 | 동일 |
| 15 | XML로 QoS를 바꾸려 함 | SystemDefaultsQoS() 아니면 안 바뀐다 | depth만 예외(3.7.12.1) |
3.7.11 체크포인트
- 코드 QoS와 XML이 충돌할 때 무엇이 우선하는가. 그 원칙에 예외가 있을 수 있다는 사실이 왜 중요한가.
SystemDefaultsQoS()가 XML 튜닝에서 갖는 전략적 가치와 대가는?- 토픽별 프로파일을 쓸 때 반드시 붙여야 하는 접두어는 무엇이며 왜인가.
useBuiltinTransports=false의 위험은?- DDS 버퍼 설정보다 먼저 확인해야 할 OS 설정은 무엇인가.
historyMemoryPolicy네 값의 실시간 적합성을 비교할 수 있는가.PREALLOCATED를 무한 배열 타입에 쓸 수 없는 이유는?- 흐름 제어가 "느리게 만드는 설정"이 아닌 이유를 설명할 수 있는가. 30 Hz·2 MB 이미지에 필요한
max_bytes_per_period를 계산할 수 있는가. leaseDuration을 늘리는 것의 이득과 대가는?- 무선·대용량·저지연·대규모 각각의 레시피 핵심 항목을 말할 수 있는가.
- XML 적용을 확인하는 다섯 가지 방법을 들 수 있는가. 가장 확실한 것은?
- XML이 조용히 무시되는 흔한 원인 일곱 가지를 말할 수 있는가.
- 코드가 명시하지 않은 정책은 XML이 정하는가(3.7.12.1).
- 코드 depth 10과 XML depth 20이 만나면 결과는 무엇이며 왜 그런가(3.7.12.1).
- XML이 적용됐는지 확인하는 유일하게 확실한 방법은(3.7.12.3).
다음 장 3.8은 같은 작업을 Cyclone DDS에서 한다. 설정 철학이 어떻게 다르고, Fast DDS 설정을 옮길 때 무엇이 대응하며 무엇이 대응하지 않는가.