← Powrót na blog

Modernizacja legacy .NET: jak przyspieszyliśmy generowanie umów 40×

Duży czeski bank generował umowy ponad 2 godziny, CPU na 95%. Po refaktorze: 3 minuty, CPU 15%. Jak znaleźliśmy wąskie gardło i co zmieniliśmy.

Generowanie umów trwało ponad dwie godziny. CPU serwera pracowało na 95%. Kolejka rosła, pracownicy banku czekali, a w weekendy nocny batch blokował wszystkie pozostałe systemy. Taki był punkt wyjścia, gdy duży czeski bank poprosił nas o pomoc.

To nie jest wyjątek. Starsze systemy .NET w bankach, ubezpieczalniach i produkcji podążają tym samym wzorcem: wszystko działa, dopóki wolumen nie urośnie. Potem zaczyna się zacierać — a każda poprawka dodaje kolejną łatę zamiast rozwiązywać przyczynę.

Objawy, które mówią „to nie jest normalne"

Rozpoznanie problemu jest łatwe. Trudniejsza jest odporność na pierwsze wytłumaczenie. Typowe objawy wydajnościowe legacy:

  • Batch joby, które rosną wraz z danymi. Rok temu zajmowały 20 minut, dziś 3 godziny — a za rok? Nikt tego nie mierzył.
  • CPU skaczący przy jednej konkretnej operacji. Nie stale, ale jeden typ żądania dusi serwer.
  • Timeout, które „czasem się zdarzają". Aplikacja działa, ale od czasu do czasu coś nie wyrabia w terminie — i nikt nie wie dokładnie dlaczego.
  • Kod, którego nikt nie chce otwierać. Kluczowa metoda ma 800 linii, komentarze z 2011 roku i wszyscy jej nie ruszają, żeby „nie zepsuć produkcji".

W przypadku banku było to jasne: 2+ godziny na batch umów, CPU nigdy poniżej 80% podczas przetwarzania. Ale dlaczego?

Nie zgadywać — profilować

Najczęstszy błąd przy optymalizacji to wdrażanie rozwiązania zanim wiesz, gdzie jest prawdziwe wąskie gardło. Doświadczenie prowadzi do hipotez — ale hipotezy to zły fundament dla refaktoryzacji produkcyjnego kodu.

Właściwe podejście:

  1. Profiler, nie intuicja. dotnet-trace, PerfView lub Application Insights pokazują, gdzie faktycznie spędza się czas. Nie tam, gdzie myślisz.
  2. Mierz konkretne metryki. Liczba zapytań SQL na żądanie, alokacje pamięci, czasy odpowiedzi per operacja. Liczby, nie odczucia.
  3. Szukaj anomalii, nie tylko wolnych części. Metoda, która trwa 50 ms, ale wywołana jest 80 000 razy podczas batcha, to większy problem niż metoda 2-sekundowa wywoływana raz.

W przypadku banku profiler był jednoznaczny: kod iterował po tej samej liście tysiące razy. Nie raz — tysiące razy. Dla każdego rekordu ponownie przeszukiwał całą kolekcję, żeby znaleźć dopasowanie. Klasyczny problem O(n²) ukryty w pozornie zwykłym kodzie.

Konkretne techniki, które zadecydowały

HashSet i Dictionary zamiast List dla wyszukiwania O(1)

Podstawowy problem w banku: kod używał List<T>.Contains() do przeszukiwania kolekcji tysięcy rekordów. Każde Contains przechodzi przez całą listę — O(n). Rób to 50 000 razy i otrzymujesz O(n²).

Rozwiązanie: zamiana kolekcji wyszukiwanych na HashSet<T> lub Dictionary<TKey, TValue>. Lookup wynosi O(1) niezależnie od rozmiaru kolekcji. Ta jedna zmiana skróciła czas przetwarzania o ponad połowę.

Cache Dictionary dla powtarzalnych obliczeń

Drugi duże źródło marnotrawstwa: te same obliczenia były wykonywane wielokrotnie dla tych samych danych wejściowych. Każdy rekord ładował i przeliczał dane, które dla danego typu umowy w ogóle się nie zmieniają.

Rozwiązanie: memoizacja przez Dictionary. Wynik obliczany jest raz, przechowywany pod kluczem i zwracany natychmiast przy następnym trafieniu. Dla dłuższej żywotności lub współdzielenia między instancjami — Redis.

Batch processing zamiast rekord po rekordzie

Kod legacy przetwarzał rekordy jeden po drugim, każdy z własnym zapytaniem SQL. 10 000 rekordów = 10 000 round tripów do bazy danych.

Rozwiązanie: ładowanie danych paczką (WHERE id IN (...)), przetwarzanie w pamięci, zapis paczką. Liczba zapytań do bazy spadła z tysięcy do dziesiątek.

Indeksy i przepisanie zapytań SQL

Profiler SQL ujawnił full table scany na tabelach z milionami wierszy. Brakowało indeksów na kolumnach używanych w klauzulach WHERE. Dodanie właściwych indeksów i przepisanie kilku zapytań (usunięcie zbędnych SELECT *, użycie projekcji) skróciło czasy zapytań 10–20×.

Wynik: 40× szybciej

Po wdrożeniu wszystkich zmian:

  • Generowanie umów: 2+ godziny → 3 minuty
  • CPU podczas przetwarzania: 95% → 15%
  • Nocny batch przestał blokować pozostałe systemy

Żadnej wymiany platformy. Żadnego przepisania na mikroserwisy. Ta sama codebase .NET, ta sama baza danych. Tylko naprawienie prawdziwych przyczyn zamiast dokupowania sprzętu.

Zasada: naprawić przyczynę, nie objaw

To jest kluczowe: każdy z powyższych problemów miał „szybką naprawę". Dodać serwer. Zwiększyć timeout. Uruchomić batch w nocy, kiedy mniejsze obciążenie. Te łatki by zadziałały — na kilka miesięcy. Potem problem byłby dwa razy większy.

Algorytm O(n²) nie zmienia się, gdy dodajesz RAM. Tysiące zbędnych zapytań SQL nie przyspieszają wraz z lepszym sprzętem. Wąskie gardło zawsze przenosi się gdzie indziej.

Atlas Copco potwierdza to z innej strony: zredukowaliśmy ich pipeline integracji SAP z 15 000 do 3 000 linii SQL i przeszliśmy na architekturę event-driven. Wynik: 80% mniej kodu, system który można faktycznie utrzymywać.

Jak sprawdzić, czy masz ten sam problem

Przejdź przez te pytania:

  • Ile zapytań SQL wykonuje Twój główny batch na jeden przetworzony rekord?
  • Czy Twoje kluczowe kolekcje to List<T> czy HashSet/Dictionary?
  • Kiedy ostatnio uruchomiłeś profiler na produkcyjnym obciążeniu?
  • Czy czas przetwarzania rośnie liniowo z danymi, czy szybciej?

Jeśli nie znasz odpowiedzi — albo odpowiedzi są nieprzyjemne — czas to zmienić.

Mamy praktyczne doświadczenie w modernizacji systemów legacy .NET w regulowanych branżach. Zobacz naszą usługę modernizacji lub skontaktuj się z nami bezpośrednio — pokażemy Ci, gdzie Twój system jest narażony.

Masz podobny problem? Napisz do nas.

Umów konsultację