throttle.module 18 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
Dries's avatar
 
Dries committed
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
 
Dries committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
/*  Call the throttle_status() function from your own modules, themes, blocks,
 * etc, to determine the current throttle status.  For example, in your theme
 * you might choose to disable pictures when your site is too busy (reducing
 * bandwidth), or in your modules you might choose to disable some complicated
 * logic when your site is too busy (reducing CPU utilization).
 *  To determine the current throttle level from your own php code, you can add
 * the following line:
 *    $throttle_level = module_invoke("throttle", "status");
 *  This will return a number from 0 to 5.  0 meaning that the current load is
 * very small, 5 meaning that the current load is as heavy as it gets.  You
 * should consider disabling logic when the throttle_level gets to 4 or 5.
 */
function throttle_status() {
  if (variable_get("throttle_enable", 0)) {
    return variable_get("throttle_level", 0);
  }
  else {
    return 0;
  }
}


function throttle_exit() {
  /*
  ** The following logic determines what the current throttle level should
  **  be, and can be disabled by the admin.  If enabled, the rand() function
  **  returns a number between 0 and N, N being specified by the admin. If
  **  0 is returned, the throttle logic is run, adding on additional database
  **  query.  Otherwise, the following logic is skipped.  This mechanism is
  **  referred to in the admin page as the 'probability limiter', roughly
  **  limiting throttle related database calls to 1 in N.
  */
  if ((variable_get("throttle_enable", 0)) && (!rand(0, variable_get("throttle_probability_limiter", 9)))) {
    /*
    ** Note:  The rand() function is supported by PHP 3+.  However, prior to
    ** PHP 4.2.0 it needs to be seeded with a call to srand().  It is important
    ** that this only happens once, so this should be managed by the Drupal
    ** engine, not this module.  The Drupal engine should use phpversion() to
    ** detect and automatically seed pre-4.2.0 systems.
    */

    $throttle = throttle_status();
    // if we're at throttle level 5, we don't do anything
    if ($throttle < 5) {
      $multiplier = variable_get("throttle_multiplier", 60);
      // count all hits in past sixty seconds
      $recent_activity = db_fetch_object(db_query("SELECT COUNT(timestamp) AS hits FROM {accesslog} WHERE timestamp >= %d", (time() - 60)));
      _throttle_update($recent_activity->hits);
    }
  }
}

Dries's avatar
 
Dries committed
56 57 58 59 60 61 62 63
function throttle_perm() {
  /*
  ** throttle module defines the following permissions:
  **   access throttle box          - see throttle statistics
  */
  return array("access throttle box");
}

Dries's avatar
 
Dries committed
64
function throttle_help($section = "admin/help#throttle") {
Dries's avatar
 
Dries committed
65

Dries's avatar
 
Dries committed
66
  switch ($section) {
Dries's avatar
 
Dries committed
67
    case "admin/system/modules#description":
Dries's avatar
 
Dries committed
68
      $output = t("Allows configuration of congestion control auto-throttle mechanism.");
Dries's avatar
 
Dries committed
69 70
      break;
    case "admin/system/modules/throttle":
Dries's avatar
 
Dries committed
71
      return t("If your site gets linked to by a popular website, or otherwise comes under a \"Denial of Service\" (DoS) attack, your webserver might become overwhelmed.  This module provides a mechanism for automatically detecting a surge in incoming traffic.  This mechanism is utilized by other Drupal models to automatically optimize their performance by temporarily disabling CPU-intensive functionality.  To use the auto-throttle, the %access must be enabled.  It is advised that you carefully read the explainations below and then properly tune this module based on your site's requirements and your webserver's capabilities.", array("%access" => l(t("access log"), "admin/system/modules/statistics")));
Dries's avatar
 
Dries committed
72
    case "admin/help#throttle":
73
      $output .= "<h3>Introduction</h3><p>This Drupal module allows you to enable and configure the auto-throttle congestion control mechanism offered by the %statistics-module.  The auto-throttle mechanism allows your site to automatically adapt to different server levels.</p>";
Dries's avatar
 
Dries committed
74
      $output .= "<p>This module also adds a block that displays the current status of the throttle.  You must have \"%throttle-block\" privileges to view the block.  As a general rule of thumb, only site administrators should be granted access to this block.</p>";
Dries's avatar
 
Dries committed
75
      $output .= "<p>The auto-throttle mechanism performs an extra database query in order to determine what the current throttle level should be.  Fortunately the throttle can be tuned so these database queries only occur on a fraction of all pages generated by your site, reducing the overhead to an insignificant amount.  Additionally, when the top-most throttle level is reached, all throttle queries are suspended for a configurable period of time.  More detail follows.</p>";
76
      $output .= "<p>As with any module, the throttle module needs to be %modules-enable before you can use it.  Also refer to the permissions section below if you wish to access the throttle statistics block.</p>";
Dries's avatar
 
Dries committed
77
      $output .= "<h3>Configuring the throttle module</h3><p>The %throttle-config for the throttle allows you to turn it on and off, as well as to fine-tune how sensitive it is.</p>";
Dries's avatar
 
Dries committed
78
      $output .= "<h4>enable auto-throttle:</h4><blockquote>This first option on the throttle module configuration screen allows you to enable or disable the auto-throttling mechanism.  Note that the access-log must also be enabled via the %statistics-config for the auto-throttling mechanism to have any effect.</blockquote>";
Dries's avatar
 
Dries committed
79 80 81
      $output .= "<h4>auto-throttle multiplier:</h4><blockquote><p>This second option allows you to tune the auto-throttle mechanism.  The auto-throttle mechanism supports six throttle levels, from 0 (off) to 5 (maximum).  The current throttle level is based upon how many pages have been accessed on your site in the past 60 seconds - the more pages being displayed, the higher the throttle level.  This multiplier defines how many hits are required to switch from one throttle level to the next.</p>";
      $output .= "<p>For example, with a throttle multiplier of 20:  Once 20 pages have been accessed on your site within a period of 60 seconds, the throttle level will be incremented to a level of 1.  Once 40 pages have been accessed on your site within a period of 60 seconds, the throttle level will be incremented to a level of 2.  And so on, until 100 pages are accessed on your site within a period of 60 seconds, at which time the throttle level will be set to a maximum level of 5.</p>";
      $output .= "<p>Upon reaching a throttle level of 5, access logs and the auto-throttle checking mechanism is automatically disabled.  It is only renabled by cron after a period of time defined by \"auto-throttle cron test\", explained below.</p></blockquote>";
Dries's avatar
 
Dries committed
82 83
      $output .= "<h4>auto-throttle probability limiter:</h4><blockquote><p>This option allows you to minimize the performance impact of the auto-throttle.  If we refer to the probability limiter as P, then P% of all pages generated by your site will perform an extra database query to verify that the current throttle level is appropriate to the current server load.</p>";
      $output .= "<p>As a rule of thumb, the higher your multiplier, the lower your probability limiter should be.  For example, if you have a multiplier of 100, then you logically don't need to check the throttle level more than once out of every 100 page views, so the probability limiter should be set to 1%.  As database queries are \"expensive\", it's recommended that you keep the probability limiter to the smallest percentage possible, while still high enough to react quickly to a change in server load.</p></blockquote>";
84
      $output .= "<h4>auto-throttle cron test:</h4><blockquote><p>The auto-throttle dynamically adjusts its level upward, but not downward.  That is to say, if you have a multiplier of 20 and you get 45 hits in one minute, your throttle level will be adjusted to a level of 2.  If a few minutes later you only get 35 hits in one minute, the throttle level will <strong>NOT</strong> be adjusted down to a level of 1.  This prevents the throttle from bouncing back and forth between two levels.</p>";
Dries's avatar
 
Dries committed
85
      $output .= "<p>In order for the throttle level to be dropped, \"cron.php\" must be called regularly.  This option then defines how often the level will be dropped by one to test the server load.  If the server load is no longer as high as it was, the level will stay where it is, until the cron test period passes again and cron drops the throttle level again.  This process repeats until the throttle is returned to a throttle level of 0.</p></blockquote>";
Dries's avatar
 
Dries committed
86 87 88
      $output .= "<h3>Throttle block</h3><p>This block displays some statistics regarding the current throttle and its configuration.  It is recommended that only site administrators receive the \"%throttle-access\" permission bit required to view this block.  It does not display information that would interest a normal site end-user.</p>";
      $output .= "<p>Don't forget to enable the block %throttle-block-enable.</p>";
      $output .= "<h3>Permissions</h3><p>This module has one permission that needs to be configured in %permissions.</p>";
Dries's avatar
 
Dries committed
89 90 91
      $output .= "<ul><li><i>access throttle block</i> - enable for user roles that get to view the throttle block.</li></ul>";
      $output .= "<h3>For programmers: throttle_status()</h3><p>The function <code>throttle_status()</code> will return a number from 0 to 5.  0 means that there is no throttle enabled at this time.  Each number above that is a progressively more throttled system...  To disable a feature when a site first begins to get busy, disable it at a throttle of 2 or 3.  To hold on to the bitter end, wait until 4 or 5.</p>";
      $output .= "<p>To implement the throttle, you should do something like this:";
92 93 94
      $output .= "<pre>
       if (module_invoke(\"throttle\", \"status\") >= \$my_throttle_value) {
          // my throttle limit was reached, disable stuff
Dries's avatar
 
Dries committed
95 96 97 98
        }
        else {
          // throttle limit not reached, execute normally
       }</pre></p>";
Dries's avatar
 
Dries committed
99
    $output = t($output, array("%statistics-module" => l(t("statistics module"), "admin/statistics"), "%throttle-block" => l(t("access throttle block"), "admin/user/permission"), "%modules-enable" => l(t("enabled"), "admin/system/modules"), "%throttle-config" => l(t("configuration section"), "admin/system/modules/throttle"), "%statistics-config" => l(t("statistics module"), "admin/system/modules/statistics"), "%throttle-access" => l(t("access throttle block"), "admin/user/permission"), "%throttle-block-enable" => l(t("here"), "admin/block"), "%permissions" => l(t("user permissions"), "admin/user/permission")));
Dries's avatar
 
Dries committed
100 101
    break;
  }
Dries's avatar
 
Dries committed
102

Dries's avatar
 
Dries committed
103
  return $output;
Dries's avatar
 
Dries committed
104 105 106
}


Dries's avatar
 
Dries committed
107
// throttle module configuration options
108
function throttle_settings() {
Dries's avatar
 
Dries committed
109
  // enable/disable auto-throttle
Dries's avatar
 
Dries committed
110
  $group = form_radios(t("Enable auto-throttle"), "throttle_enable", variable_get("throttle_enable", 0), array("1" => t("enabled"), "0" => t("disabled")), t("Enable auto-throttling congestion control mechanism.  Allows your site to adapt under extreme user loads, such as being linked by Slashdot.  To use the auto-throttle you must also enable the '%access_log' from the statistics module.", array("%access_log" => l(t("access log"), "admin/system/modules/statistics"))));
111
  $output = form_group(t("Auto-throttle status"), $group);
Dries's avatar
 
Dries committed
112 113

  // tune auto-throttle
Dries's avatar
 
Dries committed
114
  $throttles = array(1 => "1 (0,1,2,3,4,5)", 5 => "5 (0,5,10,15,20,25)", 10 => "10 (0,10,20,30,40,50)", 12 => "12 (0,12,24,36,48,60)", 15 => "15 (0,15,30,45,60,75)", 20 => "20 (0,20,40,60,80,100)", 30 => "30 (0,30,60,90,120,150)", 50 => "50 (0,50,100,150,200,250)", 60 => "60 (0,60,120,180,240,300)", 100 => "100 (0,100,200,300,400,500", 500 => "500 (0,500,1000,1500,2000,2500", 1000 => "1000 (0,1000,2000,3000,4000,5000)");
115
  $group = form_select(t("Auto-throttle multiplier"), "throttle_multiplier", variable_get("throttle_multiplier", 60), $throttles, "The 'auto-throttle multiplier' is the number of hits in the past 60 seconds required to trigger a higher throttle level.  For example, if you set the multiplier to 60, and your site is getting less than 60 hits a minute, then the throttle will be at a level of 0.  Only once you start getting more than 60 hits a minute will the throttle level go to 1.  If you start getting more than 120 hits a minute, the throttle becomes 2.  This continues until your site is sustaining more than 300 hits per minute, at which time the throttle reaches a maximum level of 5.  In the pop down menu, the first number is the multiplier, and the numbers in parenthesis are how many hits are required to switch to each throttle level.  The more powerful your server, the higher you should set the multiplier value.");
Dries's avatar
 
Dries committed
116
  $probabilities = array(0 => "100%", 1 => "50%", 2 => "33.3%", 3 => "25%", 4 => "20%", 5 => "16.6%", 7 => "12.5%", 9 => "10%", 19 => "5%", 99 => "1%", 199 => ".5%", 399 => ".25%", 989 => ".1%");
117
  $group .= form_select(t("Auto-throttle probability limiter"), "throttle_probability_limiter", variable_get("throttle_probability_limiter", 9), $probabilities, "The auto-throttle probability limiter is an efficiency mechanism to statistically reduce the overhead of the auto-throttle.  The limiter is expressed as a percentage of page views, so for example if set to the default of 10% we only perform the extra database query to update the current level 1 out of every 10 page views.  The busier your site, the lower you should set the limiter value.");
Dries's avatar
 
Dries committed
118
  $period = array(1800 => format_interval(1800), 3600 => format_interval(3600), 7200 => format_interval(7200), 10800 => format_interval(10800), 14400 => format_interval(14400), 18000 => format_interval(18000), 21600 => format_interval(21600), 43200 => format_interval(43200), 64800 => format_interval(64800), 86400 => format_interval(86400), 172800 => format_interval(172800), 259200 => format_interval(259200), 604800 => format_interval(604800));
119
  $group .= form_select(t("Auto-throttle cron test"), "throttle_cron_timer", variable_get("throttle_cron_timer", 10800), $period, "The auto-throttle cron test value specifies how frequently the throttle level will be automatically lowered, testing server load.  After this amount of time, if your throttle level is greater than 0, it will be reduced by 1.  If your site is no longer sustaining as heavy a load as it was when it increased the throttle, it will stay at this reduced level.  If your site is still sustaining a heavy load, the throttle level will soon be increased again.  Requires cron.");
Dries's avatar
 
Dries committed
120 121
  $output .= form_group(t("Auto-throttle tuning"), $group);

Dries's avatar
 
Dries committed
122 123 124
  return $output;
}

Dries's avatar
 
Dries committed
125 126 127 128 129 130 131 132 133 134 135
function throttle_cron() {
  $throttle = throttle_status();
  /* check if throttle is currently on and if it's time to drop level */
  if (($throttle) && ((time() - variable_get("throttle_cron_timer", 10800)) > variable_get("throttle_cron_timestamp", 0))) {
    /* If throttle is on, back off one notch to test server load */
    variable_set("throttle_level", $throttle - 1);
    variable_set("throttle_cron_timestamp", time());
    watchdog("warning", t("cron: decreasing throttle to level '%level' to test server load.", array("%level" => ($throttle - 1))));
  }
}

Dries's avatar
 
Dries committed
136

Dries's avatar
 
Dries committed
137
// displays admin oriented "Throttle status" block
Dries's avatar
 
Dries committed
138 139
function throttle_display_throttle_block() {
  if (user_access("access throttle block")) {
140
    if (variable_get("throttle_enable", 0)) {
Dries's avatar
 
Dries committed
141
      /* the throttle is enabled:  display the status of all throttle config */
142 143
      $throttle = module_invoke("throttle", "status");
      $multiplier = variable_get("throttle_multiplier", 60);
Dries's avatar
 
Dries committed
144
      $minimum = $throttle * $multiplier;
145
      $limiter = variable_get("throttle_probability_limiter", 9);
Dries's avatar
 
Dries committed
146 147 148
      /* calculate probability limiter's odds of updating throttle */
      $probability = substr((($limiter / ($limiter + 1) * 100) - 100) * -1, 0, 4);

Dries's avatar
 
Dries committed
149
      $output .= t("Throttle: %status", array("%status" => l(t("enabled"), "admin/system/modules/throttle"))) ."<br />\n";
Dries's avatar
 
Dries committed
150 151
      if ($throttle < 5) {
        $maximum = (($throttle + 1) * $multiplier) - 1;
Dries's avatar
 
Dries committed
152
        $output .= t("Current level: %level (%min - %max)", array("%level" => $throttle, "%min" => $minimum, "%max" => $maximum)) ."<br />\n";
Dries's avatar
 
Dries committed
153 154
      }
      else {
Dries's avatar
 
Dries committed
155
        $output .= t("Current level: %level (%min+)", array("%level" => $throttle, "%min" => $minimum)) ."<br />\n";
Dries's avatar
 
Dries committed
156
      }
Dries's avatar
 
Dries committed
157
      $output .= t("Probability: %probability%", array("%probability" => $probability)) ."<br />\n";
Dries's avatar
 
Dries committed
158 159 160 161 162
      if ($throttle < 5) {
        $recent_activity = db_fetch_object(db_query("SELECT COUNT(timestamp) AS hits FROM {accesslog} WHERE timestamp >= %d", (time() - 60)));
        $output .= "<br />". t("This site has served %pages in the past minute.", array("%pages" => format_plural($recent_activity->hits , "1 page", "%count pages")));
        _throttle_update($recent_activity->hits);
      }
Dries's avatar
 
Dries committed
163 164
    }
    else {
Dries's avatar
 
Dries committed
165
      $output .= t("Throttle:  %status", array("%status" => l(t("disabled"), "admin/system/modules/throttle"))) ."<br />\n";
Dries's avatar
 
Dries committed
166 167 168 169 170
    }
  }
  return $output;
}

Dries's avatar
 
Dries committed
171
// block hook
Dries's avatar
 
Dries committed
172 173 174 175 176 177 178 179 180 181
function throttle_block($op = "list", $delta = 0) {
  if ($op == "list") {
    $blocks[0]["info"] = t("Throttle status");
    return $blocks;
  }
  else {
    $block["subject"] = t("Throttle status");
    $block["content"] = throttle_display_throttle_block();
    return $block;
  }
Dries's avatar
 
Dries committed
182 183
}

Dries's avatar
 
Dries committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
function _throttle_update($hits) {
  $throttle = throttle_status();
  $multiplier = variable_get("throttle_multiplier", 60);

  for ($i = 0; $i <= 5; $i++) {
    if (($i * $multiplier) <= $hits) {
      $throttle_new = $i;
    }
  }

  if ($throttle_new != $throttle) {
    /*
    ** reduce throttle if new throttle would be 3+ less than current throttle,
    ** (all other throttle reduction done by _cron hook), increase throttle if
    **  new throttle would be greater than current throttle.
    */
    if (($throttle_new < ($throttle - 2)) || ($throttle_new > $throttle)) {
      /* update throttle level */
      variable_set("throttle_level", $throttle_new);
      /*
      ** update the global timestamp, preventing cron.php from jumping in
      ** too quickly, allowing for user defined period to first pass.
      */
      variable_set("throttle_cron_timestamp", time());
      /* log the change */
      if ($throttle_new < $throttle) {
        watchdog("message", "message: '". $hits ."' hits in past minute; throttle decreased to level ". $throttle_new);
      }
      else {
        watchdog("warning", "warning: '". $hits ."' hits in past minute; throttle increased to level ". $throttle_new);
      }
    }
  }
}

Dries's avatar
 
Dries committed
219
?>