Нефункционални изисквания, които се мерят
Накратко: Нефункционалното изискване казва колко добре работи системата (бързина, наличност) и се записва като сценарий с мярка, например p95 и p99, а не като „да е бързо“.
Проблем
Таблото е зелено, а клиентите чакат
Собственикът на магазина казва на екипа: „Магазинът трябва да е бърз и да не пада.“ Екипът слага на таблото средното време на плащането с цел „под 500 ms“. Ето последните 20 плащания от лога:
// 20 плащания от лога, в милисекундиint[] times = {120, 95, 130, 110, 105, 2400, 140, 98, 115, 125, 101, 135, 990, 108, 122, 99, 118, 1500, 112, 104};int average = Arrays.stream(times).sum() / times.length;System.out.println("средно: " + average + " ms");System.out.println("цел: под 500 ms " + (average < 500 ? "✓" : "✗"));средно: 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), с цели числаstatic int percentile(int[] sorted, int p) { int rank = (p * sorted.length + 99) / 100; return sorted[rank - 1];}Същите 20 плащания:
int[] sorted = times.clone();Arrays.sort(sorted);for (int p : new int[] {50, 95, 99}) System.out.println("p" + p + ": " + percentile(sorted, p) + " ms");p50: 115 msp95: 1500 msp99: 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 (int n : new int[] {1, 5, 20}) { double slow = 1 - Math.pow(0.99, n); System.out.printf("услуги: %d → бавни страници: %.2f%%%n", n, slow * 100);}Страницата е бърза само ако всички услуги са бързи: 0,99ⁿ. Бавните са останалите, 1 − 0,99ⁿ. При 20 услуги това е почти всяка пета страница, макар всяка услуга поотделно да изглежда наред.
Идеята
Наличност: процентите се умножават
Наличност (availability) е частта от времето, в която системата отговаря правилно. Обикновено се казва с „деветки“. Ето колко престой позволява всяка за 30 дни (30 × 24 × 60 = 43 200 минути):
- 99%: 432 минути, малко над 7 часа;
- 99,9%: 43,2 минути;
- 99,99%: 4,3 минути.
Поръчката става само ако работят и шлюзът, и поръчките, и плащането, и складът. Ако всяка услуга пада независимо от другите, вероятността всички да работят е произведението на процентите им. Затова веригата е по-слаба от най-слабата си услуга.
Виж го
Веригата от услуги
static double chain(List<Service> services) { double total = 1.0; for (Service s : services) { total *= s.availability() / 100; System.out.printf("%-9s %.2f%% → %.2f%%%n", s.name(), s.availability(), total * 100); } return total;}
// примерни числа, не обещанието на истински доставчикList<Service> services = List.of( new Service("шлюз", 99.95), new Service("поръчки", 99.9), new Service("плащане", 99.5), new Service("склад", 99.9));double total = chain(services);long down = Math.round((1 - total) * 30 * 24 * 60);System.out.println("престой за 30 дни: " + down + " мин");Стъпка по стъпка, с думи
- Поръчката минава през четири услуги. Тя става само ако всяка от тях работи в този момент. Процентите са примерни.
- шлюз: 99,95%. Веригата започва от 100%, а след първата услуга е 99,95%.
- поръчки: 99,90%. Веригата дотук: 99,95% × 99,90% = 99,85%.
- плащане: 99,50%. Веригата дотук: 99,85% × 99,50% = 99,35%. Външният доставчик на плащания е най-слабата връзка.
- склад: 99,90%. Веригата дотук: 99,35% × 99,90% = 99,25%.
- Веригата: 99,25%, по-малко от най-слабата ѝ услуга. За 30 дни това са 323 минути, в които никой не може да поръча.
Една услуга е описана с име и наличност:
record Service(String name, double availability) {}static double chain(List<Service> services) { double total = 1.0; for (Service s : services) { total *= s.availability() / 100; System.out.printf("%-9s %.2f%% → %.2f%%%n", s.name(), s.availability(), total * 100); } return total;}
// примерни числа, не обещанието на истински доставчикList<Service> services = List.of( new Service("шлюз", 99.95), new Service("поръчки", 99.9), new Service("плащане", 99.5), new Service("склад", 99.9));double total = chain(services);long down = Math.round((1 - total) * 30 * 24 * 60);System.out.println("престой за 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 минути без поръчки за месец, повече от пет часа. Всяка нова синхронна зависимост сваля числото. Затова складът не трябва да е в пътя на плащането: той може да научи за поръчката по-късно, от събитие в опашка. Тогава срив на склада не спира плащането.
Пробвай
Попълни
Наличността на веригата е произведението на наличностите. Какво липсва?
double total = 1.0;
for (Service s : services)
total s.availability() / 100;*=: всяка услуга умножава вероятността цялата верига да работи. Започваме от 1.0 (100%), а не от 0, защото нулата би изтрила всичко.
Пробвай
Намери грешката
Колегата смята наличността на веригата така и обещава на клиент 99,5%, „колкото е най-слабата услуга“. На кой ред е грешката?
Избери реда с грешката.
Минимумът е верен само ако всички услуги падат в едни и същи минути. Когато падат поотделно, всяко падане спира веригата, затова наличностите се умножават. С примерните числа истината е 99,25%, а обещанието от 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.
System.out.printf с %n пише нов ред, а %% е самият знак %. Форматът зависи от Locale по подразбиране: с българска ще излезе запетая.Упражнение
Бюджет за грешки за една седмица
Реши задачата в редактора си.
Целта за плащанията е 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%. Ако вторият поема, когато първият падне, колко е наличността на плащането и колко става веригата?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Стабилно сортиране и вграденият sort · Монолит, модулен монолит, услуги · Събития и опашки · ADR: записани архитектурни решения
- Продължава в: Компромиси · Когато съседът е бавен: timeout, retry, circuit breaker · Архитектурен преглед · Enterprise ландшафт и капабилитита
- Задачи по темата: Архитектът в работа