카디널리티 폭발
고친 사람 github-actions[bot]
카디널리티 폭발은 모니터링 서버가 따로 쌓아야 할 시계열이 감당 못 할 만큼 불어나는 고장입니다. 요청 수에 사용자 번호까지 붙여 세면 사용자 한 명마다 시계열이 하나씩 새로 생깁니다. 시계열마다 메모리를 따로 잡으므로 서버가 점점 느려지다가 멈춥니다.
쉽고 빠른 이해
시계열이 너무 많아져 모니터링 서버가 버티지 못하는 고장입니다. 시계열은 한 가지 값을 시간 순으로 쌓은 기록 줄입니다. 요청 수에 사용자 번호를 붙여 세면 사용자가 10만 명일 때 시계열도 10만 개가 됩니다.
레이블은 숫자를 갈래로 나누려고 붙이는 이름표입니다. 레이블 하나에 들어가는 값의 가짓수를 카디널리티라고 부릅니다. 시계열 수는 붙인 레이블마다 카디널리티를 곱한 만큼 늘어납니다. 가짓수에 끝이 없는 값을 붙이면 시계열 수에도 끝이 없어집니다.
이렇게 터집니다.
- 숫자를 셀 때 레이블을 붙여 갈래를 나눕니다
- 레이블 값이 새로 나올 때마다 서버는 시계열을 하나 새로 만듭니다
- 시계열마다 메모리를 따로 잡으므로 시계열 수를 따라 메모리도 늘어납니다
막으려면 잘게 나눠 보는 힘을 내줘야 합니다. 사용자 한 명이 몇 번 요청했는지는 그래프로 못 보고 로그에서 찾아야 합니다.
상세
이 절은 가계부 비유로 시작해 메트릭·레이블·시계열이 무엇인지부터 세웁니다. 이어서 시계열 수가 곱으로 불어나는 모습과 터지는 조건을 봅니다. 그다음 불어난 뒤 서버에 생기는 일과 알아채는 법, 헷갈리는 이웃을 차례로 봅니다.
영수증마다 칸을 여는 가계부
가계부를 식비·교통비·주거비 세 칸으로 나눠 쓴다고 해 봅시다. 한 달에 영수증이 몇 장 쌓이든 칸은 셋입니다. 칸마다 합계를 적어 두면 돈이 어디로 나갔는지 한눈에 보입니다.
이번에는 영수증 번호마다 칸을 따로 연다고 해 봅시다. 물건을 하나 살 때마다 칸이 하나 늘어납니다. 한 해가 지나면 가계부는 칸 수천 개로 빼곡해집니다. 어느 칸에도 영수증은 한 장뿐입니다.
메트릭 · 레이블 · 시계열
메트릭은 시스템을 일정한 간격으로 재서 모으는 숫자입니다. 받은 요청 수, 응답에 걸린 시간, 쓰고 있는 메모리 양이 메트릭입니다. 숫자를 모아 두어야 서비스가 평소와 다르게 움직일 때 알아챌 수 있습니다.
레이블은 메트릭 하나를 갈래로 나누려고 붙이는 이름과 값의 짝입니다. 웹 요청 수에 요청 방식을 뜻하는 method="GET" 을 붙이면 읽기 요청만 따로 셀 수 있습니다. 레이블이 없으면 전체 요청 수 하나만 보여서 어느 요청이 늘었는지 모릅니다.
시계열은 한 가지 값을 잰 시각과 함께 시간 순으로 이어 쌓은 기록 줄입니다. 메트릭 이름과 레이블 값의 조합 하나가 시계열 하나가 됩니다. 앞의 가계부로 치면 칸 하나가 시계열 하나입니다.
아래는 요청 수 메트릭 하나가 레이블 값에 따라 시계열 셋으로 갈린 모습입니다. 한 줄이 시계열 하나입니다.
http_requests_total{method="GET"}
http_requests_total{method="POST"}
http_requests_total{method="DELETE"}
이름은 셋 다 같습니다. 레이블 값이 달라서 서버는 이 셋을 서로 다른 줄로 따로 쌓습니다. 이 표기는 Prometheus 계열 도구가 쓰는 꼴입니다.
곱으로 불어나는 시계열 수
카디널리티는 레이블 하나에 서로 다른 값이 몇 가지 있는지를 세는 수입니다. 요청 방식 레이블은 값이 네다섯 가지뿐이라 카디널리티가 낮습니다. 사용자 번호 레이블은 사용자 수만큼 값이 있어 카디널리티가 높습니다.
레이블이 여럿이면 시계열 수는 레이블마다 카디널리티를 곱한 만큼까지 늘어납니다. 더하는 것이 아니라 곱하는 것입니다. 레이블을 하나 더 붙일 때마다 배수로 뜁니다. 폭발이라는 말이 붙는 것은 이 곱셈 때문입니다.
예로 들 레이블은 넷입니다. 요청 방식은 앞에서 본 GET·POST 같은 값입니다. 응답 상태는 200·404처럼 요청이 어떻게 끝났는지 알리는 숫자입니다.
경로 틀은 /users/{id} 처럼 번호 부분을 비워 둔 요청 경로입니다. 사용자 번호는 요청을 보낸 사람마다 다른 번호입니다. 아래 그림은 요청 수 메트릭에 이 넷을 차례로 붙일 때 시계열이 최대 몇 개까지 가는지 보입니다.
flowchart TD
A["레이블 없음 · 시계열 1개"] --> B["요청 방식 4가지 × 응답 상태 5가지 · 시계열 20개"]
B --> C["경로 틀 25가지를 더함 · 시계열 500개"]
C --> D["사용자 번호 10만 가지를 더함 · 시계열 5천만 개"]
앞의 세 레이블은 값의 가짓수가 코드로 정해집니다. 그래서 곱해도 수백 개에서 멈춥니다. 사용자 번호를 붙이는 순간 곱하는 수 하나가 10만이 되어 500이 5천만으로 뜁니다.
그림의 수는 상한입니다. 모든 조합이 다 나타나지는 않습니다. 그래도 사용자 한 명이 여러 경로를 부르고 여러 응답을 받으므로 조합은 빠르게 찹니다.
시계열 하나가 잡는 메모리
시계열 데이터베이스는 시계열을 쌓고 꺼내 보는 데 맞춘 저장소입니다. 모니터링 서버 안에서 메트릭을 받아 두는 곳이 대개 이것입니다. 시계열이 왜 비싼지는 이 저장소가 시계열 하나마다 무엇을 들고 있는지를 보면 알 수 있습니다.
흔한 시계열 데이터베이스는 시계열마다 두 가지를 둡니다. 첫째는 색인에 오르는 항목입니다. 색인은 레이블 값으로 시계열을 찾게 해 주는 목록입니다. 조회할 때 method="GET" 인 줄만 골라낼 수 있는 것이 색인 덕분입니다.
둘째는 방금 들어온 값을 모아 두는 버퍼입니다. 값이 들어올 때마다 하나씩 디스크에 쓰면 느리므로, 메모리에 얼마간 모았다가 한꺼번에 내려씁니다. 이 버퍼는 값이 들어오고 있는 시계열마다 하나씩 따로 있습니다.
아래 그림은 시계열이 늘 때 무엇이 따라 느는지 보입니다. 색인 안의 항목 하나가 버퍼 하나를 가리킵니다.
flowchart TD
subgraph DB["시계열 데이터베이스"]
subgraph IDX["색인 · 레이블 값으로 시계열을 찾는 목록"]
E1["항목 method=GET"]
E2["항목 method=POST"]
ER["나머지 시계열마다 항목 하나"]
end
E1 --> B1["method=GET 시계열의 버퍼"]
E2 --> B2["method=POST 시계열의 버퍼"]
ER --> BR["나머지 시계열마다 버퍼 하나"]
end
색인은 하나지만 그 안의 항목은 시계열마다 하나씩 늘어납니다. 버퍼도 시계열마다 하나씩 생깁니다. 그래서 메모리는 쌓인 값의 개수보다 시계열의 개수를 따라 늘어납니다. 값이 천 개 쌓인 시계열 하나보다 값이 하나뿐인 시계열 천 개가 대개 더 무겁습니다.
터지는 조건
셋 가운데 하나만 맞아도 터질 수 있습니다. 셋 다 레이블에 들어가는 값이 문제입니다.
| 조건 | 무슨 뜻인가 |
|---|---|
| 끝이 없는 값 | 레이블 값의 가짓수가 사용자나 요청처럼 서비스가 커지는 만큼 계속 늘어난다 |
| 곱해지는 조합 | 가짓수가 적당한 레이블 여럿이 한 메트릭에 같이 붙는다 |
| 바뀌며 쌓이는 값 | 한 번 쓰고 버리는 값이 계속 새로 나온다. 값이 더는 안 들어오는 시계열이 쌓인다 |
첫째 조건은 사용자 수를 그대로 시계열 수로 옮깁니다. 요청 수 메트릭에 사용자 번호 레이블을 붙였다고 합시다. 서로 다른 사용자 N명이 요청을 보내면 그 메트릭의 시계열은 적어도 N개가 됩니다.
둘째 조건은 레이블 하나하나가 멀쩡해도 생깁니다. 가짓수가 50인 레이블 넷을 한 메트릭에 붙이면 50을 네 번 곱해 시계열이 최대 625만 개까지 갑니다. 어느 레이블도 혼자서는 문제가 아니라서 알아채기 어렵습니다.
끝이 없는 레이블 값
레이블로 넣기 쉬운데 가짓수에 끝이 없는 값들입니다. 표의 IP(Internet Protocol) 주소는 인터넷에서 기기를 가리키는 번호입니다.
| 레이블로 넣기 쉬운 값 | 가짓수가 늘어나는 까닭 |
|---|---|
| 사용자 번호 | 가입자가 늘 때마다 늘어난다 |
| 요청 번호 | 요청마다 새로 만든다 |
번호가 박힌 요청 경로 /users/42 |
사용자나 주문마다 경로가 다르다 |
| 클라이언트 IP 주소 | 접속하는 기기마다 다르다 |
| 오류 메시지 원문 | 메시지에 번호나 시각이 섞이면 매번 다르다 |
| 잰 시각 | 잴 때마다 다르다 |
여섯 값 모두 서비스를 오래 돌리고 사용자가 늘수록 가짓수가 늘어납니다.
반대로 요청 방식, 응답 상태, 경로 틀, 서비스 이름은 가짓수가 코드와 설정으로 정해집니다. 사용자가 늘어도 이 값들은 안 늘어납니다. 레이블로 쓰기 안전한 값이 이쪽입니다.
바뀌며 쌓이는 시계열
한 순간의 가짓수는 적은데 시간이 지나며 쌓이는 경우가 있습니다. 컨테이너를 새로 띄울 때마다 새 이름이 붙는 환경이 그렇습니다. 레이블에 컨테이너 이름을 넣어 두면 배포를 한 번 할 때마다 시계열이 한 벌씩 새로 생깁니다.
옛 컨테이너의 시계열은 값이 더는 안 들어와도 곧바로 사라지지 않습니다. 보존 기간, 곧 데이터를 지우지 않고 들고 있기로 정한 기간 동안 저장소와 색인에 남습니다.
stateDiagram-v2
live: 값이 들어오는 시계열
stale: 값이 끊긴 시계열
[*] --> live: 컨테이너가 뜬다
live --> stale: 배포로 컨테이너가 바뀐다
stale --> [*]: 보존 기간이 지나 지워진다
값이 끊긴 시계열은 보존 기간 내내 쌓여 갑니다. 모든 컨테이너를 갈아 끼우는 배포를 하루 열 번 하면, 한 달 치를 조회할 때 훑는 시계열은 지금 도는 것의 삼백 벌 가까이 됩니다.
불어난 뒤 서버에 생기는 일
첫 증상은 메모리입니다. 시계열이 늘어난 만큼 색인과 버퍼가 커집니다. 그래서 서버의 메모리 사용량이 배포나 사용자 증가를 따라 계단처럼 오릅니다.
메모리를 다 쓰면 서버 프로세스가 강제로 꺼집니다. 이것을 OOM(Out Of Memory, 메모리 부족)으로 꺼졌다고 말합니다. 꺼져 있는 동안은 값을 못 받아 기록에 빈 구간이 생깁니다. 장애를 보려고 둔 도구가 먼저 넘어지는 셈입니다.
둘째 증상은 조회 속도입니다. 대시보드에 전체 요청 수 하나를 그리더라도 서버는 조건에 맞는 시계열을 전부 읽어 더해야 합니다. 사용자 번호가 붙은 메트릭이라면 그래프 하나에 수천만 줄을 읽을 수 있습니다. 같은 식을 쓰는 알림 규칙도 함께 느려집니다.
셋째는 비용입니다. 모니터링을 맡기는 호스팅 서비스 가운데는 시계열 수로 값을 매기는 곳이 있습니다. 그런 곳에서는 레이블 하나가 청구액을 몇 배로 늘리기도 합니다.
샘플링으로는 안 줄어드는 시계열 수
샘플링은 들어오는 값 가운데 일부만 골라 남기는 방법입니다. 열 개 가운데 하나만 남기면 쌓이는 값도 10분의 1이 됩니다.
다운샘플링은 오래된 값을 1분 평균처럼 굵은 간격으로 묶어 개수를 줄이는 방법입니다. 둘 다 저장소를 가볍게 하려고 씁니다.
둘이 줄이는 것은 시계열 하나 안의 값 개수입니다. 시계열의 개수는 안 바뀝니다. 레이블 조합 5천만 개를 1분 간격으로 묶어도 5천만 줄이 1분 간격으로 남습니다. 무게는 줄 수를 따라가므로 이 둘로는 카디널리티 폭발이 풀리지 않습니다.
알아채는 지표
시계열 수 자체를 지켜보는 것이 제일 곧은 방법입니다. 지금 값이 들어오는 시계열의 수를 활성 시계열 수라고 부릅니다. 이 수가 배포 한 번에 계단처럼 뛰거나 사용자 수를 따라 오르기만 하면, 어느 레이블엔가 끝이 없는 값이 들어간 것입니다.
범인을 찾을 때는 메트릭 이름별로 시계열 수를 셉니다. 대개 한두 메트릭이 전체의 대부분을 차지합니다. 그 메트릭 안에서 레이블마다 서로 다른 값이 몇 개인지 세면 가짓수가 튀는 레이블 하나가 드러납니다.
레이블에서 값을 덜어내는 방법
고치는 곳은 레이블에 넣는 값입니다. 가짓수에 끝이 있는 값만 레이블로 남기면 시계열 수에도 끝이 생깁니다. 흔히 쓰는 방법은 셋입니다.
첫째는 값을 묶는 것입니다. /users/42 대신 경로 틀 /users/{id} 를 넣습니다. 응답 상태 200·201·204 는 2xx 한 갈래로 묶습니다. 가짓수가 코드에서 정해지는 값으로 바뀝니다.
둘째는 값을 다른 그릇으로 옮기는 것입니다. 사용자 번호나 요청 번호처럼 한 건씩 가려야 하는 값은 로그나 트레이스에 적습니다. 트레이스는 요청 하나가 여러 서비스를 지나간 경로를 한 건으로 남긴 기록입니다.
로그와 트레이스는 사건마다 기록을 한 건씩 쌓습니다. 값마다 시계열을 따로 세우지는 않습니다.
셋째는 저장하기 전에 버리는 것입니다. 수집하는 쪽에서 문제의 레이블을 떼어 내거나, 메트릭마다 시계열 수의 상한을 걸어 넘는 것은 받지 않게 합니다. 상한은 서버를 지키는 대신 넘친 값을 잃습니다.
셋 다 잘게 나눠 보는 힘을 내줍니다. 사용자 한 명이 몇 번 요청했는지는 메트릭 그래프로 못 보고 로그에서 세야 합니다.
높은 카디널리티와 폭발의 경계
카디널리티가 높다고 다 폭발은 아닙니다. 서버 300대에 서버 이름 레이블을 붙이면 값이 300가지입니다. 이 수는 서버를 늘릴 때만 늡니다. 서버는 계획해서 늘립니다.
폭발을 가르는 것은 가짓수의 크기가 아니라 끝이 있는가입니다. 사용자 번호는 오늘 1만이어도 서비스가 자랄수록 계속 늘어납니다. 그래서 이 고장은 서비스가 잘될수록 모니터링이 먼저 터지는 모양으로 나타납니다.
관련 항목
카디널리티 폭발이 나는 저장 구조
시계열 · 시계열 데이터베이스 · 메트릭 · 레이블 · 카디널리티 · 인덱스 · 역색인 · 버퍼 · 메모리
카디널리티 폭발을 부르는 레이블 값
사용자 ID · 요청 ID · 추적 ID · IP 주소 · URL · 타임스탬프 · 컨테이너 · 파드
카디널리티 폭발이 번지는 장애
OOM · OOM 킬러 · 메모리 부족 · 슬로 쿼리 · 데이터 유실
카디널리티 폭발을 못 푸는 데이터 줄이기 기법
샘플링 · 다운샘플링 · 롤업 · 집계 · 보존 기간 · 압축
카디널리티 폭발을 막는 수단
경로 템플릿 · 레이블 재지정 · 시계열 상한 · 할당량 · 계측 · 구조화 로깅
카디널리티 폭발을 지켜보는 관측성 신호
관측성 · 모니터링 · 로그 · 트레이스 · 텔레메트리 · 대시보드 · 알림 · 활성 시계열
카디널리티 폭발이 잦은 모니터링 도구
Prometheus · OpenTelemetry · Grafana · Kubernetes · InfluxDB · StatsD
다른 이름: cardinality explosion · 카디널리티 폭증