PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Erstaunlich langsam beim Kopieren einer Datei auf ein NFS-Share

with 6 comments

Heute mal etwas skurriles. Eigentlich eine Alltagsaufgabe, die man mit einem 2-Zeiler lösen kann, aber ich bin doch erstaunt über das Problem und die Lösung. Es geht darum, eine Datei auf einem NFS-Share abzulegen, das vorher per PUT zum Webserver hochgeladen wurde. Wahrscheinlich hat man das selbe Problem auch wenn die Datei per POST-Formular hochgeladen wurde.

Folgender Aufruf lädt eine Datei via PUT auf einen Webserver:

curl -T 100mb.test http://localhost/put.php

Mein einfaches Testscript, das die Datei auf das NFS-Share legen soll, sieht so aus:

<?
$putdata = fopen("php://input", "r");
file_put_contents('/mnt/nfs/100mb.zip', $putdata);

Es dauert knapp 26 Sekunden bis das Script fertig ist bei einer 100MB Datei. Das muss doch schneller gehen! 4MB/s ist viel zu wenig für ein Gigabit-Netzwerk und einen schnellen NFS-Storage.

Nächster Versuch:

<?
$putdata = fopen("php://input", "r");
file_put_contents('/tmp/100mb.zip', $putdata);
copy('/tmp/100mb.zip', '/mnt/nfs/100mb.zip');

Hier gehe ich den Umweg über die lokale Festplatte. Ich speichere die Datei erst lokal auf dem Webserver, und kopiere sie dann auf das NFS-Share. Das file_put_contents() auf die lokale Festplatte dauert knapp 1 Sekunde, das ist kein Problem. Aber der copy()-Befehl ist wieder recht langsam, 21 Sekunden, sprich 5MB/s. Was ist denn nur los?

(Die PHP-Funktion rename(), die normalerweise dafür das ist eine Datei zu verschieben, kann ich nicht nutzen da man die Funktion nur zum Verschieben auf einer Partition nutzen kann, und ich die Datei auf ein NFS-Share ablegen möchte.)

Um mich zu vergewissern dass es nicht am Netzwerk oder am NFS-Storage liegt mache ich einen Test per SSH:

time cp 100mb.zip /mnt/nfs/100mb.zip

real    0m4.236s
user    0m0.007s
sys     0m0.144s

Aha! Es geht doch! 4 Sekunden, sprich 25MB/s, das ist doch schon viel besser.

Nächster Versuch:

<?
$putdata = fopen("php://input", "r");
file_put_contents('/tmp/100mb.zip', $putdata);
shell_exec("cp /tmp/100mb.zip /mnt/nfs/100mb.zip");

Die Laufzeit dieses Scripts ist nur 5 Sekunden! Ich muss also die Datei erstmal lokal auf die Festplatte schreiben, um sie dann mit dem System-Befehl „cp“ auf das NFS-Share zu kopieren.

Warum ist „cp“ so viel schneller als copy()? Ich hätte getippt dass PHP intern etwas ähnliches macht oder sogar das selbe? Oder ist die „cp“-Funktion deutlich im Vorteil weil sie den Filesystem-Cache oder Kernel-Cache oder ähnliches ausnutzen kann, was die copy()-Funktion nicht tut? Oder liegt es daran dass sowohl file_put_contents() als auch copy() intern immer in kleinen Häppchen vorgeht: „4KB lesen, 4KB schreiben, 4KB lesen, 4KB schreiben…“, und dass das sehr unperformant für eine NFS-Verbindung ist?

Kann mir das jemand erklären? Warum ist PHP so schlecht darin eine Datei auf ein NFS-Share zu kopieren?

Ich nutze in diesem Fall PHP 5.5.9-1ubuntu4.6

Ich bin dankbar über jeden Ratschlag.

Written by Michael Kliewe

März 3rd, 2015 at 10:46 am

6 Responses to 'Erstaunlich langsam beim Kopieren einer Datei auf ein NFS-Share'

Subscribe to comments with RSS or TrackBack to 'Erstaunlich langsam beim Kopieren einer Datei auf ein NFS-Share'.

  1. Hat womöglich mit dem synchronen Schreiben (sync writes) zu tun.

    Basti

    3 Mrz 15 at 11:33

  2. Hi, wie sieht es denn aus, wenn du statt file_put_contents direkt per fwrite auf den NFS share schreibst? Damit kannst du ja einen eigenen Buffer in beliebiger Größe festlegen.

    marcel

    3 Mrz 15 at 20:40

  3. @marcel:
    Wenn ich direkt fopen() und fwrite() ohne Buffer-Angabe ($length-Parameter) nutze und dann versuche $putdata wegzuschreiben auf das NFS, dann kommt die Fehlermeldung:
    PHP Warning: fwrite() expects parameter 2 to be string, resource given
    Wenn man fwrite() benutzten möchte muss man den Inhalt also in einer String-Variablen haben, was bei einer >300 MB Datei keine gute Idee ist.

    Da es sich bei der Quelle um einen Stream handelt, nutzt file_put_contents() intern die Funktion stream_copy_to_stream() (bzw. php_stream_copy_to_stream_ex). Und diese Funktion schreibt die Daten wie vermutet in kleinen Paketen, um nicht viel Arbeitsspeicher zu verbrauchen. Es werden X Bytes gelesen, dann diese X Bytes geschrieben, usw… Und das ist bei einem NFS-Share anscheinend das Problem und deshalb langsam.
    https://github.com/php/php-src/blob/2dac92b244eef1575a5a0487fe8b615be1a9e294/main/streams/streams.c#L1499
    Damit wäre schonmal geklärt warum file_put_contents() mit einer Stream-Quelle und einem NFS-Share als Ziel langsam ist.

    Für copy() scheint das selbe zu gelten wenn ich das richtig verstehe, bin kein C-Experte:
    https://github.com/php/php-src/blob/master/ext/standard/file.c#L1680
    Auch dort scheint php_stream_copy_to_stream_ex benutzt zu werden, was die Probleme erklären würde.

    Der System „cp“ Befehl braucht keine Buffer und Streams und sowas, sondern schickt die Datei per Kernel direkt an die Netzwerkkarte nehme ich an. Die Datei kommt dann „in einem großen Stück“ auf dem NFS an, und nicht in X KB großen Paketen.

    Aktuell muss ich erstmal beim system(„cp…“) bleiben.

    Michael Kliewe

    5 Mrz 15 at 18:25

  4. Mal copy(‚php://input‘, ’nfsmount‘) probiert?

    Fabian

    21 Mrz 15 at 11:17

  5. Hallo !

    man kann sich das ganze ja leicht unter Linux per „strace“ anschauen (siehe http://man7.org/linux/man-pages/man1/strace.1.html). Damit sollte man genauer raus bekommen was php da veranstaltet. Leichter ist es nicht einfach cp im strace anzuschauen 🙂
    Zitat: „Der System „cp“ Befehl braucht keine Buffer und Streams und sowas, sondern schickt die Datei per Kernel direkt an die Netzwerkkarte nehme ich an.“
    – Nein wohl eher nicht. Es wird ein stat() auf die Quelldatei gemacht und dann jeweils ein open() pro Datei. Max. werden 32KB von cp aus der Quelldatei gelesen und in die Zieldatei geschrieben. Kleine Dateien <32KB werden also in eine Zyklus gelesen und geschrieben. Für cp ist alles eine Datei – egal wo hin geschrieben wird. Ob NFS oder Lokal – cp nutzt immer die Kernel eigenen open(),read(),write(),stat() Funktionen um diesen Job zu erledigen. Wollte man das also mit PHP gleich tun, muss man fread() dazu veranlassen max. 32 KB Daten zu lesen und mit fwrite() diese dann schreiben. Mit dem zuvor ausgeführten stat() bekommt man ja die Größe der Quelldatei und kann damit das Ganze ja gut berechnen.
    Hat man mit fread/fwrite wenig Glück was die Performance angeht, könnten die Low-Level Funktionen in PHP dann noch etwas bringen: Siehe http://php.net/manual/en/function.dio-open.php

    VG Martin

    Martin

    23 Aug 15 at 19:07

  6. Danke für den Code. Hat mir etwas weiter helfen können.

Leave a Reply

You can add images to your comment by clicking here.