Diagnozowanie wycieków pamięci - wyczerpano dozwolony rozmiar pamięci wynoszący # bajtów

98

Napotkałem przerażający komunikat o błędzie, prawdopodobnie poprzez żmudny wysiłek, PHP zabrakło pamięci:

Wyczerpano dozwolony rozmiar pamięci #### bajtów (próbowano przydzielić #### bajtów) w pliku.php w linii 123

Zwiększenie limitu

Jeśli wiesz, co robisz i chcesz zwiększyć limit, zobacz memory_limit :

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

Strzec się! Możesz tylko rozwiązać objaw, a nie problem!

Diagnozowanie wycieku:

Komunikat o błędzie wskazuje na linię z pętlą, która moim zdaniem przecieka lub niepotrzebnie gromadzi pamięć. Wydrukowałem memory_get_usage()instrukcje na końcu każdej iteracji i widzę, że liczba powoli rośnie, aż osiągnie limit:

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

Dla celów niniejszego pytanie załóżmy najgorszy wyobrażalny kod spaghetti ukrywa się w globalnej zakres gdzieś w $userlubTask .

Jakie narzędzia, sztuczki PHP lub debugowanie voodoo mogą pomóc mi znaleźć i rozwiązać problem?

Mike B.
źródło
PS - ostatnio miałem problem z tego typu rzeczami. Niestety odkryłem również, że php ma problem z niszczeniem obiektów podrzędnych. Jeśli usuniesz ustawienie obiektu nadrzędnego, jego obiekty podrzędne nie zostaną zwolnione. Konieczność upewnienia się, że używam zmodyfikowanego unset, który zawiera rekurencyjne wywołanie wszystkich obiektów podrzędnych __destruct i tak dalej. Szczegóły tutaj: paul-m-jones.com/archives/262 :: Robię coś takiego: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ element -> __ destruct (); } unset ($ item); }
Josh

Odpowiedzi:

48

PHP nie ma garbage collectora. Wykorzystuje liczenie referencji do zarządzania pamięcią. Dlatego najczęstszym źródłem wycieków pamięci są cykliczne odwołania i zmienne globalne. Obawiam się, że jeśli używasz frameworka, będziesz musiał przeszukać dużo kodu, aby go znaleźć. Najprostszym narzędziem jest wybiórcze umieszczanie wywołań memory_get_usagei zawężanie ich do miejsca wycieku kodu. Możesz również użyć xdebug, aby utworzyć ślad kodu. Uruchom kod ze śladami wykonywania i show_mem_delta.

troelskn
źródło
3
Ale uważaj ... wygenerowane pliki śledzenia będą OGROMNE. Gdy po raz pierwszy uruchomiłem śledzenie xdebug w aplikacji Zend Framework, uruchomienie zajęło dłuuuga czasu i wygenerowałem plik o rozmiarze wielu GB (nie kb lub MB ... GB). Po prostu bądź tego świadomy.
rg88
1
Tak, jest dość ciężki… Chociaż GB brzmi trochę za dużo - chyba że masz duży scenariusz. Może spróbuj po prostu przetworzyć kilka wierszy (powinno wystarczyć do zidentyfikowania wycieku). Nie instaluj również rozszerzenia xdebug na serwerze produkcyjnym.
troelskn
31
Od wersji 5.3 PHP ma faktycznie garbage collectora. Z drugiej strony funkcja profilowania pamięci została usunięta z xdebug :(
wdev
3
+1 znalazło wyciek! Klasa, która miała cykliczne odwołania! Gdy te odwołania zostały cofnięte (), obiekty zostały zebrane zgodnie z oczekiwaniami! Dzięki! :)
rinogo
@rinogo, jak więc dowiedziałeś się o wycieku? Czy możesz podzielić się swoimi podjętymi krokami?
JohnnyQ,
11

Oto sztuczka, której użyliśmy do zidentyfikowania, które skrypty używają najwięcej pamięci na naszym serwerze.

Zapisz następujący fragment kodu w pliku, np . /usr/local/lib/php/strangecode_log_memory_usage.inc.php:

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

Zastosuj go, dodając do httpd.conf:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

Następnie przeanalizuj plik dziennika pod adresem /var/log/httpd/php_memory_log

Może być konieczne, aby touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logużytkownik sieci Web mógł zapisywać w pliku dziennika.

Quinn Comendant
źródło
8

Zauważyłem kiedyś w starym skrypcie, że PHP zachowywałoby zmienną „as” w zakresie nawet po wykonaniu mojej pętli foreach. Na przykład,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

Nie jestem pewien, czy przyszłe wersje PHP naprawiły to, czy nie, odkąd to widziałem. Jeśli tak jest, możesz unset($user)po doSomething()linii wyczyścić ją z pamięci. YMMV.

patcoll
źródło
13
PHP nie obejmuje pętli / warunków, takich jak C / Java / etc. Wszystko, co zostało zadeklarowane w pętli / warunku, pozostaje w zakresie nawet po wyjściu z pętli / warunku (zgodnie z projektem [?]). Z drugiej strony metody / funkcje mają zasięg, zgodnie z oczekiwaniami - wszystko jest zwalniane po zakończeniu wykonywania funkcji.
Frank Farmer
Zakładałem, że jest to zgodne z projektem. Jedną z korzyści jest to, że po wykonaniu pętli można pracować z ostatnim znalezionym elementem, np. Spełniającym określone kryteria.
joachim
Mógłbyś unset()to, ale pamiętaj, że w przypadku obiektów wszystko, co robisz, to zmienianie miejsca, na które wskazuje zmienna - tak naprawdę nie usunąłeś jej z pamięci. PHP i tak automatycznie zwolni pamięć, gdy znajdzie się poza zasięgiem, więc lepszym rozwiązaniem (pod względem tej odpowiedzi, a nie pytania OP) jest użycie krótkich funkcji, aby nie przywiązywali się do tej zmiennej z pętli długie.
Rich Court
@patcoll Nie ma to nic wspólnego z wyciekami pamięci. Jest to po prostu zmiana wskaźnika tablicy. Spójrz tutaj: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html w wersji 3a.
Harm Smits
7

Istnieje kilka możliwych punktów wycieku pamięci w PHP:

  • php
  • php rozszerzenie
  • biblioteka php, której używasz
  • Twój kod php

Trudno jest znaleźć i naprawić pierwsze 3 bez głębokiej inżynierii wstecznej lub znajomości kodu źródłowego PHP. W ostatnim przypadku możesz użyć wyszukiwania binarnego kodu przeciekającego do pamięci za pomocą funkcji memory_get_usage

Kingoleg
źródło
91
Twoja odpowiedź jest mniej więcej tak ogólna, jak mogła być
TravisO
2
Szkoda, że ​​nawet php 7.2 nie są w stanie naprawić podstawowych wycieków pamięci php. Nie można w nim uruchamiać długo działających procesów.
Aftab Naveed
6

Niedawno napotkałem ten problem na aplikacji, w okolicznościach, które, jak sądzę, są podobne. Skrypt działający w cli PHP, który wykonuje pętle w wielu iteracjach. Mój skrypt zależy od kilku podstawowych bibliotek. Podejrzewam, że przyczyną jest konkretna biblioteka i bezskutecznie spędziłem kilka godzin próbując dodać do jej klas odpowiednie metody niszczenia. W obliczu długiego procesu konwersji do innej biblioteki (która może mieć te same problemy), wymyśliłem prymitywne obejście problemu w moim przypadku.

W mojej sytuacji na linux cli przeglądałem kilka rekordów użytkowników i dla każdego z nich tworzyłem nową instancję kilku klas, które utworzyłem. Postanowiłem spróbować stworzyć nowe instancje klas przy użyciu metody exec PHP, aby proces ten działał w „nowym wątku”. Oto naprawdę podstawowa próbka tego, do czego się odnoszę:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

Oczywiście to podejście ma ograniczenia i należy zdawać sobie sprawę z niebezpieczeństw związanych z tym, ponieważ stworzenie pracy dla królika byłoby łatwe, jednak w niektórych rzadkich przypadkach może to pomóc w pokonaniu trudnego miejsca, dopóki nie zostanie znalezione lepsze rozwiązanie , jak w moim przypadku.

Nate Flink
źródło
6

Natknąłem się na ten sam problem i moim rozwiązaniem było zastąpienie wszystkich zwykłych for. Nie jestem pewien szczegółów, ale wygląda na to, że każdy tworzy kopię (lub w jakiś sposób nowe odniesienie) do obiektu. Korzystając ze zwykłej pętli for, uzyskujesz bezpośredni dostęp do elementu.

Gunnar Lium
źródło
5

Proponuję sprawdzić podręcznik php lub dodać plik gc_enable() funkcję zbierania śmieci ... To znaczy, że wycieki pamięci nie wpływają na działanie twojego kodu.

PS: php ma garbage collector, gc_enable()który nie przyjmuje żadnych argumentów.

Kosgei
źródło
3

Niedawno zauważyłem, że funkcje lambda w PHP 5.3 pozostawiają dodatkową pamięć używaną po ich usunięciu.

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

Nie jestem pewien dlaczego, ale wydaje się, że każda lambda zajmuje dodatkowe 250 bajtów, nawet po usunięciu funkcji.

Xeoncross
źródło
2
Chciałem powiedzieć to samo. Zostało to naprawione od 5.3.10 ( # 60139 )
Kristopher Ives
@KristopherIves, dzięki za aktualizację! Masz rację, to już nie jest problem, więc nie powinienem się teraz bać używania ich jak szalonego.
Xeoncross
2

Jeśli to, co mówisz o PHP wykonującym GC tylko po tym, jak funkcja jest prawdą, jest prawdą, możesz zawinąć zawartość pętli wewnątrz funkcji jako obejście / eksperyment.

Bart van Heukelom
źródło
1
@DavidKullmann Właściwie myślę, że moja odpowiedź jest błędna. W końcu to, run()co się nazywa, jest także funkcją, na końcu której powinien nastąpić GC.
Bart van Heukelom
2

Jednym z ogromnych problemów, które miałem, było użycie funkcji create_function . Podobnie jak w funkcjach lambda, pozostawia wygenerowaną tymczasową nazwę w pamięci.

Inną przyczyną wycieków pamięci (w przypadku Zend Framework) jest Zend_Db_Profiler. Upewnij się, że jest wyłączone, jeśli uruchamiasz skrypty w Zend Framework. Na przykład w moim pliku application.ini miałem:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

Uruchomienie około 25 000 zapytań + mnóstwo przetwarzania wcześniej spowodowało, że pamięć osiągnęła niezłe 128 MB (mój maksymalny limit pamięci).

Wystarczy ustawić:

resources.db.profiler.enabled    = false

wystarczyło, żeby nie przekraczał 20 Mb

I ten skrypt działał w CLI, ale tworzył instancję Zend_Application i uruchamiał Bootstrap, więc używał konfiguracji "programistycznej".

To naprawdę pomogło w uruchomieniu skryptu z profilowaniem xDebug

Andy
źródło
2

Nie widziałem tego wyraźnie, ale xdebug świetnie radzi sobie z profilowaniem czasu i pamięci (od 2.6 ). Możesz pobrać informacje, które generuje i przekazać je do wybranego interfejsu GUI: webgrind (tylko czas), kcachegrind , qcachegrind lub innych, a generuje bardzo przydatne drzewa wywołań i wykresy, które pozwolą Ci znaleźć źródła różnych nieszczęść .

Przykład (z qcachegrind): wprowadź opis obrazu tutaj

SeanDowney
źródło
1

Trochę się spóźniłem na tę rozmowę, ale opowiem o czymś związanym z Zend Framework.

Miałem problem z wyciekiem pamięci po zainstalowaniu php 5.3.8 (używając phpfarm) do pracy z aplikacją ZF, która została stworzona z php 5.2.9. Odkryłem, że wyciek pamięci był wyzwalany w pliku httpd.conf Apache, w mojej definicji wirtualnego hosta, gdzie jest napisane SetEnv APPLICATION_ENV "development". Po skomentowaniu tego wiersza wycieki pamięci ustały. Próbuję wymyślić wbudowane obejście w moim skrypcie php (głównie poprzez zdefiniowanie go ręcznie w głównym pliku index.php).

fronzee
źródło
1
Pytanie mówi, że działa w CLI. Oznacza to, że Apache nie jest w ogóle zaangażowany w ten proces.
Maxime
1
@Maxime Słuszna uwaga, nie udało mi się tego złapać, dzięki. No cóż, mam nadzieję, że jakiś przypadkowy pracownik Google skorzysta z notatki, którą tu zostawiłem, ponieważ ta strona pojawiła się dla mnie podczas próby rozwiązania mojego problemu.
fronzee
Sprawdź moją odpowiedź na to pytanie, może to też był Twój przypadek.
Andy
Twoja aplikacja powinna mieć różne konfiguracje w zależności od środowiska. "development"Środowisko ma zwykle kilka zalogowaniu i profilowania, że inne środowiska mogą nie mieć. Skomentowanie wiersza spowodowało, że aplikacja użyła zamiast tego domyślnego środowiska, którym jest zwykle "production"lub "prod". Wyciek pamięci nadal istnieje; kod, który go zawiera, po prostu nie jest wywoływany w tym środowisku.
Marco Roy
0

Nie widziałem tego tutaj, ale jedną rzeczą, która może być pomocna, jest użycie xdebug i xdebug_debug_zval ('nazwa_zmiennej'), aby zobaczyć refcount.

Mogę również podać przykład rozszerzenia php, które przeszkadza: Z-Ray serwera Zend. Jeśli zbieranie danych jest włączone, użycie pamięci będzie się powiększać przy każdej iteracji, tak jakby wyrzucanie elementów bezużytecznych było wyłączone.

HappyDude
źródło