PHP Performance Crash Course, Part 2: The Deep Dive
Join the DZone community and get the full member experience.
Join For FreeIn 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 to 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 Symfony\Component\HttpFoundation\Response;
$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 Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $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 \Doctrine\Common\Cache\MemcacheCache(); $memcacheDriver->setMemcache($memcache); $config = new \Doctrine\ORM\Configuration(); $config->setQueryCacheImpl($memcacheDriver); $config->setMetadataCacheImpl($memcacheDriver); $config->setResultCacheImpl($memcacheDriver); $entityManager = \Doctrine\ORM\EntityManager::create(array(‘driver’ => ‘pdo_sqlite’, ‘path’ => __DIR__ . ‘/db.sqlite’), $config); $query = $em->createQuery(‘select u from \Entities\User 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 \Doctrine\Common\Cache\MemcacheCache(); $memcacheDriver->setMemcache($memcache); $client = new \Guzzle\Http\Client(‘http://www.test.com/’); $cachePlugin = new \Guzzle\Plugin\Cache\CachePlugin(array( ‘storage’ => new \Guzzle\Plugin\Cache\DefaultCacheStorage( new \Guzzle\Cache\DoctrineCacheAdapter($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
I use AppDynamics, an 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:
If you prefer slide format these posts were inspired from a recent tech talk I presented:
Published at DZone with permission of Dustin Whittle, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Transactional Outbox Patterns Step by Step With Spring and Kotlin
-
Never Use Credentials in a CI/CD Pipeline Again
-
MLOps: Definition, Importance, and Implementation
-
Seven Steps To Deploy Kedro Pipelines on Amazon EMR
Comments