Fangen Sie sicher den Fehler "Zulässige Speichergröße erschöpft" in PHP ab

69

Ich habe ein Gateway-Skript, das JSON an den Client zurückgibt. Im Skript verwende ich set_error_handler , um Fehler abzufangen und trotzdem eine formatierte Rückgabe zu haben.

Es unterliegt den Fehlern "Zulässige Speichergröße erschöpft", aber anstatt das Speicherlimit mit so etwas wie ini_set ("memory_limit", "19T") zu erhöhen , möchte ich nur zurückgeben, dass der Benutzer etwas anderes ausprobieren sollte, weil es zu viel verwendet hat Erinnerung.

Gibt es gute Möglichkeiten, schwerwiegende Fehler zu erkennen?

Matt R. Wilson
quelle

Antworten:

53

Wie aus dieser Antwort hervorgeht , können Sie register_shutdown_function()einen Rückruf registrieren, der überprüft wird error_get_last().

Sie müssen weiterhin die Ausgabe verwalten, die aus dem fehlerhaften Code generiert wurde, sei es durch den @( Shut-Up- ) Operator oderini_set('display_errors', false)

ini_set('display_errors', false);

error_reporting(-1);

set_error_handler(function($code, $string, $file, $line){
        throw new ErrorException($string, null, $code, $file, $line);
    });

register_shutdown_function(function(){
        $error = error_get_last();
        if(null !== $error)
        {
            echo 'Caught at shutdown';
        }
    });

try
{
    while(true)
    {
        $data .= str_repeat('#', PHP_INT_MAX);
    }
}
catch(\Exception $exception)
{
    echo 'Caught in try/catch';
}

Beim Ausführen wird dies ausgegeben Caught at shutdown. Leider wird das ErrorExceptionAusnahmeobjekt nicht ausgelöst, da der schwerwiegende Fehler die Skriptbeendigung auslöst und anschließend nur in der Funktion zum Herunterfahren abgefangen wird.

Sie können das $errorArray in der Funktion zum Herunterfahren auf Details zur Ursache überprüfen und entsprechend reagieren. Ein Vorschlag könnte darin bestehen, die Anfrage erneut an Ihre Webanwendung zu senden ( an einer anderen Adresse oder natürlich mit anderen Parametern ) und die erfasste Antwort zurückzugeben.

Ich empfehle jedoch, einen error_reporting()hohen Wert-1 ( Wert von ) beizubehalten und ( wie andere vorgeschlagen haben ) die Fehlerbehandlung für alles andere mit set_error_handler()und zu verwenden ErrorException.

Dan Lugg
quelle
+1 für allgemeine Eignung angesichts mutwilliger Unrichtigkeit :)
Rdlowrey
4
Falls dies ein Problem für andere ist, sollten Sie bedenken, dass register_shutdown_function Ihren Aufrufstapel verliert, wenn er aufgerufen wird. Sie können es also nicht wirklich verwenden, um herauszufinden, wo Ihr Speicherfehler aufgetreten ist.
Chris Rae
1
@ChrisRae, wenn Sie xdebug enhabled haben, ist xdebug_get_function_stack (), eine debug_backtrace-Alternative, die die gesamte Ablaufverfolgung hat
Brad Kent
40

Wenn Sie bei diesem Fehler Geschäftscode ausführen müssen (Protokollierung, Sicherung des Kontexts für zukünftige Debugs, E-Mail-Versand usw.), reicht die Registrierung einer Funktion zum Herunterfahren nicht aus: Sie sollten den Speicher in gewisser Weise freigeben.

Eine Lösung besteht darin, irgendwo einen Notspeicher zuzuweisen:

public function initErrorHandler()
{
    // This storage is freed on error (case of allowed memory exhausted)
    $this->memory = str_repeat('*', 1024 * 1024);

    register_shutdown_function(function()
    {
        $this->memory = null;
        if ((!is_null($err = error_get_last())) && (!in_array($err['type'], array (E_NOTICE, E_WARNING))))
        {
           // $this->emergencyMethod($err);
        }
    });
    return $this;
}
Alain Tiemblo
quelle
6
Ich fand das völlig dumm, als ich es las - ich dachte, "sicherlich würde eine Art Bereinigung stattfinden, wenn PHP nicht mehr über genügend Speicher verfügt und wenn die Shutdown-Funktion aufgerufen wird, und Sie müssten sich darüber keine Sorgen machen ..." . Ich habe mich sehr geirrt Vielen Dank, dass Sie sich die Zeit genommen haben, dies zu posten - sehr hilfreich!
Joel Cox
1
Eine Sache zu wissen ist, dass Ihr Scheck array (E_NOTICE, E_WARNING)tatsächlich Abschreibungsbenachrichtigungen und andere unerwünschte kleinere Probleme erfasst. In einigen Fällen möchten Sie möglicherweise neu schreiben, um das! Negation und ersetzen Sie es durchin_array($err['type'], array (E_ERROR))
Gans
1
Faszinierend. Arbeiten.
Glutexo
2
Eine etwas schnellere Möglichkeit, Speicher zuzuweisen, ist z new SplFixedArray(65536);. Jedes leere Array-Element verbraucht auf meinem System 16 Bytes.
ColinM
Erstaunlich und großartig. Vielen Dank!
txmail
8

Sie können die Größe des Speichers erhalten bereits durch den Prozess verbraucht durch diese Funktion memory_get_peak_usage Dokumentationen zu verwenden sind http://www.php.net/manual/en/function.memory-get-peak-usage.php Ich denke , es würde Dies ist einfacher, wenn Sie eine Bedingung zum Umleiten oder Stoppen des Prozesses hinzufügen können, bevor das Speicherlimit durch den Prozess fast erreicht ist. :) :)

Christopher Pelayo
quelle
In der Tat ist dies eine Möglichkeit, den Fehler zu erkennen, bevor er auftritt. Ermöglicht es einem, Statusinformationen zur laufenden Aufgabe zu speichern und sie möglicherweise sogar wieder aufzunehmen, sobald sie umgeleitet wurden.
Stefgosselin
1
+1 - Ich stimme zu, dass die Verwaltung des Problems, bevor es zu einem Fehler wird, wahrscheinlich die beste Lösung ist ( und die robustesten Behandlungsoptionen bietet ). Die Fehlerbehandlung für schwerwiegende Fehler hat jedoch auch Vorteile. Eine hybride Lösung, bei der der Speicherstatus nach Möglichkeit verwaltet und bei Bedarf mit Todesfällen umgegangen wird, ist wahrscheinlich der beste Ansatz.
Dan Lugg
1
Sicher, aber Fehler bei der Zuweisung des Skriptspeichers sind oft zu schwer vorherzusagen, da PHP im Allgemeinen nicht mit uns über das interne Speichergeschäft spricht, sodass wir wenig über die Speicherkosten für den Aufruf externer Bibliotheken und DB-Abfragen wissen , Bildmanipulation oder einfach die Verwendung großer mehrdimensionaler Arrays usw. Bei welchem ​​genauen Lastniveau, um die "vorbeugende Panik" auszulösen, kann nur erraten werden. ( Manchmal trotzdem ganz gut.)
Gr.
Ich bin auf ein ähnliches Problem bei der Speicherzuweisung gestoßen, aber in einer anderen Situation: Das hochgeladene Bild, dessen Größe etwa 10 MB JPEG ändert, wächst schnell auf mehr als 100 MB im RAM. Ich habe diese Formel verwendet, um den benötigten RAM zu schätzen: $ width * $ height * 4 * 1.5 + 1048576 von diesem Site- Link . Ich weiß nicht, ob es optimal ist, aber es scheint gut zu funktionieren (~ 70 MB berechnet für einige ~ 60 MB, die wirklich vom Bild zugewiesen wurden)
Fekiri Malek
7

Während die @ alain-tiemblo-Lösung perfekt funktioniert, habe ich dieses Skript eingefügt, um zu zeigen, wie Sie Speicher in einem PHP-Skript außerhalb des Objektbereichs reservieren können.

Kurzfassung

// memory is an object and it is passed by reference
function shutdown($memory) {
    // unsetting $memory does not free up memory
    // I also tried unsetting a global variable which did not free up the memory
    unset($memory->reserve);
}

$memory = new stdClass();
// reserve 3 mega bytes
$memory->reserve = str_repeat('❤', 1024 * 1024);

register_shutdown_function('shutdown', $memory);

Vollständiges Beispielskript

<?php

function getMemory(){
    return ((int) (memory_get_usage() / 1024)) . 'KB';
}

// memory is an object and it is passed by reference
function shutdown($memory) {
    echo 'Start Shut Down: ' . getMemory() . PHP_EOL;

    // unsetting $memory does not free up memory
    // I also tried unsetting a global variable which did not free up the memory
    unset($memory->reserve);

    echo 'End Shut Down: ' . getMemory() . PHP_EOL;
}

echo 'Start: ' . getMemory() . PHP_EOL;

$memory = new stdClass();
// reserve 3 mega bytes
$memory->reserve = str_repeat('❤', 1024 * 1024);

echo 'After Reserving: ' . getMemory() . PHP_EOL;

unset($memory);

echo 'After Unsetting: ' . getMemory() . PHP_EOL;

$memory = new stdClass();
// reserve 3 mega bytes
$memory->reserve = str_repeat('❤', 1024 * 1024);

echo 'After Reserving again: ' . getMemory() . PHP_EOL;

// passing $memory object to shut down function
register_shutdown_function('shutdown', $memory);

Und die Ausgabe wäre:

Start: 349KB
After Reserving: 3426KB
After Unsetting: 349KB
After Reserving again: 3426KB
Start Shut Down: 3420KB
End Shut Down: 344KB
hpaknia
quelle
1
Eine etwas schnellere Möglichkeit, Speicher zuzuweisen, ist z new SplFixedArray(65536);. Jedes leere Array-Element verbraucht auf meinem System 16 Bytes.
ColinM