Форсаж під навантаженням на Symfony + HHVM + MongoDB + CouchDB + Varnish



Сьогодні хочемо розповісти про те, як будували систему, до якої зараз звертається більше 1 млн. унікальних відвідувачів в день (без урахування запитів до API), про тонкощі архітектури, а також про тих граблях і підводні камені, з якими довелося зіткнутися. Поїхали…

Вихідні дані
Система працює на Symfony 2.3 і крутиться на дроплетах DigitalOcean, працюють бадьоро, ніяких зауважень.

Symfony
У Symfony є чудова подія kernel.terminate. Тут тлі після того, як клієнт отримав відповідь від сервера, виконується вся важка робота (запис у файли, збереження даних в кеш, запис у БД).

Як відомо, кожен подгруженный бандл Symfony так чи інакше збільшує споживання пам'яті. Тому для кожного компонента системи подгружаем лише необхідний набір бандою (наприклад, на фронтенде не потрібні бандли адмінки, а в API не потрібні бандли адмінки і фронтенда тощо). Перелік підвантажуваних бандлів в прикладі скорочений для простоти, що в реальності їх, звичайно, більше:

Клас /app/BaseAppKernel.php
<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class BaseAppKernel extends Kernel
{
protected $bundle_list = array();

public function registerBundles()
{
// Мінімально необхідний набір бандлів
$this->bundle_list = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle()
);

// Тут коли потрібно, подгружаем всі бандли системи
if ($this->needLoadAllBundles()) {
// Admin
$this->addBundle(Sonata new\BlockBundle\SonataBlockBundle());
$this->addBundle(Sonata new\CacheBundle\SonataCacheBundle());
$this->addBundle(Sonata new\jQueryBundle\SonatajQueryBundle());
$this->addBundle(Sonata new\AdminBundle\SonataAdminBundle());
$this->addBundle(new Knp\Bundle\MenuBundle\KnpMenuBundle());
$this->addBundle(Sonata new\DoctrineMongoDBAdminBundle\SonataDoctrineMongoDBAdminBundle());

// Frontend
$this->addBundle(new Likebtn\FrontendBundle\LikebtnFrontendBundle());

// API
$this->addBundle(new Likebtn\ApiBundle\LikebtnApiBundle());
}

return $this->bundle_list;
}

/**
* Перевірка, чи потрібно завантажувати всі бандли.
* Якщо скрипт запущено в dev - або text-оточенні або виконується очищення кешу prod-оточення,
* подгружаем всі бандли системи
*/
public function needLoadAllBundles()
{
if (in_array($this->getEnvironment(), array('dev', 'test')) ||
$_SERVER['SCRIPT_NAME'] == 'app/console' ||
strstr($_SERVER['SCRIPT_NAME'], 'phpunit')
) {
return true;
} else {
return false;
}
}

/**
* Додавання бандла до списку підвантажуваних
*/
public function addBundle($bundle)
{
if (in_array($bundle, $this->bundle_list)) {
return false;
}
$this->bundle_list[] = $bundle;
}

public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}


Клас /app/AppKernel.api.php
<?php

require_once __DIR__.'/BaseAppKernel.php';

class AppKernel extends BaseAppKernel
{
public function registerBundles()
{
parent::registerBundles();
$this->addBundle(new Likebtn\ApiBundle\LikebtnApiBundle());
return $this->bundle_list;
}
}


Фрагмент /web/app.php
// Усі компоненти системи розташовуються на своїх піддоменів
// Якщо якийсь компонент розташований у піддиректорії, 
// просто потрібно перевіряти шлях в $_SERVER['REQUEST_URI']
if (strstr($_SERVER['HTTP_HOST'], 'admin.')) {
// Адмінка
require_once __DIR__.'/../app/AppKernel.admin.php';
} elseif (strstr($_SERVER['HTTP_HOST'], 'api.')) {
// API
require_once __DIR__.'/../app/AppKernel.api.php';
} else {
// Фронтенд
require_once __DIR__.'/../app/AppKernel.php';
}
$kernel = new AppKernel('prod', 'false');


Хитрість у тому, що довантажувати всі бандли потрібно тільки в dev-оточенні і в момент, коли виконується очищення кешу на prod-оточенні.

MongoDB
В якості основної БД використовується MongoDB на Compose.io. Базу розміщуємо в тому ж датацентрі, що і основні сервера — благо, Compose дозволяє розміщувати БД в DigitalOcean.

В певний момент були складності з повільними запитами, з-за яких загальна швидкодія системи починало знижуватися. Вирішено питання з допомогою грамотно складених індексів. Практично всі керівництва про створення індексів для MongoDB стверджують, що, якщо в запиті використовуються операції вибору діапазону ($in $gt або $lt), то для такого запиту індекс не буде використовуватися ні за яких обставин, наприклад:
{"_id":{"$gt":ObjectId('52d89f120000000000000000')},"ip":"140.101.78.244"}

Так от, це не зовсім так. Ось універсальний алгоритм створення індексів, який дозволяє використовувати індекси для запитів з вибором діапазонів значень (чому саме такий алгоритм, можна почитати тут):
  1. Спочатку в індекс включаються поля, за якими вибираються конкретні значення.
  2. Виберіть поля, за якими йде сортування.
  3. І нарешті, поля, які беруть участь у виборі діапазону.
І вуаля:



CouchDB
Дані статистичного характеру вирішено було зберігати в CouchDB і віддавати безпосередньо клієнтам за допомогою JavaScript, зайвий раз не смикаючи сервера. Раніше з даної БД не працювали, сподобалась фраза «CouchDB призначений саме для веба».

Коли вже все було налаштовано і настав час навантажувального тестування, з'ясувалося, що з нашим потоком запитів на запис, CouchDB просто захлиналася. Практично всі керівництва з CouchDB прямо не рекомендують використовувати її для часто оновлюваних даних, але ми, звичайно ж, не повірили і понадіялися на авось. Оперативно було зроблено акумулювання даних в Memcached і перекидання їх в CouchDB через невеликі проміжки часу.

Також у CouchDB є функція збереження ревізій документів, яку штатними засобами вимкнути неможливо. Про це дізналися, коли кидатися вже було пізно. Процедура ущільнення, яка запускається при настанні певних умов, старі ревізії видаляє, але тим не менш, пам'ять ревізії їдять.



Futon — веб-адмінка CouchDB, доступна за адресою /_utils/ всім, в тому числі анонімним користувачам. Єдиний спосіб заборонити всім бажаючим дивитися базу, що змогли знайти — просто видалити наступні записи конфігурації CouchDB в секції [httpd_db_handlers] (адмін при цьому теж втрачає можливість переглядати списки документів):
_all_docs ={couch_mrview_http, handle_all_docs_req}
_changes ={couch_httpd_db, handle_changes_req}

Загалом, розслабитися CouchDB не давала.

HHVM
Бэкенды, що готують основний контент, крутяться на HHVM, який в нашому випадку працює значно бадьоріше і стабільніше використовуваної раніше зв'язки PHP-FPM + APC. Благо Symfony 2.3 на 100% сумісна з HHVM. Встановлюється HHVM на Debian 8 без будь-яких складнощів.

Щоб HHVM міг взаємодіяти з базою MongoDB, використовується розширення Mongofill for HHVM, реалізоване наполовину на C++, наполовину на PHP. З-за невеликого бага, у разі помилки при виконанні запитів до БД вивалюється:
Fatal error: Class undefined: MongoCursorException
Тим не менш, це не заважає розширенню успішно працювати в продакшені.

Varnish
Для кешування і безпосередньо віддачі контенту використовується монстр Varnish. Тут були проблеми з тим, що з якоїсь причини varnishd періодично вбивав дітей. Виглядало це приблизно так:

varnishd[23437]: Child (23438) not responding to CLI, killing it.
varnishd[23437]: Child (23438) died signal=3
varnishd[23437]: Child cleanup complete
varnishd[23437]: child (3786) Started
varnishd[23437]: Child (3786) said Child starts

Це призводило до очищення кеша і різкого зростання навантаження на систему в цілому. Причин такої поведінки, як з'ясувалося, безліч, як і порад і рецептів лікування. Спочатку грішили на параметр
p cli_timeout=30s
в /etc/default/varnish, але справа не в ньому. Загалом, після досить тривалих експериментів і перебору параметрів, було встановлено, що відбувалося це в ті моменти, коли Varnish починав активно видаляти елементи з кешу, щоб помістити нові. Дослідним шляхом для нашої системи був підібраний параметр beresp.ttl default.vcl, відповідальний за час зберігання елемента в кеші, і ситуація нормалізувалася:

sub vcl_fetch {
/* Set how long Varnish will keep it*/
set beresp.ttl = 7d;
}

Параметр beresp.ttl потрібно було встановити таким, щоб старі елементи віддалялися (expired objects) з кешу раніше, ніж новим елементам починало бракувати місця (nuked objects) в кеші:



Відсоток кеш-влучень при цьому тримається стабільно в районі 91%:



Щоб зміни в налаштуваннях вступили в силу, Varnish потрібно перезавантажити. Перезавантаження призводить до очищення кешу зі всіма витікаючими. Ось хитрість, яка дозволяє довантажити нові параметри конфігурації без перезавантаження Varnish і втрати кешу:

varnishadm -T 0.0.0.0:6087 -S /etc/varnish/secret
vcl.load config01 /etc/varnish/default.vcl
vcl.use config01
quit

config01 — назва нової конфігурації, можна задавати довільно, наприклад: newconfig, reload і т. д.

CloudFlare
CloudFlare прикриває все це справа і кешує статику, а заодно і надає SSL-сертифікати.

У деяких клієнтів були проблеми з доступом до нашого API — вони отримували запит на введення капчі «Challenge Passage». Як з'ясувалося, CloudFlare використовує Project Honey Pot та інші подібні сервіси, щоб відстежувати сервера — потенційні рассыльщики спаму, їм-то і видавалося попередження. Техпідтримка CloudFlare довгий час не могла запропонувати зрозумілого рішення. У підсумку, допомогло просте перемикання Security Level на Essentially Off панелі CloudFlare:



Висновок
На цьому поки все. Навантаження на проекті росла стрімко, часу на аналіз та пошук рішень було мінімум, тому маємо те, що маємо. Будемо вдячні, якщо хтось запропонує більш елегантні шляхи вирішення вищеописаних завдань.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.