میانگین تأخیر به تنهایی نمی‌گوید بخش کندتر پاسخ‌ها چه وضعیتی دارد. در یک مسیر فرضی دریافت دادهٔ حسگر، می‌توان میانگین، P95 و تعداد نمونه را کنار هم گزارش کرد؛ اما پیش از مقایسه باید روش محاسبه روشن باشد. این درس روی فهرست‌های ساختگی نشان می‌دهد چرا نزدیک‌ترین رتبه و درون‌یابی لزوماً یک عدد نمی‌دهند. هیچ درخواست شبکه، سرویس واقعی، حسگر یا زمان‌سنجی اجرا نمی‌شود؛ خروجی فقط نتیجهٔ محاسبه روی دادهٔ تألیفی است.

۱. نمونهٔ تأخیر را با واحد و دامنهٔ مشخص تعریف کنیم

مجموعهٔ محدود نمونه‌ها با واحد و قرارداد ورودی روشن
تصویر مفهومی: مجموعهٔ محدود نمونه‌ها با واحد و قرارداد ورودی روشن؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فهرست اصلی ۲۰ مقدار با واحد میلی‌ثانیه دارد: هجده مقدار ۱۰، یک مقدار ۱۰۰ و یک مقدار ۱۰۰۰. هر قلم نمایندهٔ یک مدت فرضی است؛ از تفاضل دو ساعت یا گزارش سرور استخراج نشده است. مجموع برابر ۱۲۸۰ و میانگین برابر ۶۴ میلی‌ثانیه است. وجود مقدار بزرگ‌تر باعث نمی‌شود آن را بی‌دلیل حذف کنیم؛ هدف همین است که اثر بخش کندتر توزیع را در کنار مرکز آن ببینیم.

ورودی تابع فقط فهرست یا تاپل متناهی و غیرتهی است. هر عضو باید دقیقاً int یا float، نامنفی و پس از تبدیل به float متناهی باشد. بولی، رشتهٔ عددی، NaN و بی‌نهایت رد می‌شوند؛ بررسی متناهی بودن با math.isfinite انجام می‌شود. صفر در این قرارداد مجاز است؛ پذیرش آن ادعای امکان تأخیر صفر در شبکهٔ واقعی نیست. تبدیل عدد صحیح بزرگ به float ممکن است دقت را کم کند؛ دادهٔ آموزشی کوچک انتخاب شده و محاسبهٔ دقیق دلخواه برای تمام اعداد صحیح وعده داده نمی‌شود.

مرز تأخیر نیز بخشی از تعریف داده است. در اندازه‌گیری واقعی باید روشن کنید زمان صف، پردازش و انتقال کدام‌یک داخل مقدار هستند و با خطا یا پایان مهلت چه می‌کنید. درس صف محدود دادهٔ حسگر رفتار انتظار را مدل می‌کند، اما عددهای این مقاله از اجرای آن صف گرفته نشده‌اند و مقایسهٔ سرعت دو پیاده‌سازی نیستند.

۲. نزدیک‌ترین رتبه یک مقدار مشاهده‌شده را انتخاب می‌کند

روش نزدیک‌ترین رتبه یک نمونهٔ موجود را انتخاب می‌کند
تصویر مفهومی: روش نزدیک‌ترین رتبه یک نمونهٔ موجود را انتخاب می‌کند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در قرارداد این درس، برای درصد صحیح p از یک تا صد و تعداد مثبت N، رتبهٔ یک‌مبنایی را با ceil(p*N/100) می‌یابیم. سپس همان عضو فهرست مرتب‌شده را برمی‌داریم. نام نزدیک‌ترین رتبه در اینجا به معنی گرد کردن معمولی به نزدیک‌ترین عدد صحیح نیست؛ سقف کسر را می‌خواهیم. کد این سقف را با حساب صحیح محاسبه می‌کند تا گردشدن اعشاری رتبه را جابه‌جا نکند.

تفسیر معادل آن، کوچک‌ترین مقدار موجودی است که دست‌کم p درصد نمونه‌ها کوچک‌تر یا مساوی آن‌اند. راهنمای رسمی quantile در R روش معکوس توزیع تجربی را در نوع اول توضیح می‌دهد. ما این قرارداد را مستقلاً در پایتون پیاده می‌کنیم و R اجرا نمی‌کنیم. اگر مقدارهای تکراری داشته باشیم، سهم مقادیر کوچک‌تر یا مساوی صدک می‌تواند از درصد درخواستی بیشتر باشد.

برای P95 فهرست اصلی، حاصل ۹۵ ضربدر ۲۰ تقسیم بر صد، ۱۹ است؛ عضو نوزدهم ۱۰۰ میلی‌ثانیه خواهد بود. برای P99، رتبه سقف ۱۹٫۸ یعنی ۲۰ و خروجی ۱۰۰۰ است. این دو مقدار ویژگی همان ۲۰ قلم‌اند. از آن‌ها نتیجه نمی‌گیریم دقیقاً ۹۹ درصد درخواست‌های آینده در یک سرویس ناشناخته زیر هزار میلی‌ثانیه تمام می‌شوند.

رابط نمونه درصد صفر، عدد اعشاری مانند ۹۵٫۰ و بولی را نمی‌پذیرد؛ این محدودیت ورودی انتخاب ماست، نه محدودیت عمومی مفهوم صدک. تابع رتبه، N صفر یا منفی را هم رد می‌کند. تعداد نمونه از طول داده به دست می‌آید و با تعداد قسمت‌های درخواستی برای درون‌یابی فرق دارد؛ این دو را در گزارش با یک نام مبهم ثبت نکنید.

۳. درون‌یابی مقدار دیگری می‌سازد؛ کد و مرجع مستقل

درون‌یابی می‌تواند مقداری میان دو نمونه بسازد
تصویر مفهومی: درون‌یابی می‌تواند مقداری میان دو نمونه بسازد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تابع quantiles پایتون با روش inclusive میان نقاط درون‌یابی خطی می‌کند. در دادهٔ اصلی جایگاه یک‌مبنایی P95 برابر 1+0.95*(20-1)=19.05 است. بنابراین پنج صدم فاصلهٔ میان ۱۰۰ و ۱۰۰۰ را اضافه می‌کنیم و ۱۴۵ به دست می‌آید. این مقدار عضو فهرست نیست؛ اختلاف با ۱۰۰، ناشی از تعریف روش است و به تنهایی خطای محاسبه محسوب نمی‌شود.

راهنمای صدک NIST چند قرارداد رایج را توضیح می‌دهد و نبود یک شیوهٔ واحد برای درون‌یابی را یادآور می‌شود. فرمول پیش‌فرض آن صفحه را نباید همان نزدیک‌ترین رتبهٔ ما نامید. در گزارش باید روش را صریح بنویسیم. گزینهٔ inclusive نیز در اینجا برای توصیف همین مجموعه انتخاب شده؛ نشان نمی‌دهد کمینه و بیشینهٔ واقعی همهٔ تأخیرهای آینده را می‌شناسیم.

فایل را با نام latency_percentiles.py ذخیره و اجرا کنید. Python 3.14.5 استفاده شده؛ مستندات خوانده‌شده نسخهٔ 3.14.8 دارند. کد ۱۸ بررسی نام‌دار دارد و نتیجهٔ نزدیک‌ترین رتبه را برای همهٔ درصدهای صحیح با شمارش مستقل تابع توزیع تجربی مقایسه می‌کند. برای صد، پوشش‌دهندهٔ ما مستقیماً بیشینه را برمی‌گرداند؛ خود quantiles با صد قسمت، ۹۹ نقطهٔ میانی می‌دهد.

"""Synthetic latency samples only: empirical nearest-rank vs inclusive interpolation."""
import json
import math
import platform
import statistics


FIXTURE_MS = [10] * 18 + [100, 1000]


def checked_samples(values):
    if type(values) not in (list, tuple):
        raise TypeError('A finite list or tuple of milliseconds is required')
    if not values:
        raise ValueError('At least one sample is required')
    result = []
    for value in values:
        if type(value) not in (int, float):
            raise TypeError('Only int or float samples; bool and strings are excluded')
        try:
            converted = float(value)
        except OverflowError as error:
            raise ValueError('Sample cannot be represented as a finite float') from error
        if not math.isfinite(converted) or converted < 0:
            raise ValueError('Finite nonnegative milliseconds are required')
        result.append(converted)
    return result


def rank1(n, percent):
    if type(n) is not int or n < 1:
        raise ValueError('N must be an exact positive integer')
    if type(percent) is not int or not 1 <= percent <= 100:
        raise ValueError('Percent must be an exact integer from1 through100')
    # Exact ceiling of percent*N/100, without float rounding of the rank.
    return (percent * n + 99) // 100


def nearest_rank(values, percent):
    samples = checked_samples(values)
    ordered = sorted(samples)
    return ordered[rank1(len(ordered), percent) - 1]


def inclusive_percentile(values, percent):
    samples = checked_samples(values)
    rank1(len(samples), percent)  # Apply the same explicit percent contract.
    if percent == 100:
        return max(samples)
    result = statistics.quantiles(samples, n=100, method='inclusive')[percent - 1]
    if not math.isfinite(result):
        raise ArithmeticError('Interpolation result is nonfinite; review numeric scale')
    return result


def empirical_cdf_oracle(values, percent):
    # Independent finite enumeration of the smallest observed value reaching p%.
    for value in sorted(set(values)):
        if 100 * sum(sample <= value for sample in values) >= percent * len(values):
            return value
    raise AssertionError('Nonempty valid fixture must have a quantile')


def rejects(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    original = list(FIXTURE_MS)
    p95 = nearest_rank(FIXTURE_MS, 95)
    interpolated = inclusive_percentile(FIXTURE_MS, 95)
    small = [10, 20, 30, 40, 100]
    group_a = [10] * 19 + [1000]
    group_b = [100] * 20
    group_p95 = [nearest_rank(group_a, 95), nearest_rank(group_b, 95)]
    average_of_p95 = statistics.fmean(group_p95)
    pooled_p95 = nearest_rank(group_a + group_b, 95)
    checks = {
        'nonfinite_interpolation_result_rejected': rejects(lambda: inclusive_percentile([1e308, 1e308], 95), ArithmeticError),
        'mean_matches_hand_reference64ms': statistics.fmean(checked_samples(FIXTURE_MS)) == 64.0,
        'nearest_p50_matches_rank10_value10ms': nearest_rank(FIXTURE_MS, 50) == 10.0,
        'nearest_p95_matches_rank19_value100ms': rank1(20, 95) == 19 and p95 == 100.0,
        'nearest_p99_matches_rank20_value1000ms': nearest_rank(FIXTURE_MS, 99) == 1000.0,
        'inclusive_p95_matches_hand_interpolation145ms': interpolated == 145.0,
        'p100_returns_maximum_in_both_methods': nearest_rank(FIXTURE_MS, 100) == inclusive_percentile(FIXTURE_MS, 100) == 1000.0,
        'singleton_is_well_defined_for_both_methods': all(nearest_rank([7.5], p) == inclusive_percentile([7.5], p) == 7.5 for p in (1, 50, 95, 100)),
        'repeated_values_preserved': nearest_rank([2.0] * 8, 95) == 2.0,
        'input_order_does_not_change_nearest_rank': nearest_rank(list(reversed(FIXTURE_MS)), 95) == p95,
        'input_list_is_not_mutated': FIXTURE_MS == original,
        'all100_ranks_match_independent_empirical_cdf_scan': all(nearest_rank(FIXTURE_MS, p) == empirical_cdf_oracle(FIXTURE_MS, p) for p in range(1, 101)),
        'small_n_p95_is_maximum_but_methods_differ': nearest_rank(small, 95) == 100.0 and inclusive_percentile(small, 95) == 88.0,
        'zero_latency_is_allowed': nearest_rank([0, 1], 1) == 0.0,
        'empty_bad_type_negative_and_nonfinite_samples_rejected': all(rejects(lambda value=value: checked_samples(value), exception) for value, exception in
            (([], ValueError), ([True], TypeError), (['10'], TypeError), ('10', TypeError),
             ([-1], ValueError), ([math.nan], ValueError), ([math.inf], ValueError), ([10**1000], ValueError))),
        'invalid_n_rejected': all(rejects(lambda n=n: rank1(n, 95), ValueError) for n in (0, -1, True, 2.0)),
        'invalid_percent_rejected': all(rejects(lambda p=p: nearest_rank(FIXTURE_MS, p), ValueError) for p in (0, 101, True, 95.0)),
        'averaging_group_percentiles_differs_from_pooled_percentile': group_p95 == [10.0, 100.0] and average_of_p95 == 55.0 and pooled_p95 == 100.0,
    }
    assert all(checks.values()), checks
    return {
        'python': platform.python_version(),
        'input_kind': 'authored_finite_synthetic_millisecond_samples',
        'samples_ms': FIXTURE_MS,
        'sample_count': len(FIXTURE_MS),
        'mean_ms': statistics.fmean(checked_samples(FIXTURE_MS)),
        'nearest_rank': {'method': 'smallest_observed_value_with_empirical_cdf_at_least_p',
                         'percent_contract': 'exact_int_1_to100',
                         'p50_ms': nearest_rank(FIXTURE_MS, 50), 'p95_ms': p95,
                         'p99_ms': nearest_rank(FIXTURE_MS, 99), 'p100_ms': nearest_rank(FIXTURE_MS, 100)},
        'inclusive_interpolation': {'method': 'statistics.quantiles(n=100,method=inclusive)', 'p95_ms': interpolated},
        'small_n_example': {'count': len(small), 'nearest_p95_ms': nearest_rank(small, 95), 'inclusive_p95_ms': inclusive_percentile(small, 95)},
        'aggregation_counterexample': {'group_counts': [len(group_a), len(group_b)],
                                      'group_nearest_p95_ms': group_p95,
                                      'average_of_group_p95_ms': average_of_p95, 'pooled_nearest_p95_ms': pooled_p95},
        'actual_service_network_timer_or_performance_measurement': False,
        'checks': checks,
    }


if __name__ == '__main__':
    print(json.dumps(run_example(), indent=2))

۴. خروجی را همراه تعداد نمونه و محدودیت بخوانیم

ترکیب نمونه‌های گروه‌ها با میانگین صدک‌ها یکسان نیست
تصویر مفهومی: ترکیب نمونه‌های گروه‌ها با میانگین صدک‌ها یکسان نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

خروجی ثبت‌شده میانگین ۶۴، نزدیک‌ترین رتبهٔ P95 برابر ۱۰۰ و مقدار درون‌یابی‌شدهٔ آن برابر ۱۴۵ میلی‌ثانیه است. در مثال پنج‌قلمی ۱۰، ۲۰، ۳۰، ۴۰ و ۱۰۰، P95 نزدیک‌ترین رتبه برابر بیشینه یعنی ۱۰۰ می‌شود؛ inclusive مقدار ۸۸ می‌دهد. این محاسبه برای نشان دادن حساسیت روش است. داشتن پنج یا بیست قلم، شواهد قوی برای برآورد دنبالهٔ جامعهٔ واقعی فراهم نمی‌کند؛ زیاد کردن تعداد قسمت‌ها دادهٔ تازه نمی‌سازد.

{
  "python": "3.14.5",
  "input_kind": "authored_finite_synthetic_millisecond_samples",
  "samples_ms": [
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    10,
    100,
    1000
  ],
  "sample_count": 20,
  "mean_ms": 64.0,
  "nearest_rank": {
    "method": "smallest_observed_value_with_empirical_cdf_at_least_p",
    "percent_contract": "exact_int_1_to100",
    "p50_ms": 10.0,
    "p95_ms": 100.0,
    "p99_ms": 1000.0,
    "p100_ms": 1000.0
  },
  "inclusive_interpolation": {
    "method": "statistics.quantiles(n=100,method=inclusive)",
    "p95_ms": 145.0
  },
  "small_n_example": {
    "count": 5,
    "nearest_p95_ms": 100.0,
    "inclusive_p95_ms": 88.0
  },
  "aggregation_counterexample": {
    "group_counts": [
      20,
      20
    ],
    "group_nearest_p95_ms": [
      10.0,
      100.0
    ],
    "average_of_group_p95_ms": 55.0,
    "pooled_nearest_p95_ms": 100.0
  },
  "actual_service_network_timer_or_performance_measurement": false,
  "checks": {
    "nonfinite_interpolation_result_rejected": true,
    "mean_matches_hand_reference64ms": true,
    "nearest_p50_matches_rank10_value10ms": true,
    "nearest_p95_matches_rank19_value100ms": true,
    "nearest_p99_matches_rank20_value1000ms": true,
    "inclusive_p95_matches_hand_interpolation145ms": true,
    "p100_returns_maximum_in_both_methods": true,
    "singleton_is_well_defined_for_both_methods": true,
    "repeated_values_preserved": true,
    "input_order_does_not_change_nearest_rank": true,
    "input_list_is_not_mutated": true,
    "all100_ranks_match_independent_empirical_cdf_scan": true,
    "small_n_p95_is_maximum_but_methods_differ": true,
    "zero_latency_is_allowed": true,
    "empty_bad_type_negative_and_nonfinite_samples_rejected": true,
    "invalid_n_rejected": true,
    "invalid_percent_rejected": true,
    "averaging_group_percentiles_differs_from_pooled_percentile": true
  }
}

برای ترکیب گروه‌ها هم یک نمونهٔ نقض داریم: گروه نخست نوزده مقدار ۱۰ و یک مقدار ۱۰۰۰ دارد؛ گروه دوم بیست مقدار ۱۰۰. صدک نودوپنجم آن‌ها ۱۰ و ۱۰۰ است و میانگینشان ۵۵ می‌شود، در حالی که P95 همهٔ چهل قلم برابر ۱۰۰ است. پس حتی هم‌اندازه بودن گروه‌ها، میانگین صدک‌ها را به صدک مجموع تبدیل نمی‌کند. مستند رسمی Prometheus نیز دربارهٔ تجمیع صدک‌های ازپیش‌محاسبه‌شده همین محدودیت را بیان می‌کند؛ این مثال نصب یا آزمون Prometheus نیست.

بررسی متناهی بودن ورودی، مصونیت تمام عملیات اعشاری از سرریز نیست. کد نتیجهٔ نامتناهی درون‌یابی را نیز صریح رد می‌کند؛ آزمایش عددهای بسیار بزرگ این مسیر را می‌سنجد. نگهداری و مرتب‌سازی همهٔ نمونه‌ها هم حافظه می‌خواهد. آموزش tracemalloc برای دادهٔ حسگر روش مشاهدهٔ تخصیص حافظه را جدا بررسی می‌کند؛ این مقاله ادعای ظرفیت یا کارایی یک سامانهٔ زنده ندارد.

۵. چهار تمرین پاسخ‌دار برای گزارش صدک قابل بازبینی

صدک تجربی محدود به داده و روش ثبت‌شده است
تصویر مفهومی: صدک تجربی محدود به داده و روش ثبت‌شده است؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: چرا P99 دادهٔ اصلی برابر بیشینه است؟ پاسخ: رتبه سقف ۱۹٫۸، یعنی ۲۰ می‌شود؛ عضو بیستم ۱۰۰۰ است. این نتیجهٔ نمونهٔ کوچک است و تضمین دنبالهٔ جامعه نیست.

تمرین دوم: آیا مقدار ۱۴۵ باید به عنوان نمونهٔ گمشده به فهرست اضافه شود؟ پاسخ: خیر؛ فقط خروجی درون‌یابی است. افزودن آن به داده، تعداد و توزیع را عوض می‌کند و مشاهده‌ای ساختگی را جای مشاهدهٔ ورودی می‌نشاند.

تمرین سوم: P95 کل دو گروه را از ۱۰ و ۱۰۰ چگونه پیدا کنیم؟ پاسخ: میانگین ۵۵ کافی نیست. در مثال، داده‌های کامل را کنار هم مرتب می‌کنیم؛ رتبهٔ ۳۸ از چهل قلم، مقدار ۱۰۰ است. صرف دو خلاصه برای بازیابی توزیع کافی نیست.

تمرین چهارم: آیا درخواست صد قسمت از پنج نمونه دقت صد مشاهده را ایجاد می‌کند؟ پاسخ: خیر؛ فقط نقاط برش بیشتری طبق قرارداد می‌سازد. برای بازبینی، تعداد، واحد، مرز مشاهده، روش، دادهٔ ورودی و نسخهٔ اجرای واقعی را همراه خروجی ثبت کنید.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.