WordPress und das Memory Limit

Es scheint gar nicht so einfach zu sein, zu verstehen was damit konkret gemeint ist. Ich versuche in diesem Artikel mal aufzuzählen, was damit gemeint ist und wie es in WordPress eingestellt werden kann. Ich bin fehlbar und die Sache ist komplexer als ich gedacht habe, daher seht das Ganze mehr als lautes Nachdenken. Ich freue mich über Ergänzungen und Korrekturen, wenn ich irgendwo falsch liege.

Typischerweise fällt einem das memory_limit vor allem dann auf, wenn es überschritten wird und wir folgenden Fehler finden:

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 32 bytes) in your-script.php on line 4

Ein Skript hat versucht 32 Bytes mehr zu benutzen, als erlaubt sind (134217728 Bytes sind 128 MB). Die Einstellung stammt aus der Konfigurationsdatei `php.ini` und limitiert die Speicherbelegung von einzelnen PHP-Skripten.

Die Lösung ist aber nicht diesen Wert beliebig nach oben zu „korrigieren“. Die Limitierung ist absichtlich gesetzt und soll verhindern, dass schlechte Skripte (Endlosschleife, (unabsichtlich) riesige Datenbankabfragen, etc.) den gesamten Speicher eines Servers aufbrauchen und so den Server zum Crashen bringen.

Der Wert sollte also so hoch sein, wie nötig, aber möglichst nicht höher. Für die Optimierung/Reparatur dieser Programmierprobleme soll es hier nicht gehen, sondern darum wie und wo ich den richtigen Wert finde.

Die Abfrage des Limits scheint einfach. Einfach den ini-Wert auslesen:

ini_get( 'memory_limit' )

Auch das Setzen eines neuen Werts ist einfach:

ini_set( 'memory_limit', '256M' )

Problem ist nur, dass PHP nicht prüft, ob der neue Wert von 256M in diesem Fall auch wirklich genutzt werden kann. Es setzt nur einen neuen Höchstwert. Wenn der Wert, wie oben beschrieben, bei 128M liegt, dann würde das Skript bei über 128M Speichernutzung einen Fatal Error erzeugen.

Warum PHP bei dem Versuch einen zu hohen Wert kein FALSE ausgibt, verstehe ich nicht:

if ( ini_set( 'memory_limit', '256M' ) === false ) {
	echo 'FALSE';
}
else {
	echo 'TRUE';
}

Daher machte ich mich auf die Suche und fand verschiedene Möglichkeiten und Probleme:

Zum einen gibt es die Suhosin-Extension, die neben dem PHP Soft Limit noch die Möglichkeit anbietet über suhosin.memory_limit ein Hard Limit einzurichten, was nicht überschritten werden kann.

Dann gibt es noch posix_getrlimit(), was aber bei DomainFactory für soft totalmem und hard totalmem den Wert „524288000“ ausgibt. Korrekt wäre aber 128M. Bei all-inkl.com gibt es mir in beiden Fällen „unlimited“ aus. Auch Quatsch …

Und noch eine Funktion bietet PHP an: get_cfg_var()

Aber bei DomainFactory ist die Ausgabe leer, wenn ich das memory_limit oder etwas anderes abfragen möchte:

get_cfg_var( 'memory_limit' )

Als ich schon gar nicht mehr an eine Lösung glaubte bin ich über noch eine Funktion gestolpert, und zwar ini_get_all(). Ich dachte erst, dass ich hier dieselben Probleme bekomme, wie bei der normalen ini_get-Abfrage, aber mit PHP 5.3 gibt es einen zweiten Parameter DETAILS:

When details is TRUE (default) the array will contain global_value (set in php.ini), local_value (perhaps set with ini_set() or .htaccess), and access (the access level).

Ich kann also den globalen Wert abfragen, der mir bei DomainFactory auch korrekt 128M zurück gibt. Zusätzlich sehe ich aber auch, was gerade lokal gilt (egal ob der Wert unter oder über 128M liegt).

Kommen wir nun zu WordPress …

WordPress holt sich in der default-constants.php mit ini_get( 'memory_limit' ) das aktuelle Limit. Doch dann kommt ein spannender Abschnitt, wo gegebenenfalls vorhandene Konstanten abgefragt werden:

// Define memory limits.
	if ( ! defined( 'WP_MEMORY_LIMIT' ) ) {
		if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) {
			define( 'WP_MEMORY_LIMIT', $current_limit );
		} elseif ( is_multisite() ) {
			define( 'WP_MEMORY_LIMIT', '64M' );
		} else {
			define( 'WP_MEMORY_LIMIT', '40M' );
		}
	}

	if ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ) {
		if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) {
			define( 'WP_MAX_MEMORY_LIMIT', $current_limit );
		} elseif ( -1 === $current_limit_int || $current_limit_int > 268435456 /* = 256M */ ) {
			define( 'WP_MAX_MEMORY_LIMIT', $current_limit );
		} else {
			define( 'WP_MAX_MEMORY_LIMIT', '256M' );
		}
	}

Sofern in der wp-config.php keine eigenen Werte gesetzt werden, gelten die Standardwerte von 40M (bzw. 64M für Multisites) und 256M für das Backend. Der aktuelle Wert wird nur dann übernommen, wenn der Wert nicht geändert werden kann und die Funktion wp_is_ini_value_changeable ein FALSE zurück gibt.

Danach wird der Wert via ini_set( 'memory_limit', WP_MEMORY_LIMIT ) gesetzt.

Zwei Punkte sind hier interessant. Zum einen die Unterscheidung zwischen dem Limit für WordPress generell (WP_MEMORY_LIMIT) und dem Adminbereich (WP_MAX_MEMORY_LIMIT). Siehe auch diesen Helphub-Artikel. Zum anderen interessiert mich, woran WordPress erkennt, ob es den Wert ändern kann. Dafür ist die Funktion wp_is_ini_value_changeable() zuständig.

Schauen wir uns die Funktion mal an:

/**
 * Determines whether a PHP ini value is changeable at runtime.
 *
 * @since 4.6.0
 *
 * @staticvar array $ini_all
 *
 * @link https://www.php.net/manual/en/function.ini-get-all.php
 *
 * @param string $setting The name of the ini setting to check.
 * @return bool True if the value is changeable at runtime. False otherwise.
 */
function wp_is_ini_value_changeable( $setting ) {
	static $ini_all;

	if ( ! isset( $ini_all ) ) {
		$ini_all = false;
		// Sometimes `ini_get_all()` is disabled via the `disable_functions` option for "security purposes".
		if ( function_exists( 'ini_get_all' ) ) {
			$ini_all = ini_get_all();
		}
	}

	// Bit operator to workaround https://bugs.php.net/bug.php?id=44936 which changes access level to 63 in PHP 5.2.6 - 5.2.17.
	if ( isset( $ini_all[ $setting ]['access'] ) && ( INI_ALL === ( $ini_all[ $setting ]['access'] & 7 ) || INI_USER === ( $ini_all[ $setting ]['access'] & 7 ) ) ) {
		return true;
	}

	// If we were unable to retrieve the details, fail gracefully to assume it's changeable.
	if ( ! is_array( $ini_all ) ) {
		return true;
	}

	return false;
}

Es ist schon mal nicht so schön, dass die Funktion deaktiviert sein könnte und der Bug in PHP 5.2.6–5.2.17 ist nach der Anhebung der minimalen Voraussetzungen für WordPress ja auch obsolet – aber prinzipiell hilft hier der dritte Wert, den ini_get_all() noch ausgibt. Und zwar den access Wert.

Bei DomainFactory wird für access der Wert 7 ausgegeben. Aber was bedeutet das? Der Link führt mich zu einer Seite mit dem Titel „Wo Konfigurationseinstellungen gesetzt werden können„. Eine Google-Suche später finde ich auch die Werte vordefinierten Konstanten. Warum die erste Doku-Seite bei den Konstanten noch den Präfix „PHP_“ hat, verstehe ich mal wieder nicht … aber immerhin weiß ich jetzt, dass 7 für „Eintrag kann überall gesetzt werden“ steht, also in der php.ini, .htaccess, httpd.conf, .user.ini und in Benutzerskripte via ini_set().

Den Bug-Workaround mal ignorierend checkt die Abfrage in der Funktion ob INI_ALL oder INI_USER gesetzt ist, also ob „Eintrag kann überall gesetzt werden“ gesetzt ist, oder „Eintrag kann in Benutzerskripten (z.B. mittels ini_set()) oder in der Windows-Registry gesetzt werden. Seit PHP 5.3 kann die Option auch in der .user.ini gesetzt werden.“ – was dann erklärt, warum der Wert via ini_set() änderbar ist.

Warum ist das alles überhaupt interessant?

Es geht wieder mal um ein Ticket: https://core.trac.wordpress.org/ticket/49329

WordPress versucht via wp_raise_memory_limit() das Memory Limit im Backend auf den Wert von WP_MAX_MEMORY_LIMIT zu erhöhen. Für die Ausgabe im Health-Check-Plugin bzw. dessen kleinere Variante im Core verändert dieser Versuch jedoch den Wert. Der Wert müsste also früher oder anders ausgelesen werden, um die richtigen Werte zu ermitteln.

Daher meine Recherche …

Fazit

Ich verstehe nicht, warum mich PHP den Wert höher setzen lässt, als das Limit, was ich zur Verfügung habe. Und auch die Unterschiede bei den Namen der Konstanten verstehe ich nicht. Wer immer mir da helfen kann, ab damit in die Kommentare!

In der Sache scheint mir ini_get_all() der beste Weg zu sein, um verlässlich (sofern die Funktion vorhanden ist) den globalen Wert auszulesen, egal ob vorher versucht wurde den Wert zu ändern. Ein Fallback auf die normale ini_get()-Variante ist einfach möglich und kann (und sollte) so früh erfolgen, dass keine Veränderungen an dem Wert passieren.

Habe ich etwas übersehen, falsch verstanden oder vergessen? Dann ergänzt, korrigiert und überzeugt mich in den Kommentaren!

Ergänzung: Auf Twitter ergab sich bereits eine spannende Diskussion:

2 Antworten auf WordPress und das Memory Limit

  1. Hallo Torsten,

    Super Detektiv-Arbeit hat du hier geleistet!

    Ich habe hier nur ein paar kleine Anmerkungen um die restlichen Detailfragen hoffentlich zu klären.

    > Typischerweise fällt einem das memory_limit vor allem dann auf, wenn es überschritten wird und wir folgenden Fehler finden:

    Die Fehlermeldung „Allowed memory size …“ die du aufführst ist in der Tat spezifisch für das „Memory Limit“ das gesetzt wurde, also dass die „Erlaubnis“ fehlt, mehr Speicher zu reservieren (https://github.com/php/php-src/blob/bb1a68b40c90612d21e2a416e6658dc044d19f30/Zend/zend_alloc.c#L955-L959).

    Es gibt eine verwandte Fehlermeldung die ähnlich klingt aber auf ein anderes Problem hindeutet: „Out of memory …“. Diese Fehlermeldung wird benutzt um mitzuteilen dass zwar die „Erlaubnis“ da war, soviel Speicher zu reservieren, aber dass dies technisch nicht gelang (https://github.com/php/php-src/blob/bb1a68b40c90612d21e2a416e6658dc044d19f30/Zend/zend_alloc.c#L971-L978). Dies ist der Fall wenn der Prozess selbst nicht die Erlaubnis hat mehr zu reservieren, oder wenn tatsächlich nicht mehr genug physikalischer Speicher vorhanden ist.

    Diese zweite Meldung kann deshalb auch z.B. auftreten, wenn ein Speicher-Limit auf Webserver-Ebene gestzt wurde, wie z.B. durch die Apache `RLimitMEM` Direktive (https://httpd.apache.org/docs/2.4/mod/core.html#rlimitmem).

    > Warum die erste Doku-Seite bei den Konstanten noch den Präfix „PHP_“ hat, verstehe ich mal wieder nicht …

    Die `PHP_*` Konstanten sind die Konstanten die im C Quellcode für das PHP Runtime verwendet werden, nicht im PHP Quellcode von PHP Nutzern. Sie definieren, welche Konfigurationswerte wo verändert werden können. Diesen Modus kannst du für den `memory_limit` Wert hier nachlesen: https://www.php.net/manual/en/ini.core.php#ini.sect.resource-limits

    Die Konstante die ohne `PHP_`-Präfix dokumentiert wird (https://www.php.net/manual/de/info.constants.php#constant.ini-all) ist dann die Abbildung in den PHP-Prozess hinein, so dass man das dann auch innerhalb vom PHP Quellcode abfragen kann.

    Übrigens sind das Flags in einer Bitmask. Das bedeutet dass 7 eigentlich die Kombination aus den Bits 1 + 2 + 4 ist. Die Konstante `INI_ALL` is also pure „Convenience“, eigentlich wäre das `INI_USER & INI_PERDIR & INI_SYSTEM`.

    Liebe Grüsse,
    Alain

    • Vielen Dank für die Erklärungen, Alain! Das ist super-hilfreich. Kannst du auch die letzten Lücken noch schließen, warum die Werte so unterschiedlich ausgegeben werden?

      Vor allem der zentrale Punkt „Warum PHP bei dem Versuch einen zu hohen Wert kein FALSE ausgibt, verstehe ich nicht:“ – Wenn der Wert auf dem Server limitiert ist durch eine andere Konfiguration, dann sollte ich doch den Wert innerhalb meines Bereiches nicht höher setzen können als das Limit. Das scheint mir in der ganzen Debatte doch der zentrale Punkt zu sein. Danke schon mal!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.