20.4 라우터(Router) 및 인프라 장애 대응

20.4 라우터(Router) 및 인프라 장애 대응

C++/Rust로 작성된 에지 디바이스(클라이언트)가 공장의 혈관이라면, zenohd 라우터는 혈액이 멈추지 않고 전신을 돌게 하는 심장이다. 라우터가 멎으면 천 대의 센서가 데이터를 뿜어내고 백 대의 관제 웹 대시보드가 살아있더라도 시스템 전체가 침묵하는 “블랙아웃(Blackout)” 상태에 빠진다.

라우터 소프트웨어 자체는 메모리 안정성이 극도로 높은 Rust로 작성되어 있으나, 라우터가 올려진 인프라(Linux Host OS, 네트워크 브릿지)와 외부 데이터베이스를 연결하는 플러그인(Plugin) 인터페이스 지점에서 치명적인 장애 복잡성이 태어난다.

이 절에서는 zenohd 데몬이 예기치 않게 커널 패닉이나 크래시에 빠졌을 때 사후 덤프(Core Dump)를 열어 코드를 추적하는 하드코어한 커널 레벨 디버깅 기법을 다룬다. 또한 RocksDB, InfluxDB 등의 스토리지가 라우터에 탑재되었을 때 디스크 I/O 백프레셔(Backpressure)로 인해 시스템 전체의 통신 레이턴시가 파괴되는 현상을 해부하고, 데이터베이스 단절로부터 앞단의 라우터 네트워크 코어를 논리적으로 격리(Isolate)시켜 생존시키는 인프라 레벨의 인명 구조술(Resuscitation)을 마스터하라.

1. 라우터 크래시(Crash), 패닉(Panic) 및 코어 덤프(Core Dump) 분석

어느 날 아침, 라우터가 죽었다. 재시작하면 언제 그랬냐는 듯 쌩쌩하게 돌아가지만, 다음 주에 또 죽을 것이란 폭탄을 안고 갈 수는 없다. Rust 언어 기반의 zenohd가 비정상 종료(Segfault, Panic)되는 순간의 기억을 보존하고 해부하는 도구들을 가동하라.

1.0.1 단계: Rust 패닉 백트레이스(Backtrace) 활성화

Zenoh 라우터가 크래시되는 순간, 기본 설정으로는 “Panicked at …“이라는 단 한 줄의 단서만 남고 스레드가 증발한다. 이는 원인 추적에 아무런 도움이 되지 않는다. 운영 서버든 스테이징 서버든 라우터를 데몬으로 올릴 때 반드시 다음 환경 변수를 강제 주입하라.

## Systemd 서비스 파일 내부에 삽입
Environment="RUST_BACKTRACE=full"

이 변수가 켜져 있으면, 라우터 내부의 패닉 시에 수십 줄짜리 함수 호출 스택(Stack Trace)이 로그 파일에 덤프되어, “라우팅 테이블 트리를 복사하는 도중 락(Lock)을 쥐고 파열되었다“는 혐의점을 개발자가 직접 짚어낼 수 있다.

1.0.2 단계: 코어 덤프(Core Dump) 저장소 구성

OOM(Out Of Memory) 킬러가 아니라 C 라이브러리(JNI, C-Plugin 등)를 타다가 세그멘테이션 폴트가 발생했을 경우, 리눅스가 프로세스의 당시 메모리 전체를 캡처하는 코어 덤프를 남기도록 시스템을 열어두어야 한다.

## 덤프 파일 용량 무제한 해제 (기본값은 0이라 안 남음)
ulimit -c unlimited

## 코어 파일이 저장될 경로와 파일명 패턴 지정
sudo sysctl -w kernel.core_pattern=/var/crash/core-%e-%p-%t

1.0.3 단계: GNU 디버거(GDB)를 통한 사후 부검

만약 /var/crash에 수 기가바이트짜리 core-zenohd-1234-time 파일이 떨어졌다면, 부검의 시간이 도래한 것이다.

## GDB 실행 (Rust용 rust-gdb를 권장)
rust-gdb /usr/bin/zenohd /var/crash/core-zenohd-1234-5678

## GDB 쉘에 진입한 후
(gdb) bt full

명령어를 타건하는 순간, 최상단에 프로세스가 어느 C/Rust 함수를 실행하다가 포인터 에러가 발생했는지 적나라하게 드러난다. 만약 그 라인이 rocksdb::db::put_engine 근처라면 네트워크 단 버그가 아님을 확정 지을 수 있다. 이런 코어 덤프 파일과 GDB 트레이스 스냅샷은 Zenoh 커뮤니티(GitHub Issue)에 리포트하여 코어 개발자들의 핫픽스를 받아내기 위한 가장 완벽한 증거 자료가 된다.

2. 플러그인(Plugin) 로드 실패 및 의존성 라이브러리 충돌

Zenoh 라우터는 마이크로커널 구조와 유사하게 핵심 코어(Routing & Transport)를 제외한 나머지 기능(REST API, Storage, WebServer)을 동적 공유 라이브러리 ( 리눅스의 .so, 윈도우의 .dll ) 형태로 실행 런타임에 로드(Load)한다. 이러한 구조는 라우터 가벼움을 보장하지만, **“버전 파편화 및 종속성 지옥(Dependency Hell)”**이라는 장애를 수반한다.

2.0.1 시나리오 1: 플러그인 버전과 데몬 버전의 ABI 불일치

라우터를 0.7.0 버전으로 업데이트했는데 깜빡하고 스토리지 플러그인 바이너리를 0.6.0 구버전 상태로 내버려 두었다. zenohd 기동 시 다음과 같은 로그가 터지며 플러그인이 로드되지 않는다.
[WARN] Failed to load plugin /usr/lib/libzenoh_plugin_storage_manager.so: wrong ABI version, expected 7, found 6
Zenoh의 플러그인은 C언어 단의 엄격한 Application Binary Interface (ABI) 검증을 거친다. 반드시 Zenoh 깃허브 릴리즈 페이지에서 라우터 바이너리와 플러그인 압축 파일을 ’정확히 동일한 버전’으로 한 쌍으로 다운로드하여 배포 파이프라인에 태워라.

2.0.2 시나리오 2: 간접 라이브러리(Shared Object) 탐색 실패

버전이 동일함에도 “Unable to load …” 오류가 뜬다면, 그 플러그인이 내부적으로 참조하는 또 다른 OS 단의 C 라이브러리가 서버에 안 깔려 있는 것이다.
라우터가 백엔드 서버에서 실행되지 않을 때, 문제가 되는 플러그인 파일에 대고 다이렉트로 ldd (List Dynamic Dependencies) 검사를 수행하라.

## ldd 명령어로 엮여있는 라이브러리의 링크 상태를 역추적
ldd /usr/lib/libzenoh_plugin_storage_influxdb.so

출력 결과 중에 다음과 같이 **not found**가 한 줄이라도 있다면 로드는 100% 실패한다.

        linux-vdso.so.1 (0x00007ffe345ca000)
        libssl.so.1.1 => not found             <-- 핵심 장애 부위! (OpenSSL 1.1 부재)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6

이 경우 리눅스 패키지 매니저(apt-get install libssl1.1)로 해당 종속성을 채워 넣어주어야 한다.

궁극적인 모범 타개책 (Docker):
운영체제를 업데이트할 때마다 libc 버전 충돌로 서버가 멎는 것을 보고 싶지 않다면, 개발자 로컬 머신에 파일들을 복사-붙여넣기 하지 마라. 모든 종속성이 얼려져 완벽히 세팅된 공식 eclipse/zenoh:latest 도커 컨테이너를 사용하는 것만이 플러그인 의존성 트러블슈팅을 원천적으로 소멸시키는 절대적인 방법이다.

3. 스토리지 백엔드(Backend) I/O 에러 및 데이터 동기화 지연

데이터를 공중에 흩뿌리는 Pub/Sub과 달리, 과거의 이력을 보존하고자 라우터에 스토리지를 다는(Storage Manager Plugin) 것은 완전히 다른 골칫거리를 낳는다. 특히 하드 디스크 I/O가 네트워크의 대역폭 한계를 따라가지 못할 때 발생하는 역류(Backpressure) 현상은 모든 통신 속도를 절반 이하로 떨어뜨린다.

3.0.1 증상 규명: Write-Stall (쓰기 지연 마비) 현상 식별

ROS2 로봇이 고해상도 초음파 데이터를 초당 100메가바이트씩 라우터에 퍼블리시하고, 라우터는 이것을 로컬 RocksDB 플러그인에 밀어 넣는 구조다.
처음 1분간은 맹렬하게 기록되다가, 어느 순간 갑자기 로봇에서 라우터로의 통신 딜레이(Latency)가 정상 수치(1ms)에서 5초(5000ms) 이상으로 기하급수적으로 폭등한다면 디스크 병목을 확신하라.

  • 분석: Linux 터미널에서 iostat -dx 1을 쳐보라. 디스크 큐(avgqu-sz)가 수치 100을 넘어가고, %util (사용률)이 100%에 붙어있다면, SSD 드라이브 물리적 한계를 벗어나 하드웨어 쓰기 버퍼가 멈춘 상태다. Zenoh 라우터는 이 디스크 쓰레드의 동기화(Sync) 락에 걸려 다른 네트워크 통신 수신을 일시 중지(Block)시켜 버린다.

3.0.2 해결 전략 1: 비동기 논블로킹(Async/On-interval) 쓰기 정책 적용

라우터 설정 파일 zenohd.json5에서 DB 볼륨의 동기화 설정을 on_insert (데이터가 오자마자 저장) 방식에서 시계열적 묶음 저장(Batch/Buffering) 방식으로 변경하라.

plugins: {
  storage_manager: {
    volumes: {
      rocksdb: {
        // ...
        // 버퍼 모드로 캐시에 쌓아둔 후 1초에 한 번만 디스크 블록 플러쉬를 수행
        sync: { mode: "on_time", interval_ms: 1000 }
      }
    }
  }
}

강제 비동기 버퍼 모드를 채택하면, 기기가 갑작스레 재부팅될 때 최후반 1초 분량의 데이터는 소실될 수 있으나, 나머지 99.9% 평상시의 네트워크 통신 속도는 10배 이상 향상된다.

3.0.3 해결 전략 2: 읽기(Read) Lock으로 인한 네트워크 블로킹 대응

TS 클라우드 백엔드가 과거 데이터를 보려고 분산 쿼리 z_get "robot/odom/history**"를 요청했다. 이때 수천만 건의 데이터를 디스크 레코드에서 쓸어 담아 메모리로 올리느라 CPU의 로우 레벨 읽기 락(Read Lock)이 수 초간 지속될 수 있다.
해결책: 볼륨을 논리적으로 분리하라. 최신 제어 명령어가 오가는 세션과, 과거 히스토리를 대량으로 퍼가는 스토리지 세션을 별도의 라우터 컨테이너로 분할(Sharding)하거나 레플리카를 구성해 통신 통로와 검색 통로를 구조적으로 분리해야 메인 동맥경화(Arteriosclerosis)를 예방한다.

4. 백엔드 데이터베이스 연결 끊김 및 쿼리 타임아웃 대응

통계 데이터 관리를 위해 라우터의 스토리지 플러그인을 아예 외부 원격 프로덕션 데이터베이스(InfluxDB 2.x 서버 등)로 연결해 두었을 때 발생하는 외부 단절 장애에 대한 백업(Fallback) 전략이다.

4.0.1 시나리오 1: InfluxDB 서버 다운 시 라우터의 버퍼 붕괴

에지 센서망 -> 클라우드 라우터 -> AWS InfluxDB 순으로 이어지는 구조에서, InfluxDB 서버(엔드포인트)가 네트워크 설정 오류로 응답을 멈췄다.
수십만 개의 센서 값이 클라우드 라우터(storage_manager 플러그인)로 도착했지만, 플러그인은 뒷단 DB로 날리지 못하고 라우터의 램(RAM) 메모리에 캐싱(Caching)하며 무한 재시도를 감행한다. 그리고 라우터는 이내 메모리 부족(OOM)으로 박살 난다.

방어 체계 (서킷 브레이커): 외부 통신 플러그인은 “무조건 일시 단절될 수 있다“는 전제하에 운영해야 한다. 라우터 스토리지 플러그인의 config를 작성할 때 데이터베이스 지연 및 에러 시 패킷 드롭 정책을 명시하거나, 백엔드 전송 워커 큐의 용량(Queue size Limit)을 제한하여 시스템 폭발 전에 로그만 남기고 데이터를 소거(Discard) 하도록 통제하라.

4.0.2 시나리오 2: Query(z_get)의 연쇄 타임아웃 붕괴

프론트엔드 대시보드(TypeScript 클라이언트)에서 session.get("/sensor/history") 구문을 호출했다. Zenoh는 망 전체에 “이 데이타 있는 라우터 응답하라!“고 쿼리 트리를 펼친다.
이때 하필 해당 데이터를 쥐고 있던 InfluxDB 연동 라우터가 수 초 대기 연산에 빠지거나 접속 불량 상태라면, 쿼리를 던진 클라이언트 쓰레드 자체도 이 반환(Reply) 퓨처 스트림(Stream)이 닫히길 기대하며 꼼짝없이 묶여있는다.

해결 방안 - BestMatching 쿼리 타겟 설정:
분산 쿼리는 항상 지연(Timeout) 가능성을 애플리케이션 레벨에서 핸들링해야 한다.

// 단일 통신 지연에 의해 앱이 멎는 것을 막는 비동기 호출 타임아웃 핸들링
import { Target } from '@eclipse-zenoh/zenoh-ts';

try {
    // target: Target.BestMatching 옵션을 통해 
    // 데이터베이스가 일부 죽더라도, 일단 망 내에 존재하는 캐시 중 
    // 가장 가까운(빠르게 응답하는) 결과부터 즉시 반환받고 루프를 끊는다.
    const replies = await session.get("sensor/logs", { target: Target.BestMatching });
    for await (let reply of replies) {
        console.log(reply.sample.value);
    }
} catch (e) {
    console.warn("DB Timeout elapsed. Rendering default cache.", e);
}

분산 시스템의 질의(Query) 엔진은 중앙 집중형 SQL과는 달라서, 완벽한 전체 데이터셋보다는 ’가용한 최신 데이터의 지연 없는 확보’가 관제 모니터링 시스템 스크린에서 UI 프리징 현상을 막아주는 핵심 철학이다. 타임아웃을 두려워하지 말고 쿨하게 예외(Exception) 처리하는 구조를 설계하라.