Support from Acquia helps fund testing for Drupal Acquia logo

Comments

schildi’s picture

to provide some additional information I made a stack trace in function cache_set (D6: file includes/cache.inc) as follows

function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) {
  $serialized = 0;
  if (is_object($data) || is_array($data)) {
    $data = serialize($data);
    $serialized = 1;
  }       
    // creating stack trace
  if ($table == 'cache_menu') {
      if ( strlen($data) > 100000 ) {
          watchdog('cache', print_r(debug_backtrace(),TRUE));
      } 
  }     
  ...

the first 10 levels are shown here:

    [1] => Array
        (
            [file] => /html/drupal/includes/menu.inc
            [line] => 948
            [function] => cache_set
            [args] => Array
                (
                    [0] => links:navigation:tree-data:71c37be58d496330c0d6cd89b9944372
                    [1] => Array
                        (
                              HUGE NAVIGATION DATA (cut off here)
                        )

                    [2] => cache_menu
                )
        )

    [1] => Array
        (
            [file] => /html/drupal/includes/menu.inc
            [line] => 724
            [function] => menu_tree_page_data
            [args] => Array
                (
                    [0] => navigation
                )
        )

    [2] => Array
        (
            [file] => /html/drupal/modules/user/user.module
            [line] => 748
            [function] => menu_tree
            [args] => Array
                (
                )
        )

    [3] => Array
        (
            [function] => user_block
            [args] => Array
                (
                    [0] => view
                    [1] => 1
                )
        )

    [4] => Array
        (
            [file] => /html/drupal/includes/module.inc
            [line] => 461
            [function] => call_user_func_array
            [args] => Array
                (
                    [0] => user_block
                    [1] => Array
                        (
                            [2] => view
                            [3] => 1
                        )
                )
        )

    [5] => Array
        (
            [file] => /html/drupal/modules/block/block.module
            [line] => 522
            [function] => module_invoke
            [args] => Array
                (
                    [0] => user
                    [1] => block
                    [2] => view
                    [3] => 1
                )
        )

    [6] => Array
        (
            [file] => /html/drupal/includes/theme.inc
            [line] => 1641
            [function] => block_list
            [args] => Array
                (
                    [0] => left
                )
        )

    [7] => Array
        (
            [function] => theme_blocks
            [args] => Array
                (
                    [0] => left
                )
        )

    [8] => Array
        (
            [file] => /html/drupal/includes/theme.inc
            [line] => 668
            [function] => call_user_func_array
            [args] => Array
                (
                    [0] => theme_blocks
                    [1] => Array
                        (
                            [0] => left
                        )
                )
        )

    [9] => Array
        (
            [file] => /html/drupal/includes/theme.inc
            [line] => 1835
            [function] => theme
            [args] => Array
                (
                    [0] => blocks
                    [1] => left
                )
        )

as described in http://drupal.org/node/231587#comment-4804234 all entries have 0 as expiry (permanent). That results in being not purged by function cache_clear_all.

Please have a look at that issue because it is close to a show stopper (will crash a site in between several days).

TuWebO’s picture

Subs

Stefan Freudenberg’s picture

I have debugged this in D7, not sure if the same is true for D6. I have observed the same problem on a recently launched site with a small amount of data. After a while the cache_menu table was as big as the rest of the database. The majority of the cache entries are menu items (cid starts with menu_item:). The hash value in the cid is calculated from the normalized request path given to Drupal which is available in $_GET['q']. The menu system has an interesting feature:

When responding to a page request, the menu system looks to see if the path requested by the browser is registered as a menu item with a callback. If not, the system searches up the menu tree for the most complete match with a callback it can find. If the path a/b/i is requested in the tree above, the callback for a/b would be used.

api.drupal.org

This can lead to multiple cache_menu entries for the same menu_item: By appending a slash followed by a random string to the path of an existing page when making a request a different hash is produced.

Stefan Freudenberg’s picture

Priority: Normal » Major

Some more people should look at this issue, because I think it opens a possibility to attack a Drupal site. So I set the priority to major.

catch’s picture

@Stefan, thanks for the steps to reproduce. This is will be #643984: Cache menu_get_item() most likely. Patch should be easy enough to revert locally to test if that helps.

danny.ketelslegers’s picture

Version: 8.x-dev » 7.8

Any progress on this item? :-)
seems to be under different issues but no final solution so far?
caching is great, but not if it fills the database with rubish in a couple of days.
Great to see that so many people are working on fixing this!
Keep up the good spirit!

StevenPatz’s picture

Version: 7.8 » 8.x-dev
Anonymous’s picture

oh dear. this is a lot worse than the original report.

    // Since there is no limit to the length of $path, use a hash to keep it
    // short yet unique.
    $cid = 'menu_item:' . hash('sha256', $path);
    if ($cached = cache('menu')->get($cid)) {
      $router_item = $cached->data;
    } 
    else {
      $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
      $ancestors = menu_get_ancestors($parts);
      $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors
      cache('menu')->set($cid, $router_item);
    } 

$path, which is used in $cid, will be different for 'node/1', 'node/2' etc. and 'user/1', 'user/2' etc. etc. etc.

so we're caching router items like 'node/%' over, and over, and over, and over. and 'user/%', and 'term/%'. so much caching win.

we need to roll back #643984: Cache menu_get_item(), and rethink how we derive $cid from the request.

Anonymous’s picture

Status: Active » Needs review
FileSize
1.25 KB

patch to roll back #643984: Cache menu_get_item().

Fabianx’s picture

It really is sad here that there is no way to say: cache_get_fast() because then one could just traverse the anchestors and return the first match, which is costly for DB cache, but really fast for memcache, APC, etc. and even better with a cache_multiple_get_fast.

And saved would be the actual router path (or the hash thereof), which takes care of the problem with the many entries.

That would be the proper solution here to still avoid DB queries alltogether.

Ludo.R’s picture

Coming back after end of year vacation, the table has grown to 30 Mb. The database total size got to 102% of allowed space on my hosting which lead to buggy display of the site and warnings.

Please note that this site is (hopefully) not in production, but the table still grew up rapidly, even with very little traffic.

This is the second time it happens in few weeks. Our hosting allows 50Mb for database and the database size with Backup & Migrate module is less than 1Mb (gzipped).
This is a huge difference.

I'm using Drupal 7.9

catch’s picture

Status: Needs review » Reviewed & tested by the community

#9 looks good to me.

catch’s picture

Issue tags: +Needs backport to D7

Tagging.

sun’s picture

Looks good for me, too, thanks! Would make sense to get this into the Feb 1st release.

webchick’s picture

Status: Reviewed & tested by the community » Needs work

Whoopsie. :P Nice catch.

I know catch normally tries to keep a 3-day minimum RTBC time, but that will of course blow the Feb 1 release deadline. I talked to him about it, and he pointed out that since this a straight rollback of another patch, it should leave us no worse off than core already was before.

Therefore, committed and pushed to 8.x. I tried to also commit/push to 7.x, but the patch did not apply. :( If we can get a re-roll in relatively short order, I might be able to squeeze it in tomorrow morning.

catch’s picture

Version: 8.x-dev » 7.x-dev

Moving back to 7.x since this is already in 8.x.

webchick’s picture

Status: Needs work » Patch (to be ported)

Sorry.

oriol_e9g’s picture

Status: Patch (to be ported) » Needs review
FileSize
1.24 KB

Only backport from #9 to D7

oriol_e9g’s picture

Status: Needs review » Reviewed & tested by the community
BTMash’s picture

#18: 1234830-18.patch queued for re-testing.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed to 7.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

goyie’s picture

I applied #18 patch, but table cache_menu still grows. Is there anything else I need to do to fix table? I have menu with 1500 items, so cache_menu reaches 500Mb in 1 hour!

goyie’s picture

Status: Closed (fixed) » Needs work

It seems I forgot to reopen issue. Table cache_menu still grows after patch #18.

catch’s picture

Status: Needs work » Closed (fixed)

Since this particular bug has been fixed, please open a new issue if you're still having problems, since it will be a different cause to the one dealt with here.

schildi’s picture

Why do you think it is solved? The starting point was the observation that the table is growing endless in D6 (compare issue summary). For some reason the issue was opened in D8 and handled for D7. As far as I understood the patch #9 only rolls back a previous patch which didn't solve the situation either. So we are at the starting point again. I just checked (OK, just for D6, but that is where the issue came from originally):

select count(*) from cache_menu;
+----------+
| count(*) |                                                                                                  
+----------+                                                                                                  
|     8821 |                                                                                                  
+----------+                                                                                                  

select count(*) from cache_menu where length(data) > 100;
+----------+
| count(*) |
+----------+
|      576 |
+----------+

select sum(length(data)) Size from cache_menu where length(data) > 100;
+-----------+
| Size      |
+-----------+
| 405972351 |
+-----------+

Edit:
I think the issue is solved when the described erratic behavior was corrected - and not when some patch is applied. Users "Stefan Freudenberg" and "beejeebus" did some investigations - thanks a lot for that! - but possibly the offered patches did not help.

catch’s picture

Version: 7.x-dev » 8.x-dev
Status: Closed (fixed) » Active

OK I see what happened, in between the original report and the bug being fixed, I'd missed that you're still having an issue in Drupal 6, since that's what the report was for it's OK to re-open this issue to keep looking at it, I just generally try to avoid re-opening issues at all cost since they get very long and unwieldy.

I think what might be useful would be if you could attach the full results of SELECT cid, LENGTH(data) as a .txt file to this issue.

Additionally an example of the identical cached data would be handy - since it's that bug that was originally fixed back in 2008.

schildi’s picture

Since I am still running (mainly) a D6 site I would like to ask goyie for this attachment. So this means to create two files. The first one is quite clear to me

mysql> tee cid_length.txt;
mysql> SELECT cid, LENGTH(data) from cache_menu;
mysql> quit

For the second file you probably mean to extract one of the blob data?
select * from cache_menu where length(data) > 1000 limit 1;

goyie’s picture

FileSize
5.94 KB
25.27 KB

schildi, thanks for hint :)

Just for record I repeat:
I'm using drupal 7.12 (so I changed issue version).
I applied patch #18 but table still grows.

I have big menu created by taxonomy_menu module from vocabulary with ~500 terms.
Site: http://nmkt.ru

Am I attached right files?

UPDATE: I've wrote next comment with some details and new files.

goyie’s picture

Version: 8.x-dev » 7.x-dev
FileSize
23.22 KB
12.67 KB

I figured that taxonomy_menu uses term description for title attribute in menu link.
And I have big descriptions for every term (~500 items).
So I disabled this behavior and reuploaded files. I think table will grow slower, but still be.

Every record in cache_menu has 'expire' field set to zero. Is it correct?

schildi’s picture

Possibly it helps when also data of a D6 system are available (see attached files).

catch’s picture

OK looking at these, the problem seems to mainly be that the indiividual cache entry for each menu tree is huge, rather than we're necessarily creating that many entries. i.e. if you have cache items that are 1mb+, then it doesn't take many of these to end up with a 50mb+ cache table.

Really what we need to move towards doing is caching the rendered HTML for menus rather than the links arrays, but that's a very large problem to solve due to access checks, I'm not sure how fixable this is going to be in Drupal 7 let alone Drupal 6, but if the entries are that big then it's worth looking at.

cache_menu items are supposed to be kept permanently (which in practice means until the next menu link is saved or the menu router gets rebuilt). We might want to consider setting 'permanent' to mean a configurable value defaulting to a month or so for the database backend, so that stale items can be removed on sites that aren't otherwise triggering cache clears.

catch’s picture

Version: 7.x-dev » 8.x-dev

Back to 8.x

catch’s picture

Priority: Major » Normal

This only appears to be affecting people with very, very large menu trees, and there's not a lot that can be done about that, so I'm going to downgrade from major.

schildi’s picture

as far as I remember there was a menu entry created automatically in D4 for each node created. Since the site where I am reporting the issue for is migrated from D4 --> D5 --> D6 these menu entries still exist. Is there a way to set up some kind of a batch to get rid of thousands of menu entries?

webchick’s picture

Issue tags: +7.13 release notes

This seems worth calling out in the release notes for the patch that was committed.

webchick’s picture

Issue summary: View changes

Linkify related issues.

knalstaaf’s picture

Issue summary: View changes

Could one (or a combination) of the following modules fix this for D7?

Or is that a different matter?

damiankloip’s picture

May want to repurpose this slightly now as cache_menu doesn't exist anymore in D8. See #1194136: Re-organise core cache bins.

joelpittet’s picture

Version: 8.0.x-dev » 7.x-dev

Should this just be bumped back to d7 then?

@knalstaaf not really for the memcacheAPI that's actually what brought me here. There is a set limit of 1MB default in memcache which causes fatals.

Fabianx’s picture

Well, it is certainly possible to fix this in D6 to D8, but you need a different cache paradigm:

A LRU (least-recently-used) cache.

That means your most frequent pages like the homepage would be cached, but depending on how many entries you have other pages would quickly vanish from the cache.

There is already an issue by @alexpott to convert CacheArray to LRUCacheArray, so if someone really wants to see this:

It is easily possible, but it needs work.

Fabianx’s picture

Title: cache_menu: huge table size » Revert cache_menu patch removal, add a $conf setting instead (was: cache_menu: huge table size)
Priority: Normal » Major
Status: Active » Needs work

We saw a site almost go down due to this query (query cache lock) during this voting week, therefore we need to revert the revert and instead just add a little $conf setting so that high traffic sites using memcache (that use LRU by default) can still opt-in to use this functionality.

It is impossible to fix this without patching core.

Fabianx’s picture

kentr’s picture

FileSize
1.08 KB

In the case Fabianx mentioned, the DB was a slave dedicated pretty much entirely to this and a couple of other menu queries, and serving 25K read (SELECT) queries / second according to New Relic...

This should be the patch we used for D6. Based on what nnewton said was a patch from Catch.

catch’s picture

Version: 7.x-dev » 8.0.x-dev
Issue tags: +Needs backport to D7

{cache_menu} doesn't exist in Drupal 7, but a version of this query does.

catch’s picture

Issue tags: -Needs backport to D7

Opened #2480811: Cache incoming path processing and route matching for 8.0.x since the code is very different from 7.x now.

Moving back to 7.

Fabianx’s picture

Version: 8.0.x-dev » 7.x-dev
Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 43: 1234830-43-D6.patch, failed testing.

Manuel Garcia’s picture

Status: Needs work » Needs review
FileSize
1.57 KB

Here it is for D7 (reverts #18).

Status: Needs review » Needs work

The last submitted patch, 48: revert_cache_menu_patch-1234830-48.patch, failed testing.

Manuel Garcia’s picture

Status: Needs work » Needs review
FileSize
1.55 KB

Ignore the previous patch, don't know what I was thinking, let's try it again.

Fabianx’s picture

Status: Needs review » Needs work
+++ b/includes/menu.inc
@@ -462,9 +462,18 @@ function menu_get_item($path = NULL, $router_item = NULL) {
+    if ($cached = cache_get($cid, 'cache_menu')) {

Now make the cid creation part optional and disabled as before, like:

$cid = variable_get('use_menu_router_cache_for_lru_backend', FALSE) ? 'menu_item:' . hash('sha256', $path) : FALSE;
if ($cid && $cached = ...) {

and it works out. Just need to find a good variable name.

Manuel Garcia’s picture

Status: Needs work » Needs review
FileSize
1.63 KB
561 bytes

OK, used the variable name you suggested for now.

Fabianx’s picture

Status: Needs review » Needs work
+++ b/includes/menu.inc
@@ -462,9 +462,18 @@ function menu_get_item($path = NULL, $router_item = NULL) {
+      cache_set($cid, $router_item, 'cache_menu');
if ($cid) {
  cache_set(...);
}

Need this, too ...

Manuel Garcia’s picture

Status: Needs work » Needs review
FileSize
1.65 KB
564 bytes

Duh, sorry bout that.

Fabianx’s picture

  1. +++ b/includes/menu.inc
    @@ -309,7 +309,7 @@ define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91');
    - *   An array of path parts; for the above example, 
    + *   An array of path parts; for the above example,
    

    What is changed here? White space?

  2. +++ b/includes/menu.inc
    @@ -462,9 +462,20 @@ function menu_get_item($path = NULL, $router_item = NULL) {
    +    $cid = variable_get('use_menu_router_cache_for_lru_backend', FALSE) ? 'menu_item:' . hash('sha256', $path) : FALSE;
    

    I think its better to use () around the 'menu_item' . hash(...) as it looks better even though this works.

    Unless I miss some coding standard on this.

    Or maybe we use some if statement and isset($cid)?

    Not sure ...

Manuel Garcia’s picture

About 1. I haven't touched those lines, not sure why its showing up on git diff.

On 2. I agree that line is not very intuitive to read, perhaps something like this?

    $cid = FALSE;
    if (variable_get('use_menu_router_cache_for_lru_backend', FALSE)) {
      $cid = 'menu_item:' . hash('sha256', $path);
    }

In any case, $cid will always be set and not NULL (will either be a string or FALSE), so I think we are safe to keep the if statement as is. Unless I'm missing some coding standard...

Fabianx’s picture

1. Probably auto-correction of your editor. I do not care if we fix some coding standards violations on the way ...
2. Yep, lets do the if statement as its easier to read. ++

Manuel Garcia’s picture

OK, here is the patch with the new if statement to make it easier to read.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, looks great now!

joelpittet’s picture

+++ b/includes/menu.inc
@@ -462,9 +462,23 @@ function menu_get_item($path = NULL, $router_item = NULL) {
+    if (variable_get('use_menu_router_cache_for_lru_backend', FALSE)) {

This could use a comment, both explaining when to use and what LRU is maybe?

A LRU (least-recently-used) cache.

from @Fabianx

David_Rothstein’s picture

Version: 7.x-dev » 8.0.x-dev
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs backport to D7

So it was mentioned above that a similar query exists in Drupal 8 (I think it's Drupal\Core\Routing\RouteProvider::getRoutesByPath()). #2480811: Cache incoming path processing and route matching is clearly a separate D8 issue, but shouldn't we be committing a version of this patch to Drupal 8 first (acknowledging it's not an ideal solution, and that it could potentially be removed later in the other issue)?

That's my understanding of how to avoid D7->D8 regressions per https://www.drupal.org/core/backport-policy, to make sure there's some fix for this in Drupal 8 even if the more far-reaching issue never makes it.

Or are they so different for other reasons that a similar fix as in this patch would never work?

I also tend to agree regarding the code comment; it's not clear to me from the code what 'use_menu_router_cache_for_lru_backend' means or how/when the variable is supposed to be used.

Fabianx’s picture

Version: 8.0.x-dev » 7.x-dev

Agree we need a better variable name and code comments.

Any suggestions?

And would we document that in default.settings.php or where else?

--

Spoke with catch and the solution in 8.x would look different as it would introduce a different LRU cache backend interface. Also this was a very late D7 revert, therefore part of the original code base.

And the revert should have never happened in this way (we just did not see this solution back then).

catch’s picture

Also with 8.0.x the most likely place to cache would be \Drupal\Core\Routing\Router\AccessAwareRoute::matchRequest() which would mean we also skip the path alias lookup.

  • webchick committed 148f5b6 on 8.3.x
    Issue #1234830 by beejeebus, schildi: Fixed cache_menu(): huge table...

  • webchick committed 148f5b6 on 8.3.x
    Issue #1234830 by beejeebus, schildi: Fixed cache_menu(): huge table...
Fabianx’s picture

Assigned: Unassigned » Fabianx
Status: Needs work » Needs review
Issue tags: -Needs backport to D7 +Drupal 7.60 target

I thought about this again:

- a) This should live in its own cache bin, so its easily seperatable (e.g. cache_menu_router)
- b) The variable name is too long and should just be $conf['cache_menu_router'] = TRUE.
- c) The new setting needs to be added to settings.php and there the documentation needs to state that this should only be used with backends like redis or memcache.
- d) The cache item needs to be expired when the menu router table changes, which happened explicitly in the patch here as cache_menu was cleared.

I don't think D8 would profit from this as it has used a different direction and won't fixed the similar issue.

Given this was in D7 before and was reverted instead of just opt-ed out, I still stand by my opinion that this should be eligible to go back in. Even more so as D8 decided to won't fix the relevant partner issue in the mean time.

  • webchick committed 148f5b6 on 8.4.x
    Issue #1234830 by beejeebus, schildi: Fixed cache_menu(): huge table...

  • webchick committed 148f5b6 on 8.4.x
    Issue #1234830 by beejeebus, schildi: Fixed cache_menu(): huge table...
yonailo’s picture

Issue tags: +Performance

Could anyone move forward this issue ? (applying @Fabianx latest comments ?).

Fixing this issue could have an impact on performance, and it would be nice to let users opt-in to it.

yfierro’s picture

So how am I supposed to run a production site with a bug like this? It is the 3rd time that menu_cache.ibd grows to 25GB and brings down the server!!!! Is anyone doing anything about it?

munyiva’s picture

Any solutions my cache_menu table keeps growing and the expiry is set to '0'

joseph.olstad’s picture

Issue tags: -Drupal 7.60 target +Drupal 7.61 target

Bumping to 7.61. This didn't make it into 7.60.

Xoruna’s picture

I have the same issue and it drives me crazy to figure out to solve this. Should we revert to Drupal to 7.59 waiting for a new release fixing this issue?

Edit: A possible solution is to use the File Cache module.

joseph.olstad’s picture

xrxphawxby’s picture

This really needs to get into the next release. Our site fell over today with a 22GB cache_menu.

+-----------------+--------------+-------------+-------+-------------+---------------------+
| Tables          | Size on Disk | Size in DB  | Rows  | Index in DB | Index-to-data ratio |
+-----------------+--------------+-------------+-------+-------------+---------------------+
| cache_menu      | 22568.00 MB  | 22225.27 MB | 45595 | 8.56 MB     | 0.00                |
| cache_page      | 4780.00 MB   | 4468.50 MB  | 37109 | 6.58 MB     | 0.00                |
| cache_form      | 1772.00 MB   | 364.05 MB   | 5668  | 1.02 MB     | 0.00                |
| cache_field     | 256.00 MB    | 236.83 MB   | 61828 | 4.52 MB     | 0.02                |
| cache_views     | 144.00 MB    | 133.58 MB   | 1645  | 0.19 MB     | 0.00                |
+-----------------+--------------+-------------+-------+-------------+---------------------+
Fabianx’s picture

Assigned: Fabianx » Unassigned
Status: Needs review » Needs work

I am happy to get this in given the router has changed so much, but someone needs to address #66 and create a new patch for D7.

Fabianx’s picture

#70 #71 #75:

The code in question is not yet in D7, so you have the opposite problem.

Please open a bug for huge cache_menu caches, which is likely indeed menus on the site and not the issue here.

joseph.olstad’s picture

MustangGB’s picture

Issue tags: -Drupal 7.64 target +Drupal 7.69 target
MustangGB’s picture

Issue tags: -Drupal 7.69 target +Drupal 7.70 target

  • webchick committed 148f5b6 on 9.1.x
    Issue #1234830 by beejeebus, schildi: Fixed cache_menu(): huge table...