도구스개발

상대오차 분위수 스케치(로그 버킷) 계산기

지연시간처럼 여러 자릿수에 걸친 값을 담을 때 필요한 로그 버킷의 개수와 메모리를 구합니다. 버킷 비 γ = (1+α)/(1−α)로 상대오차를 고정하는 DDSketch 방식이며, 같은 범위를 등간격으로 담을 때와 나란히 비교해 줍니다.

단위는 자유입니다. 지연시간이면 ms로 두고 쓰면 됩니다.

%

같은 범위를 등간격으로 담으면 몇 칸이 필요한지 비교합니다.

바이트

상대오차 2%를 보장하는 버킷 수

289칸

같은 범위를 등간격으로 담으면 99,999칸이 필요합니다 — 346배 차이입니다. 메모리는 2.26KB로 추정됩니다.

담는 범위100,000배
버킷 비 γ = (1+α)/(1−α)1.04081633
버킷 번호 범위 ⌈log_γ v⌉0 ~ 288
로그 버킷 칸 수289칸
로그 버킷 메모리2.26KB
등간격 칸 수99,999칸
등간격 메모리781.24KB
등간격이 가장 작은 값에서 갖는 상대오차±50%

버킷을 펼쳐 보면

번호아래 끝위 끝대표값상대오차
00.960810.03920.98±2%
111.04080.04081.02±2%
21.04081.08330.04251.0616±2%
31.08331.12750.04421.105±2%
144305.1376317.592212.4546311.2404±2%
145317.5922330.555212.9629323.944±2%
28793,108.9696,909.32573,800.365794,971.1392±2%
28896,909.3257100,864.80843,955.482798,847.5123±2%

폭은 값이 커질수록 γ배씩 넓어지는데 상대오차는 모든 줄에서 같습니다. 이것이 로그 버킷의 전부입니다. 대표값을 2γⁱ/(γ+1)로 잡아야 양쪽 끝에서의 오차가 같아져 α 상한이 딱 맞습니다 — 산술 중앙으로 잡으면 아래쪽 끝에서 오차가 더 커집니다.

반대로, 칸 수를 먼저 정하면

그때의 γ1.09411381
보장되는 상대오차±4.494%

γ = (max/min)^(1/칸수)로 두고 α = (γ−1)/(γ+1)을 되짚은 값입니다. 메모리 예산이 먼저 정해져 있을 때 어느 정도 정밀도를 기대할 수 있는지 볼 수 있습니다.

가장 작은 값 10번 버킷, 가장 큰 값 100,000288번 버킷입니다. 번호가 음수여도 괜찮습니다 — 1보다 작은 값의 로그가 음수이기 때문이며, 배열에 담을 때는 최소 번호만큼 밀어 두면 됩니다.

문제는 「절대 폭」으로 자르는 데 있습니다. 지연시간을 1ms 간격으로 100s까지 담으려면 10만 칸이 필요한데, 그러고도 1ms 언저리에서는 오차가 ±0.5ms, 곧 50%입니다. 반대로 100ms 간격으로 하면 칸은 1000개로 줄지만 1ms짜리 응답이 전부 첫 칸에 뭉쳐 p50을 아예 잴 수 없습니다. 우리가 신경 쓰는 것은 「1ms냐 2ms냐」이지 「50.0ms냐 50.1ms냐」가 아닙니다.
경계를 등비수열로 두면 상대오차가 값의 크기와 무관해집니다. 버킷 번호를 ⌈log_γ(v)⌉로 잡으면 버킷 폭이 값에 비례해 커지고, 대표값을 2γⁱ/(γ+1)로 두면 상대오차 상한이 정확히 α = (γ−1)/(γ+1)이 됩니다. 1ms에서도 2%, 10s에서도 2%입니다. 이것이 DDSketch가 쓰는 관계입니다.
칸 수는 로그로만 늡니다. [min, max]를 덮는 데 필요한 칸은 log_γ(max/min)입니다. 범위가 백 배 넓어져도 칸은 로그만큼만 늘어납니다. 1ms~100s(십만 배)를 상대오차 2%로 담으면 289칸이면 끝나는데, 등간격 1ms로는 같은 범위에 10만 칸이 필요합니다.
상대오차는 값에 대한 보장이지 순위가 아닙니다. p99로 나온 값이 참 p99의 ±α 안에 있다는 뜻이며, 몇 번째 요청인지가 ±α라는 뜻이 아닙니다. 분위수 자체를 근사하는 스케치(t-digest 등)와는 보장의 성격이 다르므로, 「p99가 정확히 몇 번째인가」가 중요하면 다른 자료구조를 봐야 합니다.
0과 음수는 따로 세야 합니다. 로그를 취할 수 없기 때문입니다. 실제 구현에서는 0 근처의 값을 위한 카운터를 하나 두고, 음수가 필요하면 부호별로 스케치를 따로 둡니다. 또 아주 작은 양수까지 담으려 하면 그만큼 칸이 늘어나므로, 실무에서는 「이보다 작으면 0으로 본다」는 하한을 정해 두는 편이 낫습니다.
합치기가 쉽다는 것도 큰 장점입니다. 같은 γ를 쓰는 스케치끼리는 같은 번호의 칸을 그냥 더하면 됩니다. 서버 여러 대의 지연시간 히스토그램을 하나로 모을 때 원자료를 옮길 필요가 없고, 합친 결과도 여전히 상대오차 α를 지킵니다. 평균이나 분위수를 각자 구해서 평균 내는 것과는 전혀 다릅니다 — 그건 틀린 답이 나옵니다.

사용 방법

  1. 1담을 값의 가장 작은 값과 가장 큰 값을 넣습니다. 지연시간이면 ms 단위로 두면 됩니다.
  2. 2보장하고 싶은 상대오차(α)를 넣습니다. 2%가 흔한 선택입니다.
  3. 3견줄 등간격 버킷의 폭을 넣으면 같은 범위에 몇 칸이 필요한지 나란히 보여 줍니다.
  4. 4필요한 칸 수와 메모리를 확인합니다.
  5. 5반대로 쓸 수 있는 칸 수가 정해져 있으면 아래에서 그때의 상대오차를 되짚을 수 있습니다.

자주 묻는 질문

등간격 버킷은 값이 여러 자릿수에 걸칠 때 칸 수와 정밀도를 동시에 잃기 때문입니다. 1ms 간격으로 100s까지 담으면 10만 칸이 필요한데도 1ms 언저리 오차가 ±50%입니다. 경계를 등비수열로 두면 289칸으로 모든 구간에서 상대오차 2%를 지킬 수 있습니다.

⌈log_γ(v)⌉입니다. γ = (1+α)/(1−α)이고 α가 보장 상대오차입니다. 그 버킷의 범위는 (γ^(i−1), γ^i]이며, 값이 1보다 작으면 번호가 음수가 됩니다 — 배열에 담을 때는 최소 번호만큼 밀어 두면 됩니다.

2γⁱ/(γ+1)로 잡아야 버킷 양쪽 끝에서의 상대오차가 정확히 같아지기 때문입니다. 산술 중앙으로 잡으면 아래쪽 끝에서 오차가 α를 넘습니다. 이 대표값에서 상한이 정확히 α = (γ−1)/(γ+1)이 됩니다.

log_γ(max/min)입니다. 범위가 백 배 넓어져도 칸은 로그만큼만 늘어납니다. 1ms~100s를 상대오차 2%로 담으면 289칸, 8바이트 카운터면 2.3KB 남짓입니다.

분위수로 나온 값이 참값의 ±2% 안에 있다는 뜻입니다. 값에 대한 보장이지 순위에 대한 보장이 아니므로, p99로 나온 값이 실제로 몇 번째 요청인지는 이 보장에 들어 있지 않습니다.

따로 세야 합니다. 로그를 취할 수 없기 때문입니다. 실제 구현에서는 0 근처를 위한 카운터를 하나 두고, 음수가 필요하면 부호별로 스케치를 따로 둡니다. 아주 작은 양수까지 담으려 하면 칸이 늘어나므로 하한을 정해 두는 편이 낫습니다.

같은 γ를 쓰면 같은 번호의 칸을 그냥 더하면 됩니다. 합친 결과도 여전히 상대오차 α를 지킵니다. 서버마다 p99를 따로 구해서 평균 내는 것과는 전혀 다르며, 그 방식은 틀린 답을 줍니다.

전송되지 않습니다. 계산은 모두 브라우저 안에서 이루어지고 넣은 값은 이 기기에만 남습니다.

알아두면 좋은 점

  • 메모리는 「모든 칸이 다 쓰인다면」의 상한입니다. 실제로는 값이 나온 칸만 만들어 두는 구현이 많아 훨씬 적게 듭니다.
  • 상대오차는 분위수 값에 대한 보장입니다. 순위(몇 번째인가)에 대한 보장이 아닙니다.
  • 0과 음수는 이 방식으로 담을 수 없어 따로 세야 합니다.
  • 카운터 자체의 크기와 인덱스 자료구조(해시맵·배열)의 부가 비용은 따로입니다.

함께 보면 좋은 도구

마지막 검증: 2026년 9월 2일 · 결과는 참고용 추정치입니다.