- PVSM.RU - https://www.pvsm.ru -
Сегодня хотим рассказать о том, как строили систему, к которой сейчас обращается более 1 млн. уникальных посетителей в день (без учёта запросов к API), о тонкостях архитектуры, а также о тех граблях и подводных камнях, с которыми пришлось столкнуться. Поехали...
Система работает на Symfony 2.3 и крутится на дроплетах DigitalOcean [1], работают бодро, никаких замечаний.
У Symfony есть замечательное событие kernel.terminate [2]. Здесь в фоне после того, как клиент получил ответ от сервера, выполняется вся тяжёлая работа (запись в файлы, сохранение данных в кэш, запись в БД).
Как известно, каждый подгруженный бандл Symfony так или иначе увеличивает потребление памяти. Поэтому для каждого компонента системы подгружаем только необходимый набор бандов (например, на фронтенде не нужны бандлы админки, а в API не нужны бандлы админки и фронтенда и т.д.). Перечень подгружаемых бандлов в примере сокращён для простоты, в реальности их, конечно, больше:
<?php
use SymfonyComponentHttpKernelKernel;
use SymfonyComponentConfigLoaderLoaderInterface;
class BaseAppKernel extends Kernel
{
protected $bundle_list = array();
public function registerBundles()
{
// Минимально необходимый набор бандлов
$this->bundle_list = array(
new SymfonyBundleFrameworkBundleFrameworkBundle(),
new SymfonyBundleSecurityBundleSecurityBundle(),
new SymfonyBundleTwigBundleTwigBundle(),
new SymfonyBundleMonologBundleMonologBundle(),
new SymfonyBundleAsseticBundleAsseticBundle(),
new DoctrineBundleDoctrineBundleDoctrineBundle(),
new SensioBundleFrameworkExtraBundleSensioFrameworkExtraBundle(),
new DoctrineBundleMongoDBBundleDoctrineMongoDBBundle()
);
// Здесь когда нужно, подгружаем все бандлы системы
if ($this->needLoadAllBundles()) {
// Admin
$this->addBundle(new SonataBlockBundleSonataBlockBundle());
$this->addBundle(new SonataCacheBundleSonataCacheBundle());
$this->addBundle(new SonatajQueryBundleSonatajQueryBundle());
$this->addBundle(new SonataAdminBundleSonataAdminBundle());
$this->addBundle(new KnpBundleMenuBundleKnpMenuBundle());
$this->addBundle(new SonataDoctrineMongoDBAdminBundleSonataDoctrineMongoDBAdminBundle());
// Frontend
$this->addBundle(new LikebtnFrontendBundleLikebtnFrontendBundle());
// API
$this->addBundle(new LikebtnApiBundleLikebtnApiBundle());
}
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');
}
}
<?php
require_once __DIR__.'/BaseAppKernel.php';
class AppKernel extends BaseAppKernel
{
public function registerBundles()
{
parent::registerBundles();
$this->addBundle(new LikebtnApiBundleLikebtnApiBundle());
return $this->bundle_list;
}
}
// Все компоненты системы располагаются на своих поддоменах
// Если какой-то компонент располагается в поддиректории,
// просто нужно проверять путь в $_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 на Compose.io [3]. Базу размещаем в том же датацентре, что и основные сервера — благо, Compose позволяет размещать [4] БД в DigitalOcean.
В определённый момент были сложности с медленными запросами, из-за которых общее быстродействие системы начинало снижаться. Решён вопрос был с помощью грамотно составленных индексов. Практически все руководства о создании индексов для MongoDB утверждают, что, если в запросе используются операции выбора диапазона ($in, $gt или $lt), то для такого запроса индекс не будет использоваться ни при каких обстоятельствах, например:
{"_id":{"$gt":ObjectId('52d89f120000000000000000')},"ip":"140.101.78.244"}
Так вот, это не совсем так. Вот универсальный алгоритм создания индексов, который позволяет использовать индексы и для запросов с выбором диапазонов значений (почему алгоритм именно такой, можно почитать здесь [5]):
И вуаля:
Данные статистического характера решено было хранить в CouchDB и отдавать напрямую клиентам с помощью JavaScript, лишний раз не дёргая сервера. Ранее с данной БД не работали, подкупила фраза «CouchDB предназначен именно для веба».
Когда уже всё было настроено и пришло время нагрузочного тестирования, выяснилось, что с нашим потоком запросов на запись, CouchDB просто захлёбывалась. Практически все руководства по CouchDB прямо не рекомендуют использовать её для часто обновляемых данных, но мы, конечно же, не поверили и понадеялись на авось. Оперативно было сделано аккумулирование данных в Memcached и переброска их в CouchDB через небольшие промежутки времени.
Также у CouchDB есть функция сохранения ревизий документов, которую штатными средствами отключить невозможно. Об этом узнали, когда метаться уже было поздно. Процедура уплотнения [6], которая запускается при наступлении определённых условий, старые ревизии удаляет, но тем не менее, память ревизии кушают.
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 [7], который в нашем случае работает в разы бодрее и стабильнее используемой ранее связки PHP-FPM + APC. Благо Symfony 2.3 на 100% совместима [8] с HHVM. Устанавливается [9] HHVM на Debian 8 без каких-либо сложностей.
Чтобы HHVM мог взаимодействовать с базой MongoDB, используется расширение Mongofill for HHVM [10], реализованное наполовину на C++, наполовину на PHP. Из-за небольшого [11] бага [12], в случае ошибок при выполнении запросов к БД вываливается:
Fatal error: Class undefined: MongoCursorException
Тем не менее, это не мешает расширению успешно работать в продакшене.
Для кэширования и непосредственно отдачи контента используется монстр 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 прикрывает всё это дело и кэширует статику, а заодно и предоставляет SSL-сертификаты.
У некоторых клиентов были проблемы с доступом к нашему API — они получали запрос на ввод капчи «Challenge Passage». Как выяснилось, CloudFlare использует Project Honey Pot [13] и другие подобные сервисы, чтобы отслеживать сервера — потенциальные рассыльщики спама, им-то и выдавалось предупреждение. Техподдержка CloudFlare долгое время не могла предложить вразумительного решения. В итоге, помогло простое переключение Security Level на Essentially Off в панели CloudFlare:
На этом пока всё. Нагрузка на проекте росла стремительно, времени на анализ и поиск решений было минимум, поэтому имеем то, что имеем. Будем благодарны, если кто-то предложит более элегантные пути решения вышеописанных задач.
Автор: transpond
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/110071
Ссылки в тексте:
[1] DigitalOcean: https://m.do.co/c/d094555af590
[2] kernel.terminate: http://symfony.com/doc/current/components/http_kernel/introduction.html#the-kernel-terminate-event
[3] Compose.io: https://compose.io/
[4] позволяет размещать: https://www.compose.io/digitalocean/
[5] здесь: http://blog.mongolab.com/2012/06/cardinal-ins/
[6] уплотнения: https://wiki.apache.org/couchdb/Compaction
[7] HHVM: http://hhvm.com/
[8] на 100% совместима: http://symfony.com/blog/symfony-2-3-achieves-100-hhvm-compatibility
[9] Устанавливается: https://docs.hhvm.com/hhvm/installation/linux#debian-8-jessie
[10] Mongofill for HHVM: https://github.com/mongofill/mongofill-hhvm
[11] небольшого: https://github.com/mongofill/mongofill-hhvm/issues/45
[12] бага: https://github.com/mongofill/mongofill-hhvm/pull/47
[13] Project Honey Pot: https://www.projecthoneypot.org
[14] Источник: https://habrahabr.ru/post/275661/
Нажмите здесь для печати.