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?


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.


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.


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.


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


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.


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.


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.


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.


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.


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


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


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).


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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.