무시당하지 않는 Azure Monitor 알림 설계: 나쁜 알림 3개를 KQL로 고치기

Azure Monitor와 Log Analytics KQL로 장애 알림 설정하기 - 모니터링 대시보드

모니터링을 도입한 조직에서 몇 달 뒤 가장 흔히 벌어지는 일은, 아무도 알림을 안 보는 것입니다. 하루에 수십 건씩 오면 읽지 않게 되고, 그러다 진짜 장애 알림도 같이 묻힙니다. 알림이 없는 것보다 나쁜 상태입니다.

이 글은 KQL 문법 소개가 아니라, 울려도 무시하게 되는 알림과 실제로 행동하게 만드는 알림의 차이를 다룹니다. 나쁜 예와 개선안을 짝으로 놓고 비교하겠습니다.

좋은 알림의 조건 세 가지

규칙을 만들기 전에 기준부터 정합니다. 아래 셋 중 하나라도 어기면 그 알림은 결국 무시됩니다.

  1. 행동 가능한가 — 알림을 받고 할 일이 명확한가. “CPU 70%”는 받아도 할 게 없습니다
  2. 사용자 영향과 연결되는가 — 지표가 아니라 증상을 감시해야 합니다
  3. 새벽 3시에 깨울 만한가 — 아니라면 그건 알림이 아니라 대시보드에 있어야 할 항목입니다

3번이 특히 유용한 기준입니다. “이걸로 사람을 깨울 것인가”를 물으면 대부분의 알림 후보가 걸러집니다.

나쁜 알림 1 — CPU 임계치

가장 먼저 만들고 가장 먼저 무시하게 되는 알림입니다.

// 이렇게 만들면 안 됨
Perf
| where CounterName == "% Processor Time"
| where CounterValue > 80

문제는 CPU 80%가 정상일 수도 있다는 점입니다. 배치가 도는 중이면 오히려 자원을 잘 쓰고 있는 것입니다. 반대로 CPU 20%인데 서비스가 죽어 있을 수도 있습니다. CPU는 원인 조사에 쓰는 지표이지 알림 대상이 아닙니다.

개선 — 사용자가 겪는 실패율로 바꾸기

// 5분 창에서 5xx 비율이 5%를 넘고, 최소 요청 수를 만족할 때만
requests
| where timestamp > ago(5m)
| summarize
    total = count(),
    failed = countif(toint(resultCode) >= 500)
  by cloud_RoleName
| where total >= 20                          // 트래픽 적을 때 오탐 방지
| extend failRate = round(100.0 * failed / total, 2)
| where failRate > 5
| project cloud_RoleName, total, failed, failRate

total >= 20 조건이 핵심입니다. 새벽에 요청이 2건 들어와 1건 실패하면 실패율 50%가 되어 알림이 울립니다. 최소 표본 조건이 없는 비율 알림은 반드시 오탐을 냅니다. 실무에서 야간 오탐의 대부분이 여기서 나옵니다.

나쁜 알림 2 — 에러 로그 발생

“Error 레벨 로그가 찍히면 알림”도 흔한 구성입니다. 운영을 조금만 해보면 Error 로그는 평상시에도 계속 찍힌다는 걸 알게 됩니다. 재시도로 해결되는 일시적 오류, 클라이언트 잘못된 요청, 외부 API 간헐적 실패가 섞입니다.

개선 — 평소 대비 급증을 감지

절대 개수가 아니라 평소와 얼마나 다른가를 봅니다. 지난 7일 같은 시간대의 평균과 비교하는 방식입니다.

let baseline =
    exceptions
    | where timestamp between (ago(7d) .. ago(1h))
    | summarize hourly = count() by bin(timestamp, 1h), cloud_RoleName
    | summarize avgCount = avg(hourly), stdev = stdev(hourly)
              by cloud_RoleName;
exceptions
| where timestamp > ago(1h)
| summarize current = count() by cloud_RoleName
| join kind=inner baseline on cloud_RoleName
| extend threshold = avgCount + (3 * stdev)      // 3시그마
| where current > threshold and current > 10
| project cloud_RoleName, current,
          평소평균 = round(avgCount, 1),
          임계치 = round(threshold, 1)

이렇게 하면 평소 시간당 50건씩 나오던 에러는 조용하고, 갑자기 300건이 되면 울립니다. 표준편차를 쓰는 이유는 변동이 원래 큰 서비스에서 오탐을 줄이기 위해서입니다. 평균만 쓰면 들쭉날쭉한 서비스가 계속 알림을 냅니다.

꼭 넣어야 할 알림 4개

많이 만들 필요가 없습니다. 아래 넷이면 대부분의 장애를 잡습니다.

① 가용성 — 밖에서 본 서비스 상태

Application Insights의 가용성 테스트를 여러 지역에서 돌리고, 2개 이상 지역에서 실패할 때만 울리게 합니다. 한 지역만 실패하면 그건 대개 그 지역 네트워크 문제입니다.

availabilityResults
| where timestamp > ago(10m)
| summarize failedLocations = dcountif(location, success == false),
            totalLocations  = dcount(location)
          by name
| where failedLocations >= 2

② 응답 지연 — p95 기준

평균은 쓰지 마세요. 소수의 아주 느린 요청이 평균에 잘 안 드러납니다. p95나 p99를 봐야 실제 사용자 경험이 보입니다.

requests
| where timestamp > ago(10m)
| summarize p95 = percentile(duration, 95), cnt = count()
        by cloud_RoleName, operation_Name
| where cnt >= 20 and p95 > 3000        // 3초
| sort by p95 desc

③ 의존성 실패 — DB·외부 API

서비스 자체는 살아 있는데 DB가 안 되는 상황을 잡습니다. 원인 파악 시간을 크게 줄여줍니다.

dependencies
| where timestamp > ago(10m)
| summarize total = count(), failed = countif(success == false)
        by type, target
| where total >= 10
| extend failRate = round(100.0 * failed / total, 1)
| where failRate > 20

④ 리소스 고갈 — 디스크·인증서 만료

예측 가능한 장애입니다. 터지기 전에 알림이 와야 의미가 있습니다. 디스크는 남은 공간이 아니라 소진 예상 시점으로 보는 게 낫습니다.

// 최근 6시간 증가 추세로 디스크 소진 시점 예측
Perf
| where TimeGenerated > ago(6h)
| where CounterName == "% Free Space"
| summarize freeNow = min(CounterValue),
            slope = (min(CounterValue) - max(CounterValue)) / 6.0
        by Computer, InstanceName
| where slope < 0
| extend hoursLeft = round(freeNow / abs(slope), 1)
| where hoursLeft < 48
| project Computer, InstanceName, freeNow, hoursLeft

“48시간 내 디스크 소진 예상”은 새벽에 깨울 필요 없이 다음 근무일에 처리하면 되는 알림입니다. 심각도를 낮게 설정하고 메일로만 보내면 됩니다.

알림 규칙 만들기와 심각도 설계

#!/usr/bin/env bash
set -euo pipefail
RG="rg-monitor-prod"
WS="/subscriptions/<SUB>/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/law-prod"

# 액션 그룹 (수신자)
az monitor action-group create \
  --name ag-oncall -g "$RG" \
  --short-name oncall \
  --action email primary oncall@example.com

# 로그 검색 경보 규칙
az monitor scheduled-query create \
  --name "alert-5xx-spike" -g "$RG" \
  --scopes "$WS" \
  --condition "count 'failRate' > 0" \
  --condition-query "failRate=\$(cat query.kql)" \
  --evaluation-frequency 5m \
  --window-size 5m \
  --severity 1 \
  --action-groups ag-oncall

심각도를 전부 같게 두면 분류가 무의미해집니다. 이렇게 나누는 것을 권합니다.

심각도기준통지 방식
Sev 0서비스 전체 중단전화 + SMS (새벽 포함)
Sev 1주요 기능 장애, 실패율 급증SMS + 메신저
Sev 2성능 저하, 부분 기능 이상메신저 (업무 시간)
Sev 3디스크·인증서 만료 임박메일
Sev 4참고 정보대시보드만

Sev 0와 1만 사람을 깨웁니다. 나머지는 다음 근무일에 처리합니다. 이 구분이 없으면 온콜 담당자가 번아웃되고, 결국 알림 전체를 무시하게 됩니다.

비용 함정: 알림 만들기 전에 수집량부터 본다

Log Analytics는 수집한 데이터 양으로 과금됩니다. 알림을 만들려고 진단 설정에서 로그 카테고리를 전부 켜면, 알림보다 로그 요금이 먼저 문제가 됩니다. 실제로 Azure 비용 급증 원인 조사에서 Log Analytics가 예상 밖의 1위를 차지하는 경우가 자주 있습니다.

워크스페이스에서 어떤 테이블이 용량을 먹는지부터 확인하세요.

Usage
| where TimeGenerated > ago(30d)
| summarize GB = round(sum(Quantity) / 1000, 2) by DataType
| sort by GB desc
| take 15

상위에 ContainerLogV2, AzureDiagnostics, AppTraces가 크게 잡힌다면 손볼 여지가 있습니다. 세 가지 방법이 있습니다.

방법효과주의
불필요한 카테고리 끄기가장 확실나중에 필요해질 로그인지 확인
기본 로그(Basic Logs) 티어수집 단가 대폭 인하쿼리 기능 제한, 보존 기간 짧음
수집 시 변환(DCR)필요한 필드만 남김데이터 수집 규칙 설정 필요
보존 기간 단축저장 비용 감소규정상 보관 의무 확인

실무에서 가장 손쉬운 건 디버그·정보 수준 로그를 수집 단계에서 걸러내는 것입니다. 애플리케이션 로그 레벨을 프로덕션에서 Information 이하로 낮추기만 해도 수집량이 크게 줄어드는 경우가 많습니다. 조사할 때만 일시적으로 올리면 됩니다.

일별 수집 상한(Daily Cap)을 걸어두는 것도 방법인데, 상한에 도달하면 그 뒤 로그가 버려진다는 점을 알고 쓰셔야 합니다. 장애가 나서 로그가 폭증한 날, 정작 필요한 로그가 안 남는 상황이 생길 수 있습니다. 상한보다는 예산 알림 쪽을 권합니다.

알림 피로를 막는 운영 습관

  • 억제(Suppression) 설정 — 같은 알림이 반복될 때 일정 시간 묶습니다. 없으면 장애 하나에 알림 200건이 옵니다
  • 배포 중 알림 중지 — 배포 시간대에 경고 처리 규칙으로 일시 중지를 걸어둡니다
  • 월 1회 알림 리뷰 — 지난달에 울렸지만 아무 조치도 안 한 알림을 찾아 삭제하거나 임계값을 조정합니다
  • 알림마다 대응 문서 링크 — 알림 설명란에 “이 알림을 받으면 무엇을 확인할지”를 적어둡니다

세 번째가 가장 중요한데 가장 안 지켜집니다. 조치하지 않은 알림은 알림이 아니라 소음입니다. 지난달 울린 알림 목록을 뽑아 각각 “이걸 보고 뭘 했나”를 물으면, 절반 이상이 정리 대상으로 나옵니다.

// 지난 30일 알림 발생 현황 - 리뷰용
AlertsManagementResources
| where TimeGenerated > ago(30d)
| summarize FiredCount = count() by AlertName = tostring(properties.essentials.alertRule)
| sort by FiredCount desc

정리

  1. 지표(CPU·메모리)가 아니라 증상(실패율·지연·가용성)을 감시한다
  2. 비율 알림에는 반드시 최소 표본 조건을 넣는다
  3. 절대 개수 대신 평소 대비 급증으로 판단한다
  4. 알림은 4개면 충분하다: 가용성 · 지연 · 의존성 · 리소스 고갈
  5. 심각도를 나누고 Sev 0·1만 사람을 깨운다
  6. 월 1회 조치하지 않은 알림을 삭제한다

알림 설계의 목표는 많이 감지하는 게 아니라 울렸을 때 반드시 보게 만드는 것입니다. 개수를 줄이는 방향이 대개 옳습니다.

이어서 AKS CrashLoopBackOff 원인 추적비용 급증 원인 찾기를 보시면, 알림을 받은 뒤 실제로 조사하는 흐름이 이어집니다.

댓글 남기기