raw code

Faster PHP behind FastCGI

Robert Eisele

A few years ago, Jan Kneschke came up with the idea of using lighttpd's X-Sendfile to send dynamic content without copying it several times. I liked the idea and used it as basis of my framework. It seems, there are now some immature implementations of this idea on the lighttpd bug tracker. All of these implementations also use the shared memory, with the difference that I haven't used PHP's tempfile() function, but rather exported lighttpd's client-file-descriptor to the PHP scope in order to use it as the file-name of the temporary file. It could also be the IP or something else but the client-fd is the most unique identifier inside of the webserver <-> PHP construct.

I've just published my own PHP version with a lot of improvements and optimizations. To write the content quickly, I also added a new function ob_fwrite() to write the contents of the ob-buffer to an opened file descriptor like this:

<?php

ob_start(null, 0x20000);
echo 'Write the content into the buffer';
$fd = fopen('/pipe/' . $_SERVER['CFD']);

if (!BUFFER_CONTENTS) {
	ob_fwrite($fd);
	ob_end_clean();
} else {
	$buffer = ob_get_clean();
}
fclose($fd);

$ref =&$_SERVER['HTTP_ACCEPT_ENCODING'];

if (CACHE_CONTENTS) {
	$path = System::getCachePath($_SERVER['CFD']);
	rename('/pipe/' . $_SERVER['CFD'], '/cache/' . $path);
	exec('gzip -c '.$path.' > '.$path.'z');

	if(isset($ref) && false !== strpos($ref, 'gzip')) {
		header('Content-Encoding: gzip');
		$path.= 'z';
	}
	header('X-Sendfile: /cache/' . $path, true, $http_status);

} else {

	if(isset($ref) && false !== strpos($ref, 'gzip')) {
		header('Content-Encoding: gzip');
		exec('gzip ' . $path . ' > ' . $path . 'z');
		$path.= 'z';
	}
	header('X-Sendfile: /pipe/' . $_SERVER['CFD'], true, $http_status);
}

Please note that /pipe is a tmpfs mountpoint!

The same works, of course, with nginx's X-Accel-Redirect. Reducing the number of memory copies improves the performance alot! There are several copies before the content is finally transfered to the client; which the following sketch illustrates:

PHP ECHO > FCGI SAPI > WEBSERVER > CLIENT

The new way, the content takes, looks like this:

PHP ECHO > OB BUFFER > SHM > WEBSERVER > CLIENT

..., or if the content is already cached:

PHP > CACHE HIT > WEBSERVER > CLIENT

Thick arrows in the sketch above are expenisve copies.

The whole thing runs contrary to the best practices, published by Yahoo!. It's not possible to send the buffer early this way. This could mean, that the client can't start downloading the style-sheets and scripts as fast as possible. But as far as I can see, most of the time should not spend in the backend but with sending the data to the client. Everything else can be optimized away, like optimizing hanging database queries and so on.

Incidentally, this optimization only makes sense for web-servers that communicate over the slow FCGI bridge. Apache and other thread-based servers that integrate PHP as a module, can not save much here. Additionally, Apache would need the X-Sendfile-module.

Update of June 12

I used the $_SERVER["CFD"] variable in the example above. In order to make use of it, lighttpd and nginx must be patched. You could also use tempfile() instead but this is annoying because of the aditional required delete-operation of the file. If every file descriptor gets it's own unique file, this is much cleaner. Here you can get the Patch for lighttpd 1.4.28.