20.9 모니터링 및 시각화 시스템 연동 장애
아무리 정교한 트러블슈팅 지식을 머리에 담고 있어도, 장애 현장의 피사체를 보여주는 감시 카메라 자체가 꺼져버린다면 엔지니어는 완벽한 맹인이 된다. 앞선 챕터에서 그토록 공들여 구축한 프로메테우스(Prometheus) 메트릭 파이프라인과 그라파나(Grafana) 대시보드 시스템 자체가 오작동을 일으켜 거짓 정보를 보여주거나(False Positive), 핵심적인 분산 트레이싱(Tracing) 스팬 곡선이 누락되는 2차 재난 상황을 타개해야 한다.
시스템을 감시하는 ‘감시자(Watcher)’ 컴포넌트들은 필연적으로 메인 시스템의 자원(CPU, Network)을 일부 차입하여 동작하는 기생적 특성을 가지기 때문에, 감시 범위가 너무 과도해지면 주객이 전도되어 감시자가 호스트 시스템의 숨통을 끊는 자가당착(Overhead)에 빠지게 된다.
이 절에서는 감시자의 눈이 멀어버리거나(데이터 수집 지연), 감시 스크린 자체가 물리적 한계를 초과하여 파열하는(렌더링 폭발) 현상을 치료하고, 마이크로서비스 간의 데이터 궤적을 쫓는 오픈텔레메트리(OpenTelemetry) 트레이싱 데이터가 징검다리 홉(Hop)을 건널 때마다 증발하는 구조적 결함을 복원하여 완벽한 관제탑 시야(Visibility)를 되찾는 기술을 연마하라.
1. 프로메테우스(Prometheus) 메트릭 익스포트 지연 및 데이터 누락
Zenoh 라우터에 REST 플러그인을 활성화하고 /metrics 엔드포인트 포트로 프로메테우스 워커가 15초마다 수집(Pull)을 하도록 파이프라인을 엮어두었다. 그런데 그라파나 차트에 선이 매끄럽게 이어지지 않고 이빨이 빠진 톱니바퀴처럼 점선(Gap) 노이즈가 생기거나 수집 타임아웃 경고가 떨어진다.
1.0.1 단계: 스크랩 타임아웃(Scrape Timeout)과 라우터 부하의 불일치
라우터가 수십만 건의 패킷을 전송하며 극한의 CPU 부하를 맞으면 프로메테우스의 내부 지표 객체들을 JSON이나 텍스트 포맷으로 직렬화하는 익스포트 처리 함수도 당연히 평소(10ms)보다 훨씬 늦은 속도(5초)로 간신히 응답하게 된다.
그런데 prometheus.yml의 scrape_timeout 기본 설정이 3초로 빡빡하게 잠겨있다면? 프로메테우스 서버는 라우터가 5초 뒤에 힘들게 뽑아낸 데이터를 매몰차게 버리고 ‘수집 실패(Timeout)’ 처리해 버린다.
해법: 시스템 부하시 메트릭 생성이 지연되더라도 데이터 자체가 유실되는 것을 막기 위해 scrape_interval을 30초 정도로 넉넉히 벌리고 scrape_timeout을 15초 이상으로 늘려서 지연 응답에 대한 인내심을 시스템에 주입하라.
1.0.2 단계: 메트릭 카디널리티(Cardinality) 폭발의 참사
초보 엔지니어들이 가장 많이 저지르는 치명적 실수다. 메트릭 지표에 특정 노드를 구분 지으려고 라벨(Label)에 세션 고유 ID, 변동형 페이로드 길이, 임의 포트 번호 등 무한정으로 증가하는 다이내믹 포맷을 덧붙인 것이다.
프로메테우스 엔진은 라벨의 조합이 새로울 때마다 메모리에 새로운 시계열 데이터베이스(TSDB) 인덱스를 통째로 하나씩 창출한다. 매초 변하는 ID를 라벨에 박으면 10분 내로 수백만 개의 TSDB 인덱스 쓰레기가 양산되어(Cardinality Explosion), 프로메테우스 컨테이너 OOM은 물론 대시보드마저 동반 파괴를 야기한다.
해법 강제: 메트릭 라벨은 오직 노드명(amr-01), IP 대역, 지역 고정명(zone-A) 등 ’절대적으로 유한하고 제한된 종류’를 갖는 정적 카테고리에만 엄격하게 매핑하라.
2. 그라파나(Grafana) 등 시각화 대시보드 렌더링 오류 및 쿼리 실패
백엔드 TSDB(Loki, Prometheus)에는 이상 없이 데이터가 쌓이고 있는 걸 확인했는데, 관제실 벽면의 거대한 70인치 Grafana 대시보드 스크린에 ‘No Data’ 알람이 뜨거나 접근한 브라우저 화면 자체가 프리징(Freeze) 상태로 얼어버리는 파국을 마주했다.
2.0.1 단계: 시계열 렌더링 밀도 초과 현상 (브라우저 마비)
센서가 1밀리초(ms) 단위의 초정밀 오실로스코프(Oscilloscope) 데이터를 Zenoh를 거쳐 그라파나로 던진다. 이때 어떤 사용자가 대시보드의 시간 탐색 범위를 ’최근 10분’에서 ‘최근 7일’ 필터로 확 풀어버렸다 하자.
단일 차트 패널 컴포넌트 하나가 ‘7일 x 24시간 x 60초 x 1000개’ 즉, 수억 개의 점(Data Point) 선분을 클라이언트단 화면상 캔버스(Canvas/WebGL)에 렌더링하려 시도하면서, 클라이언트 데스크톱의 V8 메모리가 바로 증발 소멸하며 브라우저 탭에 Aw, Snap(앗, 이런!) 사형 선고가 뜬다.
다운샘플링(Downsampling) 튜닝:
데이터 소스 질의 쿼리(PromQL 등)에서 절대로 원시 데이터 뭉치를 날것으로 당겨오지 마라. 무조건 그라파나 측 변수인 $__interval 을 사용해 다운샘플링 통합 함수를 덧씌워야 한다.
## 밀도를 동적으로 깎아내는 강력한 최소/최대/평균 Aggregate 쿼리
avg_over_time(zenoh_sensor_cpu_load[1m])
그래프에 점을 1,000개 이상 찍어봐야 인간의 눈 픽셀 해상도로 구분조차 가지 않는다. 데이터는 언제나 서버단에서 요약 연산(Aggregation)된 후 요리된 가벼운 결과치만 모니터 스크린으로 포워딩(Shipping) 하도록 원칙을 조율하라.
2.0.2 단계: 리버스 프록시 연동 오류와 WebSocket 차단
Grafana를 프로덕션 환경의 Nginx 또는 Traefik 인그레스 뒤에 포위 시켜 숨겨놨을 때, Live 차트(실시간 WebSocket 업데이트) 모드 기능이 Nginx 단의 프록시 헤더 설정 누락 탓에 영원히 연결되지 않고 HTTP 400 Bad Request에 빠진다.
웹 프록시 환경의 경우 대시보드 nginx.conf 설정 최하단에 통신 프로토콜 무조건 승격 구문 스크립트를 추가 이식해 리얼타임 맥박이 시각화 보드까지 끊김 없이 방열되도록 파이프라인의 숨통을 터주어라.
3. 실시간 모니터링 플러그인의 리소스 과점유 문제
분산 시스템의 장애를 감시하려고 로봇의 뒷단에 밀착시켜 붙여놓은 Fluent Bit 같은 로그 감시 에이전트가, 방대한 에러 트래픽을 관찰하느라 오히려 자기 자신이 CPU 100%를 치고 올라간다. 결국 원래 작동해야 할 자율주행 모터 제어 스레드의 CPU 시간마저 빼앗아 로봇이 공장 하역장에 부딪히는 주객전도 상황은 자율주행계의 고전적인 아이러니 타임 라인이다.
3.0.1 단계: 마비의 주범, 전체 페이로드 도청 방지
에지단에서 Zenoh 라우터가 주고받는 통신의 모든 흔적을 전부 디버깅하겠다는 무모한 생각으로 zenoh_transport=trace 같은 초강력 감시망을 열어두면 안 된다.
100MB짜리 영상 패킷 내용물(바이트 어레이) 전체를 파싱하려고 로깅 에이전트가 덤벼들면, 통신 모듈보다 이 문자열 파싱 처리자가 백 배 무거운 연산을 먹고 시스템 백본을 기생충처럼 갉아먹는다.
제한 조치: 로깅 에이전트는 철저히 시스템 이벤트의 메타데이터(IP 발신자 경로, 핑, 크기) 등 껍질(Header) 위주로만 파싱 카운트(Count) 목적의 지표를 필터링토록 정규식을 잠그고, 메인 페이로드(Content 바이트 본문)는 절대 디버깅 늪에 들어오지 못하도록 암막 처리를 행하라.
3.0.2 단계: 에이전트 리소스 감옥(CGroups) 무조건 할당
도커(Docker) 컴포즈나 쿠버네티스(K8s) 위에서 백엔드 플랫폼 배포를 수행할 때, 절대 모니터링 컨테이너에 무제한 자원(Unrestricted)의 고삐를 풀어서는 안 된다.
모니터링 앱이 버그에 빠져 폭주하더라도 메인 기능(Zenoh Router)에 흠집 하나 남기지 못하도록, 리소스 상한선(Requests/Limits) 철창을 명확히 명기하여 감옥에 가두어라.
## docker-compose.yml
services:
fluent-bit-logger:
image: fluent/fluent-bit
deploy:
resources:
limits:
cpus: '0.2' # 어떤 상황에서도 전체 CPU 코어의 20% 이상을 침범하지 못하도록 차단!
memory: 512M
감시자가 아무리 비명을 지르더라도, 이 안전선 하나가 메인 라우터 생존의 보루 역할을 감당해 줄 것이다. 당신의 우선순위 1번은 데이터가 살아남는(라우팅) 것이지 기록이 살아남는(로깅) 것이 아니다.
4. 분산 트레이싱(Tracing) 데이터 전송 실패 및 스팬(Span) 끊김
현대의 분산 마이크로서비스 아키텍처에서는, 단순히 이벤트 발생 시점만 찍어주는 로그를 넘어 데이터 패킷 하나가 전체 노드들 간에 어떤 여정을 거치며 시간(Latency)을 소비하는지 추적하는 족적 관제(Trace Span), 즉 OpenTelemetry(OTel)나 Jaeger 기반의 트레이싱을 도입한다.
그런데 트레이스 데이터 리포트를 열어봤더니, “로봇 단말 -> 1번 안개 라우터 -> 클라우드 메인 라우터 -> 백엔드 DB“로 이어져야 할 하나의 긴 궤적(Trace Span) 줄기가 중간에 이가 빠진 채 끊겨버려, 각각 로봇 파편과 백엔드 파편으로 두 개의 연관 없는 흔적으로 조각나버리는 현상이 발생한다.
4.0.1 끊김 원인 1: 헤더 전파자(Context Propagator) 주입 누락
스팬이 이어지려면 데이터는 홉(Hop)을 뛸 때, 자신이 원래 누구에게 종속된 꼬리표(Trace ID 및 Parent Span ID)인지 메타데이터 가방에 담아 운반해야 한다.
만약 중간에 서 있는 C 언어 기반의 센서 어댑터 스크립트가 Zenoh 데이터를 건네 수신받고 가공하여 재가공 퍼블리시(z_put)를 할 때, 이전 패킷에 박혀있던 Trace Context Header를 복사해서 다음 타깃 토픽 메시지로 주입(Inject)하는 코드를 짜 넣지 않았다면 궤적 생명의 사슬은 그 위치에서 완벽하게 단절된다.
해법: 통신을 직접 중계하는 언어별 미들웨어 스크립트를 작성할 때 커스텀 헤더(UserData) 추출/복사/주입 패턴을 무조건 파이프라인 관용구(Boilerplate) 코드 블럭으로 포함하라.
4.0.2 끊김 원인 2: 샘플링 확률(Sampling Rate) 동기화 불일치
1초에 날아가는 패킷이 1만 개라 모든 데이터를 다 기록하면 비용 감당이 안 되기에, 트레이싱 에이전트를 “전체 트래픽의 10%만 무작위 난수로 뽑아서(Sampled) 추적한다.“로 고정했다고 치자.
그런데 보내는 로봇은 이 10% 주사위를 돌려 당첨(추적 대상)이라 판정짓고 스팬을 생성했는데, 데이터를 건네받는 클라우드에서는 자기 나름대로 10% 주사위를 또 굴려 꽝(추적 거부)이라 판정하여 아무 스팬도 찍어 남기지 않았다 치자. 이 경우 추적 흐름은 당연히 반파된다.
해법: 결정론적 추적 지향. 시스템 파이프라인의 최고 조상(가장 앞단 로봇 노드)이 주사위를 굴려 이 패킷이 추적(Trace) 대상임을 입증했다면, Span Context 데이터 구조체 맨 끝단에 Sampled 플래그=1을 못 박도록 조치하라. 이후 이 꼬리표를 넘겨받는 모든 하위 노드는 자기 주사위를 굴리지 않고 상위 노드의 유언(?)에 순종해 무조건 스팬을 기록하는 운명의 일체화를 달성시켜 맹점을 막아내야 한다.