Question

we're having quite a big CakePHP (currently 2.4.7) SAAS-app. It's pretty mature and until last week we never had any real problems concerning the functionality provided by CakePHPs-core.

Last weekend we ran into a very alarming situation, where our app got really slow and faulty. We looked into all kind of server monitorings and found out, that i/o stats had skyrocketed in the last couple of hours. After some more investigation we isolated the problem to Cake's 'cake_core_method_cache' file. On an average day, this file has a size of 200 kb. Last weekend, it went up to ~ 40 mb.

Part of our deploy-routine is to remove all cache files from the app/tmp/cache and app/tmp/persistent folders. Because we deploy very often (few times a day), the cache gets cleared very often. Unfortunately, holidays last week paused our deployments and it looked like this was a problem for the core/caching-functionality.

So, my two questions are:

a) does anyone know, why the cake_core_method_cache is getting bigger over time?

b) is there any other possibility to solve this problem as to delete this file on a constant basis?

Was it helpful?

Solution

How is this cache used

The only references to method_cache in CakePHP are in Dbo Source. This is a cache that is used extensively throughout that class to cache the result of (relatively) expensive quoting operations, or as indicated by a relevant docblock:

Caches result from query parsing operations. Cached results for both DboSource::name() and DboSource::conditions() will be stored here.

Consider the following shell:

<?php

class SoShell extends AppShell {

    function main() {
        $this->Post = ClassRegistry::init('Post');

        $this->Post->find('all', array(
            'conditions' => array(
                'find-this-text' => 'x'
            )
        ));

        debug(DboSource::$methodCache);
    }

}

Running it, produces the following output:

> Console/cake so
########## DEBUG ##########
array(
    'name' => array(
        'f5442d1f57be5d9d8ac80ae9613d5ff9' => '`database_name`',
        'cfacfed443d6f30e67edf9bbcb06ce30' => '`posts`',
        'e13d78515036382daf55fa083088c902' => '`Post`.`id`',
        'aafd056e6d53878a75ab4ee3f18645a1' => '`Post`.`title`',
        '4084e974416e3a7fb06e6a280721637b' => '`Post`.`body`',
        'b4f1ad1de4cdc3a3f82e905f8dfd8673' => '`Post`.`created`',
        'bfeffce70e344d7606e17c9ff24530b5' => '`Post`.`modified`',
        'e42609d9744a7d1ce80c8d838b9ea07c' => '`find-this-text`', # <-
        'c0a33fa0f04ac4682dcdc4b25167b3a8' => '`Post`'
    ),
    'fields' => array(
        '952d56c2bf037c8195bdd5fba57b1024' => array(
            (int) 0 => '`Post`.`id`',
            (int) 1 => '`Post`.`title`',
            (int) 2 => '`Post`.`body`',
            (int) 3 => '`Post`.`created`',
            (int) 4 => '`Post`.`modified`'
        )
    )
)

Note that the cache contains "user" (or developer) input, that array is what is written to the cache at the end of a request.

This example should also highlight why the cache grows with time - the cache is dependent on the queries issued to date and as time goes by more query permutations are issued by an application leading to the method cache growing (but not usually to many MB).

Easy fix

There's a very simple fix to the described problem: Disable the method cache.

i.e. put the following anywhere in your application:

DboSource::$cacheMethods = false;

This will of course mean a deterioration in performance as there will be no method caching used, so relatively expensive preg-based quoting operations will occur on every request.

Better fix

If the cache is being filled with "db-instances of models" - it's quite possible there is an application problem somewhere. It's not normal, it shouldn't happen and is dependent on exactly what the application is doing. Given that it's not possible to give a specific solution.

However, if you can identify any specific key in the cache containing a large chunk of data, you can use it to find what query is responsible. For example:

// anywhere
if (isset(DboSource::$methodCache['name']['e42609d9744a7d1ce80c8d838b9ea07c']) {
    // a query with find-this-text has been issued somewhere
}
    

You can (temporarily) add this kind of logic to where the dbo methodCache property is modified to be able to detect modifications "in situ":

public function cacheMethod($method, $key, $value = null) {
    if ($this->cacheMethods === false) {
        return $value;
    }
    if (!$this->_methodCacheChange && empty(self::$methodCache)) {
        self::$methodCache = Cache::read('method_cache', '_cake_core_');
    }
    if ($value === null) {
        return (isset(self::$methodCache[$method][$key])) ? self::$methodCache[$method][$key] : null;
    }
    $this->_methodCacheChange = true;
    
    // Added
    if ($method === 'name' && $key === 'e42609d9744a7d1ce80c8d838b9ea07c') {
        App::uses('Debugger', 'Utility');
        $this->log("Query with find-this-text in it issued", "wat-debug");      
        $this->log(Debugger::trace(), "wat-debug");     
    }
    // Added end

    return self::$methodCache[$method][$key] = $value;
}

This would permit you to identify what is directly responsible for injecting large volumes of data into the method cache; to then correct the application code so that the method cache can be enabled and used as designed.

OTHER TIPS

With the most recent old CakePHP 2 version the proposed fix from https://stackoverflow.com/a/23287510/47573 didn't work for me anymore; the property is not static and thus you can't simply override somewhere else.

The solution I came up with was to create a customized Database driver extending the one from CakePHP I use:

  • create app/Plugin/PostgresCustom/Model/Datasource/Database/PostgresCustom.php
  • this class extends Cakes Postgres which in turns extends DboSource which defines this property
  • in my PostgresCustom class I simply override the property via public $cacheMethods = false;
  • in app/Config/bootstrap.php I register the plugin via CakePlugin::load('PostgresCustom');
  • in app/Config/database.php I changed the value for datasource everywhere to PostgresCustom.Database/PostgresCustom
  • Profit 💰
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top