안녕하세요, DevBJ입니다. 🚀 오늘은 2026년 엣지 AI 시스템 개발 현장에서 엔지니어들이 겪을 법한, 그러나 결코 만만치 않은 ‘삽질’ 주제를 들고 왔습니다. 바로 고대역폭 LiDAR 데이터를 Micro-ROS 기반 엣지 AI 파이프라인에서 처리할 때 마주하는 NTP 동기화 불량과 메모리 파편화 문제입니다. 자율주행, 로보틱스 분야에서 센서 퓨전의 정확성과 AI 추론의 실시간성은 생명과도 같죠. 이 문제들을 어떻게 파고들어 해결했는지, 그 생생한 기록을 공유합니다. 💡
1. 서론: 엣지 AI, 그 미지의 지뢰밭에서
최근 몇 년간 엣지 디바이스에서의 AI 추론은 비약적으로 발전했습니다. 하지만 여전히 LiDAR 같은 고성능 센서에서 초당 수백만 개의 점군(Point Cloud) 데이터를 받아 실시간으로 전처리하고 AI 모델에 먹이는 과정은 만만치 않은 도전입니다. 특히, 자율주행과 같은 미션 크리티컬 시스템에서는 센서 데이터의 정확한 시간 동기화와 예측 가능한 메모리 관리가 필수적입니다.
우리가 개발하던 시스템은 ARM 기반 임베디드 보드에서 Micro-ROS를 통해 LiDAR 데이터를 수신하고, 이를 경량화된 텐서플로우 라이트(TensorFlow Lite) 모델로 전달하여 객체 인식을 수행하는 구조였습니다. 처음에는 POC(개념 증명) 수준에서 잘 동작했지만, 실제 환경 데이터를 장시간 처리하면서 치명적인 문제가 발견되었습니다. 바로 **“센서 퓨전 시 시간 불일치”**와 “예측 불가능한 추론 지연 및 OOM(Out Of Memory)” 현상이었습니다. 이 문제를 해결하기 위해 며칠 밤낮을 보냈던 삽질기를 공유합니다. 🔥
2. 본론: 문제 파고들기 및 해결 전략
2.1. 문제 1: 시간 동기화 불량과 센서 퓨전의 비극 (NTP 심층 분석)
우리 시스템은 LiDAR 외에도 IMU, GPS 센서 데이터를 받아 센서 퓨전을 통해 정확한 위치 및 자세 추정을 하고 있었습니다. 하지만, 특정 상황에서 LiDAR 데이터와 IMU 데이터의 타임스탬프가 어긋나는 현상이 반복되었습니다. 이는 결국 센서 퓨전 결과의 신뢰도를 떨어뜨리고, 자율주행 알고리즘의 오작동으로 이어질 수 있는 심각한 문제였습니다.
원인 분석:
임베디드 리눅스 환경에서 일반적인 ntpd 또는 chronyd를 사용하여 시스템 시간을 동기화하고 있었지만, 이는 소프트웨어적인 동기화라 하드웨어 레벨의 정밀도에는 한계가 있었습니다. 특히, 시스템 부하가 높거나 네트워크 지연이 심할 때, 또는 보드의 RTC(Real-Time Clock)가 불안정할 때 동기화 오차가 누적되는 경향이 있었습니다.
해결책: PHC(PTP Hardware Clock)를 활용한 정밀 동기화 🛠️
가장 효과적인 해결책은 PTP(Precision Time Protocol)를 활용하여 하드웨어 레벨에서 시간을 동기화하는 것이었습니다. 우리 보드의 이더넷 컨트롤러가 PHC를 지원한다는 것을 확인하고, phc2sys 유틸리티를 활용했습니다.
-
ptp4l설정: 마스터 클럭에 연결하여 PTP 동기화를 수행합니다.# /etc/ptp4l.conf (예시) [global] # 네트워크 인터페이스 이름 (예: eth0 또는 enp1s0) # 실제 환경에 맞게 조정해야 합니다. interface eth0 slaveOnly 1 # PTP 포트 상태 확인 간격 (초 단위) logAnnounceInterval 1 # PTP sync 메시지 전송 간격 (로그 2의 승수로, 0은 1초, -1은 0.5초) logSyncInterval -3 # 125ms # PTP delay_req 메시지 전송 간격 logMinDelayReqInterval -3 # 125msptp4l실행:sudo ptp4l -i eth0 -m -s -f /etc/ptp4l.conf-i는 인터페이스,-m은 마스터 모드(슬레이브는-s),-f는 설정 파일입니다. 우리는 슬레이브 모드이므로-s를 사용합니다. -
phc2sys설정:ptp4l이 동기화한 PHC 시간을 시스템 클럭으로 동기화합니다.sudo phc2sys -s eth0 -c CLOCK_REALTIME -m -w-s eth0는 이더넷 인터페이스의 PHC를 소스로 사용하고,-c CLOCK_REALTIME은 시스템 클럭을 대상으로,-m은 마스터 클럭과의 시간 차이를 지속적으로 모니터링하며 동기화합니다.-w는 대기 후 동기화합니다.
결과:
phc2sys를 도입한 후, LiDAR와 IMU 데이터 간의 타임스탬프 오차는 수십 마이크로초 이내로 감소했습니다. 이는 센서 퓨전 알고리즘의 안정성을 획기적으로 향상시켰습니다. 📈
2.2. 문제 2: Micro-ROS의 메모리 파편화와 OOM의 저주 (Custom Allocator)
LiDAR 데이터는 매우 큰 메시지(수백 KB에서 수 MB)로 구성됩니다. Micro-ROS는 이러한 메시지를 rclc_support 레이어에서 동적으로 할당하고 해제합니다. 처음에는 문제가 없었지만, 장시간 동작하면서 불규칙한 추론 지연이 발생하고, 결국 malloc 실패 메시지와 함께 시스템이 멈추는 OOM 현상이 발생했습니다.
원인 분석:
잦은 대용량 메시지 할당/해제는 힙(Heap) 메모리를 파편화시킵니다. 운영체제의 기본 malloc/free는 시간이 지남에 따라 메모리 블록들이 잘게 쪼개져, 충분한 총 여유 공간이 있더라도 연속된 큰 블록을 할당할 수 없게 만듭니다. 이는 Micro-ROS가 LiDAR Point Cloud 같은 큰 메시지를 할당하려 할 때 실패를 유발하는 주원인입니다.
해결책: Micro-ROS 커스텀 메모리 할당자 도입 💡
Micro-ROS는 rcl_allocator_t 구조체를 통해 메모리 할당 방식을 사용자 정의할 수 있는 강력한 기능을 제공합니다. 이를 활용하여 미리 할당된 고정 메모리 풀(Memory Pool)을 사용하고, 여기서 블록 단위로 메모리를 관리하는 커스텀 할당자를 구현했습니다.
-
메모리 풀 정의: 시스템이 시작될 때 미리 필요한 최대 크기의 메모리 블록을 할당합니다.
#include <stddef.h> // size_t #include <stdint.h> // uint8_t #include <stdio.h> // printf // 미리 할당할 메모리 풀의 크기 (예: 16MB) // LiDAR 데이터 크기와 예상되는 동시 할당량에 따라 조정 필요 #define CUSTOM_MEM_POOL_SIZE (16 * 1024 * 1024) static uint8_t g_custom_mem_pool[CUSTOM_MEM_POOL_SIZE]; static size_t g_custom_mem_offset = 0; // 단순한 Bump Allocator를 위한 오프셋 // 실제 프로덕션 환경에서는 Free List 등을 관리하는 더 복잡한 할당자 필요 // 이 예시는 rcl_allocator_t 인터페이스 데모용 단순화된 버전입니다. void * custom_allocate(size_t size, void * state) { // 간단한 4바이트 정렬 size = (size + 3) & ~3; if (g_custom_mem_offset + size > CUSTOM_MEM_POOL_SIZE) { printf("[DevBJ] ERROR: Custom memory pool exhausted! Requested %zu bytes.\n", size); return NULL; } void * ptr = &g_custom_mem_pool[g_custom_mem_offset]; g_custom_mem_offset += size; return ptr; } void custom_deallocate(void * ptr, void * state) { // Bump allocator에서는 deallocate가 의미가 없습니다. // 실제 시스템에서는 free list나 block pool을 관리해야 합니다. (void)ptr; (void)state; // printf("[DevBJ] WARNING: Deallocate called on simple bump allocator.\n"); } void * custom_reallocate(void * ptr, size_t size, void * state) { // 재할당은 기존 데이터를 새 위치로 복사하는 과정이 필요합니다. // 이 예시에서는 기존 크기를 추적하지 않으므로, 새 공간을 할당하고 복사는 생략합니다. // 프로덕션 코드에서는 기존 크기를 인자로 받거나 메타데이터에 저장하여 복사해야 합니다. void * new_ptr = custom_allocate(size, state); if (new_ptr && ptr) { // memcpy(new_ptr, ptr, MIN(size, old_size)); // old_size가 필요함 printf("[DevBJ] WARNING: custom_reallocate in simple allocator might be inefficient.\n"); } custom_deallocate(ptr, state); // 기존 포인터는 해제 (bump allocator에서는 no-op) return new_ptr; } -
Micro-ROS에 커스텀 할당자 등록:
rcl_init_options_init호출 시 우리가 만든custom_allocator를 전달합니다.#include <rcl/rcl.h> #include <rcl/allocator.h> // ... custom_allocate, custom_deallocate, custom_reallocate 함수 정의 ... int main() { rcl_allocator_t my_custom_allocator = { .allocate = custom_allocate, .deallocate = custom_deallocate, .reallocate = custom_reallocate, .zero_allocate = NULL, // 필요하면 구현 .state = NULL // 추가 상태 정보를 전달할 수 있습니다. }; rcl_init_options_t init_options = rcl_get_zero_initialized_init_options(); // 커스텀 할당자를 사용하여 init_options 초기화 rcl_ret_t ret = rcl_init_options_init(&init_options, my_custom_allocator); if (ret != RCL_RET_OK) { printf("[DevBJ] Failed to initialize rcl init options with custom allocator: %s\n", rcl_get_error_string().str); return -1; } // rcl 라이브러리 초기화 시에도 커스텀 할당자를 사용 ret = rcl_init(0, NULL, &init_options, my_custom_allocator); if (ret != RCL_RET_OK) { printf("[DevBJ] Failed to initialize rcl with custom allocator: %s\n", rcl_get_error_string().str); return -1; } // ... 이후 Micro-ROS 노드, 퍼블리셔, 서브스크라이버 등 설정 ... // 메모리 풀 현황 모니터링 // printf("[DevBJ] Current memory pool usage: %zu / %zu bytes\n", g_custom_mem_offset, CUSTOM_MEM_POOL_SIZE); rcl_init_options_fini(&init_options); rcl_shutdown(); return 0; }주의: 위에 제시된
custom_allocate/deallocate는 매우 단순한 ‘Bump Allocator’입니다. 이는deallocate가 실제로 메모리를 반환하지 않아 장시간 동작 시 메모리가 계속 증가하는 문제가 있습니다. 실제 프로덕션에서는malloc_r과 같은 고정 크기 블록 할당자를 구현하거나,FreeRTOS등의 RTOS 환경에서 제공하는 메모리 관리 기능을 활용해야 합니다. 중요한 것은rcl_allocator_t인터페이스를 통해 시스템에 맞는 최적화된 메모리 관리 전략을 적용할 수 있다는 점입니다.
결과:
커스텀 할당자를 적용한 후, 불규칙했던 malloc 실패가 사라지고 시스템의 메모리 사용량이 예측 가능한 수준으로 안정화되었습니다. 추론 지연 시간의 편차도 현저히 줄어들어, 실시간 AI 추론의 안정성이 확보되었습니다. 🎉
3. 결론: 삽질은 성장의 자양분
임베디드/네트워크 시스템 개발은 언제나 예상치 못한 난관의 연속입니다. 특히 엣지 AI와 같은 복합적인 시스템에서는 하드웨어, 소프트웨어, 네트워크, 그리고 알고리즘까지 전방위적인 이해가 필요합니다. 오늘의 삽질 경험이 여러분의 험난한 엔지니어링 여정에 작은 빛이 되기를 바랍니다. 🛠️
우리가 겪었던 NTP 동기화 문제와 Micro-ROS 메모리 파편화는 단순히 코드를 바꾸는 것을 넘어, 시스템 아키텍처와 저수준 동작 원리를 깊이 이해해야만 해결할 수 있었습니다. 끊임없이 파고들고, 실험하고, 데이터를 분석하는 엔지니어의 근성이야말로 최고의 무기입니다. 여러분도 포기하지 않고 문제를 해결해 나가는 멋진 엔지니어가 되리라 믿습니다. 다음 삽질 기록에서 또 만나요! 🚀
DevBJ | No Bio, Just Log 기술 삽질로그