9.5.1.2 동적 할당 단편화(Fragmentation) 및 가비지 컬렉션(GC) 지연 타임 탐지
분산 아키텍처 환경에서 네트워크 I/O 병목을 해결하고 트래픽 채널을 시원하게 뚫어놓았음에도 불구하고, 애플리케이션의 응답 주기(Response Cycle)에 간헐적인 경련(Jitter)이 관측된다면, 그다음으로 수사(Investigation)할 영역은 메모리의 영역이다.
특히 동적인 언어 환경(Go, Python, Java 등)이나 부주의한 C++ 코드가 쏟아내는 메모리 파편들(Memory Fragmentation)은 시스템의 지속 가능성(Sustainability)을 결정적으로 무너뜨리는 암세포와 같다.
본 절에서는 런타임 언어에 내장된 쓰레기 수집기(Garbage Collector, GC)의 멈춤 현상(Stop-The-World)과, 극한의 잦은 힙 할당으로 인해 발생하는 커널 레벨의 동적 할당 단편화 병목을 심층적으로 추적하고 가시화하는 방법론을 정립한다.
1. 가비지 컬렉터의 보이지 않는 독재: Stop-The-World 지연 탐지
Zenoh 통신망에서 Python이나 Go 언어로 작성된 관제 서버가 초당 10,000건의 z_sample_t 메시지를 할당(Allocation)하고 해제한다고 가정하자. 시스템 겉모습은 안정적으로 보일지 모르나, 백그라운드에서는 이 무의미한 객체 잔해들을 쓸어 담기 위해 GC가 극도로 헐떡이고 있다.
GC가 동작하는 그 찰나의 순간, 즉 ‘Stop-The-World (STW)’ 구간 동안 런타임은 안전을 위해 모든 유저 스레드의 동작을 동결시킨다. 0.5초(500ms)만 STW가 걸리더라도 자율주행 AGV가 벽을 들이받기에는 충분한 시간이다.
Go 환경의 경우, 이 지연 시간을 투명하게 들여다보기 위해 환경변수인 GODEBUG를 활성화하여 GC 추적 런타임 로그를 덤프해 내는 것이 수사의 시작이다.
# Go 기반 Zenoh 백엔드의 GC 로깅 활성화 구동
GODEBUG=gctrace=1 ./zenoh_gateway_server
# 로깅 출력 예시 분석
gc 1 @0.005s 1%: 0.01+1.2+0.05 ms clock, 0.04+0.5/1.0/0+0.2 ms cpu, 8->9->4 MB, 10 MB goal, 4 P
로그의 시간 지표를 분석해 보라. 만약 1.2 ms 라고 쓰인 Marking Time이 평소보다 치솟아 150 ms를 육박한다거나, 초당 GC 호출(gc 1, gc 2…) 횟수가 100회씩 폭발하고 있다면, 이 애플리케이션은 즉시 객체 풀링(Object Pooling) 공사 없이는 수일 내에 과도한 CPU 강탈 현상에 의해 전산 정지(Halt)에 이를 시한폭탄과 같다.
2. 진단 툴 pprof를 통햔 힙 마이크로 프로파일링
순간적인 메모리 피크(Peak)나 GC 부하의 주범을 잡아내기 위해서는, 현장에서 즉각 범행 도구를 가리킬 수 있는 프로파일러 도구가 요구된다.
pprof는 힙(Heap) 공간에 어떤 함수가 가장 많은 메모리를 낭비하고 있는지, 혹은 어떤 구조체가 GC를 압박하는지 스냅샷(Snapshot)을 그려내는 탁월한 도구다.
Zenoh 애플리케이션 내부 백그라운드에 pprof HTTP 엔드포인트를 노출해 둔 상태에서, 시스템 부하가 정점일 때 아래의 진단 명령을 발동하라.
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/allocs
이 캔버스에 그려진 콜 그래프(Call Graph) 엣지(Edge)의 두께를 관찰하라. 만약 json.Unmarshal 이나 strings.Split, 혹은 내부의 커스텀 버퍼 복사(copy()) 함수 상자가 대문짝만하게 부풀어 있다면, 메모리 생성의 본진이 적발된 것이다. 해당 구간의 코드를 파괴하고 Zero-copy 파싱 라이브러리로 대체해야 할 견고한 논리적 근거가 여기서 도출된다.
3. C/C++ 베어메탈 환경 내 동적 할당 단편화의 비극 (Malloc 파편화)
가비지 컬렉터가 부재한 zenoh-c 코어 및 C/C++ 시스템에서도 메모리 할당의 비극은 나타난다. 이번에는 GC 지연이 아니라 운영체제의 메모리 관리자 자체가 제공하는 힙 공간이 조각조각 부서지는(Fragmentation) 현상이다.
수만 번의 무작위(Random Size) 바이트 크기로 malloc() 과 free() 가 교차적으로 수행되다 보면, 커널의 물리 메모리는 총량 1GB가 남아 있음에도 불구하고 연속된 10MB짜리 공간 하나를 찾지 못하는 스위스 치즈 상태가 된다. 이때 새로운 네트워크 버퍼 배열을 뜨기 위해 malloc(10 * 1024 * 1024)을 호출하면 커널은 파편들을 이어 붙이거나 탐색하느라 수십 밀리초의 오버헤드를 유발하다가 종국엔 NULL을 뱉어낸다.
이를 진단하기 위해서는 리눅스 환경의 malloc_info() 시스템 API를 런타임 중간중간 덤프하여 **프리 리스트(Free List)**의 청크(Chunk) 상태를 매트릭스로 뽑거나, 특수 대체 할당기인 jemalloc 또는 tcmalloc을 프리로드(Preload)시켜 메모리 파편화 레벨 지수를 모니터링해야 한다.
조각난 힙 단편화 수치가 우상향 곡선을 그리기 시작한다면, 이 펌웨어는 필망의 징조를 띈 것이다. 시스템이 다운되기 전에 당장 malloc 호추를 금지하고 부팅 시점에 링 버퍼나 슬랩(Slab) 고정 사이즈 정적 풀(Pool)을 이용하는 zenoh-pico 수준의 락다운(Lockdown) 아키텍처로 선회해야 한다.