0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>В жизненном цикле многих приложений в один прекрасный момент наступает время горизонтального масштабирования. Для одного из наших сервисов такое время наступило, и мы успешно превратили один инстанс в два. Однако вместо ожидаемого роста производительности получили совсем обратный эффект: при достижении определённого уровня нагрузки скорость обработки запросов очень сильно падала. В процессе поиска причины проблемы обнаружилась интересную особенность компонента Symfony, которой я и хочу поделиться в данной статье.</p>
1
<p>В жизненном цикле многих приложений в один прекрасный момент наступает время горизонтального масштабирования. Для одного из наших сервисов такое время наступило, и мы успешно превратили один инстанс в два. Однако вместо ожидаемого роста производительности получили совсем обратный эффект: при достижении определённого уровня нагрузки скорость обработки запросов очень сильно падала. В процессе поиска причины проблемы обнаружилась интересную особенность компонента Symfony, которой я и хочу поделиться в данной статье.</p>
2
<p>В Symfony есть<a>компонент кэширования</a>, в котором, начиная с версии Symfony 4.2, для предотвращения<a>Cache Stampede</a>были добавлены внутренние блокировки ключей кэширования. Эти блокировки используют механизм файловых блокировок (flock), а их количество по умолчанию в linux небольшое, да и используются они нечасто. В результате, выбранное разработчиками компонента решение автоматически не позволяет количеству одновременно запрашиваемых ключей кэширования в разы превышать количество доступных файловых блокировок, которые, к тому же, могут использоваться и другими процессами.</p>
2
<p>В Symfony есть<a>компонент кэширования</a>, в котором, начиная с версии Symfony 4.2, для предотвращения<a>Cache Stampede</a>были добавлены внутренние блокировки ключей кэширования. Эти блокировки используют механизм файловых блокировок (flock), а их количество по умолчанию в linux небольшое, да и используются они нечасто. В результате, выбранное разработчиками компонента решение автоматически не позволяет количеству одновременно запрашиваемых ключей кэширования в разы превышать количество доступных файловых блокировок, которые, к тому же, могут использоваться и другими процессами.</p>
3
<p>В нашем случае число ключей кэширования было достаточно большим, и данный подход внутри компонента кэширования приводил к тому, что в некоторых случаях запрос ожидал 20-30 секунд освобождения блокировки только для того, чтобы проверить наличие данных в кэше перед тем, как идти за ними в базу данных. Повторные попытки получить блокировку выполняются в цикле с ожиданием, и самые неудачливые запросы получали запрашиваемую блокировку с 74-го (!!) раза.</p>
3
<p>В нашем случае число ключей кэширования было достаточно большим, и данный подход внутри компонента кэширования приводил к тому, что в некоторых случаях запрос ожидал 20-30 секунд освобождения блокировки только для того, чтобы проверить наличие данных в кэше перед тем, как идти за ними в базу данных. Повторные попытки получить блокировку выполняются в цикле с ожиданием, и самые неудачливые запросы получали запрашиваемую блокировку с 74-го (!!) раза.</p>
4
<p>Конечно, это нанесло гораздо больший удар по производительности, чем потенциальная проблема Cache Stampede, потому что в нашем случае кэширование использовалось, в первую очередь, для снятия нагрузки с базы данных, и несколько повторных обновлений кэша одинаковыми значениями было не особенно затратно.</p>
4
<p>Конечно, это нанесло гораздо больший удар по производительности, чем потенциальная проблема Cache Stampede, потому что в нашем случае кэширование использовалось, в первую очередь, для снятия нагрузки с базы данных, и несколько повторных обновлений кэша одинаковыми значениями было не особенно затратно.</p>
5
<p>В качестве вывода из такой ситуации можно порекомендовать не использовать стандартный механизм, предлагаемый компонентом кэширования, если в вашем проекте большое количество одновременно запрашиваемых ключей кэширования (100+), небольшое время вычисления данных для помещения в кэш и редкое обновление самих данных (или долгий TTL).</p>
5
<p>В качестве вывода из такой ситуации можно порекомендовать не использовать стандартный механизм, предлагаемый компонентом кэширования, если в вашем проекте большое количество одновременно запрашиваемых ключей кэширования (100+), небольшое время вычисления данных для помещения в кэш и редкое обновление самих данных (или долгий TTL).</p>
6
<p>В конфигурации компонента кэширования отключить этот механизм нельзя, поэтому необходимо реализовать свой класс MemcachedAdapter следующим образом:</p>
6
<p>В конфигурации компонента кэширования отключить этот механизм нельзя, поэтому необходимо реализовать свой класс MemcachedAdapter следующим образом:</p>
7
<?php namespace App\Symfony; use Memcached; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Marshaller\MarshallerInterface; use Symfony\Component\Cache\Traits\MemcachedTrait; class FixedMemcachedAdapter extends AbstractAdapter { use MemcachedTrait; protected $maxIdLength = 250; public function __construct(Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) { $this->init($client, $namespace, $defaultLifetime, $marshaller); $this->setCallbackWrapper(null); } }<p>Помимо предлагаемого решения проблемы с файловыми блокировками при кэшировании, напомню, что изначально проблема возникла в процессе горизонтального масштабирования. А горизонтальное масштабирование подразумевает, что инстансы сервисов ничего не знают о файловых блокировках, поставленных в других инстансах, в результате, такой механизм борьбы с Cache Stampede в принципе никакой пользы принести не может.</p>
7
<?php namespace App\Symfony; use Memcached; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Marshaller\MarshallerInterface; use Symfony\Component\Cache\Traits\MemcachedTrait; class FixedMemcachedAdapter extends AbstractAdapter { use MemcachedTrait; protected $maxIdLength = 250; public function __construct(Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) { $this->init($client, $namespace, $defaultLifetime, $marshaller); $this->setCallbackWrapper(null); } }<p>Помимо предлагаемого решения проблемы с файловыми блокировками при кэшировании, напомню, что изначально проблема возникла в процессе горизонтального масштабирования. А горизонтальное масштабирование подразумевает, что инстансы сервисов ничего не знают о файловых блокировках, поставленных в других инстансах, в результате, такой механизм борьбы с Cache Stampede в принципе никакой пользы принести не может.</p>
8
<p>Внимательный читатель может задаться вопросом, почему же тогда проблема возникла при горизонтальном масштабировании, но не возникала на одном инстансе, ведь файловые блокировки должны были кончаться и на нём. Дело здесь в том, что при масштабировании увеличилась пропускная способность вышестоящих сервисов, и на один инстанс стало поступать больше запросов в единицу времени.</p>
8
<p>Внимательный читатель может задаться вопросом, почему же тогда проблема возникла при горизонтальном масштабировании, но не возникала на одном инстансе, ведь файловые блокировки должны были кончаться и на нём. Дело здесь в том, что при масштабировании увеличилась пропускная способность вышестоящих сервисов, и на один инстанс стало поступать больше запросов в единицу времени.</p>
9
<p>Подводя итоги, хочу отметить, что не всегда использование стандартных подходов фреймворка ведёт к хорошим результатам и умение разбираться с проблемами внутри фреймворка и его компонентов может очень пригодиться на практике.</p>
9
<p>Подводя итоги, хочу отметить, что не всегда использование стандартных подходов фреймворка ведёт к хорошим результатам и умение разбираться с проблемами внутри фреймворка и его компонентов может очень пригодиться на практике.</p>
10
10