Aplikacja ColdFusion padała w godzinach szczytu przy 30 GB wolnego RAM: sterta JVM zmierzona, naprawiona i udowodniona

Aplikacja Adobe ColdFusion działała bez zarzutu przy niskim ruchu, a w godzinach szczytu zwracała błędy i zwalniała. Zreprodukowaliśmy problem na realnej bazie, zmierzyliśmy dokładny punkt, w którym sterta JVM się wyczerpuje, i naprawiliśmy konfigurację pamięci, z tym samym testem obciążeniowym przed i po jako dowodem.

  • Próg błędu zmierzony: 14 równoczesnych żądań
  • Po naprawie: 0 błędów przy 20 równoczesnych
  • Serwer miał 30 GB wolnego RAM, a aplikacja padała
Duża, prawie pusta ramka z podpisem RAM 40 GB; w niej mała ramka z podpisem JVM 1 GB wypełniona czarnymi klockami i cztery pomarańczowe klocki wychodzące ponad jej górną krawędź

Aplikacja webowa na Adobe ColdFusion z lokalną bazą MariaDB i eksportem raportu, który obciąża pamięć

Wyzwanie

Aplikacja działała poprawnie przy niewielkim ruchu. W godzinach szczytu czas odpowiedzi rósł, część żądań kończyła się błędem 500, a w skrajnych przypadkach serwer wymagał restartu. Objawy przypominały przeciążony serwer aplikacyjny, ale zużycie CPU i wolny RAM systemu nie tłumaczyły awarii.

Zadanie: ustalić, dlaczego aplikacja traci stabilność pod obciążeniem, usunąć przyczynę bez nieuzasadnionej rozbudowy infrastruktury i udowodnić pomiarem, że zmiana rozwiązuje problem. Dwie różne przyczyny mają ten sam objaw, zbyt mała sterta i wyciek pamięci, i wymagają różnych napraw.

Rozwiązanie

Zreprodukowaliśmy problem na realnej bazie: MariaDB z katalogiem 40 000 produktów i endpoint ColdFusion, który czyta piętnaście tysięcy wierszy i buduje z nich raport utrzymywany w pamięci przez czas żądania. Realistyczny wzorzec eksportu lub raportu, około 68 MB sterty na żądanie.

Rampa współbieżności od 1 do 20 żądań znalazła dokładny punkt, w którym sterta się wyczerpuje, potwierdzony w logach ColdFusion. Sprawdzenie po obciążeniu wykluczyło wyciek: pamięć wracała do poziomu bazowego, a zbiór żywy po wymuszonym odśmiecaniu był znikomy. Potem konfigurację JVM poprawiliśmy zgodnie z rzeczywistą konfiguracją produkcyjną, większy limit sterty, limity Metaspace i code cache oraz zrzut pamięci przy błędzie braku pamięci, i powtórzyliśmy identyczny test obciążeniowy.

Wyniki

Próg błędu przed naprawą
Pierwszy OutOfMemoryError przy 14 równoczesnych żądaniach, przy stercie 1 GB
Przed naprawą przy 20 równoczesnych żądaniach
10 z 20 żądań zakończyło się błędem 500
Po naprawie
Od 1 do 20 równoczesnych żądań: każda odpowiedź 200, zero błędów, przy stercie 20 GB
Szczytowe użycie sterty po naprawie
Około 4,8 GB z 18,2 GB dostępnych
Zbiór żywy po wymuszonym odśmiecaniu
Około 140 MB, czyli brak wycieku
Wolny RAM serwera podczas awarii
Około 30 GB z 40 GB, czyli problemem była sterta, nie RAM

Jak pracowaliśmy

  1. Reprodukcja na realnej bazie

    MariaDB z 40 000 produktów i endpoint raportu, który czyta 15 000 wierszy i trzyma wynik w pamięci przez czas żądania, około 68 MB sterty każdy.

  2. Pomiar progu

    Rampa od 1 do 20 równoczesnych żądań, z zapisem kodów HTTP, użycia sterty i każdego OutOfMemoryError w logach ColdFusion.

  3. Wykluczenie wycieku

    Po obciążeniu sterta wracała do poziomu bazowego, a zbiór żywy po wymuszonym odśmiecaniu miał około 140 MB. Większa sterta usuwa więc przyczynę, a nie odsuwa ją w czasie.

  4. Poprawka konfiguracji JVM

    Wartości rzeczywistej konfiguracji produkcyjnej: większy limit sterty, limity Metaspace i code cache oraz zrzut pamięci przy błędzie braku pamięci na wolumenie z miejscem.

  5. Powtórzenie identycznego testu

    Ta sama rampa po restarcie: każde żądanie od 1 do 20 równoczesnych odpowiedziało 200, katalog zrzutów pozostał pusty.

  6. Diagnostyka zostaje włączona

    Następny błąd braku pamięci, jeśli kiedykolwiek wystąpi, zostawi zrzut do analizy. Katalog jest zabezpieczony, bo zrzut może zawierać dane aplikacji.

Wyzwania, które pokonaliśmy

Na domyślnej konfiguracji, z limitem sterty 1 GB, rampa była jednoznaczna: do około 12 równoczesnych raportów aplikacja działała bez zarzutu, przy 14 sterta się wyczerpywała, a przy 20 połowa żądań kończyła się błędem. Wpisy z logów poniżej cytujemy tak, jak logi je zapisały; uogólnione są tylko ścieżka pliku i adres w nazwie wątku.

Dokładne wpisy z logów

Log aplikacji, application.log, wskazuje miejsce w kodzie CFML: linia 24 to zapytanie czytające z bazy, linie 33 i 36 to budowa struktury raportu w pamięci. Kilka wątków roboczych przewraca się w tej samej sekundzie, co pokazuje, że przyczyną jest równoczesność:

"Error","ajp-nio-...-exec-1","10/17/26","14:30:21","","Java heap space The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 24"
"Error","ajp-nio-...-exec-13","10/17/26","14:30:21","","GC overhead limit exceeded The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 36"
"Error","ajp-nio-...-exec-2","10/17/26","14:30:21","","GC overhead limit exceeded The specific sequence of files included or processed is: /app/wwwroot/report.cfm, line: 33"

Log wyjątków, exception.log, potwierdza typ błędu w obu wariantach:

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded

Pełny stacktrace w coldfusion-error.log pokazuje szczegół, który ma największe znaczenie. Najwyższe ramki są w sterowniku MariaDB czytającym pakiet wyniku; niżej warstwa zapytań ColdFusion:

SEVERE: Servlet.service() for servlet [CfmServlet] in context with path [] threw exception [ROOT CAUSE:
java.lang.OutOfMemoryError: Java heap space
    at org.mariadb.jdbc.client.socket.Reader.readPacket(Reader.java:119)
    at org.mariadb.jdbc.client.result.CompleteResult.<init>(CompleteResult.java:100)
    at org.mariadb.jdbc.message.ClientMessage.readPacket(ClientMessage.java:373)
    at org.mariadb.jdbc.client.impl.StandardClient.readResults(StandardClient.java:1463)
    at org.mariadb.jdbc.client.impl.StandardClient.readResponse(StandardClient.java:1382)
    at org.mariadb.jdbc.client.impl.StandardClient.execute(StandardClient.java:1306)
    at org.mariadb.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:270)
    at coldfusion.server.j2ee.sql.JRunPreparedStatement.execute(JRunPreparedStatement.java:98)
    at coldfusion.sql.Executive.executeQuery(Executive.java:1461)
    at coldfusion.sql.SqlImpl.execute(SqlImpl.java:368)
    at coldfusion.tagext.sql.QueryTag.executeQuery(QueryTag.java:1175)

W chwili błędu żądanie pobierało wynik zapytania i materializowało go w pamięci. Otwarte zapytanie utrzymuje żądanie aktywne, a duży wynik wciągany do sterty przewraca ją, gdy takich żądań jest wiele naraz. Ten sam wzorzec widać w ramkach budujących strukturę raportu, linie 33 i 36: sterta kończy się tam, gdzie akurat przydzielana jest pamięć.

„GC overhead limit exceeded” pojawia się przed „Java heap space”

Logi nie zaczynały się od komunikatu, którego wszyscy szukają. Najpierw, wielokrotnie, pojawiał się inny wariant tego samego błędu, GC overhead limit exceeded. Oznacza on, że garbage collector spędzał ponad 98 procent czasu na odzyskiwaniu mniej niż 2 procent sterty: aplikacja formalnie żyła, a praktycznie stała. Dopiero potem pojawił się twardy Java heap space. Kto przeszukuje logi wyłącznie pod kątem tej frazy, przegapi wcześniejszy i znacznie częstszy sygnał ostrzegawczy.

Błąd uderza poniżej limitu równoczesnych żądań ColdFusion

Błąd wystąpił przy 14 równoczesnych żądaniach, poniżej domyślnego limitu 25 jednoczesnych żądań szablonów. Wniosek jest nieintuicyjny: podniesienie tego limitu nie pomaga, pozwala większej liczbie ciężkich żądań naraz zużywać stertę i przyspiesza awarię. Dławienie żądań nie zastępuje poprawnie dobranej sterty.

Rosnące zużycie pamięci to jeszcze nie wyciek

Po obciążeniu sterta wracała do około 420 MB. Po naprawie, przy dużej stercie, użycie w spoczynku potrafiło pokazywać 4,5 GB, co na pierwszy rzut oka wygląda jak wyciek. Po wymuszonym odśmiecaniu zbiór żywy spadał do około 140 MB; reszta to niezebrane śmieci, bo przy dużej stercie collector się nie spieszy. To rozróżnienie ma znaczenie: gdyby przyczyną był wyciek, większa sterta jedynie odsunęłaby awarię w czasie.

Zrzut pamięci na pełnej partycji to zrzut, którego nie będzie

Włączyliśmy zrzut pamięci przy błędzie braku pamięci, żeby każda przyszła awaria zostawiła plik do analizy. Pułapka: przy stercie 20 GB zrzut może mieć kilkanaście gigabajtów, a partycja systemowa rzadko ma tyle wolnego. Zrzut, którego po awarii potrzeba najbardziej, po cichu by się nie zapisał. Ścieżka zrzutu wskazuje więc wolumen z realnym zapasem miejsca.

Limit 20 GB, a JVM pokazuje 18,2 GB

Po ustawieniu limitu na 20 GB ColdFusion raportował maksymalną stertę około 18 205 MB. To nie błąd ani utracona pamięć: przy równoległym garbage collectorze liczba, którą widzi aplikacja, pomija jedną z przestrzeni survivor. Brakujące dwa gigabajty są na miejscu, tylko nie liczą się do widocznego limitu. Ten sam efekt widać przy domyślnej stercie, która raportuje około 911 MB z 1024, jak opisano w projekcie instalacji.

Wykres słupkowy równoczesnych żądań od 2 do 20: pełne czarne słupki do 12, od 14 pomarańczowe obrysy słupków z pomarańczowym krzyżykiem nad każdym i przerywana linia progu między 12 a 14

Ten sam test obciążeniowy przed i po

Równoczesne żądania Przed naprawą, sterta 1 GB Po naprawie, sterta 20 GB
1 do 12 Wszystkie odpowiedzi 200 Wszystkie odpowiedzi 200
14 Pierwszy OutOfMemoryError, 4 błędy Wszystkie odpowiedzi 200
16 6 błędów Wszystkie odpowiedzi 200
18 8 błędów Wszystkie odpowiedzi 200
20 10 błędów Wszystkie odpowiedzi 200, szczyt sterty około 4,8 GB

Co z tego wynika

  • Monitorować stertę JVM, nie tylko RAM serwera. Wolna pamięć systemu i wolna sterta to różne wskaźniki; aplikacja może paść przy 30 GB wolnego RAM.
  • Alarmować na oba warianty błędu. GC overhead limit exceeded to wcześniejszy sygnał, Java heap space późniejszy.
  • Nie mylić dławienia żądań z naprawą. Jeśli błąd występuje poniżej limitu równoczesnych żądań, podnoszenie limitu pogarsza sprawę.
  • Odróżniać małą stertę od wycieku. Sprawdzić, czy pamięć wraca po obciążeniu i jaki jest zbiór żywy po wymuszonym odśmiecaniu. Większa sterta leczy małą stertę, nie wyciek.
  • Dobierać stertę do pomiarów i do RAM. Ani za mała, czyli błędy, ani za duża, czyli głodzenie systemu i dłuższe pauzy pełnego odśmiecania. 20 GB było bezpieczne na serwerze z 40 GB; na mniejszym byłoby błędem.
  • Przygotować diagnostykę, zanim będzie potrzebna. Włączyć zrzut pamięci i wskazać katalog z realnym zapasem miejsca.
  • Udowadniać pomiarem. Ten sam test obciążeniowy przed i po zamienia „chyba działa” w liczbę.

Źródła

Dwie sterty obok siebie: wąska z podpisem 1 GB, pełna po brzegi, z pomarańczowymi klockami ponad krawędzią, i szeroka z podpisem 20 GB wypełniona w jednej czwartej, z przerywaną linią na poziomie wypełnienia

Ustawienia JVM przed i po (stan na 2026-10)

Ustawienie Przed Po
Minimalny rozmiar sterty, -Xms 256 MB 256 MB
Maksymalny rozmiar sterty, -Xmx 1 024 MB 20 480 MB
-XX:MaxMetaspaceSize Nieustawione 2 560 MB
-XX:ReservedCodeCacheSize Nieustawione 256 MB
Diagnostyka Brak -XX:+HeapDumpOnOutOfMemoryError ze ścieżką zrzutu na wolumenie z zapasem
Środowisko Adobe ColdFusion 2025, Java 21 z równoległym garbage collectorem, MariaDB 10.11

Powiązane sytuacje

  • Programista złożył wypowiedzenie albo już odszedł, a system musi działać. Sprawdzamy system i w ciągu 48 godzin po uzyskaniu dostępu przekazujemy Państwu pisemny raport ze stanem systemu, ryzykiem i planem stabilizacji.

  • Firma Adobe wystosowała oficjalny komunikat: od 10 listopada 2025 r. ColdFusion 2021 nie otrzymuje poprawek bezpieczeństwa. Porównujemy cztery rozwiązania z terminami i ryzykiem (stan: 2026-10): zostać i zabezpieczyć instalację, zaktualizować do wspieranej wersji, przejść na rozwiązanie open source Lucee albo stopniowo zastąpić cały system.

Inne projekty

  • Skala punktów z przerywanym progiem: koperta phishingu przechodzi pomarańczowym łukiem przez próg

    Filtr antyspamowy, który działał, ale nie chronił

    Usługi uruchomione, konfiguracja poprawna, w statusie systemu żadnych błędów. A mimo to phishing lądował w skrzynkach odbiorczych, a newslettery partnerów w folderze spam. Nasi inżynierowie zmierzyli, które mechanizmy wykrywania naprawdę działają, usunęli przyczyny, a trzy miesiące później znaleźli i naprawili regresję, którą było widać dopiero w ujęciu godzinowym.

  • Cztery koperty przechodzą przez bramki SPF, DKIM i DMARC, odhaczone na pomarańczowo, w drodze do skrzynki odbiorczej

    Dostarczalność poczty dla czterech domen firmowych

    Serwer pocztowy czterech domen firmowych niczego nie podpisywał, nie publikował polityki DMARC i był odrzucany przez Microsoft. Nasi inżynierowie sprawdzili go, przetestowali zmianę na jednej domenie i wdrożyli ją partiami. Efekt: wszystkie trzy mechanizmy przechodzą sprawdzenie w Gmailu, blokada Microsoftu została zdjęta, a użytkownicy niczego nie odczuli.

W 20 minut ustalimy, jaki pierwszy krok pasuje do Państwa systemu

Państwo opisują cel i obecny stan, my mówimy otwarcie, czy i jak możemy pomóc. Odpowiedź w ciągu jednego dnia roboczego, bez zobowiązań.

Telefon (niemiecki, polski, angielski) +48 501 250 220
E-mail E-mail

Dostępni: poniedziałek–piątek, 9:00–17:00 (czas środkowoeuropejski); wsparcie i awarie całą dobę (24/7)