이 글은 Docker로 띄운 컨테이너 하나하나가 CPU·메모리를 얼마나 쓰는지 Prometheus로 모으는 방법을 다룬다. 컨테이너마다의 CPU·메모리·디스크 I/O·네트워크 메트릭은 Prometheus만으로는 보이지 않는다 — Prometheus는 대상이 내보내 주는 숫자를 가져다 쌓기만 할 뿐, 컨테이너 안의 자원 사용량을 직접 재는 기능은 없기 때문이다. 그래서 호스트(컨테이너들이 올라가 있는 컴퓨터)에 cAdvisor를 띄운다. cAdvisor가 컨테이너들을 재서 http://<호스트>:8080/metrics라는 주소(http://는 HTTP로 접속한다는 뜻, 8080은 포트 번호, /metrics는 그 안의 경로)에 숫자를 글자로 늘어놓으면, Prometheus가 그 주소를 찾아가 읽어 간다. 이렇게 「누가 와서 읽어 갈 수 있게 주소에 내놓는 것」을 노출이라고 부른다. 이 글은 공식 가이드(cAdvisor guide)를 기반으로 최소 설치와 Prometheus 연동 방법을 정리했다.
이 글은 리눅스 호스트 한 대에 Docker가 이미 설치돼 있다고 치고 쓴다. 명령에 나오는 /sys · /var/run 같은 경로가 전부 리눅스 경로다. Windows에서 Docker를 쓰게 해 주는 프로그램인 Docker Desktop은 컨테이너를 Windows가 아니라 그 안에 따로 띄운 리눅스 위에서 돌린다. 그래서 경로가 가리키는 곳이 달라질 수 있고, 같은 명령이 그대로 되는지는 확인하지 않았다. Prometheus 설치는 이 글에서 다루지 않는다. Prometheus가 무엇을 하는 도구인지는 Prometheus 개요에 있다.
왜 cAdvisor가 필요한가
- Prometheus는 정해 둔 주소에 주기적으로 접속해 거기 적힌 숫자를 읽어 와 저장(「긁어서 저장」)만 한다. 컨테이너별 숫자를 내놓는 엔드포인트(접속할 주소)가 없으면 읽어 올 것이 없어서 빈 결과가 나온다.
- 컨테이너를 실제로 띄우고 멈추는 프로그램을 컨테이너 런타임이라고 부르는데, 런타임은 이 숫자를 Prometheus가 읽는 형식으로 내놓지 않는다. 그 빈자리를 cAdvisor가 메운다.
- cAdvisor는 같은 호스트에서 실행 중인 모든 컨테이너의 CPU/메모리/디스크/네트워크 메트릭을 수집해
/metrics로 노출한다. 이렇게 재서 내놓는 프로그램을 Prometheus 쪽에서는 익스포터라고 부른다. - Prometheus가 cAdvisor를 스크레이프하면
container_cpu_usage_seconds_total,container_memory_working_set_bytes같은 메트릭을 PromQL로 바로 조회할 수 있다. 앞의 것은 그 컨테이너가 지금까지 CPU를 쓴 시간을 초로 더한 값(계속 늘기만 한다)이고, 뒤의 것은 컨테이너의 워킹 셋 메모리를 바이트로 잰 값이다.
설치 (Docker 한 줄 실행)
아래 명령을 호스트의 터미널에 붙여 넣고 실행한다.
docker run -d --name cadvisor --restart=unless-stopped \
-p 8080:8080 \
-v /:/rootfs:ro \
-v /var/run:/var/run:ro \
-v /sys:/sys:ro \
-v /var/lib/docker/:/var/lib/docker:ro \
gcr.io/cadvisor/cadvisor:v0.47.2
제목에 「한 줄」이라고 적었지만 화면에는 일곱 줄로 보인다. 줄 끝의 \는 「명령이 아직 안 끝났으니 다음 줄까지 이어서 한 명령으로 읽어라」라는 표시라서, 터미널 안에서 명령을 받아 실행하는 셸은 이 일곱 줄을 명령 하나로 받는다. 읽기 쉽게 끊어 적었을 뿐이다.
한 줄씩 따라가기:
docker run은 이미지(컨테이너를 찍어 내는 설계도)로 컨테이너를 새로 만들어서 실행하는 명령이다.-d는 “백그라운드에서 돌려라(detached)”는 뜻이다. 이 옵션이 없으면 터미널이 컨테이너 로그 출력에 붙잡혀서 다른 명령을 칠 수 없다.--name cadvisor는 이 컨테이너에cadvisor라는 이름을 붙인다. 이름을 붙여 두면 나중에docker stop cadvisor처럼 이름으로 다룰 수 있다.--restart=unless-stopped는 컨테이너가 죽거나 컴퓨터를 재부팅해도 Docker가 이 컨테이너를 자동으로 다시 띄우라는 뜻이다. 다시 띄우는 주체는 Docker 자신이라서, 재부팅 뒤에는 Docker가 먼저 켜져 있어야 한다. Docker 자체가 부팅 때 저절로 켜지게 설정돼 있는지는 설치 방법마다 달라서 이 글에서 확인하지 않았다. 사람이 직접 멈추면 그때부터는 다시 띄우지 않는다.-p 8080:8080은 콜론 앞이 호스트의 포트, 뒤가 컨테이너 안의 포트다. 호스트의 8080번 포트로 들어온 요청을 컨테이너 안 8080번 포트로 넘긴다. 이 연결이 있어야 호스트에서localhost:8080(지금 이 컴퓨터의 8080번 포트)으로 접속했을 때 컨테이너 안의 cAdvisor로 요청이 전달된다.-v 호스트경로:컨테이너경로:ro네 줄은 바인드 마운트다. 호스트의 디렉터리(폴더)를 컨테이너 안에서도 그대로 볼 수 있게 연결한다. 끝에 붙은ro는 읽기 전용(read-only)이라는 뜻으로, cAdvisor가 호스트 파일을 고치지 못하고 읽기만 하게 막는다. 네 곳을 각각 붙이는 이유는 아래에 따로 적었다.- 마지막 줄
gcr.io/cadvisor/cadvisor:v0.47.2는 실행할 이미지 이름과 버전(태그)이다.gcr.io는 이미지를 받아 오는 저장소(Google의 컨테이너 레지스트리) 주소다. 처음 실행하면 Docker가 여기서 이미지를 내려받는다. 공식 가이드는latest를 쓰는데 이 글이 왜v0.47.2로 고정했는지는 기록이 남아 있지 않다.
네 경로를 붙이는 이유는 cAdvisor가 컨테이너 상자 안에 갇혀 있어서다. 그대로는 자기 상자 안밖에 안 보이니, 다른 컨테이너의 사정을 알 수 있는 호스트 쪽 폴더를 창으로 뚫어 준다.
/→ 컨테이너 안/rootfs: 호스트 파일시스템(디스크 위의 폴더 전체) 전체다. 디스크 사용량 같은 호스트 쪽 정보를 읽는 데 쓴다. 컨테이너 안의/는 cAdvisor 자신의 폴더라서 겹치지 않게/rootfs라는 다른 이름에 붙인다./var/run: Docker와 대화하는 통로(docker.sock)가 여기 있다. 어떤 컨테이너가 떠 있고 이름이 무엇인지를 Docker에게 물을 때 쓴다./sys: 리눅스 커널(운영체제의 중심 프로그램)이 컨테이너마다 CPU·메모리를 얼마나 썼는지 기록해 두는 cgroup 파일이 여기 있다. 숫자 대부분의 출처다./var/lib/docker: Docker가 컨테이너의 파일과 설정을 쌓아 두는 곳이다. 컨테이너별 디스크 사용량을 여기서 읽는다.
/를 이미 /rootfs에 붙였는데 /sys를 따로 또 붙이는 까닭은 확인하지 않았다. 공식 가이드도 네 개를 모두 붙이므로 그대로 따랐다. 하나 더, 공식 가이드는 /var/run을 rw(읽기·쓰기)로 붙이고 이 글은 ro로 붙였다. Docker에게 묻는 일은 docker.sock이라는 통로로 말을 주고받는 것이지 그 폴더의 파일을 고치는 일이 아니라서 ro로도 될 것으로 보고 그렇게 적었지만, ro에서 실제로 차이가 생기는지는 재보지 않았다.
- 포트: 8080. 호스트의 8080을 이미 다른 프로그램이 쓰고 있으면
-p 8081:8080처럼 앞의 숫자만 바꾼다. 그러면 접속 주소도localhost:8081이 되고, 뒤에서 볼 Prometheus 설정의 주소도 같이 바꿔야 한다. - 필수 바인드 마운트(
-v로 붙인 네 경로, 흔히 볼륨이라고도 부른다):/var/lib/docker,/sys,/var/run,/→ 컨테이너 상태/디스크 정보를 읽기 위해 필요
설치가 됐는지는 브라우저로 두 주소를 열어 확인한다.
http://localhost:8080/metrics:container_cpu_usage_seconds_total{...} 12.34같은 「메트릭 이름 + 중괄호 + 숫자」 줄이 잔뜩 나오면 cAdvisor가 숫자를 내놓고 있는 것이다. Prometheus가 읽어 가는 것이 바로 이 화면이다.http://localhost:8080/docker/<컨테이너 이름>: cAdvisor의 웹 화면에서 컨테이너 하나의 상태를 그래프로 본다.<컨테이너 이름>자리에--name으로 붙인 이름을 넣는다고 이해하고 있지만, ID도 되는지는 확인하지 않았다.
Prometheus 스크레이프 설정
Prometheus는 prometheus.yml이라는 YAML 설정 파일을 읽고 무엇을 긁어 올지 정한다. 파일 위치는 Prometheus를 어떻게 설치했느냐에 따라 다르다(이 글은 설치를 다루지 않는다). 이 파일에 cAdvisor를 타깃(Prometheus가 찾아갈 대상)으로 추가한다(공식 예시 참고: scrape configs).
scrape_configs:
- job_name: "cadvisor"
scrape_interval: 5s
static_configs:
- targets: ["localhost:8080"]
YAML은 들여쓰기로 「무엇 안에 무엇이 들었는지」를 나타낸다. 줄 앞의 -는 목록의 한 칸이라는 뜻이다. 그래서 들여쓰기 칸 수가 틀리면 다른 뜻이 되거나 Prometheus가 파일을 못 읽는다.
한 줄씩 따라가기:
scrape_configs는 Prometheus가 주기적으로 찾아갈 대상 목록이다. 그 아래- job_name:으로 시작하는 덩어리 하나가 목록의 한 칸이다.job_name: "cadvisor"는 이 설정 묶음에 붙인 이름이다. Prometheus는 이 묶음에서 가져온 메트릭마다job="cadvisor"라는 라벨(이름=값 꼬리표)을 붙인다. 그래서 나중에 쿼리에서{job="cadvisor"}로 이 설정이 모아 온 메트릭만 골라볼 수 있다.scrape_interval: 5s는 5초마다 한 번씩 찾아가서 메트릭을 가져오라는 뜻이다.static_configs와targets: ["localhost:8080"]는 어디로 찾아갈지를 정한다.static은 「주소를 여기 손으로 박아 둔다」는 뜻이다. 대상 목록을 다른 곳에서 자동으로 알아내는 방식과 대비되는 말이다. cAdvisor가 8080번 포트에서/metrics를 내보내고 있으므로, Prometheus는 5초마다localhost:8080/metrics에 접속해 숫자를 가져간다.
localhost:8080은 Prometheus가 호스트에서 바로(컨테이너 밖에서) 돌 때만 맞는 주소다. Prometheus도 Docker 컨테이너로 띄웠다면, 그 컨테이너 안에서 localhost는 Prometheus 컨테이너 자기 자신이라 cAdvisor에 닿지 않는다. 공식 가이드는 이 문제를 Docker Compose로 푼다. 한 Compose 파일로 띄운 컨테이너들은 Docker가 만든 같은 가상 네트워크에 들어가고, 그 안에서는 서로를 컨테이너(서비스) 이름으로 부를 수 있다. 그래서 가이드는 타깃을 cadvisor:8080으로 적는다. 이 글은 그 구성까지 다루지 않는다. 앞에서 포트를 8081로 바꿨다면 여기도 8081로 맞춘다.
설정 파일을 고친 뒤에는 Prometheus가 새 내용을 읽게 해야 한다. 방법은 둘이다.
- Prometheus를 재시작한다.
- Prometheus를 띄울 때
--web.enable-lifecycle옵션을 붙여 두었다면, 터미널에서curl -X POST http://localhost:9090/-/reload를 친다. curl은 주소로 요청을 한 번 보내는 명령이고,-X POST는 요청 방식(HTTP 메서드)을 「가져오기(GET)」가 아니라 「보내기(POST)」로 하라는 뜻이다. Prometheus는/-/reload주소로 POST 요청이 오면 재시작 없이 설정 파일을 다시 읽는다. 9090은 Prometheus의 기본 포트다.
그다음 브라우저로 Prometheus 웹 화면(http://localhost:9090)을 열고, 위쪽 입력 칸에 아래 PromQL을 넣어 확인한다.
up{job="cadvisor"}→up은 Prometheus가 타깃마다 스스로 만드는 메트릭이다. 마지막 스크레이프가 성공했으면 1, 실패했으면 0이다. 1이면 정상이고, 0이면 cAdvisor가 떠 있는지와targets의 주소가 맞는지부터 본다. 떠 있는지는 터미널에서docker ps(지금 돌고 있는 컨테이너 목록)를 쳐서, 목록의 NAMES 칸에cadvisor가 있고 STATUS 칸이Up으로 시작하는지로 본다.container_cpu_usage_seconds_total→ 컨테이너마다 한 줄씩 결과가 나오면 숫자가 Prometheus까지 들어온 것이다.
자주 쓰는 컨테이너 메트릭 쿼리
아래 쿼리들은 모두 앞에서 연 Prometheus 웹 화면의 입력 칸에 넣는다. 네 쿼리에 공통으로 나오는 조각부터 따라간다.
{image!=""}: 라벨image가 빈 값이 아닌 줄만 고른다. cAdvisor는 실제 컨테이너 말고도 호스트 전체나, 커널이 컨테이너 아닌 프로그램들을 묶어 둔 cgroup처럼 이미지가 없는 대상의 숫자도 내놓는다. 이미지가 있는 줄만 남기면 Docker로 띄운 컨테이너만 남는다.[5m]: 지금부터 거꾸로 5분 동안 쌓인 값들을 가져온다.rate(...): 그 5분 동안 값이 초당 평균 얼마씩 늘었는지를 계산한다. 계속 늘기만 하는 누적값(지금까지 쓴 CPU 초, 지금까지 받은 바이트)은 그대로 보면 크기만 커지므로, 「지금 얼마나 빠르게 늘고 있나」로 바꿔 보는 것이다.sum by (container) (...): 같은container라벨 값끼리 더해서 컨테이너 하나에 한 줄로 묶는다.
컨테이너 CPU 사용률(%)
sum by (container) (rate(container_cpu_usage_seconds_total{image!=""}[5m])) * 100
rate(container_cpu_usage_seconds_total...[5m])는 「1초 동안 CPU를 몇 초 썼나」다. CPU 하나에는 명령을 동시에 따로 실행하는 코어가 여러 개 들어 있다. 0.5면 그중 코어 하나를 절반만큼 쓴 것이다.* 100으로 퍼센트로 바꾼다. 코어 하나를 100%로 친 값이라서, 코어 여러 개를 쓰는 컨테이너는 100을 넘는다. 코어 두 개를 꽉 쓰면 200이다.
컨테이너 메모리 사용량
sum by (container) (container_memory_working_set_bytes{image!=""})
- 메모리 양은 누적값이 아니라 지금 값이므로
rate를 걸지 않고 그대로 더한다. - 결과 단위는 바이트다. 커널이 언제든 거둬 갈 수 있는 쉬는 파일 캐시를 뺀 워킹 셋이라, 「이 컨테이너가 실제로 붙들고 있는 메모리」에 가깝다.
컨테이너 네트워크 수신/송신 Bps
sum by (container) (rate(container_network_receive_bytes_total{image!=""}[5m]))
sum by (container) (rate(container_network_transmit_bytes_total{image!=""}[5m]))
- 첫 줄은 받은(receive) 바이트, 둘째 줄은 보낸(transmit) 바이트다. 두 줄은 따로따로 하나씩 넣는다.
- 둘 다 누적 바이트에
rate를 걸었으므로 결과는 초당 바이트(Bytes per second, Bps)다. 비트가 아니다.
컨테이너 갱신 멈춤 감지(최근 10분) — 재시작 감지가 아니다
increase(container_last_seen{image!=""}[10m]) < 1
처음에는 이 식을 「재시작 감지」라고 적었는데, 다시 보니 그렇게 읽을 근거가 없다.
container_last_seen은 cAdvisor가 그 컨테이너를 마지막으로 본 시각(초)이다. 돌고 있는 컨테이너라면 이 시각이 계속 앞으로 가서 10분 동안 약 600(초)만큼 는다.increase(...[10m])는 10분 동안 값이 모두 합해 얼마나 늘었는지를 계산한다.rate가 「초당 얼마」라면increase는 「그 구간 동안 통틀어 얼마」다.< 1은 그 결과가 1보다 작은 줄만 남긴다. 즉 「10분 동안 마지막으로 본 시각이 거의 앞으로 가지 않은 컨테이너」만 남는다.
그러니 이 식이 노리는 것은 재시작이 아니라 cAdvisor가 갱신을 멈춘 컨테이너다. 그런데 컨테이너가 멈추거나 사라지면 cAdvisor가 그 줄의 숫자를 아예 내놓지 않게 될 수도 있다. 그러면 「늘지 않았다」로 잡히는 게 아니라 결과에서 그냥 빠진다. 둘 중 어느 쪽인지, 이 식이 실제로 무엇을 잡아내는지는 재보지 않았다. 재시작 횟수를 세는 쿼리는 이 글에 없다.
컨테이너 이름이 담긴 라벨은 환경마다 다르다
위 쿼리들은 컨테이너 이름이 container 라벨에 들어 있다고 치고 by (container)로 묶었다. 그런데 라벨 이름은 환경마다 다를 수 있다. 공식 가이드는 Docker 호스트에서 컨테이너를 name 라벨로 고른다({name="redis"}). Docker Swarm으로 띄웠다면 container_label_com_docker_swarm_service_name이라는 라벨에 서비스 이름이 들어가기도 한다. Docker도 컨테이너마다 자기 꼬리표(Docker 라벨)를 붙일 수 있는데, cAdvisor는 그것을 container_label_ 뒤에 이름을 이어 붙인 Prometheus 라벨로 바꿔 내놓기 때문이다. 이 글의 환경에서 container 라벨이 실제로 붙는지는 확인하지 않았다. 결과가 컨테이너별로 갈리지 않고 한 줄로 뭉쳐 나오면 그 라벨이 없다는 뜻이니, by (name)처럼 실제 있는 라벨로 바꾼다.
실제 라벨은 Prometheus 웹 화면에 container_cpu_usage_seconds_total만 넣어 보면 결과 줄마다 중괄호 안에 다 보인다. label_values(container_cpu_usage_seconds_total, name)라는 모양도 있는데, 이건 PromQL이 아니라 Grafana에서 쓰는 함수다. Grafana 화면 위쪽에 「어느 컨테이너를 볼까」 같은 고르기 칸(대시보드 변수)을 만들 때, 그 칸에 넣을 값 목록을 뽑는 데 쓴다. 그래서 Prometheus 입력 칸에서는 동작하지 않는다.
흐름 한눈에 보기
그림의 첫 칸에 붙은 exporter는 앞에서 본 익스포터, 즉 cAdvisor의 역할 이름이다. 마지막 칸의 Grafana는 이 글에서 설치하지 않는다. 쌓인 숫자를 그래프로 그리는 다음 단계라서 흐름에만 넣었다(Grafana 개요).
컨테이너 메트릭 :8080]:::exporter --> B[Prometheus
scrape job cadvisor]:::prom B --> C[PromQL
container_cpu_usage_seconds_total]:::query C --> D[Grafana 대시보드
컨테이너 CPU/메모리]:::dash classDef exporter fill:#1e293b,stroke:#22c55e,color:#e2e8f0; classDef prom fill:#0f172a,stroke:#22d3ee,color:#e0f2fe; classDef query fill:#1e293b,stroke:#f59e0b,color:#fef3c7; classDef dash fill:#1e293b,stroke:#ec4899,color:#fce7f3;
결론/팁
- 컨테이너 메트릭이 비어 있으면 cAdvisor가 없거나 스크레이프 실패일 가능성이 높다.
up{job="cadvisor"}부터 확인한다. - 볼륨 마운트가 빠지면 일부 메트릭이 0 또는 노출되지 않을 수 있으니 예시대로 마운트한다. 어느 경로가 빠지면 어느 메트릭이 비는지는 재보지 않았다. 설치 절에 적은 네 경로의 역할로 짐작할 수 있을 뿐이다 — 예를 들어 CPU·메모리 숫자가 통째로 비면
/sys부터 의심한다. - 포트를 바꿨다면 prometheus.yml의 타깃도 동일하게 맞춘다.
- 쿠버네티스(K8s) 환경에서는(그 안에서 Docker 대신 containerd 같은 런타임을 쓰는 경우도 포함해) cAdvisor를 따로 띄우지 않는다. kubelet 안에 이미 cAdvisor가 들어 있어서 그쪽 메트릭을 스크레이프하거나, 런타임이 쿠버네티스용 규격(CRI)으로 직접 내놓는 메트릭 주소를 스크레이프한다. 이 글은 단일 Docker 호스트 기준이라 그 설정은 다루지 않는다.
이 설정 후에는 Grafana에서 컨테이너 CPU/메모리/네트워크 패널(대시보드 화면 안의 그래프 칸 하나)을 바로 만들 수 있다. 문제가 생기면 up{job="cadvisor"}와 container_cpu_usage_seconds_total 출력 여부를 먼저 확인한다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기