memcached не нужен

в 10:42, , рубрики: highload, memcached, nginx, php, Веб-разработка, метки: , ,

Делаю полностью статичный сайт. На самом деле, php выполняет роль бэкенда: если html файла нет, то сгенерировать его и положить в кэш, а далее nginx отдает html файл из кэша сразу, не трогая php вообще.

Сайт должен получиться огромным. Покрайней мере, я сразу стараюсь делать из расчета на высокую нагрузку. И на данный момент, когда php выполняется лишь единожды для генерации страницы, теперь самое узкое место — это отдача уже готовых статичных html файлов.

Следует знать, что при обращении к файлу он помещается в кэш в оперативной памяти ядром системы, и поэтому, как многие думают, заранее хранить статику в каком-нибудь tmpfs нет смысла, совсем. memcached же именно это и делает, и по какой-то причине все его нахваливают, и особенно любят использовать для кэширования html страниц.

Я убедился в обратном и хочу поделиться с вами, что хранить кэш html страниц в memcached не стоит, и какая есть ему лучшая альтернатива.

На своем сайте я использую веб-сервер nginx, который по-умолчанию собирается с модулем memcached. И nginx при запросе файла может отдавать его прямо из memcached (то есть, из оперативной памяти).

memcached включается следующим куском «кода» в nginx.conf:

...

http {

  ...

  server {
    listen [::]:80;
    listen 80;
    server_name _;

    ...

    location / {
      set $memcached_key $uri;
      memcached_pass localhost:11211;
      default_type text/html;
      error_page 404 502 504 = @try_files;
    }

    location @try_files {
      try_files $uri $uri/ /index.php?$args&test=$uri;
    }
  }

  ...

}

Где $memcached_key это то, что nginx попытается найти в memcached, и в данном случае запрашиваемый документ $uri.
Если найдет, то отдаст его клиенту как text/html, а если нет (404), либо если memcached отвалился (502, 504), пойдет смотреть другие файлы @try_files.
И далее, если запрашиваемый файл существует, либо существует такая директория — отдаст клиенту их, а иначе запустит скрипт /index.php, который у меня в роли бэкенда. test=$uri необходим для обработки Человеко-Понятных Урлов.

При первом запросе страницы бэкенд (php скрипт) генерирует ее, сохраняет в memcached для последующих запросов, и отдает клиенту:

<?php
foreach (explode('/', $_GET['test']) as $v) {
  if (strlen($v) > 0) $uri[] = $v; unset($v);
}
ob_start();
if (implode('/', $uri) == 'about') include('includes/about.inc.php');
$contents = ob_get_clean();
?>
<?php ob_start(); ?>
<!doctype html>
<html>
...
<?php echo $contents; ?>
...
</html>
<?php
$m = new Memcached();
$m->addServer('localhost', 11211);
$m->setOption(Memcached::OPT_COMPRESSION, false);
$m->set('/'.implode('/', $path), ob_get_contents(), (2 * 60 * 60)); }
?>
<?php ob_end_flush(); ?>

Теперь я решил проверить, насколько быстро работает memcached.
Для чистоты эксперимента отключил php-fpm, обновил страницу в браузере — открывается, значит, nginx теперь берет ее из memcached.

ab -c 50 -n 100000 http://localhost/about

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        nginx/1.3.0
Server Hostname:        localhost
Server Port:            80

Document Path:          /about
Document Length:        9546 bytes

Concurrency Level:      50
Time taken for tests:   46.139 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      966700000 bytes
HTML transferred:       954600000 bytes
Requests per second:    2167.36 [#/sec] (mean)
Time per request:       23.070 [ms] (mean)
Time per request:       0.461 [ms] (mean, across all concurrent requests)
Transfer rate:          20460.78 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   2.1      0      15
Processing:     1   21   7.1     22      97
Waiting:        1   20   7.1     21      97
Total:          1   23   6.4     23      99

Percentage of the requests served within a certain time (ms)
  50%     23
  66%     24
  75%     25
  80%     26
  90%     30
  95%     34
  98%     39
  99%     43
 100%     99 (longest request)

100000 запросов с 50 клиентов и итоговое время выполнения 46 секунд, для отдачи одного статичного html файла. Еще хочу отметить, что во время тестирования memcached съедал процессорное время наравне с nginx (а это много).

Насколько это хорошо или плохо — решать вам, но я решил попробовать другой, кстати более простой и не требующих лишних движений вообще, метод кэширования, который обеспечивает сам nginx (HttpCoreModule).

Выключаем memcached, выкидываем его из nginx.conf вообще, и вместо него пишем:

...

http {

  ...

  open_file_cache max=1024;
  open_file_cache_valid 30s;
  open_file_cache_min_uses 2;
  open_file_cache_errors on;

  ...

  server {
    listen [::]:80;
    listen 80;
    server_name _;

    ...

    try_files $uri /cache/$uri.html $uri/ /index.php?$args&test=$uri;
  }

...

}

И ничего лишнего. Добавлена проверка наличия запрашиваемого .html файла в директории cache, но за то избавились от двух конструкций location.

Возвращаемся к бэкенду, — запускаем выключенный php-fpm, и предпоследнюю php-конструкцию, где делается запись в memcached, меняем на:

<?php
file_put_contents('cache/'.implode('/', $path).'.html', ob_get_contents(), LOCK_EX);
?>

Кода тоже поубавилось и он стал проще. Создаем директорию cache, убеждаемся, что есть права на запись в нее пользователю под которым запущен php-fpm, запрашиваем один раз файл и видим, что в директории появилась его .html копия. Снова, для чистоты эксперимента отключаем php-fpm, проверяем — в браузере документ продолжает открываться, а значит уже берется nginx'ом из директории cache напрямую.

open_file_cache кэширует дескрипторы открытых файлов, информацию об их размере, дате модификации и кое-что еще. Не уверен, но кажется это кэш команды stat и теперь она не выполняется при каждом запросе файла.

Снова, проверяем нагрузку.

ab -c 50 -n 100000 http://localhost/about

This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        nginx/1.3.0
Server Hostname:        localhost
Server Port:            80

Document Path:          /about
Document Length:        9546 bytes

Concurrency Level:      50
Time taken for tests:   20.012 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      966700000 bytes
HTML transferred:       954600000 bytes
Requests per second:    4996.92 [#/sec] (mean)
Time per request:       10.006 [ms] (mean)
Time per request:       0.200 [ms] (mean, across all concurrent requests)
Transfer rate:          47173.07 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   1.2      3      10
Processing:     1    8   2.3      7      72
Waiting:        0    5   2.8      4      70
Total:          2   10   1.9     10      74

Percentage of the requests served within a certain time (ms)
  50%     10
  66%     10
  75%     10
  80%     10
  90%     11
  95%     12
  98%     15
  99%     17
 100%     74 (longest request)

100000 запросов с 50 клиентов и итоговое время выполнения 20 секунд, против предыдущих 46 секунд при использовании memcached.


И еще раз:

  • memcached — 100000 запросов с 50 клиентов, время выполнения 46 секунд.
  • open_file_cache — 100000 запросов с 50 клиентов, время выполнения 20 секунд.

Вы все еще используете memcached для кэширования html страниц?

В моем случае .html файлы хранятся на винте и имеют точно такой же путь, как у запрашиваемого ЧПУ. И таким образом, ЧПУ необходим для кэширования, и ЧПУ без кэширования безполезен.


Вдогонку, немного хочу рассказать про частично-динамичный контент.

Страницы сайта кэшируются полностью и отдаются статичным .html файлами, но есть на сайте и блок с авторизацией пользователей. Вариантов я вижу два, — либо ajax запросом спрашивать с сервера php скрипт авторизации, либо делать SSI вставку того же php скрипта.

В случае ajax у нас есть возможность проверить наличие сессионной куки при помощи javascript, и если ее нет, то не отправлять запрос на сервер вообще.
В случае SSI такой проверки на стороне клиента уже не сделать и скрипт будет размещаться на странице всегда, но php скрипт в несколько строк кода и он и не должен создавать большой нагрузки на сервер.

И для возможности размещения SSI блоков на странице, для все той же авторизации пользователей, бэкенд (php скрипт) выглядит так:

<?php
function ob_return($data, $authentication) {
  return str_replace('<!--# include virtual="scripts/authentication.php" -->', $authentication, $data);
}
foreach (explode('/', $_GET['test']) as $v) {
  if (strlen($v) > 0) $uri[] = $v; unset($v);
}
ob_start(); include('scripts/authentication.php'); $authentication = ob_get_clean();
ob_start();
if (implode('/', $uri) == 'about') include('includes/about.inc.php');
$contents = ob_get_clean();
?>
<?php ob_start(array('ob_return', $authentication)); ?>
<!doctype html>
<html>
...
<!--# include virtual="scripts/authentication.php" -->
...
<?php echo $contents; ?>
...
</html>
<?php file_put_contents('cache/'.implode('/', $uri).'.html', ob_get_contents(), LOCK_EX); ?>
<?php ob_end_flush(); ?>

И при отдаче страницы SSI блок будет заменен на результат выполнения скрипта авторизации, но а при записи в кэшируемый .html-файл блок останется не тронутым. И в дальнейшем будет обрабатываться на серверной стороне сразу, при отдаче .html документа. Код скрипта для вывода авторизации пользователя у меня такой:

<?php
if (isset($_COOKIE[(session_name())])) {
  session_start();
  if (sizeof($_SESSION) < 2) {
    session_unset();
    session_destroy();
    session_write_close();
  }
}
if (isset($_SESSION['ID']))
  echo 'Hi, <b>'.$_SESSION['nick'].'</b> (IP: '.$_SERVER['REMOTE_ADDR'].'). <a href="/user/signoff">Sign Off</a>';
else
  echo 'Hi, <b>Guest</b> (IP: '.$_SERVER['REMOTE_ADDR'].'). <a href="/user/signin">Sign In</a> | <a href="/user/signup">Sign Up</a>';
?>

Кстати, для включения SSI еще нужно добавить всего одну опцию в nginx.conf:

  server {

    ...

    ssi on;

    ...

  }

Такой хайлоад…

Автор: Spoofing

* - обязательные к заполнению поля