Speicherverbrauch von wp_insert_post beim Massenimport umgehen
Veröffentlicht: – Kommentar hinterlassen Letzte Aktualisierung:
Für ein kleines Plugin war es erforderlich, eine CSV-Liste in einen individuellen Inhaltstyp (Custom Post Type, CPT) zu importieren. Dabei stieß ich auf ein paar Performance-Probleme bei der Verwendung von wp_insert_post()
, die ich loswerden wollte.
Auf das Problem stieß ich, indem ich die CSV-Datei Zeile für Zeile einlas und dann die Daten jeder Zeile per wp_insert_post()
in die Datenbank importierte. Nicht nur war diese Methode sehr langsam, sodass möglicherweise eine Zeitüberschreitung stattfinden würde – immerhin beinhaltet die CSV-Datei knapp 30.000 Zeilen – sondern der Arbeitsspeicher-Verbrauch war zu hoch. Während ich mit 512 MB memory_limit
in PHP noch hinkam, waren 256 MB (was der Wert auf dem verwendeten Produktivsystem ist) zu wenig.
Overhead in wp_insert_post()
wp_insert_post()
tut noch einige Dinge mehr als nur Inhalte in die Datenbank zu schreiben, weshalb ich mich erst einmal darauf konzentrierte, genau diese Funktion nachzubilden, ohne den für mich in diesem Zusammenhang unnötigen Überhang von wp_insert_post()
zu verwenden.
Erster Versuch: $wpdb->insert
Letztendlich werden die Daten via $wpdb->insert
in die Datenbank geschrieben. Also verwendete ich dieselbe Funktion direkt. Zum Vergleich: Ein normaler Aufruf der Seite im Backend ohne einen Import braucht 63,0577 MiB Arbeitsspeicher. Mit $wpdb->insert
sind es nun mit Import 228,0297 MiB. So würde es zwar nach aktuellem Maßstab im Produktivsystem auch funktionieren, allerdings war mir dieses Limit dennoch zu knapp am Maximum von 256 MiB. Ebenso kann ich nicht sagen, inwieweit die CSV-Datei, die regelmäßig aktualisiert werden soll, noch in Zukunft wächst.
Es scheint demnach so, als ob auch $wpdb->insert
einige Aktionen vornimmt, die viel Arbeitsspeicher benötigen. Als Alternative fand ich das noch direktere $wpdb->query
in Verbindung mit $wpdb->prepare
. Das brachte mich auf einen Arbeitsspeicherverbrauch von 186,1205 MiB, was für mich für den Import aller Daten auf einmal als in Ordnung erschien.
Die Lösung: $wpdb->query
Allerdings war die Ausführungszeit hier ziemlich lang, ca. 25 – 30 Sekunden. Zu diesem Zeitpunkt wurde jede Zeile in einer einzelnen Abfrage in die Datenbank geschrieben. Also spielte ich mit den Werten, wie viele Daten ich über eine einzelne Abfrage in die Datenbank schrieb. Zusätzlich zur besseren Performance wurde auch der Arbeitsspeicherverbrauch drastisch reduziert.
100 Einträge pro Insert: 82,7393 MiB Arbeitsspeicher; 3,19194 s Importdauer
1000 Einträge pro Insert: 82,7328 MiB Arbeitsspeicher; 2,8335 s – 3,02511 s Importdauer
Noch mehr Einträge pro Insert in die Datenbank zu schreiben, brachte keine weiteren Vorteile.
Um noch das letzte Bisschen herauszuholen, verwendete ich SET autocommit = 0;
, um die Daten wirklich erst am Ende in die Datenbank zu schreiben. Dadurch spart man sich noch etwas I/O auf der Festplatte, was dann folgendermaßen aussah:
1000 Einträge pro Insert: 82,7378 MiB Arbeitsspeicher; 2,65201 s Importdauer
Der Code
Am Ende sieht die Funktion bei mir so aus:
/**
* Import CSV data as custom post.
*
* @param string $file Path to the CSV file
* @return string 'success' or an empty string
*/
private function import_csv( string $file ): string {
ini_set( 'auto_detect_line_endings', true );
$fields = [];
$handle = fopen( $file, 'r' );
if ( $handle === false ) {
return '';
}
// data that will be stored later in the database
$time = current_time( 'mysql' );
$time_gmt = current_time( 'mysql', 1 );
$user_id = get_current_user_id();
global $wpdb;
$wpdb->query( "SET autocommit = 0;" );
// delete all posts before a new import
// do it with a manual query to speed up deletion
$wpdb->query(
"DELETE post,
term,
meta
FROM $wpdb->posts post
LEFT JOIN $wpdb->term_relationships term
ON post.ID = term.object_id
LEFT JOIN $wpdb->postmeta meta
ON post.ID = meta.post_id
WHERE post.post_type = 'custom_post_type';"
);
$count = 0;
$sql_data = [];
$sql_prepare_values = '';
while ( ( $data = fgetcsv( $handle ) ) !== false ) {
$count++;
// we have data - store them
if ( ! empty( $data ) ) {
$post_name = sanitize_title( $row_data['zip'] );
$sql_data[] = $user_id; // post_author
$sql_data[] = serialize( $data ); // post_content
$sql_data[] = ''; // post_content_filtered
$sql_data[] = $post_name; // post_name
$sql_data[] = $data['zip']; // post_title
$sql_data[] = ''; // post_excerpt
$sql_data[] = 'publish'; // post_status
$sql_data[] = 'custom_post_type'; // post_type
$sql_data[] = 'closed'; // comment_status
$sql_data[] = 'closed'; // ping_status
$sql_data[] = ''; // post_password
$sql_data[] = ''; // to_ping
$sql_data[] = ''; // pinged
$sql_data[] = 0; // post_parent
$sql_data[] = home_url( '/' . $post_name ); // guid
$sql_data[] = 0; // menu_order
$sql_data[] = $time; // post_date
$sql_data[] = $time_gmt; // post_date_gmt
$sql_data[] = $time; // post_modified
$sql_data[] = $time_gmt; // post_modified_gmt
$sql_prepare_values .= '(%d, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %s, %d, %s, %s, %s, %s),';
}
// after getting data of 1000 entries, send query
if ( $count === 1000 ) {
// using $wpdb directly is much faster than wp_post_insert()
// and $wpdb->query() alongside $wpdb->prepare() uses nearly
// 20% less memory
$wpdb->query( $wpdb->prepare(
"INSERT INTO $wpdb->posts (
post_author,
post_content,
post_content_filtered,
post_name,
post_title,
post_excerpt,
post_status,
post_type,
comment_status,
ping_status,
post_password,
to_ping,
pinged,
post_parent,
guid,
menu_order,
post_date,
post_date_gmt,
post_modified,
post_modified_gmt
)
VALUES " . rtrim( $sql_prepare_values, ',' ),
...$sql_data
) );
// reset data to prepare a new query
$count = 0;
$sql_data = [];
$sql_prepare_values = '';
}
}
$wpdb->query( "COMMIT;" );
$wpdb->query( "SET autocommit = 1;" );
fclose( $handle );
ini_set( 'auto_detect_line_endings', false );
return 'success';
}
Code-Sprache: PHP (php)
Noch ein Hinweis am Ende: Durch die Verwendung von $wpdb->query
bist du selbst dafür verantwortlich, die Daten zu maskieren. Deshalb sollte es immer nur in Verbindung mit $wpdb->prepare
verwendet werden.