С OctoberCMS я работаю почти 4 года в индустрии игровых серверов. Эта индустрия наиболее подвержена DDoS атакам, чем любые другие.
Я хочу поделиться своим опытом проектирования приложений на OctoberCMS. Расскажу то, что ни в одной статье вы не найдете. Действительно полезные, грамотные и рабочие советы.
1. Никакого сложного кода при инициализации приложения.
Речь идет о таких операциях, где используются базы данных или длительные вычисления. Приложение должно инициализироваться как можно быстрее.
В некоторых проектах видел, как разработчик в файле регистрации плагина при каждом запросе отправляет UPDATE запрос к базе данных:
public function boot()
{
SettingsModel::set('field', $value);
}
Мало того, что запросы к базе сами по себе являются ресурсоёмкими задачами, так при одновременном обновлении одной и той же записи MySQL сервер может зависнуть, пока его не перезагрузишь вручную.
Никаких запросов к базам данных для передачи переменных в шаблон.
Исключением являются такие случаи, когда страница должна находиться в поисковиках. Например, описание товара. Поисковые боты не умеют (или умеют, но очень примитивно) обрабатывать JS скрипты. В таких кейсах без запроса к базе данных невозможно установить meta теги. Ниже объясняю, как можно минимизировать такие запросы.
2. Дозагрузка контента через Ajax везде, где это возможно.
Если нельзя делать запросы к базе данных при инициализации приложения, то остается единственный вариант — дозагрузка через Ajax.
В OctoberCMS есть удивительный и очень удобный API для реализации таких запросов. Буквально в 5 строчек кода:
$.request('Products::onRender', {
update: {
products_row: '.products-row'
}
})
Явно указывайте имя компонента, — так OctoberCMS быстрее найдет его среди всех остальных.
3. Проектирование Компонентов.
Компоненты имеют особенность: методы onRun()
, onRender()
не запускаются во время Ajax запросов. А init()
вызывается всегда.
Поэтому в методе init()
можно инициализировать простые переменные, но не более (лучше переложить сложную логику на сам Ajax обработчик). Например, можно получить объект Request
, если в компоненте несколько обработчиков форм:
private $request;
public function init()
{
$this->request = request();
}
4. Кэширование HTTP запросов.
Поскольку теперь приложение спроектировано так, что весь контент подгружается через Ajax, можно задуматься о том, как ускорить инициализацию приложения при первом запросе (переходе на сайт). Существует прекрасный плагин BizMark.Quicksilver.
Вкратце: плагин кэширует сгенерированный сервером HTML файл в папку /storage/quicksilver/cache
. Таким образом, следующий запрос не требует повторной генерации такого же HTML файла.
Не рекомендую кэшировать Ajax запросы. К сожалению, из коробки плагин не умеет их игнорировать, поэтому требует модификации (файл /plugins/bizmark/quicksilver/classes/middlewares/QuicksilverMiddleware.php
):
public function handle(Request $request, Closure $next)
{
// Is request Ajax?
if ($request->ajax()) {
return $next($request);
}
if ($this->cache->has($request)) {
return $this->cache->get($request);
}
$response = $next($request);
if ($this->cache->validate($request, $response)) {
$this->cache->store($request, $response);
}
return $response;
}
Существуют такие кейсы, когда необходимо сгенерировать HTML страницу без Ajax подгрузки (например, описание товара для поисковых роботов). В таком случае рекомендую кэшировать страницу целиком и отслеживать изменения модели, чтобы своевременно очистить кэш:
class Product extends Model
{
public function beforeUpdate()
{
$this->clearPageCache();
}
public function beforeDelete()
{
$this->clearPageCache();
}
public function clearPageCache()
{
try {
Artisan::call("quicksilver:clear /product/{$this->id}");
} catch (Exception $e) {
Log::error($e->getMessage());
throw new ApplicationException('Something went wrong...');
}
}
}
5. Отдаем закэшированные файлы NGINX’ом.
В своих проектах в качестве веб-сервера я использую исключительно NGINX. Примеров кода на Apache не будет 😁
location / {
set $rewrite 0;
if ($http_x_october_request_handler) {
set $rewrite 1;
}
if ($request_method != POST) {
set $rewrite 0;
}
if ($rewrite) {
rewrite ^/.*$ /index.php last;
}
try_files /storage/quicksilver/cache$uri.html /storage/quicksilver/cache/qs_index_qs.html /index.php?$query_string;
}
В официальной документации к плагину происходит проверка $request_method = POST
. Поскольку JavaScript API октября предполагает, что любой Ajax запрос сопровождается заголовком X-October-Request-Handler
, достаточно просто проверить его существование. Он хранится в переменной $http_x_october_request_handler
. В случаях, когда заголовок существует, но метод не POST (что может произойти только при подмене запроса) все равно будет отдаваться закэшированный контент.
Теперь HTML страница генерируется без участия PHP скриптов.
6. Ограничиваем HTTP запросы.
Вишенка на торте 😋
Во всех статьях по ограничению HTTP запросов говорят делать что-то подобное:
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=3r/s;
server {
location / {
limit_req zone=req_zone burst=3 nodelay;
}
}
Но здесь есть один существенный недостаток: ограничение касается ВСЕХ запросов, в том числе и Ajax. В рамках этой статьи предполагается, что клиент в секунду отправит минимум 2-3 запроса на сервер. Поэтому настройка должна быть более деликатной, универсального правила не существует.
Будем ограничивать в запросах уязвимые места. Уязвимыми являются прямые запросы до Ajax обработчиков. Ограничим до трех POST запросов в секунду на каждый обработчик по отдельности с одного IP адреса:
map $http_x_requested_with $limit_requests_by_ajax_handler {
default "";
"XMLHttpRequest" $binary_remote_addr$http_x_october_request_handler;
}
limit_req_zone $limit_requests_by_ajax_handler zone=limit_ajax_requests_per_handler:10m rate=2r/s;
server {
location ~ ^/index.php {
limit_req zone=limit_ajax_requests_per_handler burst=1 nodelay;
}
}
Также можно ограничить обычные запросы (не Ajax), которые не доходят до PHP скриптов. Здесь можно задать более лояльные условия, поскольку сильной нагрузки на сервер не несут. В целях экономии трафика
map $http_x_requested_with $limit_requests {
default $binary_remote_addr;
"XMLHttpRequest" "";
}
limit_req_zone $limit_requests zone=limit_requests:10m rate=5r/s;
server {
location / {
limit_req zone=limit_requests burst=2 nodelay;
}
}
Таким образом, в комплексе эти рекомендации дают следующий буст:
1) Заметное увеличение скорости загрузки веб-сайта (примерно в 2-3 раза)
2) Уменьшение нагрузки на сервер за счет кэширования HTTP запросов
3) Распределение нагрузки за счет Ajax подгрузки контента
4) Повышение позиции в индексе, поскольку поисковики отдают предпочтение сайтам, которые загружаются быстрее
5) Мощная защита от DDoS атак своими силами на уровне приложения (L7), которая реально работает
6) Зависть конкурентов