לדלג לתוכן

תקציבים והוכחת ביצועים

נניח שהחלפנו parser והוא מהיר פי שניים. המשתמשים עדיין מחכים, ובשעות עומס אפילו יותר מקודם. ייתכן שהparser מעולם לא היה החלק שקבע את זמן התשובה. ייתכן גם שהשיפור מאפשר להכניס עבודה מהר יותר לתור אחר שכבר היה מלא.

כדי למדוד מערכת משולבת נתחיל מהזמן שהמשתמש רואה ומהעומס שנשלח אליה. מדידת פונקציה פנימית נשארת שימושית, אבל היא צריכה להסביר חלק מהתוצאה הכוללת.

מפרידים תור מעבודה

לכל בקשה נשמור זמן מתוכנן, זמן התחלה וזמן סיום באותו בסיס זמן מונוטוני של מחולל העומס. ההפרש בין התחלה לזמן מתוכנן מראה כמה איחר המחולל או כמה זמן הבקשה המתינה עד נקודת ההתחלה שהגדרנו. ההפרש בין סיום להתחלה מראה את משך המסלול שנמדד. ההפרש בין סיום לזמן המתוכנן שומר גם את עלות האיחור.

המשמעות של Start חייבת להיות כתובה. אם היא זמן שליחה מהclient, ההמתנה בתוך השירות כלולה בהפרש הבא. אם היא תחילת עבודה בworker, צריך timestamp נוסף של קבלה. אי אפשר לשנות את ההגדרה באמצע השוואה ולהמשיך לקרוא לשני המספרים אותו metric.

מנתחים קובץ בלי להעלים כישלונות

הסקריפט הבא מקבל CSV ממחולל עומס. השדות הם RequestId,ScheduledMs,StartMs,EndMs,Status, והסטטוס הוא Success, Failed או Timeout. הזמן נמדד במילישניות יחסית לתחילת אותו ניסוי. השדה Expected הוא מספר הבקשות שתוכננו, כדי שאובדן שורה לא ייראה כמו שיפור.

param(
    [Parameter(Mandatory=$true)][string]$Path,
    [Parameter(Mandatory=$true)][ValidateRange(1,10000000)][int]$Expected
)
$ErrorActionPreference = 'Stop'
$culture = [Globalization.CultureInfo]::InvariantCulture
$rows = @(Import-Csv -LiteralPath $Path)
if ($rows.Count -ne $Expected) { throw 'Missing or extra observations' }
$ids = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
$latency = [Collections.Generic.List[double]]::new()
$queueDelay = [Collections.Generic.List[double]]::new()
$failed = 0
$timeouts = 0
foreach ($row in $rows) {
    if ([string]::IsNullOrWhiteSpace($row.RequestId) -or -not $ids.Add($row.RequestId)) {
        throw 'Empty or duplicate request id'
    }
    $times = @($row.ScheduledMs, $row.StartMs, $row.EndMs) | ForEach-Object {
        $value = [double]::Parse($_, $culture)
        if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) {
            throw 'Invalid time'
        }
        $value
    }
    if ($times[1] -lt $times[0] -or $times[2] -lt $times[1]) { throw 'Invalid order' }
    switch -CaseSensitive ($row.Status) {
        'Success' {
            $latency.Add($times[2] - $times[0])
            $queueDelay.Add($times[1] - $times[0])
        }
        'Failed' { $failed++ }
        'Timeout' { $timeouts++ }
        default { throw 'Unknown status' }
    }
}
function Get-NearestRank([double[]]$Values, [double]$Percentile) {
    if ($Values.Count -eq 0) { return $null }
    $ordered = @($Values | Sort-Object)
    $index = [int][Math]::Ceiling($Percentile * $ordered.Count) - 1
    return $ordered[[Math]::Max(0,$index)]
}
[pscustomobject]@{
    expected = $Expected
    success = $latency.Count
    failed = $failed
    timeout = $timeouts
    successRate = $latency.Count / [double]$Expected
    successP50Ms = Get-NearestRank $latency.ToArray() 0.50
    successP95Ms = Get-NearestRank $latency.ToArray() 0.95
    successP99Ms = Get-NearestRank $latency.ToArray() 0.99
    successStartDelayP99Ms = Get-NearestRank $queueDelay.ToArray() 0.99
} | ConvertTo-Json

הקובץ הבא הוא קלט סינתטי לבדיקת החישוב בלבד. הוא אינו תוצאת benchmark של המוצר.

RequestId,ScheduledMs,StartMs,EndMs,Status
a,0,0,10,Success
b,10,15,40,Success
c,20,25,120,Timeout
d,30,30,35,Failed

הדוח צריך להציג שתי הצלחות, כישלון אחד וtimeout אחד. האחוזונים מוצגים במפורש רק עבור הצלחות. כדי לטעון לעמידה בSLO צריך לשלב אותם עם שיעור הצלחה ועם כלל ברור לגבי deadline; אסור להסיר timeouts מהמדידה ולתאר את המערכת כולה כזריזה.

למה מחולל העומס הוא חלק מהניסוי

מחולל ששולח בקשה חדשה רק כשהקודמת הסתיימה מפחית עומס כאשר המערכת נעשית איטית. זאת יכולה להיות סימולציה נכונה של משתמש מסוים, אבל היא אינה זהה להגעה בקצב חיצוני קבוע. במודל הגעה קבוע מתעדים גם בקשות שלא הצלחנו לשלוח בזמן, אחרת נוצר coordinated omission.

המחולל עצמו צריך תקציב CPU, חיבורים וbuffers. אם הוא נתקע, הtrace של השירות לבדו לא יסביר למה קצב ההגעה ירד. לכן שומרים תצפיות משני הצדדים ומזהה ניסוי משותף.

מחברים זיכרון וקצב

נניח שמגיעות בממוצע 400 עבודות בשנייה, וזמן השהייה הממוצע במערכת יציבה הוא 0.2 שניות. תחת התנאים המתאימים מתקבל ממוצע של כ80 עבודות במערכת לפי הקשר בין קצב, זמן וכמות. זהו ממוצע, ולא קיבולת מספקת לכל burst.

אם כל עבודה מחזיקה buffer של מגהבייט, הגדלת concurrency אינה חינמית. מוסיפים תקציב לbuffers פעילים, לתורים, לjournal ולcache. במוצר מנוהל מסתכלים גם על allocation rate, pauses וpinning; במוצר מעורב לא מחברים מספרים משתי תצוגות זיכרון אם הם סופרים אותם bytes פעמיים.

מוצאים את המסלול שקובע

הtrace מציב native spans, פעילות השירות ופעילות GC על ציר שמאפשר קשר לפי מזהה. מחפשים אם בקשות איטיות ממתינות לlock, לI/O, לdispatcher או לרכיב חיצוני. מתאם עם GC אינו מספיק להוכחת סיבה; בודקים אם אותו thread באמת היה מנוע מלהתקדם ואם הסבר אחר מתאים לראיות.

לא מחברים p99 של רכיבים ומכריזים שזה p99 של הבקשה. האחוזונים אינם בהכרח מתייחסים לאותן בקשות. שומרים זמני קצה לקצה לכל מזהה, ורק אחר כך משתמשים במדידות הרכיבים כדי להסביר את הבקשות שבזנב.

השיפור מתקבל רק אם החוזה הפונקציונלי, שיעור ההצלחה ותקציבי המשאבים נשמרו. מריצים שוב במכונה נוספת עם אותה הגדרת workload, מדווחים את ההבדלים ומציינים מה נשאר תלוי בחומרה. כך benchmark הופך לראיה שאפשר להשתמש בה בהחלטה.