HTMHell
HTMHell

Reputation: 6016

Download script using php-fpm/Nginx Causes High Load CPU

I have a good server for a file sharing use, and I am having trouble with the download script. I use PHP-FPM running on nginx.

The server specs:

2x Intel Xeon E5
CPU: 92GB RAM
10x2TB (RAID6)
And we use 1 SSD disk for CashCade

When I apllay this script on apache server, it works fine, but I want to run it on nginx server, because apache takes a lot of memory. (RAM) But when I run this script on nginx, something really wired is happening - it takes 30% from the CPU, only one download! Please note that after 3-4 minutes from the begining of the download, the CPU load is go back to normal (but the download continiues).

This is the "TOP" in LINUX When im downloading... When I'm Downloading

I don't know why, but the PHP-FPM script is taking a lot from the CPU. The script:

class ResumeDownload {
    private $file;
    private $name;
    private $boundary;
    private $delay = 0;
    private $size = 0;

    function __construct($file, $delay = 0) {
        if (! is_file($file)) {
            header("HTTP/1.1 400 Invalid Request");
            die("<h3>File Not Found</h3>");
        }

        $this->size = filesize($file);
        $this->file = fopen($file, "r");
        $this->boundary = md5($file);
        $this->delay = $delay;
        $this->name = basename($file);
    }

    public function process() {
        $ranges = NULL;
        $t = 0;
        if ($_SERVER['REQUEST_METHOD'] == 'GET' && isset($_SERVER['HTTP_RANGE']) && $range = stristr(trim($_SERVER['HTTP_RANGE']), 'bytes=')) {
            $range = substr($range, 6);
            $ranges = explode(',', $range);
            $t = count($ranges);
        }

        header("Accept-Ranges: bytes");
        header("Content-Type: application/octet-stream");
        header("Content-Transfer-Encoding: binary");
        header(sprintf('Content-Disposition: attachment; filename="%s"', $this->name));

        if ($t > 0) {
            header("HTTP/1.1 206 Partial content");
            $t === 1 ? $this->pushSingle($range) : $this->pushMulti($ranges);
        } else {
            header("Content-Length: " . $this->size);
            $this->readFile();
        }

        flush();
    }

    private function pushSingle($range) {
        $start = $end = 0;
        $this->getRange($range, $start, $end);
        header("Content-Length: " . ($end - $start + 1));
        header(sprintf("Content-Range: bytes %d-%d/%d", $start, $end, $this->size));
        fseek($this->file, $start);
        $this->readBuffer($end - $start + 1);
        $this->readFile();
    }

    private function pushMulti($ranges) {
        $length = $start = $end = 0;
        $output = "";

        $tl = "Content-type: application/octet-stream\r\n";
        $formatRange = "Content-range: bytes %d-%d/%d\r\n\r\n";

        foreach ( $ranges as $range ) {
            $this->getRange($range, $start, $end);
            $length += strlen("\r\n--$this->boundary\r\n");
            $length += strlen($tl);
            $length += strlen(sprintf($formatRange, $start, $end, $this->size));
            $length += $end - $start + 1;
        }
        $length += strlen("\r\n--$this->boundary--\r\n");
        header("Content-Length: $length");
        header("Content-Type: multipart/x-byteranges; boundary=$this->boundary");
        foreach ( $ranges as $range ) {
            $this->getRange($range, $start, $end);
            echo "\r\n--$this->boundary\r\n";
            echo $tl;
            echo sprintf($formatRange, $start, $end, $this->size);
            fseek($this->file, $start);
            $this->readBuffer($end - $start + 1);
        }
        echo "\r\n--$this->boundary--\r\n";
    }

    private function getRange($range, &$start, &$end) {
        list($start, $end) = explode('-', $range);

        $fileSize = $this->size;
        if ($start == '') {
            $tmp = $end;
            $end = $fileSize - 1;
            $start = $fileSize - $tmp;
            if ($start < 0)
                $start = 0;
        } else {
            if ($end == '' || $end > $fileSize - 1)
                $end = $fileSize - 1;
        }

        if ($start > $end) {
            header("Status: 416 Requested range not satisfiable");
            header("Content-Range: */" . $fileSize);
            exit();
        }

        return array(
                $start,
                $end
        );
    }

    private function readFile() {
        while ( ! feof($this->file) ) {
            echo fgets($this->file);
            flush();
            usleep($this->delay);
        }
    }

    private function readBuffer($bytes, $size = 1024) {
        $bytesLeft = $bytes;
        while ( $bytesLeft > 0 && ! feof($this->file) ) {
            $bytesLeft > $size ? $bytesRead = $size : $bytesRead = $bytesLeft;
            $bytesLeft -= $bytesRead;
            echo fread($this->file, $bytesRead);
            flush();
            usleep($this->delay);
        }
    }
}

And the download.php (where I run the script)

// ... some code that get the file details from extrenal Database...

$fileArr = $query->fetch_assoc();
$file = 'uploads/' . $download['fileid'] . '/' . $fileArr['name'];

if(file_exists($file)) {
    $mysqli->close();
    require 'class.download.php';
    set_time_limit(0);
    $download = new ResumeDownload($file, 0); //delay about in microsecs 
    $download->process();
}

Upvotes: 0

Views: 1376

Answers (2)

Emamie
Emamie

Reputation: 2872

Increase delay in this method :

 $download = new ResumeDownload($file, 500); //delay about in microsecs

OR use nginx x-accel-redirect Instead of ResumeDownload class

Upvotes: 0

Danack
Danack

Reputation: 25711

I don't know why, but the PHP-FPM script is taking a lot from the CPU.

And what makes you think that's a problem? When a server isn't CPU limited the programs will run as fast as they can using as much CPU as is available to them. The only time when high CPU usage really matters is when the server can't process requests fast enough, as all the scripts are competing to use CPU resource, and blocking each other from running.

You script is running as fast as it possibly can, (i.e. using as much CPU as it can) because you've told it to:

$download = new ResumeDownload($file, 0); //delay about in microsecs

i.e. your download script isn't ever waiting and so it's looping as fast as it can to push data into the network connection.

Pretty obviously a server can push data into a network connection faster than it can be sent over the internet, so a lot of the time your script it just looping and waiting for the network connection to be able to send data - as evidenced by the high idle time at the top of the 'top' output.

You could just set the delay to make the script actually sleep or you could use Nginx x-accel to completely remove the load from PHP. There's a config for that here: Serve large file with PHP and nginx X-Accel-Redirect That would be much more efficient in situations where you are actually CPU bound.

Upvotes: 1

Related Questions