노드 크래시(Crash) 시 분석 방법
개요
ROS2 Humble 환경에서 노드가 갑작스럽게 크래시(Crash)하는 문제는 흔히 발생하며, 특히 실제 로봇 시스템이나 복잡한 센서 네트워크에서 잦은 편이다. 크래시가 발생할 때는 단순히 노드가 종료되는 것을 넘어, 시스템 전체에 영향을 줄 수 있으므로 정확한 원인 분석이 매우 중요하다. 이를 위해서는 콜 스택(Call Stack) 확인, core dump 활용, ROS2 자체 로그 확인, 의존 라이브러리 버전 충돌 여부 확인 등 다양한 접근 방법이 필요하다. 여기서는 노드가 크래시되었을 때 적용할 수 있는 종합적인 분석 방법과 핵심 도구 사용 방법에 대해 소개한다.
노드 크래시와 ROS2 런타임 구조
ROS2 노드는 rclcpp나 rclpy 등의 클라이언트 라이브러리를 통해 DDS(Datadistribution Service) 네트워크 계층과 연결된다. 노드가 크래시되는 원인은 다양하지만, 대체로 다음과 같은 범주로 분류할 수 있다.
메모리 접근 에러(Segmentation Fault)
잘못된 포인터 접근
동적 할당 해제 후 재사용(Use-After-Free)
스택 오버플로(Stack Overflow)
라이브러리나 의존 패키지 충돌
호환되지 않는 버전의 의존 라이브러리가 섞여 있을 때
잘못된 설정 옵션이나 ABI(Application Binary Interface) 차이
스레드 경합(Race Condition) 또는 데드락(Deadlock)
Node 내부의 멀티스레드 로직에서 발생하는 데이터 경쟁
ROS2 Executor(멀티스레드Executor 등) 설정 오류
외부 환경 문제
하드웨어 에러, 예를 들어 GPU 드라이버 충돌
네트워크 지연이나 QoS 설정에 따른 문제
노드가 크래시를 일으키는 이유가 어디에 있든, 먼저 재현 가능한 환경을 만들고 크래시 순간의 상태를 파악하는 것이 1차적인 목표다.
노드 크래시 시 로그 확인
가장 먼저 확인해야 할 정보는 시스템 로그와 ROS2 로그다. ROS2 노드는 표준 출력(stdout), 표준 에러(stderr), 또는 자체 로그 파이프라인으로 로그 메시지를 남긴다.
ROS2 콘솔 출력: 일반적으로 $ ros2 run <패키지명> <노드명> 명령으로 노드를 실행하면, 터미널 콘솔에 로그가 출력된다. 크래시 직전에 어떠한 에러 로그가 찍혔는지 확인할 수 있다.
systemd 기반 환경: Ubuntu, Debian 등 systemd 시스템에서는 $journalctl -f 명령을 통해 실시간 로그를 모니터링하거나 $ journalctl -u <서비스명> 형태로 특정 서비스 로그를 조회한다.
dmesg 확인: 커널 메시지는 $ dmesg 명령을 통해 조회할 수 있다. 세그멘테이션 폴트(segfault) 같은 심각한 오류가 발생하면 커널 레벨에서 관련 메시지가 찍히므로, 이를 통해 메모리 참조 문제인지 아닌지를 대략적으로 유추할 수 있다.
core dump 활용
노드 크래시 원인을 정확히 파악하려면 core dump를 생성해서 확인하는 방법이 가장 효과적이다. core dump는 프로세스가 비정상 종료되는 시점의 메모리 상태를 캡처한 파일이다.
core dump 설정
시스템에 따라 core dump가 기본적으로 비활성화되어 있을 수 있으므로, 아래 명령으로 활성화한다.
또는 /etc/security/limits.conf 파일에 항목을 추가해 영구적으로 설정할 수 있다. 예:
이렇게 하면 프로세스가 크래시될 때마다 현재 디렉토리에 core라는 이름의 파일(혹은 core.12345 형태)이 생성된다.
gdb로 core dump 분석
가장 일반적으로는 gdb(또는 lldb)를 사용해 core dump 파일을 로드한다. 다음 예시는 gdb를 활용한다.
core dump를 로드한 후에는 backtrace(줄여서 bt) 명령으로 콜 스택(Call Stack)을 확인할 수 있다.
콜 스택에서 어느 함수에서 예외가 발생했는지, 어떤 라이브러리 호출 중 문제가 생겼는지를 파악할 수 있다. 이 정보로부터 segfault인지, 특정 예외를 처리하지 못했는지 등을 구별하여 문제 해결 방향을 잡는다.
실시간 디버거(gdb)를 활용한 노드 구동
core dump로 사후(Post-mortem) 분석이 가능하지만, 때로는 노드를 gdb 환경에서 바로 실행해 크래시 순간에 멈추도록 하는 방법이 더 효율적일 수 있다.
빌드 시 디버그 심볼 포함: CMAKE_BUILD_TYPE=Debug 설정을 통해 디버그 심볼을 포함하도록 빌드한다. 예:
gdb에서 노드 실행: 빌드가 완료되면 gdb를 통해 노드를 직접 실행한다.
--args 옵션 뒤에 원하는 실행 인자를 그대로 넘길 수 있다.
실행 및 브레이크포인트 설정: gdb 프롬프트에서 run으로 노드를 구동하고, 특정 함수나 파일 라인에 브레이크포인트를 설정한다.
이후 크래시 발생 시점에서 프로그램은 일시정지되고, 함수 호출 스택, 변수 상태 등을 열람할 수 있다.
rclcpp와 에러 핸들링
C++ 기반의 ROS2 노드에서 자주 사용하는 클라이언트 라이브러리인 rclcpp는 예외 처리가 잘못되거나 스레드 동시 접근이 적절히 제어되지 않을 때 크래시를 일으킬 수 있다. 다음은 rclcpp와 관련된 오류를 분석할 때 주의해야 할 사항이다.
rclcpp::spin():
rclcpp::spin()은 싱글 스레드 Executor로 노드를 실행한다. 콜백 처리 과정에서 예외가 발생하면, 이 예외가 적절히 catch되지 않을 경우 프로세스가 종료될 수 있다.콜백 내부에서 모든 예외를 처리하거나, 상위 레벨에서
try-catch를 배치해 안전장치를 두는 방식이 권장된다.
MultithreadedExecutor:
여러 개의 콜백이 동시에 동작하므로, 공유 자원에 대해 적절한 동기화가 필요하다.
잘못된 뮤텍스 사용이나 lock 순서 역전(deadlock) 상황이 생기면 노드가 멈추거나(crash 대신 hang) 비정상 상태에 빠질 수 있다.
크래시 상황이라면 보통 세그멘테이션 폴트나 race condition에서 오는 double free 등이 원인일 수 있으므로, thread sanitizer 같은 도구 활용도 고려한다.
라이프사이클(Lifecycle) 노드:
Lifecycle 노드는 상태(State) 전이 과정에서 오류가 발생하면 쉽게 크래시로 이어질 수 있다. 예를 들어
on_activate()단계에서 null 포인터 참조가 일어나거나, DDS 통신 설정이 올바르지 않다면 크래시가 발생할 수 있다.상태 전이별로 적절한 오류 처리가 반드시 들어가야 하며, 특히
rclcpp_lifecycle::State객체 사용 시점에 유효성 체크를 잊지 말아야 한다.
메모리 분석 기법
노드 크래시 중 상당수는 메모리 관련 오류에서 비롯된다. 특히 C++ 기반 노드에서 빈번하다. 아래 몇 가지 기법을 정리한다.
AddressSanitizer(ASan)
Clang이나 gcc에서
-fsanitize=address플래그를 통해 AddressSanitizer를 활성화할 수 있다.예:
런타임에 유효하지 않은 메모리 접근이 발생하면 프로그램을 중단시키고 콜 스택 정보를 보여준다.
Valgrind:
동적 분석 도구로, 메모리 누수나 잘못된 메모리 접근을 추적할 수 있다.
노드를 Valgrind로 실행:
출력되는 에러 레포트를 통해 크래시를 일으키는 악성 메모리 접근 여부를 알 수 있다.
스택 크기 부족:
ROS2 콜백에서 지나치게 큰 지역 변수를 선언하거나, 재귀 호출이 심화되면 스택이 부족해진다.
이는 세그멘테이션 폴트로 이어질 수 있으므로, 큰 배열은 동적 할당(예:
std::vector)을 권장하며 재귀 깊이가 큰 알고리즘은 iterative 방식으로 대체하는 것이 바람직하다.
아래는 예시로 지역 배열 크기가 과도할 때 스택 오버플로가 발생할 가능성을 수식으로 표현하면, 단일 스레드 환경에서 대략적인 스택 사용량 $S_{\text{stack}}$은
$S_{\text{base}}$: 프로그램 시작 시 기본적으로 예약되는 스택 크기(함수 호출 전역 영역)
$N$: 함수 호출 깊이 또는 재귀 호출 횟수
$S_{\text{frame}}$: 각 함수 호출 시 스택 프레임이 차지하는 평균 메모리 크기
이때 $S_{\text{stack}}$가 시스템에서 할당된 최대 스택 크기를 초과하면 스택 오버플로가 발생할 수 있다.
ROS2 로그 레벨 및 추가 정보 수집
ROS2는 기본적으로 다양한 로그 레벨(DEBUG, INFO, WARN, ERROR, FATAL)을 제공한다. 노드 크래시가 빈번하거나 원인을 알기 어려운 경우, DEBUG 레벨 로그를 활성화해 더 상세한 정보를 얻을 수 있다.
또한 $RMW_IMPLEMENTATION 환경 변수나 $ RMW_LOG_LEVEL을 조정해 DDS 통신 레벨에서의 로그를 확인하는 것도 도움이 된다. 예를 들어, Fast DDS에서 네트워크 이슈가 발생할 때 콘솔에 더 자세한 로그가 찍히도록 설정할 수 있다.
DDS 계층 및 네트워크 이슈
노드가 크래시되는 원인 중 DDS 계층이나 네트워크 설정 문제가 연관된 경우도 많다. ROS2는 DDS를 기반으로 각 노드 간 메시지를 주고받기 때문에, DDS 설정이 올바르지 않으면 비정상 종료가 발생할 수 있다.
QoS 설정 불일치로 인한 충돌
서로 다른 노드가 QoS(Quality of Service) 설정을 일관성 없이 사용하면, 노드 간 통신 시 비정상 동작이 발생할 수 있다. 예컨대, 한 노드는 RELIABLE를 사용하고 다른 노드는 BEST_EFFORT를 사용하는 경우, 커뮤니케이션이 간헐적으로 끊기거나 메시지 처리 지연이 발생하다가 특정 조건에서 크래시로 이어지기도 한다.
정확히는 노드가 즉시 크래시하기보다, 메시지를 정상적으로 받지 못해 내부 버퍼가 이상 동작하는 과정에서 시스템적 오류가 생길 가능성이 높다.
Participant/Publisher/Subscriber 충돌
DDS에서는 Participant, Publisher, Subscriber, Topic 개념이 서로 엄격히 분리되어 있으며, 각 엔티티(Entity)가 올바르게 생성·소멸되지 않으면 리소스 충돌이 발생할 수 있다.
예를 들어 노드 종료 시점에 Publisher나 Subscriber가 깨끗하게 해제되지 않으면, DDS 레이어에서 메모리 접근 오류가 발생해 노드가 강제 종료되기도 한다.
이 문제를 방지하려면
rclcpp::shutdown()등 노드 종료 루틴을 명시적으로 호출해주는 습관이 필요하다.
DDS RMW 구현체 버그
ROS2에는 여러 RMW(Robot Middleware) 구현체(Fast DDS, Cyclone DDS, Connext DDS, Gurum DDS 등)가 존재하며, 버전마다 고유한 버그나 이슈가 있을 수 있다.
RMW 구현체 버그로 인한 크래시 가능성을 점검하기 위해서는, 문제 발생 노드에 대해 다른 RMW 구현체로 교체 테스트를 진행해보는 방법이 있다. 예를 들어 Fast DDS 대신 Cyclone DDS로 바꿔서 동일 증상이 재현되는지 확인해 본다.
교체 방법 예:
만약 교체 후 문제가 사라진다면, 특정 RMW에 내재된 충돌 가능성을 의심할 수 있다.
네트워크 지연 및 패킷 손실
ROS2는 내부적으로 DDS Discovery 프로토콜을 사용하여 노드 정보를 브로드캐스트하고, 데이터 교환 시에는 UDP 기반 데이터 전송이 많이 쓰인다.
패킷 손실이 심하거나 RTT(Round Trip Time)가 지나치게 큰 환경에서는, DDS participant가 제대로 동기화되지 않아 노드가 통신 에러를 일으킬 수 있다.
이런 환경에서는 DDS 구성 파라미터(Heartbeat, Lease Duration 등)를 조정하거나, 멀티캐스트 라우팅 설정을 점검해야 한다.
VPN, Docker, 가상머신 등 특별 환경
Docker 컨테이너, VPN, 가상머신 같은 환경에서 멀티캐스트 패킷이 원활히 송수신되지 못하는 경우가 흔하다.
기본적으로 ROS2 DDS Discovery가 브로드캐스트/멀티캐스트 기반이라는 점을 기억해야 하며, 만약 이를 사용할 수 없는 네트워크 구조라면 unicast-only 모드나 static discovery file 설정 등을 검토해야 한다.
ABI 및 라이브러리 버전 충돌
ROS2 Humble은 C++17을 사용하는 경우가 많고, rclcpp, rmw, DDS 라이브러리 등 여러 컴포넌트가 동일한 ABI(Application Binary Interface)를 준수해야 한다. 그런데 다음 같은 상황에서 크래시가 잦아질 수 있다.
다른 버전의 C++ 런타임 라이브러리
예:
libstdc++6버전 차이가 크면, STL 객체 내부 구조가 달라져서 ABI 충돌이 일어날 수 있다.ROS2 자체는 apt 저장소에서 제공되는 공식 빌드를 사용했는데, 외부에서 설치한 라이브러리가 다른 컴파일러 버전으로 빌드된 경우 크래시가 발생할 수도 있다.
클라이언트 라이브러리(rclcpp, rclpy) 혼용 시 문제
C++ 노드(rclcpp)와 Python 노드(rclpy)가 상호 연동하는 것은 정상적이지만, 동일 프로세스 내에서 임의로 섞어 사용하는 것은 의도치 않은 문제를 야기할 수 있다.
예를 들어 C++ Extension을 사용하는 Python 코드가 rclcpp를 통해 노드를 생성하고, 동시에 rclpy 라이프사이클을 초기화하는 과정에서 인스턴스 충돌이 발생할 수 있다.
빌드 옵션 불일치
ROS2 핵심 라이브러리를 Debug 모드로 빌드하고, 특정 서드파티 라이브러리는 Release 모드로 빌드하면, 결국 링킹 과정에서 심볼 충돌이 발생할 수 있다.
특히
-D_GLIBCXX_USE_CXX11_ABI=0/-D_GLIBCXX_USE_CXX11_ABI=1플래그가 엇갈리면, 문자열이나 STL 컨테이너 ABI가 달라져 크래시로 이어질 수 있다.
Docker 컨테이너 환경에서의 크래시 디버깅
최근에는 Docker 컨테이너를 활용해 ROS2 환경을 구성하는 경우가 많다. 컨테이너 환경에서 노드가 크래시하면 호스트 환경과 달리 접근 및 디버깅 절차가 조금 달라진다.
호스트와 다른 커널 네임스페이스:
컨테이너 내부에서 dmesg나 journalctl을 확인해도 호스트와 별도로 관리된다. core dump 파일을 호스트에 직접 생성하기 위해서는 --ulimit core=-1 등의 옵션을 docker run 명령에 포함해야 한다.
--privileged 옵션은 컨테이너가 호스트 커널 기능에 접근할 수 있도록 하는데, 보안 측면에서 주의가 필요하다.
컨테이너 내부 디버거 사용:
gdb, valgrind 등을 컨테이너 내부에 설치하여 바로 크래시 분석을 진행할 수 있다.
다만, 컨테이너 이미지를 가볍게 유지하기 위해 기본적으로 빌드 도구와 디버거가 포함되어 있지 않을 수 있으므로, Dockerfile에 추가로 설치 명령을 넣어야 한다.
멀티 컨테이너 통신 시 DDS 설정:
여러 컨테이너가 다른 호스트나 네트워크 상에 분산 배치되어 있다면, DDS Discovery를 위해 멀티캐스트 포트를 열어야 하며, 브리지 네트워크나 호스트 네트워크 모드로 실행하는 것이 권장될 때도 있다.
네트워크가 완전히 단절된 상태에서 기본 DDS Discovery가 동작하지 않으면, 노드가 내부적으로 재시도하다가 에러 스택이 쌓여 크래시가 날 가능성이 있다.
Python(rclpy) 노드 크래시 분석
C++에 비해 Python은 메모리 접근 오류가 직접적으로 드러나는 경우가 상대적으로 적지만, rclpy 노드에서도 여러 형태로 크래시가 일어날 수 있다. 대표적인 예시는 다음과 같다.
Python 콜백에서의 예외 처리 누락
Python에서 발생하는 예외는 기본적으로 인터프리터에서 캐치되어 스택 트레이스가 출력된다.
그러나 C++ Extension(예: Python에서 C++ 모듈을 호출하는 경우) 내에서 발생한 예외가 제대로 처리되지 못하면, 그대로 세그멘테이션 폴트 등으로 이어질 수 있다.
rclpy 콜백 내부에서 모든 예외를 적절히 잡아주고, 필요한 경우 로그를 남기거나 에러 핸들링 루틴을 호출하도록 한다.
다중 스레드(멀티쓰레딩) 환경에서의 GIL 문제
Python에는 GIL(Global Interpreter Lock)이 존재하기 때문에, 멀티쓰레드에서 동시에 Python 객체를 조작할 경우 lock 획득 순서가 꼬여 교착상태(Deadlock)가 생길 수 있다.
rclpy에서 Executor를 이용해 다중 콜백을 처리하는 경우, C++ 레벨에서 멀티스레드 Executor가 동작하고, Python 레벨에서 다시 GIL이 동작하는 이중 구조가 된다. 이때 lock 획득이 꼬여 크래시가 발생할 가능성이 있다.
Python Garbage Collection과 노드 자원 관리
rclpy 노드는 메시지, Publisher/Subscriber 객체를 Python 레벨에서 관리하며, 참조가 끊어지면 GC(Garbage Collector)가 동작한다.
GC가 동작하는 시점과 DDS 레벨에서 해당 리소스를 해제하는 타이밍이 어긋나면, 내부적으로 충돌이 생길 수 있다.
Publisher/Subscriber를 객체 변수로 명시적으로 유지(references)하고, 불필요해진 시점에서만 안전하게 해제하는 방식을 권장한다.
서드파티 Python 라이브러리 버전 문제
NumPy, SciPy 등을 비롯한 서드파티 라이브러리가 C/C++ 기반 모듈을 포함하는 경우, 버전 불일치나 ABI 충돌로 인해 갑작스러운 크래시가 발생할 수 있다.
특히 ROS2 패키지와 별도로 설치된 Conda 환경, virtualenv 등이 섞여 있으면, Python 런타임이 참조하는 동적 라이브러리가 달라질 수 있으므로 주의해야 한다.
디버깅 접근
Python 노드가 크래시될 때는 기본적으로 Python 스택 트레이스를 살펴본 후, 추가적으로 gdb에서 Python 심볼을 인식할 수 있도록 빌드된 CPython 디버그 버전을 사용하는 방법도 있다.
$ gdb python으로 진입해 rclpy 노드 파일을 실행하거나, core dump를 로드해 분석하는 방식으로 접근 가능하다. 다만, CPython 디버그 빌드는 일반 릴리스 빌드보다 성능에 영향을 줄 수 있다.
스레드 간 자원 충돌(Race Condition) 추가 사례
앞서 멀티쓰레드 Executor나 Python GIL 관련 문제를 언급했지만, 복잡한 로직에서는 좀 더 다양한 스레드 충돌 상황이 벌어진다.
Shared Pointer/Unique Pointer 오용:
C++에서
std::shared_ptr와std::unique_ptr를 적절히 사용하지 않으면, 이미 해제된 객체에 다른 스레드가 접근하여 크래시가 발생한다(Use-After-Free).예: 콜백에서 특정 메시지의 수명을 넘어선 시점에 재사용하려 하거나, global 변수를 잘못 공유하는 경우.
ROS2 Timer 콜백과 Service/Client 콜백의 동시 접근:
Timer 콜백이 주기적으로 글로벌 데이터를 갱신하는 동안, Service 콜백이 같은 데이터를 읽거나 수정하려고 하면 락이 제대로 걸려야 한다.
락 사용이 잘못되면 Race Condition이 발생하여 예측 불가능한 동작(데이터 손상, 크래시 등)으로 이어질 수 있다.
비주기적 콜백(Events) 간 동기화:
센서 데이터 콜백, 사용자 입력 콜백, 타이머 콜백 등이 서로 다른 주기로 동시에 실행되면서 공유 자원을 건드릴 수 있다.
이 경우 lock-free 구조를 사용하거나, mutex나 condition variable로 동기화를 엄격히 관리해야 한다.
Thread Sanitizer 활용:
C++17 이상 컴파일러(Clang 또는 gcc)에서
-fsanitize=thread옵션을 사용하면 Race Condition을 검출할 수 있다.ROS2 노드를 Thread Sanitizer로 빌드 예:
런타임 시 스레드 충돌이 발생하면, 어디서 어떤 스레드가 어떤 변수에 접근했는지 리포트를 남겨준다.
고급 시스템 트레이싱(LTTng, eBPF 등) 활용
대규모 시스템이나 배포 환경에서 특정 노드가 간헐적으로 크래시한다면, 기존의 gdb나 로그 분석만으로는 원인을 찾기 어려울 수 있다. 이럴 때 커널 레벨 혹은 시스템 전역 트레이싱 툴을 활용하면, 더 세밀한 관찰이 가능하다.
LTTng(Linux Trace Toolkit next generation)
LTTng은 커널과 유저 공간 애플리케이션을 동시에 트레이싱할 수 있는 강력한 툴이다.
LTTng 세션을 생성한 뒤, ROS2 노드를 실행하여 크래시가 발생할 때까지 트레이스를 수집하면, 스케줄링 이벤트, 시스템 콜, 스레드 간 컨텍스트 스위칭 정보를 모두 살펴볼 수 있다.
추출된 trace 파일은
babeltrace같은 도구로 분석하며, 콜백 실행 지점이나 Executor의 동작 시퀀스 등도 일부 간접적으로 파악할 수 있다.
eBPF(extended Berkeley Packet Filter)
eBPF는 리눅스 커널 내부에서 동적으로 코드를 삽입해, 특정 이벤트(함수 호출, 시스템 콜, 커널 트레이스포인트 등)만 필터링하여 기록할 수 있는 강력한 기술이다.
eBPF 기반 툴(BCC, bpftrace 등)을 이용하면, 예컨대 특정 라이브러리 함수 호출 시점이나, 메모리 할당/해제 시점을 실시간으로 포착하고, 누가 그 함수를 호출하는지 추적 가능하다.
ROS2 노드가 내부적으로 DDS 라이브러리 함수를 호출할 때마다, 혹은 특정 Subscriber 콜백이 처리되는 시점에 커널 이벤트를 찍도록 설정하면 크래시 직전에 어떤 함수 호출이 있었는지 상세 정보를 수집할 수 있다.
Perf Events
리눅스의
perf도구를 활용하면 CPU 프로파일링, 스케줄링 이벤트 관찰, 하드웨어 카운터(CPU branch misprediction 등) 모니터링 등이 가능하다.크래시가 발생하기 전후로 CPU 사용률이 급증하거나, 특정 콜백이 비정상적으로 많은 명령어를 실행하고 있는지를 파악함으로써 문제 지점을 좁혀갈 수 있다.
perf record → perf report 과정을 통해, 문제 발생 시점 직전까지의 함수 호출 빈도나 성능 지표를 확인할 수 있다.
추가 이벤트 계측 삽입
ROS2 소스 내부에 직접 이벤트 로깅(예:
RCUTILS_LOG_DEBUG) 지점을 추가하거나,TRACEPOINT매크로 등을 통해 트레이스 정보를 더 세밀히 넣을 수도 있다.노드 크래시 시, 해당 이벤트 로깅을 통해 DDS 콜백 처리 순서, 타이머 만료 시점, 멀티스레드 간 작업 스케줄 순서를 명확히 확인할 수 있다.
이 방법은 ROS2 프레임워크를 일부 수정해야 하므로, Humble 버전용 소스 빌드를 수행하거나 별도의 패치 레이어를 적용해야 할 수도 있다.
분석 워크플로 예시 (LTTng 활용)
위와 같은 과정을 통해, 단순 콜 스택 이상의 시스템 전반 동작을 관찰할 수 있다.
Last updated