제3부 미들웨어: DDS·RTPS·RMW
3.9 네트워크 — 멀티캐스트·무선·컨테이너
3.9.1 이 장에서 배우는 것 / 선수 지식
"내 노트북에서는 되는데 로봇에서는 안 된다." ROS 2에서 이 문장의 원인은 대개 코드가 아니라 네트워크다. 그리고 이 층의 문제는 특히 진단이 어렵다 — 오류 메시지가 없고, QoS도 맞고, 코드도 옳은데도 상대가 안 보인다.
이 장은 그 층을 체계적으로 다룬다. 앞의 3.7·3.8이 "미들웨어를 어떻게 설정하는가"였다면, 이 장은 "그 설정이 무의미해지는 지점이 어디인가" 다.
배우는 것:
- 멀티캐스트 — ROS 2가 왜 의존하고, 어디서 막히며, 어떻게 시험하는가.
- 무선 — Wi-Fi가 DDS에 적대적인 구조적 이유.
- 다중 NIC — 로봇에서 가장 흔한 함정.
- 컨테이너 — Docker 네트워크 모드별 동작과 Kubernetes.
- 컨테이너 경계 실측(3.9.12) — 어디까지 되고 어디서 끊기는가, 통념의 어디가 틀렸는가.
- 유실 주입 실측(3.9.13) —
RELIABLE의 실제 대가와, 크기가 만드는 급변 지점. - 조각화 모형 검증(3.9.14) — 조각 크기를 실측하고, 두 번째 유실률로 모형을 확인하고,
RELIABLE이 포기하는 지점을 찾는다. - 방화벽·포트 — 3.2.6의 계산식을 규칙으로.
- MTU·조각화 — VPN·터널에서 생기는 문제.
- 커널 튜닝 — 버퍼·큐.
- 진단 플레이북 — 순서대로 좁혀 가는 절차.
선수 지식: 3.2(포트·조각화), 3.3(디스커버리), 3.7·3.8(벤더 설정), 0.4(환경변수).
정직: 네트워크 동작은 하드웨어·드라이버·커널·AP 펌웨어에 크게 의존한다. 아래 서술은 대부분 일반적으로 알려진 동작이며 이 환경에서 검증하지 않았다(목차 3). 예외는 셋이다 — 컨테이너 경계(3.9.12)·유실 주입(3.9.13)·조각화 모형(3.9.14)은 컨테이너에서 실행 확인했고, 그 결과로 이 장의 서술 일부를 정정했다. 특히 Wi-Fi의 멀티캐스트 처리는 AP·드라이버마다 다르므로 자신의 장비에서 측정해야 한다.
3.9.2 멀티캐스트 — ROS 2의 숨은 전제
기본 디스커버리는 멀티캐스트에 의존한다(3.3.2). 이것이 ROS 2의 가장 큰 숨은 전제이며, 개발 환경(단일 머신·평범한 스위치)에서는 잘 동작하기 때문에 현장에 나가서야 문제가 드러난다.
멀티캐스트가 성립하려면 필요한 것:
| 요건 | 확인 방법 |
|---|---|
| 인터페이스가 멀티캐스트 지원·활성 | ip link show 에 MULTICAST 플래그 |
| 라우팅 테이블에 멀티캐스트 경로 | ip route show |
| 방화벽이 허용 | iptables -L·ufw status |
| 스위치가 IGMP를 올바르게 처리 | 장비 설정 |
| AP가 멀티캐스트를 전달 | Wi-Fi에서 자주 실패(3.9.3) |
| 라우터를 넘지 않음 | 멀티캐스트는 기본적으로 라우팅되지 않는다 |
마지막 항목이 중요하다. 멀티캐스트는 기본적으로 서브넷 안에서만 동작한다. 다른 서브넷의 로봇은 기본 설정으로 절대 발견되지 않는다 — 이때는 ROS_STATIC_PEERS(3.3.7)나 Discovery Server(3.4)가 필요하다.
시험 — 가장 먼저 할 일:
# 머신 A
ros2 multicast receive
# 머신 B
ros2 multicast send
이 쌍이 통하지 않으면 ROS 2 이전에 네트워크 문제다. QoS·코드·벤더 설정을 보기 전에 이것부터 확인한다(0.7.7).
더 낮은 수준의 시험:
# ping으로 멀티캐스트 응답 확인
ping -c 3 224.0.0.1 # 모든 호스트 멀티캐스트
# 인터페이스별 멀티캐스트 그룹 가입 확인
ip maddr show
netstat -g
# 실제 RTPS 멀티캐스트 패킷 관찰
sudo tcpdump -i eth0 -n 'host 239.255.0.1'
함정:
ros2 multicast send·receive가 어느 인터페이스를 쓰는지 주의하라. 다중 NIC 머신에서는 의도한 인터페이스가 아닐 수 있다(3.9.4).
3.9.3 무선 — Wi-Fi가 DDS에 적대적인 이유
핵심 문제 넷:
① 멀티캐스트가 느리고 불안정하다. Wi-Fi는 멀티캐스트를 모든 클라이언트가 받을 수 있는 최저 속도로 보낸다. 802.11 표준상 멀티캐스트에는 ACK·재전송이 없어 유실률이 유니캐스트보다 훨씬 높다. 디스커버리 패킷이 SPDP(BEST_EFFORT 멀티캐스트)라는 점(3.3.2)이 정확히 여기 걸린다.
② 많은 AP가 멀티캐스트를 억제한다. 대역 절약을 위해 멀티캐스트를 차단하거나 유니캐스트로 변환하는 기능(multicast-to-unicast conversion)이 기본 활성인 장비가 많다. 기업용 AP일수록 그렇다.
③ RELIABLE이 악순환을 만든다(1.6.5·3.2.5). 유실 → 재전송 → 대역 잠식 → 유실 증가. 단, 실측에서 확인된 것은 "붕괴"가 아니라 "꼬리 지연 100~200배"였다(3.9.13.2) — 악순환은 링크가 이미 포화일 때 성립한다.
④ 로밍·간섭으로 순간 단절이 잦다. lease duration 안에 회복되면 문제없지만, 넘으면 참가자가 사라졌다 다시 나타나며 디스커버리가 재발생한다.
대처 — 효과 큰 순서:
| # | 조치 | 관련 장 |
|---|---|---|
| 1 | 보내는 양을 줄인다(압축·다운샘플링·토픽 선별) — 실측에서 압도적 1위(3.9.13.5) | 7.2·7.8 |
| 2 | BEST_EFFORT + lifespan + 작은 depth | 1.6.3 |
| 3 | 데이터 멀티캐스트 비활성(AllowMulticast=spdp 등) | 3.8.3 |
| 4 | 흐름 제어로 버스트 억제 | 3.7.6 |
| 5 | 디스커버리 주기 조정 | 3.7.7·3.8.5 |
| 6 | ROS_STATIC_PEERS·Discovery Server | 3.3.7·3.4 |
| 7 | 아키텍처 변경 — 로봇에서 요약해 보내기 | 7.8 |
| 8 | zenoh 검토 | 3.10 |
견해: 무선 문제의 실제 해법은 대개 1번과 7번이다. 튜닝으로 얻는 개선은 한계가 있고, "원격에서 전체 ROS 그래프를 본다"는 구조 자체가 무선에 맞지 않는다. 로봇 안에서 처리하고 요약만 내보내는 설계가 근본적이다. 이것은 성능 문제이면서 안전 문제이기도 하다 — 무선이 끊겨도 로봇은 동작해야 한다(3.4.5·14.10).
측정:
# 무선 링크 품질
iwconfig wlan0 # 또는
iw dev wlan0 link # 신호 강도·비트레이트
# 유실률
ping -f -c 1000 <상대IP> # flood ping (권한 필요)
# 실제 처리량
iperf3 -s # 서버 측
iperf3 -c <서버IP> -u -b 50M # UDP 50 Mbps 시험
iperf3로 먼저 링크 용량을 측정하는 것이 순서다. 링크가 20 Mbps인데 30 Mbps를 보내려 하고 있다면 DDS 튜닝은 무의미하다.
3.9.4 다중 NIC — 로봇에서 가장 흔한 함정
로봇은 대개 인터페이스를 여럿 갖는다 — 유선 이더넷, Wi-Fi, USB 이더넷(센서·MCU), 도커 브리지, 가상 인터페이스.
문제: DDS가 어느 인터페이스를 쓸지 자동으로 고르는데, 그 선택이 틀릴 수 있다.
예: eth0 192.168.1.5 · wlan0 10.0.0.5 · usb0 192.168.7.2 · docker0 172.17.0.1
증상:
| 증상 | 원인 |
|---|---|
| 상대가 안 보임 | 서로 다른 인터페이스에서 디스커버리 |
| 통신이 되다 말다 | 여러 인터페이스로 중복 시도 |
| 대역이 비정상적으로 소모 | 모든 인터페이스로 발송 |
docker0로 디스커버리 시도 | 가상 인터페이스가 목록에 포함 |
해결 — 인터페이스를 명시한다:
# Cyclone (3.8.3)
export CYCLONEDDS_URI='<CycloneDDS><Domain><General><Interfaces>
<NetworkInterface name="eth0"/></Interfaces></General></Domain></CycloneDDS>'
# Fast DDS — XML transport descriptor의 whitelist (3.7.4)
# <interfaceWhiteList><address>192.168.1.5</address></interfaceWhiteList>
# (확인 필요: 요소 이름은 버전 의존)
진단:
# 인터페이스 목록과 상태
ip -brief addr show
# DDS가 실제로 어느 인터페이스에서 듣고 있나
ss -ulnp | grep -E '74[0-9][0-9]'
# 어느 인터페이스로 패킷이 나가나
sudo tcpdump -i any -n 'udp portrange 7400-7700' | head -20
견해: 로봇 배포 설정에는 인터페이스 지정을 명시적으로 넣으라. 개발 머신에서는 인터페이스가 하나뿐이라 문제가 안 드러나고, 로봇에 배포한 뒤 처음 겪게 된다. 이것은 튜닝이 아니라 기본 설정 항목으로 취급하는 편이 좋다.
3.9.5 컨테이너 — Docker와 Kubernetes
Docker 네트워크 모드별 동작:
| 모드 | 멀티캐스트 | 호스트 간 통신 | 격리 | ROS 2 적합성 |
|---|---|---|---|---|
--network=host | ✅ 동작 | ✅ | ❌ 없음 | 가장 쉬움 |
bridge(기본) | ✅ 같은 네트워크 안에서는 동작(3.9.12) | ❌ 포트 매핑 필요 | ✅ | 호스트를 넘으면 어렵다 |
--network=container:X | 공유 | 공유 | 부분 | 사이드카 패턴 |
macvlan | ✅ 가능 | ✅ | ✅ | 설정 복잡 |
none | — | — | 완전 | ROS 2 불가 |
host 모드가 사실상의 표준이다.
docker run --network=host \
-e ROS_DOMAIN_ID=42 \
-e RMW_IMPLEMENTATION=rmw_fastrtps_cpp \
my_robot_image
왜 bridge가 어려운가:
핵심 문제: DDS는 자기 IP 주소를 SPDP에 실어 광고한다(3.3.2). NAT 뒤에 있으면 광고한 주소로 아무도 연결할 수 없다. 포트 매핑을 해도 광고 내용은 여전히 내부 주소다.
bridge에서 쓰려면:
| 방법 | 설명 |
|---|---|
| Discovery Server | 서버가 중계, 유니캐스트(3.4) |
ROS_STATIC_PEERS | 피어 주소 명시(3.3.7) |
| 사용자 정의 브리지 + 고정 IP | 같은 네트워크의 컨테이너끼리는 그대로 동작(3.9.12 실측) |
| 벤더 설정으로 광고 주소 지정 | 벤더별 방법(확인 필요) |
host 모드로 전환 | 가장 단순 |
견해: 컨테이너 격리와 ROS 2 디스커버리는 근본적으로 상충한다. 격리가 정말 필요하면 Discovery Server나 zenoh(3.10)를 함께 도입할 각오를 해야 하고, 그렇지 않다면
host모드가 실용적이다. "격리를 위해 bridge를 쓰겠다"는 결정은 디스커버리 인프라 결정과 한 세트다.
Kubernetes:
k8s는 파드마다 독립 네트워크 네임스페이스를 주므로 위 문제가 그대로 발생하고, 여기에 파드 IP가 재시작마다 바뀐다는 문제가 더해진다.
| 접근 | 설명 |
|---|---|
hostNetwork: true | 가장 단순. 격리 포기 |
| Discovery Server + Service | 서버를 k8s Service로 노출 |
| zenoh 라우터 | 라우터를 Service로(3.10) |
| CNI 멀티캐스트 지원 | 플러그인 의존(확인 필요) |
정직: ROS 2를 Kubernetes에서 운영하는 것은 여전히 성숙한 관행이라고 보기 어렵다. 가능하지만 디스커버리 인프라를 별도로 설계해야 하며, 로봇 온보드에서는 대개 systemd(14.8)가 더 단순하고 견고하다. k8s는 클라우드 측 구성 요소에 쓰고, 로봇 안은 다른 방식을 쓰는 조합이 흔하다.
3.9.6 방화벽과 포트
3.2.6의 계산식을 실제 규칙으로 바꾼다.
# 도메인 0, 참가자 0~20 가정
# 디스커버리 멀티캐스트: 7400
# 디스커버리 유니캐스트: 7410 + 2*P → 7410~7450
# 데이터 멀티캐스트: 7401
# 데이터 유니캐스트: 7411 + 2*P → 7411~7451
sudo ufw allow 7400:7460/udp comment 'ROS 2 domain 0'
# 멀티캐스트 주소 허용
sudo iptables -A INPUT -d 239.255.0.1 -j ACCEPT
sudo iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT # 더 넓게
# IGMP (멀티캐스트 그룹 관리)
sudo iptables -A INPUT -p igmp -j ACCEPT
Discovery Server를 쓰면 그 포트도:
sudo ufw allow 11811/udp comment 'Fast DDS Discovery Server'
확인:
# 실제 사용 포트
ss -ulnp | grep -E '7[0-9]{3}'
# 방화벽 규칙
sudo iptables -L -n -v
sudo ufw status verbose
# 상대 포트 도달성
nc -zvu <상대IP> 7400
함정:
ufw·iptables외에 클라우드 보안 그룹·회사 네트워크 정책이 별도로 있을 수 있다. 로컬 방화벽을 다 열었는데 안 되면 상위 계층을 확인해야 한다.
함정: IGMP를 막으면 멀티캐스트 그룹 가입이 실패한다. 방화벽 규칙에서 UDP만 생각하고 IGMP를 빠뜨리는 경우가 있다.
3.9.7 MTU와 조각화
VPN·터널·일부 클라우드에서 MTU가 1500보다 작다. DDS가 이를 모르면 IP 조각화가 발생하고, 3.2.7에서 본 유실 증폭이 일어난다.
# 경로 MTU 확인
ping -M do -s 1472 <상대IP> # 1472 + 28(헤더) = 1500
# 실패하면 크기를 줄여 가며 최대값 탐색
ping -M do -s 1400 <상대IP>
# 인터페이스 MTU
ip link show eth0 | grep mtu
# tracepath로 경로 MTU
tracepath <상대IP>
대응:
<!-- Cyclone: 조각 크기를 MTU 안으로 (3.8.4) -->
<General><FragmentSize>1344B</FragmentSize></General>
계산: MTU 1500 − IP 헤더 20 − UDP 헤더 8 − RTPS 오버헤드(수십) ≈ 1400 안팎. VPN이면 터널 오버헤드를 더 빼야 한다(보통 1400 → 1300대).
견해: VPN 너머로 ROS 2 데이터를 전달하는 구성은 신중해야 한다. MTU·지연·유실이 모두 악화되고, 멀티캐스트는 대개 안 된다. 원격 접속은 SSH·전용 브리지로 하고, ROS 그래프 자체를 VPN 너머로 확장하지 않는 편이 안전하다.
3.9.8 커널 튜닝
# 소켓 버퍼 (1.4.4 · 3.7.4)
sudo sysctl -w net.core.rmem_max=16777216
sudo sysctl -w net.core.wmem_max=16777216
sudo sysctl -w net.core.rmem_default=1048576
sudo sysctl -w net.core.wmem_default=1048576
# 수신 큐 길이 (버스트 대응)
sudo sysctl -w net.core.netdev_max_backlog=5000
# IP 조각 재조립 버퍼 (대용량 + 조각화 시)
sudo sysctl -w net.ipv4.ipfrag_high_thresh=8388608
sudo sysctl -w net.ipv4.ipfrag_time=3
# 영구 적용
sudo tee /etc/sysctl.d/10-ros2.conf <<'EOF'
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.core.netdev_max_backlog=5000
EOF
sudo sysctl --system
효과 확인:
# UDP 수신 오류 (버퍼 부족 시 증가)
netstat -su | grep -iE 'receive errors|buffer errors|packet receive'
# 반복 관찰
watch -n 1 "netstat -su | grep -i 'receive errors'"
# 인터페이스 드롭
ip -s link show eth0
receive errors가 계속 증가하면 버퍼 부족이 확실하다. 이 숫자는 ROS 2 도구로는 절대 볼 수 없는 정보이며, 대용량 토픽 유실 진단에서 결정적이다.
3.9.9 진단 플레이북
순서대로 좁혀 간다. 위에서부터 확인하고, 실패하는 지점에서 멈춘다.
각 단계의 명령:
# 1
echo "$ROS_DOMAIN_ID"
# 2
echo "$RMW_IMPLEMENTATION"
# 3
ping -c 3 <상대IP>
# 4
ros2 multicast receive # 한쪽
ros2 multicast send # 다른 쪽
# 5
ss -ulnp | grep -E '74[0-9][0-9]'
nc -zvu <상대IP> 7400
# 6 · 7
sudo tcpdump -i any -n -c 20 'udp portrange 7400-7700'
# 8
ros2 topic info -v /topic
견해: 이 순서를 지키는 것이 핵심이다. 사람들은 대개 8번(QoS·코드)부터 의심하는데, 네트워크 문제일 때 그쪽을 아무리 봐도 답이 없다. 4번(멀티캐스트 시험)이 가장 비용 대비 효과가 크다 — 30초에 문제 절반을 가른다.
추가 진단 도구:
ros2 doctor --report # 환경 요약
ros2 daemon stop && ros2 node list # 데몬 캐시 배제 (0.7.3)
3.9.12 컨테이너에서 어디까지 되는가 — 실측
"컨테이너에서는 ROS 2 디스커버리가 안 된다"는 말은 널리 퍼져 있고, 이 책도 앞에서 그렇게 적었다. Docker 기본 브리지와 사용자 정의 네트워크에서 직접 측정해 보니 경계가 통념보다 훨씬 안쪽에 있었다.
3.9.12.1 되는 것과 안 되는 것
| 경계 | 결과 | 확인 방법 |
|---|---|---|
| 같은 Docker 네트워크의 컨테이너끼리 | ✅ 된다 | ros2 multicast 왕복 + talker/ros2 topic echo 실제 수신까지 |
같은 사용자 정의 브리지(docker network create) | ✅ 된다 | 동일 |
| 다른 Docker 네트워크 사이 | ❌ 안 된다 | 수신 측이 끝까지 대기만 함 |
| 호스트 ↔ 컨테이너 | ⚠️ 인터페이스를 지정해야 된다 | 아래 3.9.12.3 |
| 다른 물리 호스트 사이 | (확인 필요 — 장비가 하나뿐이라 측정하지 못했다) | — |
즉 한 컨테이너 안, 그리고 같은 네트워크에 배치한 여러 컨테이너 사이에서는 기본 브리지로도 별도 설정 없이 된다. --network host가 필요해지는 지점은 호스트를 넘거나 네트워크를 나눌 때다.
3.9.12.2 왜 되는가 — 브리지는 멀티캐스트를 전달한다
리눅스 브리지의 멀티캐스트 처리 설정을 보면 이유가 나온다.
$ cat /sys/class/net/docker0/bridge/multicast_snooping
1
$ cat /sys/class/net/docker0/bridge/multicast_querier
0
스누핑은 켜져 있지만 쿼리어가 없다. 이 조합에서 리눅스 브리지는 멤버십 표를 신뢰할 수 없다고 보고 멀티캐스트를 모든 포트로 플러딩한다 — 그래서 같은 브리지의 컨테이너가 서로의 디스커버리 패킷을 받는다.
함정: 이것은 "설계된 보장"이 아니라 기본값의 부수 효과다. 브리지에 쿼리어가 생기거나(
multicast_querier=1) 상위 스위치가 IGMP 스누핑으로 가지치기를 하면 같은 조건에서도 끊길 수 있다. 운영에서 이 동작에 의존하려면 그 환경에서 직접 측정하고, 재현 조건을 문서로 남겨야 한다. 확실히 하려면 Discovery Server(3.4)나ROS_STATIC_PEERS(3.3.7)로 유니캐스트를 쓴다.
3.9.12.3 호스트 ↔ 컨테이너는 "막힌" 것이 아니라 "잘못된 곳을 듣는" 것이다
호스트에서 멀티캐스트를 받아 보면 가입할 인터페이스를 지정했는지에 따라 결과가 갈린다.
| 호스트가 가입한 인터페이스 | 컨테이너가 보낸 패킷 |
|---|---|
0.0.0.0(지정 안 함) | ❌ 못 받는다 |
docker0의 주소(172.17.0.1) | ✅ 받는다 |
이유는 라우팅이다.
$ ip route get 225.0.0.1
multicast 225.0.0.1 dev enp4s0 src 192.168.102.11 …
인터페이스를 지정하지 않으면 커널이 기본 경로 — 물리 NIC — 를 고른다. 호스트는 docker0이 아니라 enp4s0에서 그룹에 가입하므로, 컨테이너가 docker0으로 보낸 패킷을 영영 듣지 못한다. 패킷이 차단된 것이 아니라 듣는 귀가 다른 쪽에 있는 것이다.
이것이 다중 NIC 함정(3.9.4)과 정확히 같은 문제다 — 호스트에 docker0이 생기는 순간 그 머신은 다중 NIC 머신이 된다. 대처도 같다: 인터페이스를 명시한다(벤더 설정의 인터페이스 화이트리스트, 3.8.3).
3.9.12.4 ros2 multicast로 시험할 때의 한계
DEFAULT_GROUP = '225.0.0.1'
DEFAULT_PORT = 49150
ros2 multicast가 쓰는 그룹·포트는 DDS가 디스커버리에 쓰는 것과 다르다(RTPS는 도메인 0에서 239.255.0.1:7400 계열). 즉 이 명령은 "이 경로로 멀티캐스트가 흐르는가"를 보는 대리 시험이지 DDS 트래픽 자체를 보는 것이 아니다.
- 대리 시험이 실패하면 → 경로 문제가 거의 확실하다(강한 신호).
- 대리 시험이 성공해도 → DDS가 반드시 통한다는 보장은 아니다(방화벽이 포트 단위로 막을 수 있다, 3.9.6).
이번 실측에서는 대리 시험이 성공한 곳에서 실제 talker→echo까지 확인해 두 층이 일치함을 확인했지만, 일반적으로 보장되지는 않는다. 확실히 하려면 ros2 doctor hello로 두 층을 함께 본다(11.1.15).
3.9.13 유실을 주입해 RELIABLE을 측정해 봤다 — 실측
3.9.3은 "RELIABLE이 악순환을 만든다"고 적었고, 대처를 효과 큰 순서로 나열했다. 무선 장비 없이도 tc netem으로 유실률을 주입하면 그 주장의 상당 부분을 검증할 수 있다. 두 컨테이너를 같은 네트워크에 기동하고(3.9.12) 양쪽 eth0에 유실을 주입했다.
정직 — 이것은 Wi-Fi가 아니다.
netem의 유실은 균일 무작위인 반면 실제 무선은 버스트성이고, 전송률 적응·재시도·멀티캐스트 저속 전송 같은 802.11 고유 동작이 없다. 아래 숫자는 "유실이 있는 링크"의 거동이지 "무선"의 거동이 아니다. 미들웨어는 Fast DDS 기본 설정만 봤다.
3.9.13.1 작은 메시지 — RELIABLE은 복구한다, 지연을 대가로
1 KB · 200개 · 50 Hz(각 3회 반복, 범위로 표기).
| 유실 | 신뢰성 | 수신 | p50 지연 | p99 지연 |
|---|---|---|---|---|
| 5% | RELIABLE | 199~200(거의 전부) | 0.5~1.4 ms | 175~280 ms |
| 5% | BEST_EFFORT | 187~192(94~96%) | 0.5~1.0 ms | 0.8~2.3 ms |
| 20% | RELIABLE | 141~190 | 1~52 ms | 191~202 ms |
| 20% | BEST_EFFORT | 113~165 | 0.5~0.6 ms | 0.8~1.1 ms |
RELIABLE은 약속을 지킨다 — 5% 유실에서 사실상 전부 전달했다. 대가는 꼬리 지연이다. p99가 BEST_EFFORT의 약 100~200배로 증가한다. p50은 거의 그대로인데 p99만 급증하는 것이 특징이다 — 평균만 보면 안 보인다(2.5.6).
이것이 제어 루프에
RELIABLE을 쓰면 안 되는 이유를 숫자로 보여 준다. 200 ms 늦게 오는 속도 명령은 안 온 것보다 나쁠 수 있다(1.6.5).
3.9.13.2 재현되지 않은 것 — "악순환"은 관측하지 못했다
3.9.3의 ③은 유실 → 재전송 → 대역 잠식 → 유실 증가의 악순환을 말한다. 이 실험에서는 그 붕괴가 나타나지 않았다. 20% 유실에서도 RELIABLE이 71~95%를 전달했고, 처리량이 무너지는 대신 지연이 무너졌다.
정직하게 적으면 이렇다 — 악순환이 성립하려면 링크가 이미 포화 상태여야 한다. 이 실험의 링크는 여유가 많은 브리지였으므로 재전송이 잠식할 대역이 있었다. 무선처럼 대역이 빠듯하고 유실이 버스트로 오는 환경에서는 다르게 나타날 수 있다(확인 필요: 포화 링크에서의 거동). RELIABLE의 확인된 비용은 "붕괴"가 아니라 "꼬리 지연"이다.
3.9.13.3 실제 급변 지점은 유실률이 아니라 메시지 크기였다
유실 5%를 고정하고 크기만 키웠다(BEST_EFFORT, 100개 · 20 Hz).
| 크기 | 대략 조각 수 | 예측 도달률 $(1-p)^{\lceil S/M \rceil}$ | 실측 수신률 |
|---|---|---|---|
| 1 KB | 1 | 95% | 92% |
| 8 KB | 6 | 74% | 67% |
| 64 KB | 47 | 9% | 4% |
| 256 KB | 188 | 0.007% | 0% |
여기서 $p$는 패킷 유실률, $S$는 메시지 크기, $M$은 조각 하나에 실리는 바이트(MTU 1500 기준 약 1400)다.
$$P_{\text{도달}} = (1-p)^{\lceil S/M \rceil}$$
모델이 실측을 잘 설명한다(예측이 조금씩 높은 것은 헤더 부담을 무시했기 때문). 핵심은 지수다 — 유실률이 아니라 조각 수가 지수에 올라간다. 유실 5%는 1 KB에서는 거의 무해하지만 256 KB에서는 전부 실패이다.
그래서 "유실률 몇 %까지 괜찮은가"는 잘못된 질문이다. 올바른 질문은 "내 메시지가 몇 조각으로 분할되는가"다. 이미지·포인트클라우드를 그대로 무선으로 전송하면 조각이 수백 개이고, 그 지점에서 어떤 QoS도 답이 아니다.
3.9.13.4 BEST_EFFORT는 큰 메시지의 대안이 아니다
가장 실무적인 발견이다. 256 KB 이상에서는 RELIABLE도 BEST_EFFORT도 똑같이 0%였다(512 KB·5% 유실에서 45초 동안 단 한 개도 도착하지 않았고, ros2 topic info -v로 양쪽이 정상 매칭된 것은 확인했다 — 디스커버리 실패가 아니다).
"무선에서는 BEST_EFFORT로 바꿔라"는 조언은 작은 메시지에만 유효하다. 큰 메시지에서는 일부라도 받는 것이 아니라 아무것도 못 받는다 — 한 조각만 잃어도 표본 전체가 버려지기 때문이다.
3.9.13.5 3.9.3의 대처 순서가 옳았다
| 순위 | 조치 | 실측이 말하는 것 |
|---|---|---|
| 1 | 보내는 양을 줄인다 | 결정적 — 256 KB에서 0%, 1 KB에서 92%. 다른 어떤 조치보다 효과가 크다 |
| 2 | BEST_EFFORT + 작은 depth | 작은 메시지에서 꼬리 지연 100~200배 개선. 큰 메시지에는 무효 |
1번이 2번보다 위에 있는 것이 맞다. 크기를 줄이지 않은 채 QoS만 바꾸는 것은 256 KB 구간에서 아무것도 바꾸지 못한다. 3.9.3의 견해 — "실제 해법은 대개 1번과 7번" — 이 숫자로 뒷받침된다.
3.9.13.6 한 번의 측정으로 결론 내지 않았다
처음 실행했을 때 1 KB·20% 유실·RELIABLE에서 수신 0이 나왔다. "높은 유실에서 RELIABLE이 완전히 무너진다"로 읽힐 만한 결과였다. 반복하니 재현되지 않았다 — 이후 네 번은 141~195개를 받았다. 유실 때문에 디스커버리가 늦어져 발행이 끝난 뒤에야 매칭된 것으로 보인다.
함정: 유실이 있는 링크에서는 측정 자체가 불안정하다. 디스커버리도 같은 링크를 타므로 준비 시간이 불규칙해진다. 한 번의 결과로 결론 내지 말고 반복하고 범위로 보고하라 — 위 표를 모두 범위로 적은 이유다.
3.9.14 조각화 모형을 두 번째 유실률로 검증하고, RELIABLE이 포기하는 지점을 찾았다 — 실측
3.9.13.3은 $(1-p)^{\lceil S/M \rceil}$ 모형을 세우고 유실 5% 한 열로 맞춰 봤다. 한 열만 맞는 모형은 우연일 수 있다. 그래서 ① 유실 1%를 추가하고, ② 조각 크기 $M$을 가정하지 않고 패킷을 세어 실측하고, ③ 크기마다 RELIABLE 대조군을 추가했다(실행 확인, Jazzy · rmw_fastrtps_cpp · 컨테이너 둘, 발행자 쪽 eth0에 netem).
3.9.14.1 조각 크기를 가정하지 않고 셌다
tcpdump로 1000바이트를 넘는 UDP 패킷만 캡처해 메시지 20건으로 나눴다.
| 메시지 크기 | 1000 B 초과 패킷 / 20건 | 건당 조각 | 조각당 바이트 |
|---|---|---|---|
| 1 KB | 27 | 1 | — |
| 16 KB | 227 | 11 | 1489 |
| 64 KB | 887 | 44 | 1489 |
| 256 KB | 3,547 | 177 | 1481 |
조각 하나에 약 1,480바이트가 실린다. 3.9.13.3이 "MTU 1500 기준 약 1400"으로 가정했던 값이 실측으로 1,480이다 — 조각 수를 약 5% 적게 보게 되므로, 큰 메시지에서는 예측이 실제보다 낙관적이었다.
3.9.14.2 모형이 두 번째 유실률에서도 맞았다
BEST_EFFORT로 50건씩 보냈다.
| 크기 | 조각 $N$ | 모형 $(0.99)^N$ | 실측(유실 1%) |
|---|---|---|---|
| 1 KB | 1 | 99.0% | 98% (49/50) |
| 16 KB | 11 | 89.5% | 92% (46/50) |
| 64 KB | 44 | 64.1% | 70% (35/50) |
| 256 KB | 177 | 17.0% | 22% (11/50) |
네 칸 모두 모형의 몇 %p 안에 들어왔다. 유실률을 바꿔도 맞으므로, "한 조각만 잃어도 표본 전체가 버려진다"는 설명이 우연이 아니라 실제 메커니즘임이 확인된다 — 3.9.13.4의 인과 문장이 뒷받침됐다.
실측이 일관되게 모형보다 몇 %p 높은데, netem의 균일 무작위 유실이 같은 표본의 조각들에 완벽히 독립적으로 걸리지 않기 때문으로 보인다(확인 필요 — 원인은 가르지 못했다).
3.9.14.3 유실 5%에서는 모형보다 더 열악하다
| 크기 | 조각 $N$ | 모형 $(0.95)^N$ | 실측 BEST_EFFORT | 실측 RELIABLE |
|---|---|---|---|---|
| 1 KB | 1 | 95% | 96% (48/50) | — |
| 16 KB | 11 | 56.9% | 60% (30/50) | 82% (41/50) |
| 64 KB | 44 | 10.4% | 0% (0/50) | 2% (1/50) |
| 256 KB | 177 | 0.01% | 0% | 0% |
64 KB에서 모형은 10%를 예측했는데 실측은 0이다. 조각이 많아지고 유실이 커지면 조각 손실만으로 설명되지 않는 추가 붕괴가 있다 — 재조립 버퍼·타임아웃·재전송 폭주가 후보지만 이 실험으로는 가르지 못했다(확인 필요).
모형은 낙관적이다. $(1-p)^N$은 도달률의 상한으로 쓰는 것이 안전하다 — "모형이 10%라니 그래도 좀 오겠지"가 0%로 나온다.
3.9.14.4 RELIABLE이 포기하는 지점은 64 KB 근처다
3.9.13.4는 "256 KB 이상에서는 RELIABLE도 BEST_EFFORT도 똑같이 0%"라고 적었다. 이번 실측은 그 경계를 훨씬 아래로 내렸다.
- 16 KB에서는
RELIABLE이 일한다 — 60% → 82%. 재전송이 실제로 표본을 복구해 낸다. - 64 KB에서는 사실상 포기한다 — 0% → 2%(50건 중 1건).
이것이 실무적으로 가장 쓸모 있는 숫자다. "무선에서는
RELIABLE을 쓸까BEST_EFFORT를 쓸까"라는 질문은 메시지가 수십 KB를 넘으면 의미가 없어진다 — 둘 다 안 온다. 답은 QoS가 아니라 크기이며, 이는 3.9.13.5가 "보내는 양을 줄이는 것이 1순위"로 꼽은 것과 정확히 같은 결론이다.경계는 조건 의존이다 — 유실률·MTU·rmw·전송률에 따라 움직인다. 여기서 얻은 것은 "64 KB가 특별한 임계값"가 아니라 "조각이 수십 개가 되는 순간 QoS 선택이 무력해진다"는 규칙이다. 조각 수는
크기 ÷ 1480으로 미리 계산할 수 있다.
3.9.14.5 책 자신의 표기에서 발견한 것
이 절을 쓰며 3.9에 실측 절이 둘이나 있는데 본문에 실행 확인이라는 표기가 한 번도 없었다는 것을 발견했다. 그래서 이 책의 자동 점검과 통계가 3.9를 미검증 장으로 세고 있었다 — 85차에 원인 단정을 기계로 추출할 때 3.9가 후보에 들어온 것도 이 때문이다(실제로는 이미 측정해 본 주장이었다).
표기가 곧 상태다. 내용이 검증돼 있어도 약속된 표기를 안 쓰면 도구에게는 없는 것이다. 이 사례를 계기로 점검 도구에 ㉑ 실측 절이 있는데
실행 확인표기가 없는 장을 검출하는 검사를 추가했다(tools/book_check.py).
3.9.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 멀티캐스트 시험 생략 | 잘못된 층을 오래 봄 | ros2 multicast 먼저 |
| 2 | 다른 서브넷에서 자동 발견 기대 | 절대 안 됨 | ROS_STATIC_PEERS·Discovery Server |
| 3 | 다중 NIC에서 인터페이스 미지정 | 통신 불안정 | 명시 지정(3.9.4) |
| 4 | "컨테이너라서 디스커버리 실패"로 단정 | 잘못된 처방(host 모드부터) | 경계를 먼저 좁힌다 — 같은 네트워크 안은 동작(3.9.12) |
| 5 | 포트만 열고 멀티캐스트 주소 누락 | 디스커버리 실패 | 224.0.0.0/4 허용 |
| 6 | IGMP 차단 | 그룹 가입 실패 | IGMP 허용 |
| 7 | VPN MTU 미고려 | 대용량 토픽 유실 | FragmentSize 조정 |
| 8 | 커널 버퍼 미조정 | 대용량 수신 유실 | sysctl(3.9.8) |
| 9 | 무선에서 RELIABLE 대용량 | 링크 붕괴 | 3.9.3의 대처 순서 |
| 10 | 링크 용량 미측정 | 불가능한 목표로 튜닝 | iperf3 먼저 |
| 11 | 클라우드 보안 그룹 간과 | 로컬 방화벽만 봄 | 상위 정책 확인 |
| 12 | 원격에서 전체 그래프 관측 | 무선 포화 | 요약·선별 전송(7.8) |
| 13 | 큰 메시지에 BEST_EFFORT면 안전하다고 생각 | 0% 수신 — 조각 하나만 잃어도 표본 폐기 | 크기부터 줄여라(3.9.13.4) |
| 14 | "유실률 몇 %까지 괜찮나"로 접근 | 조각 수가 지수에 올라간다 | 조각 수로 따져라(3.9.13.3) |
| 15 | 유실 링크에서 한 번만 측정 | 디스커버리 지연으로 결과가 불규칙해진다 | 반복해 범위로(3.9.13.6) |
| 16 | 큰 메시지에 RELIABLE을 켜 두고 안심 | 조각 수십 개부터는 재전송으로도 복구하지 못한다(64 KB·5% 유실에서 2%) | 크기를 줄여라 — 조각 수 = 크기 ÷ 1480(3.9.14.4) |
3.9.11 체크포인트
- ROS 2의 "숨은 전제"는 무엇이며, 왜 개발 환경에서는 드러나지 않는가.
- 멀티캐스트가 성립하기 위한 요건 여섯 가지를 들 수 있는가. 서브넷을 넘을 때 무엇이 필요한가.
- 네트워크 문제 진단에서 가장 먼저 실행할 명령 쌍은 무엇인가.
- Wi-Fi가 DDS에 적대적인 구조적 이유 네 가지를 설명할 수 있는가. SPDP가 특히 취약한 이유는?
- 무선 대처를 효과 큰 순서로 나열할 수 있는가. 실제 해법이 튜닝이 아닌 이유는?
- 다중 NIC 로봇에서 생기는 증상 네 가지와 해결책을 말할 수 있는가. 왜 이것을 "기본 설정 항목"으로 취급해야 하는가.
- Docker bridge 모드에서 ROS 2가 어려운 근본 원인을 SPDP와 연결해 설명할 수 있는가.
- "컨테이너 격리와 ROS 2 디스커버리는 상충한다"는 명제가 실무 결정에 무엇을 의미하는가.
- 도메인 0에서 참가자 20개를 허용하는 방화벽 규칙을 쓸 수 있는가. UDP 외에 허용해야 할 프로토콜은?
- VPN 환경에서 MTU를 확인하는 명령과, 조각 크기를 어떻게 산정하는가.
- 커널 버퍼 부족을 확인하는 명령은 무엇인가. 왜 ROS 2 도구로는 볼 수 없는가.
- 컨테이너에서 디스커버리가 되는 경계와 안 되는 경계를 각각 말할 수 있는가. 호스트↔컨테이너가 안 될 때 실제로 무엇이 잘못된 것인가(3.9.12).
ros2 multicast가 성공했는데도 DDS가 안 통할 수 있는 이유는(3.9.12.4).- 진단 플레이북 여덟 단계를 순서대로 말할 수 있는가. 사람들이 대개 어디부터 의심하며, 왜 그것이 비효율적인가.
- 유실이 있는 링크에서
RELIABLE이 치르는 대가는 처리량인가 지연인가. 실측은 어느 쪽이었나(3.9.13.1). - 메시지 도달률을 유실률과 크기로 예측하는 식을 쓸 수 있는가. 왜 "유실률 몇 %까지"가 잘못된 질문인가(3.9.13.3).
- 큰 메시지에서
BEST_EFFORT로 바꾸면 무엇이 개선되는가(3.9.13.4). - 메시지가 몇 조각으로 분할되는지 미리 계산하려면 무엇으로 나누는가. 그 값은 실측으로 얼마였나(3.9.14.1).
- $(1-p)^N$ 모형은 어느 쪽으로 틀리는가. 그래서 어떻게 써야 하는가(3.9.14.3).
RELIABLE이 도움이 되는 크기와 무의미해지는 크기는 각각 얼마였나(3.9.14.4).
다음 장 3.10은 제3부의 마지막이자 이 장의 문제들에 대한 다른 답 — rmw_zenoh다. DDS의 LAN 전제를 버리고 설계된 미들웨어가 무선·WAN·대규모에서 무엇을 다르게 하는가.