Devin.KR

성공률·지연·자원 지표

65분 안팎

학습 목표

집계 지표와 개별 요청 기록을 구분합니다.

개념

한 요청과 집계의 질문이 다릅니다

로그 한 줄은 어떤 요청이 실패했는지 알려 줍니다. 지표는 정한 시간창에 실패가 얼마나 자주 생겼는지를 알려 줍니다. 배포 직후 한 번 성공했다는 관측은 수십 번의 실패를 가릴 수 있습니다. 이 레슨은 안내판 요청 fixture를 집계하여 표본 수·HTTP 성공률·지연 p95를 함께 출력합니다. 숫자만 보고 배포를 승인하지 않고 분모·실패 정의·수집 경계까지 설명하는 것이 목표입니다. 로그의 요청 ID는 개별 사건 조사에 남기고 지표의 분류 키로 사용하지 않습니다.

입력 계약부터 고정합니다

표준 입력은 status와 elapsed_ms를 가진 객체들의 JSON 배열입니다. status는 100~599의 정수이고 elapsed_ms는 0 이상 정수입니다. 공백뿐인 입력과 빈 배열은 요청이 없는 상태로 봅니다. 잘못된 JSON, 빠진 필드, 음수 시간, 문자열 상태와 불리언 숫자는 ERROR를 출력합니다. Python에서 bool은 int의 하위 형식이므로 isinstance만으로 true를 상태 숫자로 받지 않도록 type(value) is int를 사용합니다. 관측 오류를 정상 표본으로 바꾸지 않는 것이 계약의 핵심입니다.

성공의 범위를 명시합니다

이번 집계의 HTTP 성공은 200~299입니다. 200만 정상으로 보는 앞 모듈의 특정 사용자 응답 비교와 목적이 다릅니다. 204도 이 집계에서는 성공이며 302는 성공에 포함하지 않습니다. 성공률은 성공 수를 전체 유효 요청 수로 나눈 뒤 100을 곱합니다. 이 값은 응답 내용의 정확성이나 사용자가 목표 작업을 완료했는지를 증명하지 않습니다. 실제 안내판 검증은 id·title 비교도 필요하고 다음 모듈에서 사용자 흐름에 맞는 SLI의 정의를 별도로 정합니다.

실패도 분모와 지연에 포함합니다

500과 시간 초과의 요청을 지우면 성공률이 좋아 보입니다. 브라우저 문제는 timeout을 status 504와 관측한 elapsed_ms로 전달하는 입력 계약입니다. 연결 오류와 HTTP 504 응답의 원인이 같다는 뜻은 아닙니다. 미션은 timeout 여부를 추가 기록하고 실측 클라이언트 시간을 저장합니다. 앞 모듈의 정책용 1000ms 정규화 값과 섞지 않습니다. 이 문제의 지연 p95는 성공·실패 모두의 경과 시간이며 성공 요청만의 p95와 이름을 구별합니다.

빈 표본은 100%가 아닙니다

요청이 없으면 0 NA NA를 출력합니다. 첫 값은 표본 수이고 뒤의 NA는 성공률과 p95를 계산할 자료가 없다는 뜻입니다. 0으로 나누는 오류를 피하려고 100%를 넣으면 수집이 끊긴 장애가 정상처럼 보일 수 있습니다. 0ms도 사용하지 않습니다. 0ms는 실제 값이 0인 요청이 있었음을 뜻하기 때문입니다. 상위 판단기는 NA를 관측 부족으로 받아 추가 수집이나 수집 경로 점검을 요청해야 합니다. ERROR는 자료가 없다는 경우와 달리 입력 계약 자체가 깨졌음을 나타냅니다.

p95는 정렬한 순위의 값입니다

교육용 방식은 최근접 순위입니다. 요청 수 n에 대해 ceil(0.95 곱하기 n)번째 값을 고르고 Python 인덱스로는 하나를 뺍니다. 정수 계산은 (95*n+99)//100-1로 구현합니다. 지연을 오름차순으로 정렬한 뒤 해당 인덱스를 읽습니다. 다섯 표본이면 다섯 번째, 스무 표본이면 열아홉 번째입니다. 평균에 0.95를 곱하는 계산과 다르고 보간으로 두 표본 사이 숫자를 만드는 방식도 아닙니다. 도구마다 정의가 다를 수 있어 보고서에 계산 방법을 같이 적습니다.

작은 표본의 한계를 남깁니다

다섯 값의 p95가 최댓값이라고 해서 계산이 틀린 것은 아닙니다. 표본이 작으면 높은 백분위가 큰 값 하나에 좌우됩니다. 이번 문제는 알고리즘과 경계값을 재현하는 연습이며 실제 운영 분포를 대표하는 부하 시험이 아닙니다. 정상 표본을 많이 덧붙여 일부 실패를 희석하거나 특정 결과를 얻기 위해 표본을 골라내지 않습니다. 배포 비교에는 요청 수, 기간, 경로와 부하 조건을 맞추고 표본이 부족하면 결론을 좁힙니다. 재현 fixture와 현장 측정값을 이름부터 분리합니다.

계산 순서를 작은 함수로 분리합니다

summary는 입력 검사 후 빈 표본을 처리하고 성공 수를 센 뒤 지연을 정렬합니다. 반환값은 count·success_pct·p95_ms입니다. 출력 단계만 성공률을 소수점 두 자리로 표현합니다. 계산 전에 반올림하면 여러 창의 집계에서 오차를 키울 수 있습니다. 브라우저 starter는 JSON 입력 읽기와 빈 배열 출력을 제공하지만 성공률과 p95가 0인 상태입니다. 먼저 한 요청과 20개 순위 경계를 맞추고, 그 다음 실패 혼합과 손상 입력을 처리합니다. 테스트 입력의 숫자를 바꾸어 통과시키지 않습니다.

백분위끼리 평균하지 않습니다

서로 다른 창의 p95를 평균하면 전체 요청의 p95가 되지 않습니다. 표본 수와 분포가 사라지기 때문입니다. 이번 함수는 원시 지연 목록으로 정렬하여 계산합니다. 실제 지표 시스템은 histogram bucket이나 다른 분포 표현을 써서 합산 가능한 구조를 제공합니다. 그 설정을 하지 않았는데 정확한 전체 p95를 얻었다고 말하지 않습니다. 평균 성공률도 창마다 요청 수가 다르면 왜곡되므로 성공 수와 전체 수를 합산합니다. 같은 기간을 비교할 때 분자와 분모를 같이 보관하는 습관을 익힙니다.

자원 지표의 단위를 함께 씁니다

요청 수는 count, 요청 처리 시간은 ms, CPU 사용량은 도구가 정의한 %, 메모리는 MiB처럼 단위를 붙입니다. 요청 카운터는 누적값이고 두 시점의 차이가 구간 증가량입니다. 재시작으로 카운터가 줄어들면 음수 요청 수로 보고하지 않고 reset을 구분합니다. CPU와 메모리는 요청의 성공률을 대신하는 값이 아닙니다. 이 레슨의 입력은 요청 집계만 계산하며 실제 Docker 자원 관측은 마지막 레슨과 미션에서 같은 시간창에 나란히 놓습니다. 단위 없는 80이라는 값으로 여유를 판단하지 않습니다.

라벨의 개수가 수집 비용을 바꿉니다

지표를 method·route·status 범주로 나누면 배포 전후 비교가 가능합니다. 모든 request_id와 trace_id를 라벨로 넣으면 요청마다 새로운 조합이 생겨 시계열 수가 늘어납니다. 이메일과 토큰은 정보 노출까지 일으킵니다. 특정 ID는 로그·트레이스 검색으로 찾고 집계 라벨은 유한한 범주로 제한합니다. 지표 시스템에 보낼 때 사용자 입력 경로를 템플릿화하는 이유도 같습니다. 세분화하면 좋다는 생각으로 원시 URL을 그대로 붙이는 실수를 피합니다. 필요한 분류와 보관 비용을 함께 검토합니다.

오류 출력에서 조사 대상을 좁힙니다

ERROR는 서비스가 전부 실패했다는 성공률 0.00과 다릅니다. 전자는 파싱이나 필드 계약 문제이고 후자는 유효한 요청이 모두 HTTP 실패라는 결과입니다. JSONDecodeError를 보았다면 배열 시작과 따옴표부터 확인하고, 음수 시간을 받았다면 수집 시계나 단위를 조사합니다. 이 문제의 출력은 기대값 비교를 위해 ERROR 한 줄로 통일하므로 실제 수집기는 필드명과 안전한 오류 종류를 별도 남길 수 있습니다. 민감한 원시 줄 전체를 오류 메시지에 넣는 방식은 선택하지 않습니다.

검증된 집계를 다음 판단의 입력으로 씁니다

완료 기준은 정상·빈 입력·0ms·204 경계·20개 순위·실패 혼합·잘못된 JSON·음수·불리언 사례에서 기대 출력이 일치하는 것입니다. 출력은 요청 수, 두 자리 성공률, 정수 p95 순서입니다. 1 100.00 0은 실제 0ms 표본이고 0 NA NA와 의미가 다릅니다. 집계 결과만으로 어떤 저장소 호출이 느렸는지는 알 수 없습니다. 다음 레슨에서 해당 실패 trace를 찾고 마지막 레슨에서 자원 관측을 연결합니다. 여러 파일의 입력 정리·오류 보고 방법은 더 읽기로 보냅니다.

따라하기

성공·실패를 함께 집계하기

표준 입력은 JSON 배열입니다. status 204도 성공이며 500도 p95의 지연 표본에 포함됩니다. Python 3로 코드를 실행할 때 입력 배열을 stdin으로 전달합니다.

import json, math, sys

def summary(rows):
    if not isinstance(rows,list): raise ValueError("requests array required")
    for r in rows:
        if not isinstance(r,dict) or type(r.get("status")) is not int or not 100 <= r["status"] <= 599 or type(r.get("elapsed_ms")) is not int or r["elapsed_ms"] < 0:
            raise ValueError("invalid request")
    n=len(rows)
    if not n: return {"count":0,"success_pct":None,"p95_ms":None}
    good=sum(200 <= r["status"] <= 299 for r in rows)
    values=sorted(r["elapsed_ms"] for r in rows)
    return {"count":n,"success_pct":100*good/n,"p95_ms":values[(95*n+99)//100-1]}

def main():
    try:
        raw=sys.stdin.read().strip()
        s=summary(json.loads(raw) if raw else [])
        if not s["count"]: print("0 NA NA")
        else: print(f'{s["count"]} {s["success_pct"]:.2f} {s["p95_ms"]}')
    except (ValueError,TypeError): print("ERROR")
if __name__=="__main__": main()

실행 결과

3 66.67 100

최근접 순위 경계 직접 읽기

브라우저 입력 전에 순위를 확인합니다. Python 인덱스는 순위보다 하나 작습니다.

for n in (1,5,20,21):
    rank=(95*n+99)//100
    print("n",n,"rank",rank,"index",rank-1)

실행 결과

n 1 rank 1 index 0
n 5 rank 5 index 4
n 20 rank 19 index 18
n 21 rank 20 index 19

빈 표본과 손상 표본 구별하기

같은 계산 함수를 사용해 빈 자료는 계산 없음, 문자열 상태는 입력 오류로 구분합니다. 함수는 위 예제와 같은 계약을 사용합니다.

import json, math, sys

def summary(rows):
    if not isinstance(rows,list): raise ValueError("requests array required")
    for r in rows:
        if not isinstance(r,dict) or type(r.get("status")) is not int or not 100 <= r["status"] <= 599 or type(r.get("elapsed_ms")) is not int or r["elapsed_ms"] < 0:
            raise ValueError("invalid request")
    n=len(rows)
    if not n: return {"count":0,"success_pct":None,"p95_ms":None}
    good=sum(200 <= r["status"] <= 299 for r in rows)
    values=sorted(r["elapsed_ms"] for r in rows)
    return {"count":n,"success_pct":100*good/n,"p95_ms":values[(95*n+99)//100-1]}

for rows in [[],[{"status":"200","elapsed_ms":1}]]:
    try:
        s=summary(rows)
        print("0 NA NA" if s["count"]==0 else "valid")
    except ValueError:
        print("ERROR")

실행 결과

0 NA NA
ERROR

확인 문제

실습

stdin의 JSON 배열에서 status 200~299인 성공률과 모든 요청 지연의 최근접 순위 p95를 구합니다. 출력은 n 성공률(소수점 두 자리) p95(정수)입니다. status는 100~599 정수, elapsed_ms는 0 이상 정수이며 bool은 거부합니다. 공백 입력·빈 배열은 0 NA NA, JSON·형식·필드 오류는 ERROR입니다. 순위는 ceil(0.95*n)이고 보간하지 않습니다. 추가 필드는 무시합니다. 예: 200/10ms,204/20ms,500/100ms는 3 66.67 100입니다.

모범 답안
import json, math, sys

def summary(rows):
    if not isinstance(rows,list): raise ValueError("requests array required")
    for r in rows:
        if not isinstance(r,dict) or type(r.get("status")) is not int or not 100 <= r["status"] <= 599 or type(r.get("elapsed_ms")) is not int or r["elapsed_ms"] < 0:
            raise ValueError("invalid request")
    n=len(rows)
    if not n: return {"count":0,"success_pct":None,"p95_ms":None}
    good=sum(200 <= r["status"] <= 299 for r in rows)
    values=sorted(r["elapsed_ms"] for r in rows)
    return {"count":n,"success_pct":100*good/n,"p95_ms":values[(95*n+99)//100-1]}

def main():
    try:
        raw=sys.stdin.read().strip()
        s=summary(json.loads(raw) if raw else [])
        if not s["count"]: print("0 NA NA")
        else: print(f'{s["count"]} {s["success_pct"]:.2f} {s["p95_ms"]}')
    except (ValueError,TypeError): print("ERROR")
if __name__=="__main__": main()

더 읽기

면접 질문

  • 지표·로그·트레이스로 확인하는 정보를 설명합니다.