Grafana Alloy 로그 수집이 서버 메모리에 미치는 영향
Intro
“운영 중인 서버에 Grafana Alloy와 같은 옵저빌리티 에이전트를 추가하면 메모리 사용량은 얼마나 증가할까요?”
위 질문을 시작으로 한번 공부해봤습니다.
단순히 Alloy 프로세스가 사용하는 메모리만 고려하면 될 것 같지만, Docker 컨테이너의 로그를 수집하는 과정에서도 Linux의 Page Cache를 통해 메모리가 사용될 수 있습니다.
이번에는 Docker가 로그를 기록하고 Alloy가 이를 수집하는 과정에서 메모리가 어떻게 사용되는지 살펴보겠습니다.
1. Docker가 로그를 기록하는 과정
Docker는 컨테이너에서 발생하는 stdout/stderr 로그를 로깅 드라이버를 통해 관리합니다.
예를 들어 json-file 로깅 드라이버를 사용하면 컨테이너의 로그를 호스트 파일 시스템에 기록하며, 로그 파일은 일반적으로 다음 경로에 저장됩니다.
/var/lib/docker/containers/<container-id>/<container-id>-json.logDocker가 로그 파일에 데이터를 기록하는 과정에서는 Linux의 Page Cache가 사용됩니다.
Application
│
▼ stdout/stderr
Docker
│
▼ write()
Linux Page Cache
│
▼ writeback (flush)
Disk / EBSDocker가 write() 시스템 콜을 통해 로그 파일에 데이터를 기록하면, 해당 데이터는 일반적으로 RAM의 Page Cache에 반영된 후 디스크에 기록됩니다.
즉, Docker가 로그를 파일에 기록하는 과정에서도 서버의 메모리가 사용됩니다.
2. Alloy가 로그를 읽는 과정
Docker가 기록한 로그를 Alloy가 읽을 때도 Linux의 Page Cache가 사용될 수 있습니다.
Docker ── write() ──▶ Page Cache ──▶ Disk
│
▼
Alloy read()Alloy가 로그 파일을 read() 시스템 콜로 읽는 경우, Linux는 먼저 Page Cache에 해당 데이터가 존재하는지 확인합니다.
특히 Docker가 로그를 기록한 직후 Alloy가 동일한 데이터를 읽는다면, 해당 로그 데이터가 이미 Page Cache에 존재할 가능성이 높습니다.
따라서 Docker가 로그를 기록할 때 1GB의 캐시를 사용하고, Alloy가 해당 로그를 읽을 때 추가로 1GB의 캐시를 사용하는 것처럼 메모리 사용량이 단순히 두 배로 증가하는 것은 아닙니다.
Page Cache에 로그 데이터가 존재하지 않는 경우
반대로 Alloy가 로그를 읽는 시점에 해당 데이터가 Page Cache에 존재하지 않는다면 어떻게 될까요?
Alloy read()
│
▼
Page Cache 조회
│
▼
Cache MISS
│
▼
Disk / EBS Read I/O
│
▼
Page Cache에 데이터 적재
│
▼
Alloy예를 들어 Alloy의 로그 수집이 지연되는 동안 Linux가 메모리 확보를 위해 기존 Page Cache를 회수했다면, Alloy가 해당 로그를 읽는 과정에서 실제 디스크 읽기 I/O가 발생할 수 있습니다.
이때 디스크에서 읽어 온 데이터가 다시 Page Cache에 적재되므로 메모리 사용량이 일시적으로 증가할 수 있습니다.
다만 Page Cache는 일반적인 애플리케이션 메모리와 달리, 다른 프로세스가 메모리를 필요로 하면 Linux가 회수(Reclaim)할 수 있는 메모리입니다.
3. loki.source.docker를 사용하는 경우
앞서 설명한 내용은 Alloy가 로그 파일을 직접 읽는 구조를 기준으로 한 것입니다.
하지만 Grafana Alloy에서 loki.source.docker 컴포넌트를 사용한다면 로그 수집 과정에 차이가 있습니다.
loki.source.docker는 Docker의 로그 파일을 직접 읽는 대신 Docker Engine API를 통해 로그 스트림을 전달받습니다.
전체 로그 수집 과정은 다음과 같습니다.
Container stdout/stderr
│
▼
dockerd
│
│ json-file logging driver
▼
<container-id>-json.log
│
│ Linux buffered I/O
▼
Page Cache
│
▼
Disk
────────────────────────────────
Alloy
loki.source.docker
│
│ Docker Socket
│ /var/run/docker.sock
▼
Docker Engine API
│
│ container logs + follow
▼
dockerd
│
│ json-file logger의 ReadLogs()
▼
<container-id>-json.log
│
│ Linux Page Cache를 통해
│ 파일 데이터 제공 가능
▼
dockerd
│
│ Unix Socket Stream
▼
Alloy
│
▼
Loki여기서 중요한 점은 Alloy가 Docker의 .log 파일이나 Page Cache에 직접 접근하지 않는다는 것입니다.
Alloy는 Docker Socket(/var/run/docker.sock)을 통해 Docker Engine API에 로그 스트림을 요청합니다. Docker Daemon은 요청에 따라 로그 파일을 읽고, 해당 데이터를 Unix Socket 스트림으로 Alloy에 전달합니다. 이때 Docker Daemon이 로그 파일을 읽는 과정에서도 Linux의 Page Cache가 사용될 수 있습니다.
따라서 loki.source.docker를 사용하더라도 로그 기록 및 읽기 과정에서 Page Cache가 활용된다는 점은 동일합니다. 다만 실제 로그 파일을 읽는 주체가 Alloy가 아닌 Docker Daemon이라는 차이가 있습니다.