Modernizace legacy .NET: jak jsme zrychlili generování smluv 40×
Velká česká banka generovala smlouvy 2+ hodiny, CPU na 95 %. Po refaktoru: 3 minuty, CPU 15 %. Jak jsme na to přišli a co jsme změnili.
Generování smluv trvalo přes dvě hodiny. CPU serveru jelo na 95 %. Fronta narůstala, bankéři čekali a o víkendu se spouštěl noční batch, který blokoval ostatní systémy. Takhle vypadal výchozí stav u velké české banky, když nás požádali o pomoc.
Tohle není výjimka. Starší .NET systémy v bankách, pojišťovnách a výrobě mají stejný vzor: funguje to, dokud objem nenaroste. Pak to začne drhnout — a každý fix přidá vrstvu obezličky místo toho, aby řešil příčinu.
Symptomy, které říkají „tohle není normální"
Poznat, že máte problém, je snadné. Obtížnější je nepodlehnout prvnímu vysvětlení. Typické symptomy legacy výkonnostního problému:
- Batch joby, které rostou s daty. Před rokem trvaly 20 minut, dnes 3 hodiny — a příští rok? Nikdo to neměřil.
- CPU skokově skáče při konkrétní operaci. Ne průběžně, ale při jednom typu požadavku se server zadýchá.
- Timeout chyby, které „občas jsou". Aplikace funguje, ale jednou za čas se něco nestihne — a nikdo přesně neví proč.
- Kód, který nikdo nechce otevřít. Klíčová metoda má 800 řádků, komentáře z roku 2011 a nikdo se jí netýká, aby „nerozbil produkci".
U banky to bylo jasné: 2+ hodiny pro dávku smluv, CPU nikdy pod 80 % při zpracování. Ale proč?
Nehádat — profilovat
Nejčastější chyba při optimalizaci je začít implementovat řešení předtím, než víte, kde je skutečný bottleneck. Zkušenosti vedou k hypotézám — ale hypotézy jsou špatný základ pro refaktoring produkčního kódu.
Správný postup:
- Profiler, ne intuice. dotnet-trace, PerfView nebo Application Insights ukážou, kde se tráví čas. Ne kde si myslíte, že se tráví čas.
- Měřte konkrétní metriky. Počet SQL dotazů na požadavek, alokace paměti, doby odpovědi per operace. Čísla, ne pocity.
- Hledejte anomálie, ne pomalé části. Metoda, která trvá 50 ms, ale volá se 80 000× za batch, je větší problém než metoda, která trvá 2 sekundy a volá se jednou.
V případě banky ukázal profiler jasně: kód procházel stejný seznam tisíckrát. Ne jednou — tisíckrát. Při každém záznamu znovu iteroval přes celou kolekci, aby našel shodu. Klasický O(n²) problém skrytý za normálně vypadajícím kódem.
Konkrétní techniky, které rozhodly
HashSet a Dictionary místo List pro O(1) vyhledávání
Základní problém v bance: kód používal List<T>.Contains() pro vyhledávání v kolekci tisíců záznamů. Každé Contains projde celý seznam — O(n). Když to děláte 50 000×, dostanete O(n²).
Řešení: přepsat vyhledávací kolekce na HashSet<T> nebo Dictionary<TKey, TValue>. Lookup je pak O(1) bez ohledu na velikost kolekce. Tato jedna změna snížila dobu zpracování o více než polovinu.
Dictionary cache pro opakované výpočty
Druhý velký zdroj plýtvání: stejné výpočty se prováděly znovu a znovu pro stejné vstupy. Každý záznam načítal a přepočítával data, která se pro daný typ smlouvy vůbec nemění.
Řešení: memoizace přes Dictionary. Výsledek se spočítá jednou, uloží pod klíč a při dalším výskytu se rovnou vrátí. Pro delší platnost nebo sdílení napříč instancemi Redis.
Batch processing místo řádek po řádku
Legacy kód zpracovával záznamy jeden po druhém, každý s vlastním SQL dotazem. 10 000 záznamů = 10 000 round trips do databáze.
Řešení: načíst data dávkou (WHERE id IN (...)), zpracovat v paměti, uložit dávkou. Počet databázových dotazů klesl z tisíců na desítky.
Indexy a přepisování SQL dotazů
Profiler SQL dotazů odhalil full table scany na tabulkách s miliony řádků. Chyběly indexy na sloupcích, které se používaly v WHERE klauzulích. Přidání správných indexů a přepsání několika dotazů (odstranění zbytečných SELECT *, použití projekcí) zkrátilo dobu dotazů 10–20×.
Výsledek: 40× rychleji
Po implementaci všech změn:
- Generování smluv: 2+ hodiny → 3 minuty
- CPU při zpracování: 95 % → 15 %
- Noční batch přestal blokovat ostatní systémy
Žádná výměna platformy. Žádný přepis do microservices. Stejná .NET codebase, stejná databáze. Jen oprava skutečných příčin místo přidávání dalšího hardwaru.
Princip: opravit příčinu, ne symptom
Toto je klíčové: každý z výše uvedených problémů měl „rychlou opravu". Přidat server. Zvýšit timeout. Spustit batch v noci, kdy je méně zátěže. Tyto záplaty by fungovaly — na pár měsíců. Pak by byl problém dvakrát větší.
O(n²) algoritmus se nezmění tím, že přidáte RAM. Tisíce zbytečných SQL dotazů se nezrychlí lepším hardwarem. Bottleneck se vždy přesune jinam.
Atlas Copco to potvrzuje z jiné strany: jejich SAP integrační pipeline jsme zredukovali z 15 000 na 3 000 řádků SQL a přešli na event-driven architekturu. Výsledek: 80 % méně kódu, systém, který lze skutečně udržovat.
Jak poznat, jestli máte stejný problém
Projděte si tyto otázky:
- Kolik SQL dotazů provede váš hlavní batch na jeden zpracovaný záznam?
- Jsou vaše klíčové kolekce
List<T>neboHashSet/Dictionary? - Kdy jste naposledy spustili profiler na produkční zátěži?
- Roste čas zpracování lineárně s daty, nebo rychleji?
Pokud nevíte odpovědi, nebo odpovědi jsou nepříjemné — je čas to změnit.
Máme zkušenosti s modernizací legacy .NET systémů v regulovaných odvětvích. Podívejte se na naši službu modernizace nebo nás kontaktujte přímo — řekneme vám, kde je váš systém zranitelný.