In my first post on this series I covered some basic tips for optimizing performance in php applications. In this post we are going to dive a bit deeper into the principles and practical tips in scaling PHP.
Top engineering organizations think of performance not as a nice-to-have, but as a crucial feature of their product. Those organizations understand that performance has a direct impact on the success of their business.
Ultimately, scalability is about the entire architecture, not some minor code optimizations. Often times people get this wrong and naively think they should focus on the edge cases. Solid architectural decisions like doing blocking work in the background via tasks, proactively caching expensive calls, and using a reverse proxy cache will get you much further than arguing about single quotes or double quotes.
Just to recap some core principles for performant PHP applications:
- Upgrade to PHP 5.5 with Zend OpCache using PHP-PFM + Nginx
- Stay up to date with your framework + dependencies (using Composer)
- Optimize your session store to use signed cookies or database with caching
- Cache your database and web service access with Memcache or Redis
- Do blocking work in the background with queues and tasks using Resque
- Use HTTP caching and a reverse proxy cache like Varnish
- Profile code with Xdebug + Webgrind and monitor production performance
The first few tips don’t really require elaboration, so I will focus on what matters.
Optimize your sessions
In PHP it is very easy to move your session store to Memcached:
1) Install the Memcached extension with PECL
pecl install memcached
2) Customize your php.ini configuration to change the session handler
session.save_handler = memcached
session.save_path = "localhost:11211"
If you want to support a pool of memcache instances you can separate with a comma:
session.save_handler = memcached
session.save_path = "10.0.0.10:11211,10.0.0.11:11211,10.0.0.12:11211"
The Memcached extension has a variety of configuration options available, see the full list on Github. The ideal configuration I have found if using a pool of servers:
session.save_handler = memcached
session.save_path = "10.0.0.10:11211,10.0.0.11:11211,10.0.0.12:11211"
memcached.sess_prefix = “session.”
memcached.sess_consistent_hash = On
memcached.sess_remove_failed = 1
memcached.sess_number_of_replicas = 2
memcached.sess_binary = On
memcached.sess_randomize_replica_read = On
memcached.sess_locking = On
memcached.sess_connect_timeout = 200
memcached.serializer = “igbinary”
That’s it! Consult the documentation for a complete explanation of these configuration directives.
Leverage caching
Any data that is expensive to generate or query and long lived should be cached in-memory if possible. Common examples of highly cacheable data include web service responses, database result sets, and configuration data.
Using the Symfony2 HttpFoundation component for built-in http caching support
I won’t attempt to explain http caching. Just go read the awesome post from Ryan Tomako, Things Caches Do or the more in-depth guide to http caching from Mark Nottingham. Both are stellar posts that every professional developer should read.
With the Symfony2 HttpFoundation component it is easy to add support for caching to your http responses. The component is completely standalone and can be dropped into any existing php application to provide an object oriented abstraction around the http specification. The goal is to help you manage requests, responses, and sessions. Add “symfony/http-foundation” to your Composer file and you are ready to get started.
use SymfonyComponentHttpFoundationResponse;
$response = new Response(‘Hello World!’, 200, array(‘content-type’ => ‘text/html’));
$response->setCache(array(
‘etag’ => ‘a_unique_id_for_this_resource’,
‘last_modified’ => new DateTime(),
‘max_age’ => 600,
‘s_maxage’ => 600,
‘private’ => false,
‘public’ => true,
));
If you use both the request and response from the http foundation you can check your conditional validators from the request easily:
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
$request = Request::createFromGlobals();
$response = new Response(‘Hello World!’, 200, array(‘content-type’ => ‘text/html’));
if ($response->isNotModified($request)) {
$response->send();
}
Find more examples and complete documentation from the very detailed Symfony documentation.
Caching result sets with Doctrine ORM
If you aren’t using an ORM or some form of database abstraction you should consider it. Doctrine is the most fully featured database abstraction layer and object-relational mapper available for PHP. Of course, adding abstractions comes at the cost of performance, but I find Doctrine to be exteremly fast and efficient if used properly. If you leverage the Doctrine ORM you can easily enable caching result sets in Memcached:
$memcache = new Memcache();
$memcache->connect('localhost', 11211);
$memcacheDriver = new DoctrineCommonCacheMemcacheCache();
$memcacheDriver->setMemcache($memcache);
$config = new DoctrineORMConfiguration();
$config->setQueryCacheImpl($memcacheDriver);
$config->setMetadataCacheImpl($memcacheDriver);
$config->setResultCacheImpl($memcacheDriver);
$entityManager = DoctrineORMEntityManager::create(array(‘driver’ => ‘pdo_sqlite’, ‘path’ => __DIR__ . ‘/db.sqlite’), $config);
$query = $em->createQuery(‘select u from EntitiesUser u’);
$query->useResultCache(true, 60);
$users = $query->getResult();
Find more examples and complete documentation from the very detailed Doctrine documentation.
Caching web service responses with Guzzle HTTP client
Interacting with web services is very common in modern web applications. Guzzle is the most fully featured http client available for PHP. Guzzle takes the pain out of sending HTTP requests and the redundancy out of creating web service clients. It’s a framework that includes the tools needed to create a robust web service client. Add “guzzle/guzzle” to your Composer file and you are ready to get started.
Not only does Guzzle support a variety of authentication methods (OAuth 1+2, HTTP Basic, etc), it also support best practices like retries with exponential backoffs as well as http caching.
$memcache = new Memcache();
$memcache->connect('localhost', 11211);
$memcacheDriver = new DoctrineCommonCacheMemcacheCache();
$memcacheDriver->setMemcache($memcache);
$client = new GuzzleHttpClient(‘http://www.test.com/’);
$cachePlugin = new GuzzlePluginCacheCachePlugin(array(
‘storage’ => new GuzzlePluginCacheDefaultCacheStorage(
new GuzzleCacheDoctrineCacheAdapter($memcacheDriver)
)
));
$client->addSubscriber($cachePlugin);
$response = $client->get(‘http://www.wikipedia.org/’)->send();
// response will come from cache if server sends 304 not-modified
$response = $client->get(‘http://www.wikipedia.org/’)->send();
Following these tips will allow you to easily cache all your database queries, web service requests, and http responses.
Moving work to the background with Resque and Redis
Any process that is slow and not important for the immediate http response should be queued and processed via non-blocking background tasks. Common examples are sending social notifications (like Facebook, Twitter, LinkedIn), sending emails, and processing analytics. There are a lot of systems available for managing messaging layers or task queues, but I find Resque for PHP dead simple. I won’t provide an in-depth guide as Wan Qi Chen’s has already published an excellent blog post series about getting started with Resque. Add “chrisboulton/php-resque” to your Composer file and you are ready to get started. A very simple introduction to adding Resque to your application:
1) Define a Redis backend
Resque::setBackend('localhost:6379');
2) Define a background task
class MyTask
{
public function perform()
{
// Work work work
echo $this->args['name'];
}
}
3) Add a task to the queue
Resque::enqueue('default', 'MyTask', array('name' => 'AppD'));
4) Run a command line task to process the tasks with five workers from the queue in the background
$ QUEUE=* COUNT=5 bin/resque
For more information read the official documentation or see the very complete tutorial from Wan Qi Chen:
- Part 1 : Introduction
- Part 2 : Queue system
- Part 3 : Installation
- Part 4 : Worker
- Part 5 : Job class and implementation
- Part 6 : Integrate Resque into CakePHP with CakeResque
- Part 7 : Start and stop workers with Fresque
- Part 8 : A look into php-resque-ex, a fork with more features
- Part 9 : Resque analytics with ResqueBoard
Monitor production performance
AppDynamics is application performance management software designed to help dev and ops troubleshoot performance problems in complex production applications. The application flow map allows you to easily monitor calls to databases, caches, queues, and web services with code level detail to performance problems:
Take five minutes to get complete visibility into the performance of your production applications with AppDynamics Pro today.
If you prefer slide format these posts were inspired from a recent tech talk I presented:
As always, please feel free to comment if you think I have missed something or if you have a request for content in an upcoming post.