Devin.KR

운영과 장애 질문 - 로그·모니터링·배포

개발자KR 조회 1

이 장에서 배우는 것

앞 장에서 중고 거래 서비스가 커질 때의 설계를 살펴보았다. 이번에는 설계한 서비스가 느려지거나 실패할 때 무엇을 관찰하고 어떻게 복구할지 다룬다. 운영 면접에서 중요한 것은 도구 이름을 많이 아는 일이 아니다. 사용자가 겪는 문제를 설명하고, 확인한 사실과 추측을 구분하며, 피해를 줄이는 순서로 행동하는 능력이다.

신입 지원자에게 대규모 서비스를 직접 운영한 경험이 반드시 필요한 것은 아니다. 과제 프로젝트에서 배포 직후 요청이 실패했거나, 외부 연동이 늦어 화면이 멈췄던 경험도 좋은 재료다. 다만 자신이 실제로 확인한 범위와 해보지 않은 대응을 구분해야 한다.

  • 장애 경험을 사용자 영향, 완화 조치, 원인 확인, 재발 방지 순서로 설명한다.
  • 로그, 지표, 알림의 역할을 구분하고 오류율과 지연 백분위를 해석한다.
  • 배포 전략과 되돌리기의 조건을 설명한다.
  • 타임아웃과 재시도를 함께 설계하고 작은 프로그램으로 백오프 일정을 확인한다.

문제 상황

가상의 중고 거래 서비스 팀에 지원한 지원자는 운영 면접에서 다음 상황을 받는다. 새 버전을 배포한 뒤 상품 상세 화면이 간헐적으로 열리지 않는다. 일부 요청은 빨리 끝나지만 일부는 오래 기다린 뒤 실패한다. 서버 프로세스는 실행 중이고, 상태 확인 주소도 정상 응답을 돌려준다.

면접관: 서버가 살아 있다는 보고는 받았다. 그런데 사용자는 상품을 볼 수 없다고 한다. 무엇부터 확인하겠는가?

여기서 바로 “서버를 재시작한다”라고 답하면 판단 근거가 빠진다. 반대로 원인을 모두 알아낼 때까지 기다리겠다고 하면 사용자 피해가 길어질 수 있다. 먼저 영향 범위와 시작 시점을 확인하고, 최근 변경과의 관련성을 살펴보며, 되돌리기처럼 안전한 완화 수단이 있는지 판단해야 한다.

이 장의 실험에서는 한 구간의 요청 완료 로그 열 줄과 손상된 로그 한 줄을 읽는다. 오류율과 지연 분포를 계산하고, 정해진 시간 예산 안에서 재시도 대기 일정이 어떻게 늘어나는지 출력한다. 실제 장애를 재현하는 서버는 만들지 않는다. 면접에서 말한 판단을 작은 수치로 검증하는 데 집중한다.

장애 경험은 복구의 판단으로 설명한다

운영 면접에서 확인하는 열 가지 질문
질문의도답변 핵심꼬리 질문
장애를 겪은 경험이 있는가?관찰과 행동의 구체성영향, 완화, 원인, 개선을 구분한다.직접 한 일은 무엇인가?
로그 레벨을 어떻게 정하는가?운영 정보의 선별 능력정상 흐름과 조치할 실패를 구분한다.모든 실패가 ERROR인가?
구조화 로그는 왜 필요한가?검색과 집계의 이해필드 이름과 자료형을 일관되게 유지한다.어떤 정보는 기록하지 않는가?
무엇을 지표로 보는가?사용자 영향의 수치화요청량, 오류율, 지연, 자원 상태를 본다.평균만 보면 안 되는가?
어떤 조건에서 알림을 보내는가?대응 가능한 신호의 설계영향, 지속 시간, 요청량을 함께 본다.트래픽이 적으면 어떻게 하는가?
장애 대응 순서는 무엇인가?피해와 조사 사이의 우선순위완화 후 근거를 모아 원인을 좁힌다.완화가 증거를 지우면 어쩌는가?
배포 전략을 비교해 보라.노출 범위와 자원 비용의 이해교체 방식과 실패 시 전환 방법을 설명한다.신구 버전이 공존해도 되는가?
언제 배포를 되돌리는가?중단 기준과 복구 가능성배포 전 기준과 호환성을 확인한다.데이터 변경도 되돌릴 수 있는가?
타임아웃은 어떻게 정하는가?자원 점유와 응답 예산의 이해전체 기한 안에 하위 호출 예산을 나눈다.너무 짧으면 어떤 일이 생기는가?
실패한 요청은 재시도하면 되는가?중복 처리와 부하 증폭의 이해재시도 대상, 횟수, 간격, 기한을 제한한다.응답만 유실되었다면 어떻게 하는가?

깊게 보기: 장애를 겪은 경험이 있는가?

이 질문의 중심은 사건의 규모가 아니라 판단의 근거다. “배포가 원인이었다”와 “배포 직후부터 오류가 증가했다”는 다른 말이다. 후자는 관찰이고 전자는 검증할 설명이다. 시간상 앞뒤가 맞는다는 이유만으로 원인을 확정해서는 안 된다.

같은 장애 경험을 설명하는 답변의 차이
수준답변부족하거나 나아진 점
나쁜 답서버가 이상해서 재시작했고 해결했다.영향, 근거, 재발 가능성이 드러나지 않는다.
보통 답배포 뒤 오류 로그가 보여 이전 버전으로 되돌렸다.행동은 구체적이지만 복구 확인과 원인 검증이 빠졌다.
좋은 답상품 상세 요청 실패를 확인하고 배포를 되돌렸다. 오류율 회복을 확인한 뒤 변경 설정을 비교하고 같은 조건에서 실패를 재현했다.완화와 원인 조사를 나누고 행동의 결과를 검증한다.

지원자: 장애에서는 사용자 영향을 줄이는 조치를 먼저 선택하겠다. 원인을 조사하는 동안에도 실패 요청은 계속 쌓일 수 있기 때문이다. 예를 들어 과제 프로젝트에서 배포 직후 상품 상세 요청이 실패했다면, 영향이 시작된 시점과 최근 변경을 확인하고 데이터 호환성이 유지되는 이전 버전으로 되돌리는 방안을 검토하겠다. 이후 같은 구간의 오류율과 지연이 회복되었는지 확인하겠다. 다만 되돌렸다는 사실만으로 원인이 증명되지는 않으므로 설정과 변경 코드를 비교하고 재현 결과로 설명을 보강하겠다.

위 답변은 경험이 없는 사람이 실제 경험처럼 말할 문장이 아니다. 실제 사건이 있다면 “검토하겠다” 대신 자신이 한 행동과 관찰 결과를 넣는다. 사건이 없다면 “실서비스 장애 경험은 없고, 프로젝트에서 다음과 같은 실패를 재현했다”라고 범위를 먼저 밝힌다.

면접관: 재시작하면 조사 자료가 사라질 수도 있지 않은가?

지원자: 복구를 늦추지 않는 범위에서 오류 시각, 배포 버전, 대표 요청 식별자와 로그를 남기겠다. 메모리 상태처럼 사라질 수 있는 자료도 수집 비용과 사용자 영향을 비교하겠다. 자료 수집 자체가 복구를 오래 지연시키면 완화를 먼저 진행하고, 남은 자료로 조사 범위를 정하겠다.

완화는 정상 기능을 일부 줄여서라도 영향을 낮추는 행동이다. 원인 조사는 왜 그런 일이 생겼는지 증거로 좁히는 과정이다. 재발 방지는 수정뿐 아니라 탐지, 검증, 복구 절차의 빈틈을 줄이는 일까지 포함한다. 각 단계에서 담당자와 다음 확인 시점을 정하면 조사와 대응이 서로 기다리는 시간을 줄일 수 있다.

장애 대응은 사용자 영향 완화에서 시작해 원인 검증과 재발 방지로 이어진다

로그와 지표로 사용자 영향을 읽는다

로그는 개별 사건의 기록이고, 지표는 일정 구간의 상태를 숫자로 모은 결과다. 알림은 그 결과 중 사람이 대응해야 할 조건을 골라 전달한다. 요청 하나의 실패 이유를 찾을 때는 로그가 필요하고, 서비스 전체가 나빠졌는지 판단할 때는 지표가 유용하다. 하나가 다른 하나를 대신하지는 않는다.

로그 레벨은 팀이 정한 의미를 일관되게 적용해야 한다. DEBUG는 상세 조사 정보, INFO는 정상적인 주요 흐름, WARNING은 주의가 필요한 이상 징후, ERROR는 작업 실패를 나타내는 방식으로 사용할 수 있다. 사용자의 잘못된 입력으로 예상대로 거절한 요청까지 모두 ERROR로 남기면 조치할 실패가 묻힌다. 반대로 중요한 실패를 INFO로만 남기면 탐지가 어려워진다.

구조화 로그(structured logging)는 메시지 문장뿐 아니라 이름이 있는 필드로 사건을 기록한다. 요청 완료 로그라면 시각, 요청 식별자, 경로 양식, 응답 상태, 처리 시간, 배포 버전을 정해 둘 수 있다. 상품 번호가 붙은 실제 주소 대신 경로 양식을 쓰면 집계 항목이 불필요하게 늘어나는 문제를 줄인다. 비밀번호, 인증 토큰, 대화 내용과 같은 정보는 기록 대상에서 제외하거나 필요한 범위만 가린다.

필드의 단위와 자료형도 계약이다. 어떤 서버는 처리 시간을 초로, 다른 서버는 밀리초로 기록하면 합친 결과가 틀어진다. 요청 시작과 종료를 각각 기록하는 환경에서 두 종류를 함께 세면 요청 수도 부풀려진다. 이 장의 코드는 한 요청마다 완료 로그가 정확히 하나 있다는 가정으로 계산한다.

깊게 보기: 평균 지연과 오류율이면 충분한가?

지표를 고르는 답변의 차이
수준답변판단의 차이
나쁜 답평균이 짧으면 서비스가 빠르다.느린 요청 집단을 놓친다.
보통 답오류율과 p95도 보겠다.지표 이름은 알지만 분모와 구간이 없다.
좋은 답같은 시간 구간의 요청량과 서버 오류율, 지연 p50과 p95를 함께 보겠다. 요청 수가 적으면 원시 값과 개별 실패도 확인하겠다.계산 조건과 해석의 한계를 설명한다.

지원자: 평균만으로 판단하지 않고 오류율과 지연 분포를 함께 보겠다. 소수 요청이 매우 느려도 평균에서는 영향이 작게 보일 수 있기 때문이다. 상품 상세 API라면 같은 구간의 요청 수, 서버 오류 비율, 지연 중앙값과 p95를 확인하겠다. 이번 실험처럼 요청이 열 개뿐이면 p95가 최댓값이 되므로, 큰 표본의 안정적인 특성처럼 해석하지 않겠다. 성공 요청과 실패 요청의 지연도 따로 확인할 필요가 있다.

백분위수(percentile)는 정렬한 값에서 특정 비율의 경계가 되는 값이다. 계산 방식에는 차이가 있으므로 도구 간 결과를 비교할 때 정의를 확인한다. 여기서는 최근접 순위 방식을 사용한다. 표본 수를 n, 구하려는 비율을 q라고 하면 순위는 올림한 n × q다. 열 개의 값에서 p95의 순위는 올림한 9.5, 즉 열 번째다.

이 장의 오류율은 유효한 요청 완료 로그 중 상태 코드가 500 이상인 비율이다. 따라서 모든 실패를 대표하는 보편적인 정의는 아니다. 기능에 따라 응답이 성공이어도 결과가 잘못되었을 수 있고, 일부 4xx 응답이 사용자 문제를 드러낼 수도 있다. 운영에서는 목적에 맞게 성공과 실패를 정의해야 한다.

면접관: 오류가 한 건만 나도 알림을 보내겠는가?

지원자: 한 건의 영향과 전체 요청 수를 함께 보겠다. 일반 조회의 단발성 실패와 중요한 작업의 데이터 손실 징후는 다르게 다루겠다. 비율 조건에는 지속 시간과 최소 요청량을 함께 두고, 요청이 적은 구간에서는 실패 건수나 핵심 기능의 주기적 확인도 보완하겠다. 알림에는 영향 범위와 확인할 대시보드, 첫 대응 절차를 연결하겠다.

이번 코드는 손상된 로그를 제외한 개수도 출력한다. 분석할 수 없는 로그를 조용히 버리면 오류율이 실제보다 좋아 보일 수 있다. 로그가 누락되거나 특정 요청만 수집되는 상황에서는 계산이 정확해도 서비스 전체를 대표하지 못한다. 지표를 믿기 전에 관측 자료의 범위를 확인해야 한다.

배포와 재시도에는 멈출 조건이 필요하다

깊게 보기: 어떤 배포 전략을 선택하겠는가?

롤링 배포(rolling deployment)는 실행 중인 인스턴스를 순차적으로 새 버전으로 바꾼다. 블루그린 배포(blue-green deployment)는 두 환경을 준비하고 트래픽의 목적지를 전환한다. 카나리 배포(canary deployment)는 일부 요청이나 사용자에게 새 버전을 먼저 노출하고 관찰 결과에 따라 범위를 넓힌다.

배포 전략에서 함께 판단할 조건
전략변경 범위되돌리기 방식확인할 제약
롤링인스턴스를 차례로 교체한다.교체를 멈추고 이전 버전을 다시 배치한다.신구 버전 공존과 처리 용량을 확인한다.
블루그린준비한 새 환경으로 전환한다.이전 환경이 유효하면 목적지를 돌린다.추가 자원, 연결 처리, 공유 데이터 호환성을 확인한다.
카나리일부 트래픽부터 노출한다.새 버전으로 가는 트래픽을 줄이거나 차단한다.비교 집단과 표본 수, 관찰 시간을 정한다.

“블루그린이면 바로 복구된다”는 나쁜 답이다. 보통 답은 세 전략의 정의를 나열한다. 좋은 답은 서비스의 자원과 관측 능력에 맞는 선택을 제시하고, 되돌릴 수 없는 변경을 함께 설명한다.

지원자: 일부 트래픽을 분리하고 버전별 지표를 비교할 수 있다면 카나리 배포를 검토하겠다. 새 버전의 영향을 제한하며 확인할 수 있기 때문이다. 예를 들어 상품 조회 변경을 작은 범위에 적용하고, 기존 버전과 오류율과 지연을 비교한 뒤 노출을 늘리겠다. 다만 표본이 적으면 문제가 드러나지 않을 수 있다. 분리 기능이나 관측 여건이 부족하면 롤링 배포와 배포 후 검증으로 시작하고, 중단 기준부터 명확히 하겠다.

면접관: 이전 버전으로 돌리면 데이터베이스 변경도 해결되는가?

지원자: 코드 되돌리기와 데이터 복구는 따로 판단해야 한다. 이전 코드가 읽는 필드를 삭제했다면 코드만 돌려도 실행되지 않을 수 있다. 배포 전에 신구 코드가 함께 사용할 수 있는 변경인지 확인하고, 필드 추가와 사용 전환, 이전 필드 제거를 나누겠다. 이미 변경된 데이터는 별도의 복구 계획이 필요하다.

배포 전에는 정상 기준과 중단 기준을 정한다. 배포 후에는 프로세스가 실행되는지에 더해 실제 사용자 경로가 성공하는지 확인한다. 되돌린 뒤에도 같은 지표가 회복되는지 확인해야 한다. 새 요청이 정상화되어도 이미 실패한 작업이나 중간 상태의 데이터는 별도로 확인할 수 있다.

깊게 보기: 실패하면 몇 번 재시도하겠는가?

타임아웃(timeout)은 기다림의 한계를 정하고, 재시도는 실패한 작업을 다시 시도한다. 둘은 함께 설계해야 한다. 연결 대기와 응답 읽기 제한만 설정하고 전체 처리 기한을 두지 않으면 여러 번의 호출과 대기가 합쳐져 사용자 대기 시간이 길어진다. 클라이언트가 기다리기를 멈췄다고 서버 작업까지 취소되었다고 단정해서도 안 된다.

“성공할 때까지 반복한다”는 나쁜 답이다. “세 번 재시도한다”는 보통 답이다. 좋은 답은 횟수 외에 대상 오류, 중복 실행 위험, 각 시도의 제한 시간, 전체 기한, 대기 간격을 함께 정한다.

지원자: 일시적인 실패이고 중복 실행이 안전한 요청에 한해 제한적으로 재시도하겠다. 실패가 오래 지속되는 상황에서 반복 호출은 부하를 더할 수 있기 때문이다. 상품 조회에서 일시적인 연결 실패가 발생했다면 전체 응답 기한 안에서 횟수를 제한하고 대기 간격을 늘리겠다. 반면 구매 확정처럼 상태를 바꾸는 요청은 응답만 유실되었을 가능성이 있으므로, 중복 요청을 식별하는 장치나 처리 결과 조회 없이 자동으로 반복하지 않겠다.

지수 백오프(exponential backoff)는 재시도 사이의 대기 시간을 배수로 늘리는 방식이다. 이 장에서는 100밀리초로 시작해 두 배씩 늘리되 800밀리초에서 제한한다. 실제 운영에서는 여러 호출자가 같은 시각에 다시 몰리지 않도록 지터(jitter), 즉 대기 시간의 무작위 변동도 고려한다. 실험은 간격의 변화를 확인하도록 무작위 변동을 넣지 않는다.

면접관: 여러 계층에서 각각 재시도하면 어떤가?

지원자: 하위 호출 횟수가 곱으로 늘어날 수 있다. 상위와 하위가 각각 세 번씩 호출하면 한 요청이 하위 시스템에 최대 아홉 번 도달할 수 있다. 재시도를 담당할 계층을 정하고 전체 기한을 전달하겠다. 상대 시스템이 대기 시간을 안내한다면 그 값과 남은 예산도 함께 확인하겠다.

재시도 간격은 증가하지만 전체 시간 예산을 넘는 다음 시도는 중단한다

완성 코드

다음 내용을 main.py로 저장한다. 외부 파일을 읽지 않고 프로그램 안의 고정 로그를 분석한다. 모든 요청 시각은 고정되어 있으며 실제 대기나 네트워크 호출은 없다. 재시도 일정의 기준은 최초 요청이 실패한 직후인 0밀리초다.

import json
import math


LOG_TEXT = """\
{"ts":"2026-01-01T00:00:00Z","status":200,"duration_ms":40}
{"ts":"2026-01-01T00:00:01Z","status":200,"duration_ms":50}
{"ts":"2026-01-01T00:00:02Z","status":200,"duration_ms":60}
{"ts":"2026-01-01T00:00:03Z","status":200,"duration_ms":70}
{"ts":"2026-01-01T00:00:04Z","status":200,"duration_ms":80}
{"ts":"2026-01-01T00:00:05Z","status":200,"duration_ms":90}
{"ts":"2026-01-01T00:00:06Z","status":200,"duration_ms":100}
{"ts":"2026-01-01T00:00:07Z","status":500,"duration_ms":120}
{"ts":"2026-01-01T00:00:08Z","status":503,"duration_ms":400}
{"ts":"2026-01-01T00:00:09Z","status":504,"duration_ms":900}
broken log
"""


def read_records(text):
    records = []
    rejected = 0
    for line in text.splitlines():
        if not line.strip():
            continue
        try:
            record = json.loads(line)
            if not isinstance(record, dict):
                raise ValueError("object required")
            status = record.get("status")
            duration = record.get("duration_ms")
            if type(status) is not int or not 100 <= status <= 599:
                raise ValueError("invalid status")
            if type(duration) not in (int, float):
                raise ValueError("invalid duration type")
            if not math.isfinite(duration) or duration < 0:
                raise ValueError("invalid duration value")
        except (ValueError, OverflowError):
            rejected += 1
            continue
        records.append(record)
    return records, rejected


def percentile(values, q):
    if not values or not 0 < q <= 1:
        raise ValueError("nonempty values and 0 < q <= 1 required")
    ordered = sorted(values)
    rank = math.ceil(len(ordered) * q)
    return ordered[rank - 1]


def retry_schedule(max_retries, base_ms, cap_ms, budget_ms):
    elapsed_ms = 0
    delay_ms = base_ms
    for retry_no in range(1, max_retries + 1):
        delay_ms = min(delay_ms, cap_ms)
        candidate_ms = elapsed_ms + delay_ms
        if candidate_ms >= budget_ms:
            print(
                f"재시도 {retry_no}: 중단 "
                f"(예정 {candidate_ms} ms, 예산 {budget_ms} ms)"
            )
            break
        elapsed_ms = candidate_ms
        print(
            f"재시도 {retry_no}: 대기 {delay_ms} ms, "
            f"누적 {elapsed_ms} ms"
        )
        delay_ms *= 2


def main():
    records, rejected = read_records(LOG_TEXT)
    print(f"유효 로그: {len(records)}개")
    print(f"제외 로그: {rejected}개")
    if not records:
        print("요청이 없어 지표를 계산하지 않는다.")
        return

    failures = sum(record["status"] >= 500 for record in records)
    durations = [record["duration_ms"] for record in records]
    error_rate = failures / len(records) * 100
    mean_ms = sum(durations) / len(durations)
    print(f"서버 오류: {failures}개")
    print(f"서버 오류율: {error_rate:.1f}%")
    print(f"평균 지연: {mean_ms:.1f} ms")
    print(f"p50 지연: {percentile(durations, 0.50):.0f} ms")
    print(f"p95 지연: {percentile(durations, 0.95):.0f} ms")
    print("재시도 일정 (호출 시간 0 ms, 지터 없음)")
    retry_schedule(
        max_retries=5,
        base_ms=100,
        cap_ms=800,
        budget_ms=2000,
    )


if __name__ == "__main__":
    main()

줄별 해설

import json은 각 로그 줄을 객체로 읽고, import math는 유한한 수 검사와 순위 올림에 사용한다. 시계를 읽거나 난수를 생성하지 않으므로 실행할 때마다 같은 결과가 나온다.

LOG_TEXT는 요청 완료 기록 열 개와 손상된 줄 하나다. ts는 관측 구간을 보여 주는 고정 시각이며 이번 집계에서는 별도로 필터링하지 않는다. 실제 분석에서는 집계할 시작과 종료 시각을 먼저 정해야 한다.

read_records의 records와 rejected는 계산에 포함한 자료와 제외한 자료를 구분한다. 빈 줄은 기록으로 세지 않는다. json.loads가 성공해도 객체인지, 필요한 필드가 올바른지 다시 확인한다. 문법이 맞는 JSON이라는 사실만으로 운영 로그의 형식이 맞는 것은 아니다.

type(status) is not int는 상태 코드 자리에 참이나 거짓이 들어오는 경우까지 거절한다. Python에서는 불리언이 정수의 하위형이므로 단순한 정수형 검사와 의도가 다를 수 있다. 처리 시간도 불리언을 제외하고 수치만 받으며, 무한대나 숫자가 아닌 값, 음수를 거절한다. OverflowError도 세어 표현 범위를 넘는 수치가 집계를 중단하지 않게 한다.

except는 입력 해석과 검증에서 예상한 오류만 처리한다. 프로그램의 모든 예외를 숨기지 않는다. 검증을 통과한 줄만 records.append로 모으므로 이후 계산은 유효한 자료를 대상으로 한다. 다만 제외율이 높을 때 결과를 믿어도 되는지 판단하는 정책은 별도로 필요하다.

percentile은 빈 입력과 잘못된 비율을 거절한다. sorted는 원본을 바꾸지 않고 정렬한 목록을 만든다. math.ceil로 구한 순위는 1부터 시작하므로 목록에서는 rank - 1 위치를 읽는다. 이 정의에서 열 개 표본의 p50은 다섯 번째 값인 80이다. 가운데 두 값을 평균하는 중앙값 계산과는 다를 수 있다.

retry_schedule의 elapsed_ms는 누적 대기 시간이고 delay_ms는 다음 재시도 직전의 대기 시간이다. 반복문의 첫 회는 최초 호출이 아니라 첫 재시도다. min으로 간격 상한을 적용하고 다음 예정 시각을 계산한다.

candidate_ms >= budget_ms이면 반복을 중단한다. 기한과 정확히 같은 시각에도 새 호출을 시작하지 않는 정책이다. 허용된 일정만 출력한 뒤 대기 시간을 두 배로 늘린다. 다섯 번째 재시도의 예정 시각은 2300밀리초이므로 2000밀리초 예산을 넘는다.

main은 유효 자료가 없으면 계산을 멈춘다. sum에 전달한 비교식은 참일 때 1로 합산되어 서버 오류 개수가 된다. 평균과 백분위수는 성공과 실패를 합친 전체 지연에서 구한다. 마지막 호출은 재시도를 최대 다섯 번 검토하되 예산을 먼저 적용한다.

실험은 실제 호출 시간을 0으로 놓았다. 따라서 출력은 대기 일정의 검증이며 실제 요청을 완료할 수 있다는 보장은 아니다. 운영 코드에서는 호출에 쓴 시간도 전체 기한에서 빼고, 남은 시간 안에 다음 호출의 타임아웃을 배정해야 한다. 재시도 대상인지 판단하는 로직도 이 일정 계산 앞에 필요하다.

실행 결과

macOS나 Linux에서 Python 3.12 이상으로 실행한다.

python3 main.py

예상 출력은 다음과 같다.

유효 로그: 10개
제외 로그: 1개
서버 오류: 3개
서버 오류율: 30.0%
평균 지연: 201.0 ms
p50 지연: 80 ms
p95 지연: 900 ms
재시도 일정 (호출 시간 0 ms, 지터 없음)
재시도 1: 대기 100 ms, 누적 100 ms
재시도 2: 대기 200 ms, 누적 300 ms
재시도 3: 대기 400 ms, 누적 700 ms
재시도 4: 대기 800 ms, 누적 1500 ms
재시도 5: 중단 (예정 2300 ms, 예산 2000 ms)

오류율의 분모는 손상된 줄을 뺀 열 건이다. 세 건이 서버 오류이므로 30.0%다. 처리 시간의 합은 2010밀리초이며 평균은 201.0밀리초다. p50과 p95의 차이는 이 자료에서 일부 요청이 상대적으로 오래 걸렸다는 사실을 보여 준다. 다만 열 건만으로 평소 서비스의 성능이나 배포의 인과관계까지 판단할 수는 없다.

실무에서 자주 틀리는 것

손상된 로그를 정상 요청처럼 분모에 넣는다

다음 코드는 입력 줄 수를 요청 수로 취급한다. 손상된 줄이나 빈 줄이 섞이면 오류율이 낮아 보일 수 있다.

# 틀린 코드
total = len(LOG_TEXT.splitlines())
error_rate = failures / total * 100

분모와 분자는 같은 검증 기준을 통과한 자료에서 구한다. 제외한 자료 수도 함께 확인한다.

# 고친 코드
records, rejected = read_records(LOG_TEXT)
if records:
    failures = sum(r["status"] >= 500 for r in records)
    error_rate = failures / len(records) * 100
else:
    error_rate = None

백분위수 위치를 정렬 없이 읽는다

입력 순서는 지연 순서와 다르다. 또한 비율에 길이를 곱한 값을 그대로 인덱스로 쓰면 범위와 순위의 의미가 어긋난다.

# 틀린 코드
p95 = durations[int(len(durations) * 0.95)]

정의한 방식에 따라 정렬과 순위를 함께 적용한다. 완성 코드의 함수는 빈 입력도 검사한다.

# 고친 코드
p95 = percentile(durations, 0.95)

예상된 거절과 내부 실패를 같은 레벨로 기록한다

없는 상품을 조회한 요청을 모두 내부 장애처럼 기록하면 조사 우선순위가 흐려진다. 다음은 기록 레벨을 구분하는 예다.

# 틀린 코드
logger.error("상품 조회가 끝남: status=%s", status)

팀의 정책에 맞게 실패 의미를 나눈다. 로그 레벨을 낮춰도 4xx 응답 수를 지표로 관찰할 수 있다.

# 고친 코드
if status >= 500:
    logger.error("상품 조회 실패: status=%s", status)
else:
    logger.info("상품 조회 완료: status=%s", status)

실제 구조화 로그에서는 상태와 요청 식별자를 별도 필드로 보낸다. 오류 상세를 남길 때도 토큰이나 원문 개인정보가 섞이지 않도록 기록 경계를 정한다.

백오프만 적용하고 전체 기한은 확인하지 않는다

간격 상한이 있어도 반복 횟수가 많으면 총 대기 시간은 계속 늘어난다. 다음 코드는 전체 예산이 없다.

# 틀린 코드
for attempt in range(5):
    delay_ms = min(100 * 2 ** attempt, 800)
    print(delay_ms)

다음 시점이 기한 안인지 확인한 뒤 일정을 승인한다. 아래의 elapsed_ms는 이미 사용한 시간이다.

# 고친 코드
delay_ms = min(100 * 2 ** attempt, 800)
candidate_ms = elapsed_ms + delay_ms
if candidate_ms >= budget_ms:
    break
elapsed_ms = candidate_ms

이 조각은 반복문 안에서 사용한다. 실제 구현에서는 대기 직전과 호출 직전에 남은 기한을 다시 확인한다. 경과 시간 측정에는 시스템 시각 조정의 영향을 덜 받는 단조 시계를 사용하고, 재시도 횟수와 전체 기한을 함께 제한한다.

한눈에 보기

운영 질문에 답할 때 연결할 근거와 한계
주제답변 핵심확인할 근거한계와 보완
장애 경험영향을 줄인 뒤 원인을 검증한다.시각, 영향 범위, 조치 전후 지표시간적 연관만으로 원인을 확정하지 않는다.
로그사건을 일관된 필드로 남긴다.요청 식별자, 상태, 단위, 버전민감 정보와 수집 누락을 관리한다.
지표오류율과 지연 분포를 함께 본다.같은 구간과 같은 모집단표본 수와 계산 정의를 밝힌다.
알림사람이 행동할 조건을 알린다.영향, 지속 시간, 요청량저트래픽과 무응답 구간을 보완한다.
배포노출과 중단 기준을 정한다.버전별 지표와 기능 확인코드와 데이터의 복구를 구분한다.
재시도안전한 대상에 한해 제한한다.중복 안전성, 횟수, 기한, 간격계층별 증폭과 동시 재시도를 고려한다.

면접에서는 결론을 먼저 말하고 그 결론을 고른 근거를 붙인다. 이어 자신이 확인한 경험이나 구체적인 상황을 제시하고, 적용되지 않는 조건과 대안을 설명한다. 이 순서는 장애가 없었던 사람도 사용할 수 있다. 실제 경험의 크기를 부풀리는 대신 실험으로 확인한 것과 운영에서 더 필요한 것을 구분하면 된다.

연습 문제

  1. 완성 코드에서 900밀리초인 마지막 유효 로그를 제거한다. 서버 오류율, 평균, p50, p95를 구하고 원래 결과와 비교하라.
  2. 같은 요청 두 개가 서로 다른 지연으로 기록되었다. 요청 식별자 없이 집계하면 어떤 문제가 생기는가? 로그 형식과 집계 규칙을 어떻게 보완할지 설명하라.
  3. 재시도 예산을 1500밀리초로 바꾸면 어디서 중단되는가? 각 호출이 100밀리초를 사용한다면 어떤 정보를 더 반영해야 하는가?
  4. 상품 상세 변경을 일부 사용자에게 배포한 뒤 오류율이 증가했다. 첫 대응, 복구 확인, 원인 조사, 재발 방지를 각각 한 문장으로 작성하라. 이전 버전이 새 데이터 형식을 읽지 못하는 조건도 포함하라.

정답과 해설

  1. 유효 요청은 아홉 건, 서버 오류는 두 건이므로 오류율은 소수 첫째 자리까지 22.2%다. 처리 시간 합은 1110밀리초여서 평균은 123.3밀리초다. p50 순위는 올림한 4.5인 다섯 번째이므로 80밀리초다. p95 순위는 올림한 8.55인 아홉 번째이므로 400밀리초다. 한 건을 제거하자 p95가 크게 바뀐다. 작은 표본에서는 개별 관측값이 결과에 큰 영향을 준다.

  2. 두 줄이 서로 다른 요청인지, 같은 요청의 중복 수집인지 구분하기 어렵다. 중복된 자료가 오류와 느린 요청에 치우치면 요청량뿐 아니라 오류율과 지연 분포도 왜곡된다. 요청 식별자와 사건 종류를 남기고, 요청 완료 기록을 어느 지점에서 한 번 생성할지 정한다. 여러 서비스의 하위 호출은 별도 호출 식별자로 구분한다. 중복 제거에도 기준 구간과 보관 비용이 필요하다.

  3. 누적 대기는 100, 300, 700밀리초까지 허용된다. 네 번째 예정 시각이 1500밀리초로 기한과 같으므로 네 번째에서 중단한다. 호출마다 100밀리초가 필요하면 대기 합만으로 판단할 수 없다. 이미 소비한 호출 시간과 남은 기한을 계산하고 다음 호출의 제한 시간을 그 안에 배정해야 한다. 최초 호출 시간까지 포함하는 예산인지도 명시해야 한다.

  4. 첫 대응은 새 버전 노출을 멈추고 호환성이 확인된 경로로 트래픽을 전환하거나 문제 기능을 일시 제한하는 일이다. 복구 확인은 사용자 경로의 성공 여부와 같은 구간의 오류율·지연이 회복되었는지 확인하는 일이다. 원인 조사는 버전별 로그와 변경 사항을 비교하고 재현 가능한 조건으로 가설을 검증하는 일이다. 재발 방지는 호환성 검증과 배포 중단 기준, 복구 절차를 보완하는 일이다. 이전 버전이 새 데이터를 읽지 못하면 단순한 코드 되돌리기를 선택하지 않고 호환 버전 배포나 기능 제한 등 안전한 완화 수단을 판단한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.