클라우드 Mac에서 검증하는 iOS 지연 백분위와 최근접 순위

클라우드 Mac에서 검증하는 iOS 지연 백분위와 최근접 순위

같은 요청 집합인데 로컬 보고서의 P95는 190밀리초이고 파이프라인에서는 다른 값이 나온다면, 클라우드 Mac의 성능부터 의심하지 마세요. 한쪽은 최근접 순위 방식을, 다른 쪽은 선형 보간을 사용했을 수 있습니다. VMFly 물리 노드에서 통계 검증을 수행할 때는 인터페이스가 느려졌는지 논하기 전에 알고리즘, 표본 범위, 빈 집합의 처리 방식을 먼저 고정해야 합니다. 이 글에서는 네트워크 서비스에 의존하지 않는 Swift 스크립트를 제공하며, iOS 프로젝트의 통계 모듈 회귀 테스트에 활용할 수 있습니다.

백분위 계산 규약 고정하기

이 글에서는 1부터 100까지의 정수 백분위만 허용합니다. 유한한 0 이상의 지연 시간을 오름차순으로 정렬했을 때, 백분위가 q이고 표본 수가 n이면 최근접 순위는 ceil(q × n / 100)입니다. 순위는 1부터 셉니다. 해당 위치의 실제 표본을 반환하며, 인접한 값 사이를 보간하지 않습니다.

예를 들어 10밀리초부터 200밀리초까지 증가하는 표본 20개의 P50, P95, P99는 각각 100, 190, 200밀리초입니다. 표본이 하나뿐이면 세 결과 모두 그 표본과 같습니다. 이는 알고리즘의 정의일 뿐, 꼬리 지연 시간이 충분히 관측되었다는 뜻은 아닙니다.

빈 집합에는 백분위가 없습니다. 이를 0으로 기록하면 성공한 요청이 하나도 없는 구간이 정상 구간보다 더 빠른 것처럼 보입니다.

입력에 음수, 무한대 또는 NaN이 있으면 해당 계산은 실패합니다. 잘못된 값을 조용히 걸러 내면 수집 오류가 정상 보고서로 포장됩니다. 운영 시스템에서 잘못된 표본을 격리하는 정책을 사용한다면, 거부된 표본 수도 별도로 집계해야 합니다.

부동소수점 곱셈 없이 순위 계산하기

다음 내용을 percentile-check.swift로 저장하세요. 이 스크립트는 전체 Xcode 도구 체인에 포함된 Swift를 사용하며, 앱 프로젝트를 수정할 필요가 없습니다. 지연 시간의 단위는 밀리초로 통일합니다. 함수는 숫자만으로 단위를 판별할 수 없으므로 호출하는 쪽에서 변환을 마쳐야 합니다.

import Foundation

enum MetricError: Error, Equatable {
    case invalidPercentile
    case invalidDuration
}

func percentile(_ values: [Double], q: Int) throws -> Double? {
    guard (1...100).contains(q) else {
        throw MetricError.invalidPercentile
    }
    guard values.allSatisfy({ $0.isFinite && $0 >= 0 }) else {
        throw MetricError.invalidDuration
    }
    guard !values.isEmpty else {
        return nil
    }

    let sorted = values.sorted()
    let n = sorted.count
    let rank = (n / 100) * q + ((n % 100) * q + 99) / 100
    return sorted[rank - 1]
}

func expectError(_ expected: MetricError,
                 _ operation: () throws -> Void) {
    do {
        try operation()
        preconditionFailure("Expected metric error")
    } catch let error as MetricError {
        precondition(error == expected)
    } catch {
        preconditionFailure("Unexpected error type")
    }
}

let samples = (1...20).map { Double($0 * 10) }
let p50 = try percentile(samples, q: 50)
let p95 = try percentile(samples, q: 95)
let p99 = try percentile(samples, q: 99)
precondition(p50 == 100 && p95 == 190 && p99 == 200)

let empty = try percentile([], q: 95)
let single = try percentile([7], q: 99)
let repeated = try percentile([4, 4, 4], q: 95)
let zero = try percentile([0], q: 50)
let reversed = try percentile(Array(samples.reversed()), q: 95)
precondition(empty == nil && single == 7)
precondition(repeated == 4 && zero == 0 && reversed == p95)

for q in [0, 101] {
    expectError(.invalidPercentile) {
        _ = try percentile(samples, q: q)
    }
}
for value in [-1.0, Double.nan, Double.infinity] {
    expectError(.invalidDuration) {
        _ = try percentile([value], q: 95)
    }
}

순위 계산에서는 n을 100으로 나눈 몫과 나머지로 분해합니다. 이렇게 하면 정수 경계 근처에서 Double 곱셈으로 발생할 수 있는 반올림 오차를 피하고, 오버플로가 발생할 수 있는 n × q도 직접 계산하지 않습니다. q는 100을 넘지 않으므로 나머지 부분의 중간 계산값은 일정 범위 안에 머물며, 최종 순위도 표본 수를 넘지 않습니다.

노드에서 결정적 검증 실행하기

코드 저장소의 루트 디렉터리에서 다음 명령을 실행하세요. 스크립트가 통과하면 아무것도 출력하지 않고, 실패하면 프로세스가 0이 아닌 종료 상태를 반환합니다. 파이프라인은 종료 상태로 바로 판정할 수 있으므로 현지화된 로그를 파싱할 필요가 없습니다.

set -eu
xcrun swiftc percentile-check.swift -o /tmp/percentile-check
/tmp/percentile-check

이 예제는 고정된 임시 경로를 사용하므로 단일 작업 시연에 적합합니다. 파이프라인 작업을 동시에 실행할 때는 서로 덮어쓰지 않도록 각 작업의 독립된 작업 디렉터리에 실행 파일을 두어야 합니다. -Ounchecked는 추가하지 마세요. 이 모드에서는 여기서 통과 여부를 판정하는 데 사용하는 사전 조건 검사가 제거됩니다.

독립 스크립트가 검증하는 대상은 예제 구현입니다. 프로젝트에 통합한 뒤에는 같은 테스트 벡터를 테스트 타깃으로 옮겨 운영 코드의 통계 함수를 직접 호출해야 합니다. 테스트 전용 알고리즘 복사본을 장기간 따로 유지해서는 안 됩니다. 고정된 난수 시드로 순열 테스트를 추가할 때도 실패를 재현할 수 있도록 원본 입력을 보관해야 합니다.

집계 구간과 표본의 경계 조건 보완하기

운영에 적용하기 전에 최소한 다음 네 가지 변화를 확인하세요.

  • 입력 순서를 섞어도 결과가 같아야 하며, 통계 함수가 입력 배열 자체를 변경해서는 안 됩니다.
  • 같은 지연 시간의 표본을 추가해도 표본 개수를 기준으로 순위를 매겨야 합니다. 중복 값을 먼저 제거해서는 안 됩니다.
  • 표본 수가 99, 100, 101일 때의 순위를 확인해 100개 단위 경계에서 순위가 어긋나지 않도록 합니다.
  • 백분위를 높여도 결과가 작아지지 않아야 하며, 결과는 항상 원본 표본 집합에 속해야 합니다.

모든 백분위는 동일한 불변 스냅샷에서 계산해야 합니다. 수집기가 배열에 값을 추가하는 동안 P50과 P99를 각각 계산하면 두 결과가 서로 다른 집계 구간을 반영할 수 있습니다. 수집기의 동기화 영역 안에서 먼저 표본을 복사하고, 해당 영역을 벗어난 뒤 정렬해야 정렬 작업이 수집 경로를 점유하지 않습니다.

시간이 초과된 요청을 지연 시간 0으로 배열에 넣어서는 안 됩니다. 성공한 요청만 집계한다면 지표를 “성공 요청 지연 시간”으로 명확히 표시하고, 실패 건수와 시간 초과 건수도 함께 출력해야 합니다. 그렇지 않으면 성공률이 떨어질 때 오히려 지연 시간이 개선된 것처럼 보일 수 있습니다.

회귀 보고서를 재검증할 수 있게 만들기

보고서마다 알고리즘 식별자, 단위, 집계 구간의 시작과 끝, 성공 표본 수, 실패 건수, 시간 초과 건수, 세 백분위를 저장하세요. 고정 테스트 픽스처는 전체 벡터를 저장해도 됩니다. 실제 요청은 문제 해결에 필요한 데이터만 보관하고, 요청 인증 정보나 본문은 포함하지 마세요.

여러 구간의 P95를 평균 내 전체 구간의 P95인 것처럼 사용해서는 안 됩니다. 구간을 합쳐야 한다면 합친 표본으로 다시 계산하거나, 병합을 명시적으로 지원하고 오차가 명시된 통계 자료구조를 사용해야 합니다. 이 글의 정렬 기반 구현은 정확한 검증에는 적합하지만, 끝없이 증가하는 데이터 스트림에는 적합하지 않습니다.

마지막으로 “통계 구현의 정확성”과 “성능 기준 충족 여부”를 별개의 검증 관문으로 나누세요. 전자는 고정 벡터로 정확히 비교하고, 후자는 충분한 표본과 동일한 집계 구간을 바탕으로 서비스 기준값과 비교합니다. 그래야 검증이 실패했을 때 알고리즘 회귀, 수집 이상, 실제 지연 시간 변화 중 무엇이 원인인지 명확히 구분할 수 있습니다.

자주 묻는 질문

최근접 순위와 선형 보간 중 무엇을 사용해야 하나요?

기존 측정 계약에 맞춰 선택합니다. 최근접 순위는 실제 표본 값을 반환합니다. 보간 방식에서 전환한다면 방식 식별자를 바꾸고 비교 기준도 다시 만들어야 합니다.

성공한 요청이 없으면 P95를 0으로 기록해도 되나요?

안 됩니다. 결과 없음과 성공 표본 수 0을 기록하고 실패 및 시간 초과는 따로 집계합니다. 0은 유효한 지연 값이므로 데이터 없음 표시로 사용할 수 없습니다.

빌드 파이프라인에 전용 Mac을 추가하세요

Apple Silicon M4 또는 M4 Pro를 선택할 수 있습니다. 전체 시스템을 한 고객만 사용하며, 가상 머신이 아닙니다. 빌드 작업량에 맞는 모델과 팀에 적합한 결제 주기를 선택하세요.

전용 물리 Mac 대여하기

다음 빌드는 전용 서버에서 실행하세요

하드웨어 사양과 결제 주기를 확인한 뒤 팀 전용 물리 Mac을 이용해 보세요.