Sunday, December 25, 2011

APC

APC is a performance-enhancing extension. It should not be confused with a magic pill, although having it around does provide a positive impact on performance! If configured incorrectly, APC can cause unexpected behaviour, however when implemented optimally APC can be a useful weapon in your arsenal. In this post we will examine APC's capabilities and it's application both as an opcode cache and in its less common usage as a data cache.

How APC differs from Memcached

APC is not a replacement for memcached. They are designed for different roles. memcached is a generic, distributed caching daemon. APC on the other hand is PHP-specific and limited in size. As such, you'll want to only use APC for local caching of articles and objects that are relatively small. This begs the question: why not just use memcached universally?
The answer to this question, and indeed the question of whether to use memcache or APC, depends entirely on your circumstance. If you have a small application under a lot of load, then APC is a good choice. APC is a PECL extension, so it is very easy to add into your PHP ecosystem if you have control over your platform. Memcache on the other hand has a dependency on the memcache libraries and an external daemon running. The additional dependencies can imply additional maintenance costs in terms of administering the system, so APC, which does not have these dependencies, can be a better choice.
As generic caches, we can store many types of item in either APC or memcached. These include opcodes (which we will examine in more depth later in this article), strings, or other result representation from database queries or HTTP calls to feeds and services external to your application. We can also store objects that are expensive to create within our application, ready to use again later. The key is that all these objects can or will be in the same state for a certain period of time.
A key metric is the projected sum of the size of all objects; this is a determining factor between chosing APC or memcached since this dictates the size of the cache required. Memcached is more relevant than APC when a large cache is required because the maximum allocated memory is only limited by the architecture in which it runs. As it is language-independent, memcached can be helpful when used creatively within applications constructed in multiple languages. The distributed nature of memcached also enable resource sharing and makes it flexible enough to fit within a scalable environment.
We should view APC and memcached as tools that have different applications, although some part of their functionalities do intersect. That said, APC and memcached can be used together if the application architecture would benefit from this approach. They can be part of a layer of caches, covering anything from database results to pages or snippets of pages for the view layer. This article examines the use of APC for opcode and data caching, if you want to know more about using memcached, read our other posts about getting started with Memcached and regarding the new memcached extension.

How important is APC?

How important APC is for your setup depends on whether speed is important, and that depends on where you use PHP. Certain scripts may not be time critical, but user-facing applications usually are. Another aspect to consider is whether there is a need to minimise resource utilisation. Usually, the full potential of APC can be seen best in environment where usage/traffic volume is high, i.e. there is a small code base being executed very frequently.
As you will know, PHP is an interpreted language. This means that a PHP script has to be read, parsed and compiled before it is executed. (There is a very interesting resource on slideshare regarding this, from Joseph Scott - Anatomy of a PHP Request). The process of serving a PHP script ends up looking something like this:

This process is not ideal in a deployment where speed and resource minimalisation is a requirement, because the code is repeatedly parsed and compiled. APC is an excellent tool under such circumstances as it caches compiled code for execution so that there is no need for code to be parsed and compiled every time it needs to be executed. APC is not the only solution available for this purpose, there are others ( see the wikipedia page on PHP accelerators), however APC is mature and widely-used. For those facing optimisation challenges similar to those of Facebook's, there is always HipHop PHP.

What does APC actually cache?

A PHP script is converted into opcodes in the parsing and compilation process. APC caches these opcodes, making opcodes available to be executed without going through the parsing and compilation process.

There is a PECL extension, VLD (Vulcan Logic Dumper/Disassembler), which allows you to check out the opcodes generated from the compilation and execution of a PHP script. For instance if you consider the following code example:
$testvar = "Test variable";
Will show the following opcodes:
Finding entry points 
Branch analysis from position: 0 
Return found 
filename:       vldtest.php 
function name:  (null) 
number of ops:  2 
compiled vars:  !0 = $testvar 
line     # *  op                           fetch          ext  return  operands 
--------------------------------------------------------------------------------- 
   2     0  >   ASSIGN                                                   !0, 'Test+variable' 
   3     1    > RETURN                                                   1 
 
branch: #  0; line:     2-    3; sop:     0; eop:     1 
path #1: 0,

Limitations of Caching with APC

APC caches in memory, so inherently the size of the available cache is not massive. The default size of the cache is 30 MB. This can be changed with the apc.shm_size configuration directive. For APC instances that are configured to use the System V-style kernel shared memory, the maximum size of shared memory is dependent on the shmmax kernel parameter. For Debian machines, this is usually 33MB. You can check this by doing:
$ cat /proc/sys/kernel/shmmax
However, since v3.0.11, APC uses mmap by default and with that you can a specify a cache size that is unconstrained by the limit in the OS's shmmax parameter. The cache size specified can go beyond the size of the physical RAM but that will result in swapping (which means that part of the memory utilised is actually taken from the hard disk) which is undesirable for those without a solid state drive. In any case, the apc_sma_info() function shows memory allocation information pertinent to your APC instance.
Another consideration of using AP is that it only caches to the host machine where the PHP instance is executed. As a result we cannot share an APC cache among multiple load-balanced PHP servers serving the same application in parallel. Each PHP instance on a server has to maintain and utilise their own cache.

Storing Data, Rather Than Opcodes, With APC

APC also caches user data via the apc_add/apc_store functions, essentially this means we can place the values of a variable into APC, not too unlike caches such as memcached. Although this can be very useful, especially in systems without another convenient caching mechanism, do be wary of the memory limitations of APC and remember that user data stored within cache could be vying for space with cached opcodes from files. Let's take a look at how the value of a particular variable is stored:
$apcValTest = "This is a value to be stored in cache. ";
apc_store("sampleKey1", $apcValTest);
The first parameter to the apc_store() method is the key. Here, the md5() function maybe useful to create unique keys from any string that could be used to identify the value. To retrieve stored values:
echo apc_fetch("sampleKey1");
A conventional method with user caches is to first check if an item exist in the cache first before getting it from source and storing it in the cache if it is not available in cache:
if (($text = apc_fetch('key')) === false) {
        $text = file(FILENAME);
        apc_store('key', $text, 120);
    }
    print_r($text);

Benefits of User Data Caching

Although user data caching is often perceived as a secondary purpose of having APC, it is nevertheless a very useful feature for a certain type of data. APC is faster than a cache residing in another server that needs to be reached via a socket connection. The overall size of the cache however is relatively small. Because of these characteristics, data that can fully reap APC's benefits should be relatively small, used often within a short time, and take a long time to generate. One example of such a data is the accessible resources of a particular role in an ACL component. If this list is stored in a database, effort to retrieve this list typically looks like the following:
function getResourceList() { 
 
    $conn = mysql_connect("localhost", USERNAME, PASSWORD); 
 
    if (!$conn) { 
        echo "Unable to connect to database server"; 
        exit; 
    } 
 
    if (!mysql_select_db("sample_db") ) { 
        echo "Unable to connect to database"; 
        exit; 
    } 
 
    $result = mysql_query("SELECT resource_id FROM acl WHERE role_id = 1 ORDER BY resource_id ASC"); 
 
    $resourceList = array(); 
 
    if (!$result) { 
        echo "Unable to retrieve resource list"; 
    } else { 
        while ($row = mysql_fetch_array($result)) { 
            $resourceList[] = (int) $row["resource_id"]; 
        } 
    } 
 
    return $resourceList; 
} 
 
$resourceList = getResourceList();
With Xdebug profiling, we are able to determine the execution time of the above code, which is about 5 ms. The breakdown of this operation looks like this:

Since it is likely that we are going to need this resource list very often, we can cache it in APC to give us improved execution speed, caching the data and eliminating the overhead of fetching it each time. We would use some caching code along the lines of:
if (($resourceList = apc_fetch('resource_list')) === false) { 
    $resourceList = getResourceList(); 
    apc_store('resource_list', serialize($resourceList), 120); 
} else { 
    $resourceList = unserialize($resourceList); 
}
When running this from the CLI, do remember to set apc.enable_cli=1 in your APC runtime configuration. Notice that we serialize() the $resourceList before caching it and unserialize() it after retrieving it from APC. $resourceList is a serializable() array and thus can be cached. The $result itself in getResourceList is a resource that is not cacheable. Retrieving the list from APC for the first time takes 6 ms, so our breakdown on the first iteration looks like this:

This is slightly longer than when it is not cached but only happens initially when $resourceList is not available in the cache. As long as subsequent requests are made within the TTL of the cached data, retrieval of the list takes 0.112 second, as we can see from this third breakdown:

By caching the resource list and retrieving it whenever its needed, we save that amount of time per request. In this example, using APC is about 50x faster and certainly helps when request volume is large and frequent. getResourceList() is not too lengthy to begin with, imagine the time that can be saved on heavier functions. The speed in which we see here is specific to APC. Other data caches will have different times since their implementation is different and there are other factors may be involved, such as the network latency that we touched on earlier. We will not delve too deeply into the performance differences of the various data cache solutions here, but consideration given to the difference between data caches helps in identifying the best cache for your situation.

File Cache

A file cache is also known as the "system" cache. By default, APC caches compiled files whenever they are executed. Pre-compilation before execution can be triggered with the apc_compile_file() function if so desired. To obtain a list of compiled files, call the apc_cache_info() function. To turn off this system caching, set apc.cache_by_default to 0. Using these functions, we can "warm up" a cache, filling the cache with the opcodes of our scripts.

Time To Live

Cached opcodes and user values has a default TTL (Time To Live) of 0, which means that nothing will be removed until the entire cache becomes full, in which case the whole cache is expunged. To avoid such an expunge, you can set a TTL on your opcode caches with the apc.ttl configuration directive and customise TTLs to specific keys based on the need of the application and specific data being stored. The TTL for cached user values can be supplied as the third parameter to the apc_store or apc_add function. For the user cache, an item place with a specific TTL will be expunged even if the space used by the item is not needed.

Deleting from cache

To delete a compiled file stored within the opcode cache, we call apc_delete_file(), passing in the filename to be deleted from cache. For user cache, the apc_delete() function can be used, passing in the key of the item that we wish to remove. However, such deletion may be costly as it might involve re-structure of internal hash tables. It is more ideal to work on a basis of auto-expiry of cache items, or to clear the user cache via a call to apc_clear_cache(“user”) if you need to do so.

Stat

apc.stat is an important configuration directive, especially if you're looking at optimising APC for a production environment. This configuration is turned on by default and causes APC to check a requested file to see if there are any modifications and serve from cache only if there are none. Turning this check off will speed things up and is typically done in a live or stage environment where deployments are usually followed by a restart of the webserver, clearing the cache and forcing the loading of the newly modified files. After the webserver restart, APC will no longer check for file modifications, this is useful since scripts should not normally be modified directly on these environments. On a development environment however, it is more common to set apc.stat to be turned off as scripts will be changing during development.

Monitoring the cache

Distributed along with the APC source is an apc.php script which provides a browser-based front-end that shows useful information about the current state of your APC instance. Here's an example of the interface:

The information shown is quite intuitive and is a great place to start looking to improve configuration to optimise performance. One value of importance to cache performance is the Cache full count, which can be seen under File Cache Information when in the View Host Stats tab. This indicates the number of times the cache became full, upon which it is expunged if apc.ttl is set to 0. If apc.ttl is larger than 0, APC removes items in the cache that are older than the allowed apc.ttl to make way for newer items. The expugning process has a negative impact on performance and thus the number of times this occurs should be reduced if possible. This can be done by either increasing memory allocation or reducing cached scripts via the apc.filters configuration directive. Lastly, remember to place apc.php in location that is not publicly accessible.

Summary

APC is a performance-enhancing tool that is a key part of the stack for most live environments where PHP-based web-applications are deployed. The system cache will definitely complement other performance-enhancing endeavours while the user cache can be used if a fast and small object cache is desired. Whatever your situation, it is beneficial to invest some time into understanding how APC behaves with your application and configure APC accordingly, in order to get the best advantage from this tool or any other.

No comments:

Post a Comment