Сегодня хотим рассказать о том, как строили систему, к которой сейчас обращается более 1 млн. уникальных посетителей в день (без учёта запросов к API), о тонкостях архитектуры, а также о тех граблях и подводных камнях, с которыми пришлось столкнуться. Поехали...
Исходные данные
Система работает на Symfony 2.3 и крутится на дроплетах DigitalOcean, работают бодро, никаких замечаний.
Symfony
У Symfony есть замечательное событие kernel.terminate. Здесь в фоне после того, как клиент получил ответ от сервера, выполняется вся тяжёлая работа (запись в файлы, сохранение данных в кэш, запись в БД).
Как известно, каждый подгруженный бандл Symfony так или иначе увеличивает потребление памяти. Поэтому для каждого компонента системы подгружаем только необходимый набор бандов (например, на фронтенде не нужны бандлы админки, а в API не нужны бандлы админки и фронтенда и т.д.). Перечень подгружаемых бандлов в примере сокращён для простоты, в реальности их, конечно, больше:
<?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(new Sonata\BlockBundle\SonataBlockBundle());
$this->addBundle(new Sonata\CacheBundle\SonataCacheBundle());
$this->addBundle(new Sonata\jQueryBundle\SonatajQueryBundle());
$this->addBundle(new Sonata\AdminBundle\SonataAdminBundle());
$this->addBundle(new Knp\Bundle\MenuBundle\KnpMenuBundle());
$this->addBundle(new Sonata\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');
}
}
<?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;
}
}
// Все компоненты системы располагаются на своих поддоменах
// Если какой-то компонент располагается в поддиректории,
// просто нужно проверять путь в $_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"}
Так вот, это не совсем так. Вот универсальный алгоритм создания индексов, который позволяет использовать индексы и для запросов с выбором диапазонов значений (почему алгоритм именно такой, можно почитать здесь):
- Сначала в индекс включаются поля, по которым выбираются конкретные значения.
- Затем поля, по которым идёт сортировка.
- И наконец, поля, которые участвуют в выборе диапазона.
И вуаля:
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:
Заключение
На этом пока всё. Нагрузка на проекте росла стремительно, времени на анализ и поиск решений было минимум, поэтому имеем то, что имеем. Будем благодарны, если кто-то предложит более элегантные пути решения вышеописанных задач.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий