Jak navrhnout systém, který se pod loadem nerozsype?

Vojtěch Bobek, Zakladatel

Většina systémů funguje skvěle ve chvíli, kdy se nic zvláštního neděje.

API odpovídá rychle. Databáze má rezervu. Fronty jsou prázdné. Externí služby reagují. Na server přijde tolik požadavků, kolik jsme čekali.

Pak přijde marketingová kampaň, Black Friday, větší zákazník, import několika milionů záznamů nebo prostě rychlejší růst, než jsme plánovali.

Latency začne stoupat. Požadavky čekají. Některé začnou timeoutovat. Klienti je zkusí znovu. Tím vytvoří ještě větší load. Databáze nestíhá, connection pool je plný a během několika minut se problém, který původně existoval v jedné části systému, rozšíří všude.

To je moment, kdy se ukáže rozdíl mezi systémem, který funguje, a systémem, který je skutečně robustní.

Reliability totiž podle mě nezačíná otázkou, jak zabránit každé chybě.

Začíná otázkou:

Co se stane, až některá část systému nevyhnutelně přestane stíhat?

Load není jen počet uživatelů

Když se řekne load, snadno si představíme návštěvnost.

Deset tisíc lidí otevře stránku místo obvyklé tisícovky a potřebujeme více serverů.

V praxi může být problém mnohem méně viditelný.

Stačí, aby jeden endpoint začal dělat dražší databázový query. Aby background worker zpracovával více jobů, než databáze zvládne. Aby externí API začalo odpovídat místo za 100 ms za tři sekundy. Nebo aby několik větších zákazníků spustilo náročnou operaci ve stejnou chvíli.

Systém má omezené množství CPU, paměti, databázových spojení, file descriptors, workerů, bandwidthu i kapacity jednotlivých dependencies.

A většinou neselže proto, že dojdou všechny zdroje najednou.

Stačí jeden.

Kapacitu celého systému určuje jeho nejužší bottleneck.

Pokud frontend zvládne 20 000 requests za sekundu, ale databáze začne kolabovat při 3 000, skutečná kapacita systému není 20 000.

Je blíž těm třem tisícům.

Nejhorší systémy jsou stabilní až do chvíle, kdy najednou nejsou

Ideální chování pod rostoucím loadem je relativně nudné.

Latency začne postupně růst. Některé méně důležité operace se omezí. Část požadavků můžeme odmítnout. Systém dál zpracovává tolik práce, kolik zvládá, a po odeznění špičky se vrátí do normálu.

Mnohem horší je systém, který funguje perfektně na 95 % kapacity a při 105 % se kompletně rozpadne.

Právě to se může stát, když nemáme kontrolu nad tím, kolik práce systém v jednu chvíli přijímá.

Příchozí requesty začnou čekat. Fronta se prodlužuje. Každý request tak v systému stráví více času, takže spotřebovává zdroje déle. Rostoucí latency vede k timeoutům. Klienti začnou requesty opakovat a load dál roste.

Najednou problém není těch původních 10 % provozu navíc.

Systém začal vytvářet další load sám na sebe.

Google ve svých SRE materiálech popisuje právě overload jako jeden z hlavních spouštěčů cascading failures: selhání části kapacity přesune práci na zbytek systému, ten se přetíží také a problém se může rychle šířit dál.

Proto je podle mě jedna z nejdůležitějších vlastností robustního systému schopnost říct:

„Víc práce teď nepřijmu.“

Fronta nevytváří kapacitu

Queues jsou extrémně užitečné.

Oddělí producer od consumeru, umožní zpracovávat práci asynchronně a absorbovat krátké špičky.

Zároveň ale velmi snadno vytvoří falešný pocit bezpečí.

Pokud nám do fronty dlouhodobě přichází 1 000 jobů za minutu a dokážeme jich zpracovat 800, nemáme queueing solution.

Máme rostoucí backlog.

Po hodině čeká 12 000 jobů. Po dni 288 000.

Fronta pouze odložila okamžik, kdy problém uvidíme.

Stejně nebezpečná může být neomezená request queue. Místo okamžitého odmítnutí požadavku necháme tisíce requestů čekat, spotřebujeme paměť a výsledkem je odpověď tak pozdě, že ji klient už stejně nepotřebuje.

Proto AWS mezi doporučeními pro reliable distributed systems explicitně uvádí mimo jiné omezování front, throttling, timeouts a fail-fast chování.

Queue má smysl ve chvíli, kdy víme:

  • jak velký burst chceme absorbovat,
  • kolik práce dokážeme zpracovat,
  • jak dlouho může položka čekat,
  • a co uděláme, když kapacitu překročíme.

Bez limitů se z bufferu snadno stane místo, kam pouze ukládáme budoucí incident.

Retry může outage ještě zhoršit

Retry vypadá jako jeden z nejjednodušších reliability mechanismů.

Request selhal?

Zkusíme ho znovu.

To je rozumné například při krátkém network problému. Jenže při overloadu může být přesný opak toho, co potřebujeme.

Představme si službu, která zvládne 10 000 requestů za sekundu. Dostane 11 000 a tisíc z nich selže.

Pokud všech tisíc klientů okamžitě provede retry, další sekundu už nemusíme řešit 11 000 requestů, ale 12 000.

Část z nich znovu selže.

A další retry wave bude ještě větší.

Google SRE tento mechanismus uvádí jako typickou cestu ke cascading failure a doporučuje retries limitovat a používat exponential backoff s jitterem.

Retry proto není:

„Když to nevyšlo, zkoušej to, dokud to nevyjde.“

Musíme řešit alespoň:

  • jestli je chyba vůbec retryable,
  • kolikrát požadavek zopakovat,
  • jak dlouho mezi pokusy čekat,
  • jestli máme celkový deadline,
  • a kolik retry trafficu systém ještě dokáže bezpečně absorbovat.

Jinak si velmi snadno vytvoříme vlastní DDoS.

Systém musí umět fungovat i hůř

Jedna z mých oblíbených myšlenek v reliability engineeringu je graceful degradation.

Když nemáme dost kapacity na všechno, nemusí být jedinou možností úplný výpadek.

E-shop může například dál umožnit procházet produkty a nakupovat, ale dočasně vypnout personalizovaná doporučení.

Dashboard může zobrazit poslední dostupná data místo toho, aby celý spadl kvůli jednomu pomalému analytickému dotazu.

Search může vrátit jednodušší výsledky.

AI funkce může dočasně přejít na levnější nebo rychlejší model.

Obrázky mohou být servírované v nižší kvalitě.

Background operace mohou počkat.

Princip je jednoduchý:

Ne všechny části produktu mají ve chvíli problému stejnou hodnotu.

Pokud jedna sekundární dependency způsobí, že přestane fungovat celý checkout, vytvořili jsme z ní hard dependency, i když jí být nemusela.

Dobře navržený systém proto potřebuje vědět, co je skutečně kritické a co může při problému degradovat nebo na chvíli zmizet. Google i AWS graceful degradation doporučují jako jednu z cest, jak systém udržet použitelný i při přetížení nebo výpadku dependencies.

Failure by měl zůstat co nejvíc lokální

Distributed systém má jednu nepříjemnou vlastnost: komponenty na sobě závisejí.

Frontend volá API. API databázi. Databáze může záviset na síti a dalších službách. API zároveň používá payment provider, search, storage nebo několik interních microservices.

To znamená, že chyba na jednom místě se velmi snadno přenese dál.

Pokud pomalá dependency nemá timeout, začnou na ni jednotlivé requesty čekat.

Tím obsadí workery nebo connections.

Ty potom chybí ostatním requestům.

A služba, která sama o sobě funguje správně, přestane stíhat kvůli něčemu úplně jinému.

Robustní systém se proto snaží failure izolovat.

Timeouty zabraňují nekonečnému čekání. Circuit breakers mohou dočasně přestat volat dependency, která evidentně nefunguje. Bulkheads oddělují kapacitu tak, aby jeden workload nemohl spotřebovat všechno. Rate limits chrání drahé endpointy před neomezeným provozem.

Smyslem není vytvořit kolem každého requestu deset abstrakcí.

Je jím zabránit tomu, aby selhání jedné části automaticky znamenalo selhání všech ostatních.

Redundance není totéž jako reliability

„Máme dva servery“ zní bezpečněji než „máme jeden server“.

Ale pouze pokud skutečně nemají stejný single point of failure.

Dvě aplikace běžící na dvou instancích příliš nepomohou, pokud obě závisí na jedné databázi, jednom regionu, stejné konfiguraci nebo stejné externí službě.

Stejně tak replika databáze není příliš užitečná, pokud při incidentu nikdo neví, jak na ni přepnout.

Redundance vytváří hodnotu až tehdy, když failure jednoho prvku nezpůsobí zároveň failure jeho zálohy.

Proto bych se u kritických komponent neptal jen:

„Máme backup?“

Ale:

„Jakým způsobem může primární komponenta a její backup selhat současně?“

Často právě tahle otázka odhalí skutečný single point of failure.

Autoscaling není řešení každého load problému

Cloud vytvořil příjemnou představu, že pokud přijde více trafficu, jednoduše přidáme další instance.

U stateless compute vrstvy to může fungovat velmi dobře.

Jenže celý systém se automaticky neškáluje jen proto, že Kubernetes vytvořil dalších deset podů.

Nové instance mohou okamžitě otevřít další databázová spojení a přetížit databázi ještě víc. Mohou potřebovat zahřát cache. Startup může trvat déle než samotná traffic spike. Externí API může mít vlastní rate limit, který počet našich serverů vůbec nezajímá.

Autoscaling řeší pouze kapacitu vrstvy, kterou dokážeme horizontálně škálovat.

Neřeší bottleneck automaticky.

Někdy může situaci dokonce zhoršit.

Proto dává smysl přemýšlet o kapacitě end-to-end, ne podle počtu běžících instancí.

Data jsou pod loadem často nebezpečnější než samotný compute

Aplikaci můžeme často relativně jednoduše spustit desetkrát.

Databázi už méně.

Právě data layer se proto velmi často stává místem, kde se skutečné limity systému projeví.

Najednou narazíme na connection pool. Lock contention. Pomalé query. Hot partitions. Příliš mnoho synchronních zápisů nebo operaci, která byla nad tisícem řádků neškodná, ale nad deseti miliony už není.

Navíc je rozdíl mezi chybou v aplikační vrstvě a chybou v datech.

Spadlá instance se restartuje.

Nekonzistentní nebo duplicitně zapsaná data mohou zůstat problémem dlouho po skončení incidentu.

Proto při návrhu operací, které se mohou opakovat, považuji za důležitou idempotency.

Pokud klient kvůli timeoutu neví, zda payment request proběhl, retry nesmí automaticky vytvořit druhou platbu.

Totéž platí pro objednávky, webhooky, background jobs nebo jakoukoliv operaci, kterou může infrastruktura legitimně provést více než jednou.

Reliability není jen dostupnost.

Je to také schopnost zachovat správný stav systému, když věci neproběhnou ideálně.

Co neměříme, zjistíme až od uživatele

Systém může být technicky online a zároveň prakticky nepoužitelný.

CPU je na 40 %. Servery odpovídají. Health check je zelený.

Jen p99 latency checkoutu je 18 sekund.

Proto nestačí sledovat pouze uptime a utilization.

Chci vědět alespoň:

Latency — a ne jen average. P95 a p99 často ukážou problémy, které průměr schová.

Traffic — kolik skutečné práce jednotlivé části systému dostávají.

Errors — co selhává a jakým způsobem.

Saturation — jak blízko jsme limitům CPU, memory, connection poolů, workerů nebo queue depth.

A potom samozřejmě metrics specifické pro konkrétní systém.

U front mě zajímá backlog a stáří nejstaršího jobu.

U databáze slow queries a connections.

U externí dependency latency a error rate.

U business-critical workflow například procento skutečně dokončených objednávek.

Observability není něco, co přidáváme až po prvním incidentu.

Pokud systém nedokážeme pozorovat, ve chvíli problému pouze hádáme.

Load test má pokračovat i za bod, kde systém začne selhávat

Testovat systém při očekávaných 1 000 requests za sekundu a zjistit, že 1 000 zvládá, je užitečné.

Ještě zajímavější je zjistit, co se stane při 1 500.

A při 2 000.

Kde se objeví první bottleneck? Roste latency postupně? Začne systém odmítat část provozu? Nebo se při určité hranici kompletně zhroutí?

Google SRE doporučuje služby testovat nejen do bodu selhání, ale i za něj, protože právě tak zjistíme, jestli overload vede k řízené degradaci, nebo ke cascading failure.

Zároveň nestačí jeden typ load testu.

Postupný růst trafficu může systém zvládnout jinak než okamžitý spike. Krátký test nemusí objevit memory leak nebo pomalu rostoucí queue. A test s warm cache může mít úplně jiné výsledky než cold start po deploymentu.

Mě proto nezajímá jen:

„Kolik toho systém zvládne?“

Ale také:

„Jak přesně se chová ve chvíli, kdy už to nezvládá?“

Ta druhá odpověď bývá pro produkci důležitější.

Headroom není plýtvání

Pokud systém stabilně funguje do 100 jednotek loadu, provozovat ho dlouhodobě na 99 není efektivita.

Je to čekání na incident.

Potřebujeme rezervu na spike, výpadek části kapacity, deployment, pomalejší dependency nebo jednoduše nepřesnost našeho capacity planningu.

Jak velká má být, záleží na systému.

Jinou rezervu potřebuje interní nástroj, který může být deset minut pomalejší, a jinou payment infrastructure, kde každá minuta výpadku přímo znamená ztracené objednávky.

Reliability je vždycky trade-off.

100% dostupnost není realistický požadavek a extrémní redundance má vlastní cenu i komplexitu.

Cílem proto není postavit systém, který nikdy neselže.

Cílem je vědět:

  • které failures jsou přijatelné,
  • jak velký dopad mohou mít,
  • jak rychle je odhalíme,
  • jak zabráníme jejich šíření,
  • a jak rychle se dokážeme vrátit do normálu.

To je podle mě mnohem praktičtější definice robustnosti.

Co bych chtěl vědět před větším spuštěním

Před významným launch eventem, větší kampaní nebo přechodem na výrazně větší load bych neřešil jen to, jestli jsme „přidali dost serverů“.

Chtěl bych znát odpovědi na několik otázek.

Která komponenta dosáhne limitu jako první?

Co systém udělá, když ten limit překročí?

Máme bounded queues?

Jsou retries omezené a používají backoff?

Mají externí calls rozumné timeouty?

Dokáže produkt degradovat bez úplného výpadku?

Máme kritické single points of failure?

Co se stane, pokud odejde databáze nebo důležitá dependency?

Vidíme problém dřív, než nám napíše zákazník?

Dokážeme rychle snížit load nebo vypnout nekritickou funkcionalitu?

A otestovali jsme tyto mechanismy někdy reálně?

Pokud odpověď na poslední otázku zní ne, není příliš bezpečné předpokládat, že budou fungovat právě během incidentu.

Robustní systém není ten, který nikdy nespadne

Failures jsou v komplexních systémech normální.

Server přestane odpovídat. Network request se ztratí. Databáze se zpomalí. Externí služba má outage. Traffic překročí očekávání. Deployment obsahuje chybu.

Samotná existence failure proto není známkou špatné architektury.

Mnohem víc mě zajímá, co se stane potom.

Rozšíří se chyba do celého systému?

Začnou retries situaci zhoršovat?

Přestane fungovat kritický workflow kvůli nepodstatné dependency?

Budeme deset minut hledat, co se vlastně děje?

Nebo systém problém izoluje, odmítne část práce, zachová kritické funkce a dá nám dostatek informací k rychlé reakci?

Právě v tom podle mě spočívá skutečná reliability.

Ne v pokusu vytvořit prostředí, ve kterém nic nikdy neselže.

Ale v návrhu systému, který dokáže selhat kontrolovaně.

Protože pokud je jediný failure mode vašeho systému úplný kolaps, problém nezačal ve chvíli, kdy přišel vysoký load.

Začal mnohem dřív — při jeho návrhu.

Zdroje

  • Google — Site Reliability Engineering: Handling Overload
  • Google — Site Reliability Engineering: Addressing Cascading Failures
  • AWS — Well-Architected Framework: Reliability Pillar
  • Martin Kleppmann — Designing Data-Intensive Applications
  • Len Bass, Paul Clements & Rick Kazman — Software Architecture in Practice

Další články

Kolik stojí web v roce 2026? Co skutečně ovlivňuje jeho cenu?

Proč může jeden web stát desítky tisíc a jiný stovky? Vysvětlujeme, co cenu webu skutečně ovlivňuje a za co při tvorbě platíte.

Přečíst článek

WordPress, Shopify, Shoptet, nebo web na míru? Jak vybrat správné řešení

WordPress, Shopify, Shoptet, nebo vlastní vývoj? Porovnáváme, kdy jednotlivá řešení dávají smysl a kdy vám naopak začnou překážet.

Přečíst článek

Pojďme probrat Váš projekt

Máte konkrétní zadání, nápad nebo problém, který potřebujete vyřešit? Ozvěte se nám a společně se podíváme na to, jaký další krok dává smysl.

Ozvěte se nám

Kde nás najdete

  • Praha
    Příčná 1892/4
    110 00 Praha 1, Česká republika