Към съдържанието
wannacode

Нефункционални изисквания, които се мерят

Накратко: Нефункционалното изискване казва колко добре работи системата (бързина, наличност) и се записва като сценарий с мярка, например p95 и p99, а не като „да е бързо“.

Проблем

Таблото е зелено, а клиентите чакат

Собственикът на магазина казва на екипа: „Магазинът трябва да е бърз и да не пада.“ Екипът слага на таблото средното време на плащането с цел „под 500 ms“. Ето последните 20 плащания от лога:

# 20 плащания от лога, в милисекунди
times = [120, 95, 130, 110, 105, 2400, 140, 98, 115, 125,
101, 135, 990, 108, 122, 99, 118, 1500, 112, 104]
average = sum(times) // len(times)
print(f"средно: {average} ms")
print("цел: под 500 ms", "✓" if average < 500 else "✗")
Изход
средно: 341 ms
цел: под 500 ms ✓

Таблото е зелено. А поддръжката получава оплаквания: „Натиснах „Плати“ и чаках две секунди. Натиснах пак.“ И таблото, и клиентите казват истината.

„Бързо“ не казва бързо за кого, при какво натоварване и мерено как. „Да не пада“ не казва колко минути в месеца са приемливи. Такова изискване не може да се провери, затова и не може да се изпълни.

Идеята

Работата на архитекта започва с числа

Този модул е за истинската работа на архитекта: решения, които струват скъпо, ако се сменят по-късно, и как се защитават с числа, а не с вкус. В края те чака първата ти архитектурна задача: магазинът тръгва в три държави и пуска външни търговци. Ти я превръщаш в измерими изисквания, предлагаш поне два варианта с цената им, записваш решението в ADR и планираш миграцията на живата система така, че всяка стъпка да може да се върне.

Първото умение е тук. Функционалното изискване казва какво прави системата: „клиентът плаща с карта“. Нефункционалното изискване (NFR, non-functional requirement) казва колко добре: колко бързо, колко често работи, колко клиенти поема, колко струва. Архитектурата се решава главно от вторите: една и съща функция може да е един сървър или три държави с реплики.

NFR, което се мери, се пише като сценарий на качеството:

  • източник: клиент в мобилното приложение;
  • стимул: натиска „Плати“;
  • среда: обикновен ден, до 50 плащания в секунда;
  • артефакт: веригата на поръчката;
  • отговор: поръчката е платена или клиентът вижда ясна грешка;
  • мярка: 95% от плащанията отговарят до 800 ms, а 99% до 2 s, мерено на сървъра за последните 7 дни.

Числата тук са пример: истинските идват от бизнеса и от това, което системата днес може. Важното е, че след седмица всеки може да каже „изпълнено“ или „не“.

Идеята

Персентили вместо средно

Подреди времената от най-бързото до най-бавното. Персентил p95 е времето, под което са 95% от заявките: само 5 от 100 са по-бавни. p50 е средата (медианата), а p99 е времето, което чака един клиент от сто.

Ето как се намира с най-близкия ранг: от n подредени времена p-тият персентил е числото на място ⌈p · n / 100⌉. Делението с цели числа и +99 закръгля нагоре, без дроби:

# най-близкият ранг: ceil(p · n / 100), с цели числа
def percentile(ordered, p):
rank = (p * len(ordered) + 99) // 100
return ordered[rank - 1]

Същите 20 плащания:

ordered = sorted(times)
for p in [50, 95, 99]:
print(f"p{p}: {percentile(ordered, p)} ms")
Изход
p50: 115 ms
p95: 1500 ms
p99: 2400 ms

Половината клиенти чакат до 115 ms. Но p95 е 1,5 секунди: всеки двадесети чака поне толкова. Средното от 341 ms не описва нито един клиент: бързите са под него, а бавните са далеч над него. Затова изискванията за скорост се пишат с персентили, а средното остава за други неща, например колко процесорно време плащаш.

Идеята

Бюджет на закъснението по веригата

Плащането не е една програма. Заявката минава през шлюза, услугата за поръчки, външния доставчик на плащания и склада. Ако целта е p95 до 800 ms, всяка част получава своя дял:

  • шлюз: 50 ms;
  • поръчки: 150 ms;
  • плащане (външен доставчик): 450 ms;
  • склад: 100 ms;
  • резерв: 50 ms.

Това е бюджет на закъснението. Всеки екип знае своето число, а когато складът поиска 300 ms, разговорът е за цифри: откъде ще дойдат 200 ms? Числата отново са пример.

Има и капан. Когато една страница вика няколко услуги наведнъж и чака всичките, тя е бавна, щом поне една е бавна. Колкото повече услуги, толкова по-често се случва.

Пробвай

Предвиди изхода

Всяка услуга е бавна в 1% от заявките. Страницата вика 1, 5 или 20 такива услуги наведнъж и чака всичките. Каква част от страниците са бавни?

# всяка услуга е бавна в 1% от заявките (над своя p99)
for n in [1, 5, 20]:
slow = 1 - 0.99 ** n
print(f"услуги: {n} → бавни страници: {slow * 100:.2f}%")
Отговор

Идеята

Наличност: процентите се умножават

Наличност (availability) е частта от времето, в която системата отговаря правилно. Обикновено се казва с „деветки“. Ето колко престой позволява всяка за 30 дни (30 × 24 × 60 = 43 200 минути):

  • 99%: 432 минути, малко над 7 часа;
  • 99,9%: 43,2 минути;
  • 99,99%: 4,3 минути.

Поръчката става само ако работят и шлюзът, и поръчките, и плащането, и складът. Ако всяка услуга пада независимо от другите, вероятността всички да работят е произведението на процентите им. Затова веригата е по-слаба от най-слабата си услуга.

Виж го

Веригата от услуги

def chain(services):
total = 1.0
for s in services:
total *= s.availability / 100
print(f"{s.name:<9} {s.availability:.2f}% "
f"→ {total * 100:.2f}%")
return total
# примерни числа, не обещанието на истински доставчик
services = [
Service("шлюз", 99.95),
Service("поръчки", 99.9),
Service("плащане", 99.5),
Service("склад", 99.9),
]
total = chain(services)
down = round((1 - total) * 30 * 24 * 60)
print(f"престой за 30 дни: {down} мин")
Стъпка по стъпка, с думи
  1. Поръчката минава през четири услуги. Тя става само ако всяка от тях работи в този момент. Процентите са примерни.
  2. шлюз: 99,95%. Веригата започва от 100%, а след първата услуга е 99,95%.
  3. поръчки: 99,90%. Веригата дотук: 99,95% × 99,90% = 99,85%.
  4. плащане: 99,50%. Веригата дотук: 99,85% × 99,50% = 99,35%. Външният доставчик на плащания е най-слабата връзка.
  5. склад: 99,90%. Веригата дотук: 99,35% × 99,90% = 99,25%.
  6. Веригата: 99,25%, по-малко от най-слабата ѝ услуга. За 30 дни това са 323 минути, в които никой не може да поръча.

Една услуга е описана с име и наличност:

@dataclass
class Service:
name: str
availability: float
def chain(services):
total = 1.0
for s in services:
total *= s.availability / 100
print(f"{s.name:<9} {s.availability:.2f}% "
f"→ {total * 100:.2f}%")
return total
# примерни числа, не обещанието на истински доставчик
services = [
Service("шлюз", 99.95),
Service("поръчки", 99.9),
Service("плащане", 99.5),
Service("склад", 99.9),
]
total = chain(services)
down = round((1 - total) * 30 * 24 * 60)
print(f"престой за 30 дни: {down} мин")
Изход
шлюз 99.95% → 99.95%
поръчки 99.90% → 99.85%
плащане 99.50% → 99.35%
склад 99.90% → 99.25%
престой за 30 дни: 323 мин

Всяка услуга е 99,5% или повече, но поръчката е 99,25%: 323 минути без поръчки за месец, повече от пет часа. Всяка нова синхронна зависимост сваля числото. Затова складът не трябва да е в пътя на плащането: той може да научи за поръчката по-късно, от събитие в опашка. Тогава срив на склада не спира плащането.

Пробвай

Попълни

Наличността на веригата е произведението на наличностите. Какво липсва?

total = 1.0
for s in services:
    total  s.availability / 100

Пробвай

Намери грешката

Колегата смята наличността на веригата така и обещава на клиент 99,5%, „колкото е най-слабата услуга“. На кой ред е грешката?

Избери реда с грешката.

Идеята

Бюджет за грешки

Три думи, които ще срещнеш:

  • SLI (service level indicator): какво мериш, например „дял на успешните плащания“;
  • SLO (objective): целта, например „99,5% за седмицата“;
  • SLA (agreement): договор с клиент, с неустойка, ако целта не е изпълнена. SLA обикновено е по-слабо от SLO, за да има резерв.

Ако целта е 99,5%, останалите 0,5% са бюджет за грешки. При 60 000 плащания на седмица това са 300 неуспешни. Докато има бюджет, екипът пуска нови версии. Щом свърши, до края на периода се пускат само поправки за надеждност. Така спорът „бързо нови неща или стабилно“ става сметка, а не мнение.

Идеята

Кога вреди

  • „99,99% за всичко“. Всяка следваща деветка иска още копия, дежурства и по-сложна система. Седмичният отчет за счетоводството не иска същото като плащането. Поставяй висока цел само там, където парите или доверието го искат.
  • Изисквания, които никой не мери. Списък от 40 NFR без измерване е шум. По-добре пет, всяко с мярка и с човек, който го следи.
  • Персентил от малко данни. От 20 времена p99 е само най-бавното. Трябват хиляди измервания.
  • Изисквания, които се бият. Повече сигурност е повече проверки, а те са време. По-силна съгласуваност е по-малка наличност. Това е следващият урок: компромисите.

Самото измерване (метрики, табла, аларми) е инфраструктура и се учи в курса по DevOps ZeroToOps. Тук е частта, която решава архитектът: какво се мери и колко е достатъчно.

По-точно, за любопитните

Персентили не се усредняват. p95 на два сървъра не е средното на двата p95. Системите за метрики пазят хистограма (броя заявки във всеки интервал от време) и смятат персентила от сбора: HdrHistogram, хистограмите в Prometheus, OpenTelemetry. Персентилите и по веригата не се събират точно: p95 на сбора не е сборът на p95. Бюджетът на закъснението е план, а не формула.

Coordinated omission. Ако инструментът за натоварване чака отговора, преди да прати следващата заявка, той не мери времената, в които системата е била задръстена. Тогава p99 изглежда по-добре от истината.

Независимост. Умножението приема, че услугите падат независимо. Ако всички са в един център за данни и токът спре, падат заедно. Обратното, две копия на една услуга, които падат независимо, дават 1 − (1 − a)²: две копия по 99% са 99,99%. Затова копията са в различни зони.

Откъде са думите. Сценарият на качеството е от методите на SEI (Software Engineering Institute), например ATAM. ISO/IEC 25010 изброява характеристиките на качеството: производителност, надеждност, сигурност, поддържаемост и други. SLI, SLO и бюджет за грешки идват от книгата на Google за SRE.

@dataclass прави класа с конструктор и __repr__ от полетата, като record в Java и C#.

Упражнение

Бюджет за грешки за една седмица

Реши задачата в редактора си.

Целта за плащанията е 99,5% успешни. Планът е 60 000 плащания за седмицата. Неуспешните по дни са 20, 35, 150, 60 и 50. Напиши budget, който смята колко грешки позволява целта, вади неуспешните ден по ден и накрая казва дали има бюджет за нови версии.

Изход
бюджет: 300 грешки от 60000 заявки
пон: 20 → остават 280
вт: 35 → остават 245
ср: 150 → остават 95
чет: 60 → остават 35
пет: 50 → остават -15
бюджетът е изчерпан: до края на седмицата само поправки

Още задачи

  • Бързина. Добави SLO за скорост: p95 до 800 ms. За всеки ден имаш 20 времена. Кои дни нарушават целта?
  • Две копия. Плащането има два доставчика, които падат независимо, по 99,5%. Ако вторият поема, когато първият падне, колко е наличността на плащането и колко става веригата?

Тест

Провери се

1. Кое от тези е изискване, което може да се провери?
2. Средното време е 341 ms, а p95 е 1500 ms. Какво значи това?
3. Поръчката минава през четири услуги, всяка с наличност 99,9%. Колко е наличността на поръчката?
4. Целта е 99,5% успешни плащания, а бюджетът за грешки за седмицата свърши в сряда. Какво се прави обикновено?
5. Защо p99 от 20 измервания не казва много?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·