2026-09-02 (수) 개발일지
오늘의 트러블슈팅: 리플레이 스냅샷 복구와 RHEL IP 충돌
오늘은 두 가지 이슈를 잡았다. 하나는 omnitrace 세션 리플레이가 증분 스냅샷으로 바뀌면서 와이어프레임 UI를 전혀 못 받던 문제, 다른 하나는 RHEL .50 노드로 SSH가 타임아웃 나는 현상이었다. 둘 다 첫 가설은 빗나갔고, 로그와 네트워크를 직접 까보면서 진짜 원인을 찾아야 했다.
omnitrace 리플레이 복구 — 서버가 아니라 클라이언트가 snapshot.frame을 안 보내고 있었다
증상: 7월~8월초 리플레이 캡처가 갑자기 끊겼다. omnitrace에서 세션 리플레이 와이어프레임 UI를 전혀 받지 못하고, 새 세션은 snaps=0으로 기록됐다. 기존 세션(s_b805ecbd…)도 snap이 0으로 박혀 있었다.
첫 가설: 서버 쪽 applyDiff가 증분 스냅샷을 제대로 풀지 못하는 게 원인이라 생각했다. rum_mobile_store.go의 applyDiff와 applyFullKeyframe 로직을 까봤다. decodeSnapshotPayload 주석에 “frame 미상 시 full 취급”이라고 적혀 있었고, 2742·2799 라인에서 var d snapDiff를 확인했다. 서버가 snapshot.frame 필드를 읽고 if r.frame == "incremental" 분기로 들어가는 구조였다 — 여기까지는 정상처럼 보였다.
전환점: search_repo 결과가 중복 제거로 잘려 나온 걸 보고 “서버에 snapshot.frame이 없다”고 오판했는데, 실제 파일 본문은 정반대였다. rum_mobile_store.go:2699 — LogAttributes['snapshot.frame']을 읽고 있었고, :2744 — if r.frame == "incremental" 분기가 정상 동작하고 있었다. 서버는 무죄였다. 진짜 문제는 클라이언트가 snapshot.frame 필드를 아예 보내지 않는 것이었다.
원인 추적: iOS ReplaySnapshotCapturer.swift와 Android ReplaySnapshotCapturer.kt를 비교했다. 두 플랫폼 모두 증분 스냅샷을 보낼 때 frame 값을 설정하지 않고 있었다. iOS 쪽은 sendSnapshot(type: .incremental) 호출에서 snapshot.frame을 명시적으로 주입하지 않았고, Android도 마찬가지였다. 서버는 frame이 없으면 기본값("")을 받아 full 취급했는데, 그 사이에 증분 데이터의 diff가 쌓이면서 snap 수는 늘지만 실제 화면은 갱신되지 않는 상태였다.
해결: ReplaySnapshotCapturer.kt와 ReplaySnapshotCapturer.swift 두 파일을 수정했다. 증분 스냅샷 전송 시 snapshot.frame = "incremental"을 명시적으로 설정하고, full 스냅샷은 "full"을 넣도록 통일했다. 추가로 서버가 frame 미상 시 full로 fallback하는 건 유지하되, 클라이언트에서 명시적으로 보내는 쪽이 안정적이라 판단했다.
검증: 수정 빌드를 올리고 iOS/Android 모두 재설치 후 -autoDemo로 기동했다. 결과는 명확했다.
| 세션 | snaps | nodes_med | screens | 상태 |
|---|---|---|---|---|
s_656129d1… (iOS) |
35 | 56 | 16 | 신규, 정상 |
s_63b81c35… (Android) |
13 | 41 | 8 | 신규, 정상 |
s_b805ecbd… (기존) |
0 | - | - | 구 세션, 아직 미복구 |
리플레이가 복구됐다. 기존 세션(s_b805ecbd…)은 이미 snap=0으로 기록된 상태라 손을 못 댔지만, 새 세션부터는 정상적으로 와이어프레임이 그려졌다.
남은 과제: 기존 snap=0 세션은 backfill 스크립트로 재캡처를 고려 중이다. 다만 증분 스냅샷의 frame 필드가 누락된 게 7월~8월초 커밋군에서 시작된 걸로 보이니, 해당 기간의 다른 세션들도 영향받았을 가능성이 있다 — cron으로 일괄 재처리할지 검토 중.
RHEL .50 SSH 타임아웃 — IP 충돌이었다
증상: ssh: connect to host <INTERNAL_IP> port 22: Connection timed out — RHEL .50 노드로 SSH가 전혀 안 열렸다. 같은 서브넷의 Rocky 3대는 정상 접속됐고, .51에서 .50을 ARP 스캔하면 응답이 왔다 갔다 했다.
첫 가설: RHEL만 서브스크립션 개념이 있어서 subscription-manager 등록이 안 되면 dnf가 막히는 건 알고 있었다. 그래서 .50에 Red Hat Developer Subscription을 등록했다 — 등록 자체는 성공했다. The system has been registered with ID: 196cfc44-...까지 나왔는데, subscription-manager repos --list를 찍어보니 저장소가 하나도 안 붙어 있었다. 구독이 비어 있는 상태였다.
시행착오: 계정 쪽 문제인가 싶어 Red Hat 포털에서 액티베이션 키를 재발급받고 다시 등록했지만 동일했다. .51(다른 RHEL)은 정상이었고, .50만 유독 저장소가 안 붙었다. 이쯤에서 “등록은 됐는데 구독이 안 걸렸다”는 건 노드 자체의 문제보다 네트워크 경로 문제일 가능성이 떠올랐다.
전환점: .51에서 .50으로 ARP를 10번 쏴봤다. 결과가 충격적이었다.
6회 ← A0:D7:F3:3F:EB:12 (정체불명 물리장비 — 우세)
1회 ← BC:24:11:61:44:70 (우리 RHEL VM, Proxmox)
두 장비가 같은 IP를 쓰고 있었다. A0:D7:F3 장비가 ARP 경쟁에서 이기고 있어서 트래픽이 전부 그쪽으로 가고 있었고, 그 장비는 22번 포트가 닫혀 있었기 때문에 SSH가 타임아웃 난 거였다. 우리 RHEL VM은 살아 있었지만 ARP 테이블에서 밀려서 응답을 거의 못 보내고 있었다.
해결: IP 충돌의 근원을 찾아야 했다. A0:D7:F3 MAC의 정체를 추적하니 이전에 폐기된 물리 장비가 DHCP 예약에서 해제되지 않고 살아 있었고, 동일 IP를 할당받은 상태였다. 해당 장비를 네트워크에서 분리하고, RHEL VM의 IP를 고정으로 재설정한 뒤 ARP 캐시를 플러시했다.
검증: .51에서 다시 ARP 스캔 — 이번에는 BC:24:11:61:44:70만 10/10 응답. SSH 접속 즉시 성공. subscription-manager repos --list도 정상적으로 저장소 목록을 뱉었다. 등록 자체는 처음부터 돼 있었고, IP 충돌이 해소되면서 Red Hat 서버와의 통신도 복구된 것이다.
남은 과제: DHCP 예약에서 폐기 장비의 MAC을 제거하지 않아서 생긴 문제다. 인프라 관리 측면에서 Active IP 스캔과 예약 클린업을 주기적으로 돌릴 필요가 있다. 또한 .50의 구독이 “Registered” 상태였지만 저장소가 안 붙었던 건 IP 충돌로 인해 subscription-manager가 Red Hat 서버와의 인증 토큰 교환을 실패한 것으로 추정된다 — 정확한 원인은 아직 불분명하지만, IP 충돌 해소 후 문제가 말끔히 사라진 걸로 봐서는 네트워크 경로 불안정이 근본 원인이었을 가능성이 높다.
오늘의 한줄
로그만 봐서는 원인이 안 보일 때는 네트워크 레이어를 직접 까보라 — ARP는 거짓말을 하지 않는다.