Нужен ли нам сейчас кеш-слой перед СУБД
- пятница, 21 февраля 2025 г. в 00:00:14
Уже лет 20 существует миф (или не миф), что современный Highload-проект невозможен без кэшей. Они всегда нас выручали, когда не справлялись базы данных. Но с тех пор, как появились первые кэши, key-value баз данных и другие технологии, многое изменилось и традиционные СУБД значительно эволюционировали. И так ли теперь нужен кэш?
Мы протестировали самые известные кэш-сервисы и СУБД и попробовали выжать из них миллион запросов в секунду в разных условиях. Делимся с вами результатами в этой статье.
Привет, Хабр! Я Алексей Рыбак, предприниматель и основатель R&D-лаборатории DevHands, автор телеграм-канала про System Design и Highload. В прошлом — СТО и руководитель московского офиса Badoo. Работал во втором по размеру такси-сервисе «Везёт», который мы после продажи интегрировали с Яндекс.Такси. Сейчас наша компания разрабатывает образовательные программы по Highload и перформансу.
Мы в DevHands проводим много исследований, и часть из них делаем открытыми. Не у всех компаний есть ресурсы, чтобы сравнивать СУБД и кэши, подробно исследовать их поведение под высокой нагрузкой, с целью выбрать оптимальное решение. Я уверен, что подобные исследования нужны всему сообществу, поэтому решил поделиться нашими результатами.
Чтобы сравнить производительность баз данных и кэшей, мы взяли самые современные версии продуктов — Postgres, MySQL, Valkey, Redis и Memcached и провели серию исследований, где выжали из них миллион RPS на определённой нагрузке.
В мире высоконагруженных проектов есть два противоречащих друг другу мнения:
«Умение использовать кэш-слой — это основа успешного Highload-проекта».
Его сторонники считают, что без кэша ничего нельзя сделать. Что он решает если не все, то подавляющее большинство проблем с производительностью.
«Правильно настроенная база данных делает кэш ненужным».
Если мы хорошо знаем свою базу и её возможности, то, скорее всего, умеем выжать максимум при определённых условиях, и тогда кэш становится ненужным.
Первое мнение скорее отражает мир инженеров, активно экспериментирующих с архитектурными компонентами, новыми базами данных и открытых к новым решениям. Они развиваются значительно быстрее, чем рынок традиционных СУБД — key-value хранилища, NoSQL, NewSQL. Второе мнение представляет мир «традиционных» СУБД и всех, кто «прикипел» к родным инструментам.
За удобство и скорость работы мы платим существенную цену: когда данные «живут» в нескольких местах, растёт неконсистентность. Вероятность, что код всегда обеспечит согласованную работу кэша и базы данных, крайне низка. Поэтому, если выбираешь кэш — получай рассогласованность.
С точки зрения архитектуры было бы здорово, если бы кэшей не было. Почему же кэши появились и стали таким важным элементами архитектуры? Заглянем в историю развития серверов и баз данных.
В начале 2000-х Дэн Кигель создал страницу, посвящённую «проблеме 10 тысяч соединений» (C10K), эта страница существует до сих пор. Уже тогда было понятно, что существующие подходы не справляются с одновременным обслуживанием такого количества соединений, и Кигель собрал наиболее распространенные способы решения «проблемы 10 тысяч соединений». С тех пор произошли существенные трансформации в серверостроении, они затронули все виды серверов, включая базы данных. Ведь количество онлайн-пользователей постоянно росло, а базы не успевали обрабатывать поступающие запросы.
И хотя комьюнити и рынок осознали ресурсоёмкость и сложность парсинга SQL, поддержки транзакций ACID и других особенностей мира традиционных СУБД, сами базы данных были и остаются довольно консервативными. В результате произошло несколько важных событий:
2004 год. Для Livejournal Брэд Фитцпатрик и Анатолий Воробей запустили проект Memcached, который позволял значительно ускорить генерацию динамических веб-страниц, существенно превосходил по показателям производительности базы данных и стал повсеместно использоваться в крупных интернет-проектах.
2009 год. Salvatore Sanfilippo создал Redis, добавив в него множество возможностей, которых не хватало программистам в кэш-сервисе и не было в Memcached. Это дало толчок к разработке множества KV-, NoSQL- и NewSQL-решений.
С тех пор на рынке появилось множество решений. Ни одно из них не смогло полностью заменить традиционные СУБД. Но и они менялись: улучшалась производительность, пропускная способность.
В нулевом приближении кэш или легковесная база обладает такими свойствами:
«Мультиплексирование» соединений
В отличие от традиционного подхода, когда на каждое соединение создаётся отдельный воркер или тред, трейды или процессы кэша умеют одновременно работать с большим количеством открытых соединений. Такой метод позволяет эффективно управлять нагрузкой в среде с большим числом серверов и бэкенд-воркеров. Именно поэтому в экосистеме Postgres появились инструменты вроде bouncer.
Лёгкий протокол: отсутствует SQL
Протокол, по которому приложение общается с кэш-сервисом, априори значительно проще SQL. Значит, меньше ресурсов будет тратиться на приём-передачу данных.
Лёгкий «внутри»
Кэш использует простые структуры данных, не работает с транзакциями, всё хранится в памяти. Поэтому каждый запрос с точки зрения количества операций значительно легче запросов, которые выполняют СУБД.
Как результат, кэш способен обрабатывать свыше 100K RPS на одно ядро и выполнять миллион запросов в секунду для тысяч (а то и десятков тысяч) одновременных соединений.
Давайте теперь сравним современные кэши с традиционными СУБД:
Мультиплексирование соединений
У традиционных СУБД есть ограничения. Например, PostgreSQL изначально не поддерживал мультиплексирование соединений, так как использует модель «process per connection». Однако, начиная с версии 15 (возможно, уже с 14) PostgreSQL сделал фантастический скачок вперёд. Теперь он может работать с большим количеством соединений, благодаря менее ресурсоёмким процессам и ряду оптимизаций. Конечно, это не скейлится бесконечно и в какой-то момент перестаёт работать, но ситуация значительно лучше, чем 5−10 лет назад.
А с MySQL иная ситуация. Здесь используется модель «thread per connection». Она потенциально лучше с точки зрения скейлинга по количеству одновременных соединений, но в последние годы, по сообщениям многих ресёчеров, у MySQL были некоторые деградации по производительности.
Лёгкий протокол
Мир СУБД претерпел огромное количество оптимизаций. Но и в целом в нашем кейсе SQL-запросы очень простые, поэтому есть шанс, что здесь базы данных если и проиграют, то несильно.
Лёгкие структуры данных
Кэши действительно работают с более лёгкими структурами данных, но традиционные СУБД постоянно улучшаются и оптимизируются.
Скейлинг по ядрам для тысяч одновременных соединений
Остаётся открытым вопрос: какая ситуация со скейлингом по ядрам или по большому количеству одновременных соединений и у кэшей и у СУБД? Сравним их производительность в реальных тестах.
Для исследования взяли кэши:
Redis — самый распространенный KV-Storage и кэш.
Valkey — клон Redis, свежий ответ от cloud-провайдеров на смену лицензии Redis.
Memcached — отличный, очень простой и по-прежнему популярный кэш-сервис.
И СУБД:
PostgreSQL — одна из самых распространённых и открытых баз данных, наиболее популярная в России.
MySQL — также распространённая и открытая база данных, в мире не менее популярная, чем PostgreSQL. Тестировали сборки с дополнительными модулями: Heap, Handler Socket, Memcached plugin. Также протестировали специализированную сборку от первого российского вендора — MyDB. Но по сути это отечественный форк MySQL, так что особенной разницы между MyDB и MySQL нет.
Наши задачи:
Попробовать выжать миллион RPS на разных ворклоудах.
Проверить, как скейлятся СУБД по количеству открытых соединений.
Выяснить, справляются ли они вообще с нагрузкой, рассчитанной для кэшей.
Среда тестирования:
Инструменты: sysbench, memtier и redis-benchmark.
Железку для тестов взяли не самую дорогую, с процессором Xeon Gold 6312U, 48 виртуальных ядер в режиме гипертрейдинга, 128 ГБ памяти.
Тесты выполнялись в режиме ReadOnly — «чистый» кэш, всё в памяти, никакой дисковой нагрузки.
Мы проверили несколько популярных гипотез, чтобы понять, какие подтвердятся на практике:
Redis не скейлится по ядрам. Многие считают, что Redis плохо скейлится по ядрам, но это не совсем так — Redis предпринял шаги для масштабирования. Я не видел соответствующих графиков и хотел сам проверить.
А Valkey тогда что — получается, скейлится? Про его перфоманс было очень много пиар-статей в последнее время, проверим и это.
Модель «thread-per-connection» в MySQL якобы превосходит модель «process-per-connection» в PostgreSQL. Значит ли это, что MySQL скейлится лучше PostgreSQL?
Обратный миф: PostgreSQL во всём превосходит MySQL. А это мнение удивительно популярно у нас, мол, MySQL старый и проприетарный, PostgreSQL развивающийся и свободный, и ничем не уступает.
Современные традиционные СУБД уже настолько хороши, что кэш-слой не нужен. Это, собственно, главная тема нашего исследования.
И наконец сопутствующая гипотеза, которая интересна сама по себе: мы находимся в эпохе «миллионов RPS», что легко достигается при должной сноровке и сравнительно недорогом железе.
Мы тестировали Redis 7.2.5 с redis_benchmark, с базой из 10 млн ключей по 256 байт и режимом appendonly no + save “”. График ниже ликвидирует миф о том, что Redis не масштабируется по ядрам. Redis скейлится, хоть и не идеально:
На втором графике по оси X отображается параметр io-threads, который конфигурируется в redis.conf, а по оси Y — медианное latency запросов:
В режиме кэша, без использования append-only и снэпшотов, Redis отдаёт примерно 300–400 тысяч запросов в секунду при использовании около восьми io-threads. Притом, что с одного ядра в режиме, когда io-threads равен единице, число достигаемых запросов — около 160000 в секунду. Несмотря на то, что ядер на машине значительно больше, мы не можем выжать ничего сверх этих значений. Latency остаётся на приемлемом уровне: при конфигурации с восемью io-threads всё стабильно. Таким образом, Redis скейлится, но не идеально.
Если сравнить Redis и Valkey становится очевидно, что Valkey смог устранить многие узкие места характерные для Redis. На графике ниже видно, что там, где Redis достигает максимума, Valkey без специальных оптимизаций отдаёт чуть меньше миллиона запросов в секунду. Значит, есть потенциал на более мощном железе:
В целом графики для Valkey и Redis похожи, что указывает на то, что оба решения не очень хорошо скейлятся по ядрам. Valkey в целом делает это лучше, но нет смысла увеличивать число ядер свыше шести или восьми — прирост производительности уже не будет таким эффективным.
При аналогичном наборе ключей Memcached демонстрирует значительно большую пропускную способность по сравнению с Valkey. Он способен обрабатывать почти 1,7 млн запросов в секунду. Тогда как Valkey при примерно 1000 одновременных соединениях выдаёт чуть менее 1 млн запросов в секунду.
Из этого можно сделать вывод, что у Memcached, как и Redis, хороший потенциал, если нужен только кэш. На практике Memcached обеспечивает примерно в 1,5 раза лучший throughput.
Настало время взглянуть на базы данных. Первое, что нас интересовало — как масштабируется PostgreSQL при большом количестве одновременных соединений. Это оказалось для меня огромным сюрпризом. График обрывается на определённом значении, и видно, где PostgreSQL перестаёт справляться на достаточно большом числе одновременных соединений, в нашем случае — больше 5000 (в общем случае этот лимит пропорционален размеру памяти):
Представьте сервер, где load average достигает 5000. Вы выполняете команду, вроде ps aux | grep postgres, видите 5000 реальных процессов, и машина при этом отзывается и консоль нормально работает. В общем, сделан действительно значительный шаг вперёд, но сама по себе модель не позволяет скейлиться до очень большого числа соединений на таком размере памяти. В нашем случае уже при превышении 4500 начали возникать проблемы.
На конференции я встретил ребят, которые показывали шкаф с предустановленным и настроенным PostgreSQL, где используется в 5–10 раз больше памяти. Я видел их презентационные материалы: число одновременных соединений достигает нескольких десятков тысяч. Но это очень серьёзная машина с более чем 700 ГБ памяти, и для работы PostgreSQL на такой нагрузке требуется огромное количество оперативной памяти.
Для теста MySQL мы взяли «ванильную» MySQL 8.4 и нагрузили её примерно так же, как PostgreSQL:
MySQL 8.4 с оптимизациями (buffer pool + ещё около 10 параметров).
PostgreSQL с минимальными изменениями (только max_connections и shared_buffers).
В ходе тестирования в глаза бросилось сразу несколько вещей. Долгое время ходил миф, что MySQL «из коробки» может работать с большими нагрузками без особого тюнинга, а в PostgreSQL нужно менять миллион конфигураций, чтобы добиться хорошего RPS.
По моему опыту, ситуация скорее обратная. Postgres мне совсем не пришлось тюнить. Подкрутили буквально пару параметров, в результате он выдал почти миллион RPS на тестовой кэш-нагрузке. Чтобы MySQL достиг аналогичной производительности, параметров пришлось менять больше.
Помимо стандартного MySQL 8.4, мы взяли специально оптимизированную сборку MyDB — российский клон, построенный на основе MySQL, который уже есть в реестре российского ПО.
В MyDB уже применены оптимизации: использование huge pages, особые флаги компиляции и дополнительные изменения в конфигурации. Оказалось, что небольшие исправления и пересборка бинарного файла позволяют выжать из MySQL значительно больше.
Известно, что за последние несколько лет C++ код MySQL оброс большим количеством усложнений, и многое ещё предстоит оптимизировать. Но большое количество вещей уже оптимизируется определёнными флагами или подходами. Большое спасибо фаундеру MyDB, Алексею Копытову, который подсказал мне эти нюансы.
Итак, наша сборка MyDB была построена на MySQL 8.4 с оптимизациями: LTO, PGO, huge pages и рядом других настроек. В такой конфигурации всё отлично скейлится, выдавая более миллиона запросов в секунду:
У нас было много идей, что ещё проверить в экосистеме MySQL. Но больше других хотелось протестировать Heap engine, давно живущий в MySQL и Handler Socket — упрощённый протокол для работы с InnoDB. Это in-memory движок, и, по идее, он должен работать значительно быстрее, чем InnoDB. Мы настроили его и провели тесты.
Результаты, как видно на графике выше, показали, что ни Heap, ни Handler Socket не дали нужного эффекта. Оба решения масштабируются по ядрам значительно хуже.
У MySQL есть по-настоящему волшебный memcached-плагин, который довольно редко используется в продакшене.
Как утверждают эксперты, знакомые с этой экосистемой изнутри, ни один человек, не говорящий по-русски, не интересуется этим плагином или не использовал его в продакшене. Что в целом кажется странным, учитывая, что по слухам этот плагин был сделан для Facebook, но экспертам виднее.
Memcached-плагин берёт InnoDB таблицу и создаёт к ней интерфейс через протокол Memcached, когда можно открыть соединение на дефолтный 11211 порт, по Memcached-протоколу выполнить операции GET и SET. Всё это сохранится в персистентную таблицу, с которой можно работать, в том числе, через SQL.
Мы выставили следующие значения для тестирования:
innodb_memcache.cache_policies
get_policy: caching
set_policy, delete_policy, flush_policy: innodb_only
Этот плагин оказался мега-производительным в плане кэша. Конечно, он уступает по скорости родному Memcached, но работает достаточно быстро. Вот сравнительный график:
Получается, что если у вас есть Memcached не персистентный, а нужна персистентность, то в качестве альтернативы перехода на Redis или Valkey есть дешёвый способ реализовать это с помощью Memcached-плагина MySQL.
Чтобы плагин заработал, пришлось даунгрейдиться до дистрибутива MySQL 8.0, и не использовать более новую версию 8.4. Это третий дистрибутив, в котором отсутствуют многие оптимизации, применяемые в тестах MyDB. Возможно, если пересобрать MySQL с определёнными флагами и ещё что-то сделать с инсталляцией, то с Memcached плагином из неё можно выжать и больше.
Вернёмся к мифам о скейлинге и производительности современных кэшей и СУБД. Что мы получили в этом исследовании:
Redis не скейлится по ядрам → Формально — нет, но вообще — да.
Redis скейлится, но не идеально. При использовании примерно 8 io-threads Redis достигает около 300–400 тыс. запросов в секунду, тогда как при одном io-thread — около 160 тыс.
Valkey скейлится → Не совсем, но точно лучше Redis.
Valkey показывает примерно в 2,5 раза больший RPS по сравнению с Redis при схожих условиях. При этом, увеличение числа ядер свыше 6–8 уже не даёт существенного прироста.
MySQL скейлится лучше PostgreSQL → Тут скорее паритет по производительности.
Несмотря на паритет, у модели «thread-per-connection» в MySQL показатели скейлинга по количеству одновременных соединений лучше, чем у «process-per-connection» в PostgreSQL.
PostgreSQL во всём превосходит MySQL → Это не так.
Лично меня сильно обрадовало то, как PostgreSQL справляется с большими нагрузками. Да, в какой-то момент возникают проблемы, всё-таки модель даёт о себе знать. Но было сюрпризом увидеть машину, на которой штатно работают тысячи соединений Postgres.
Современные традиционные СУБД уже настолько хороши, что кэш-слой не нужен → Во многом, да, если рассматривать СУБД в целом, но есть важные нюансы.
Гибридные решения, key-value базы данных и подобное появились как раз для того, чтобы обрабатывать нагрузки в сотни тысяч и миллионы RPS. Но если говорить о традиционных СУБД, таких как MySQL и PostgreSQL, они уже могут обрабатывать большое количество RPS и достаточно хороши, если выполнено два условия: нагрузка строго Read-Only и запросы строго point select (по первичному ключу).
В реальной жизни всегда присутствует какой-то процент операций не Read-Only, и когда нагрузка смешанная, перформанс СУБД значительно падает. Если нагрузка почти полностью на чтение или близка к этому, то можно обойтись без кэш-слоя, но при большой доле записи — надо смотреть, и вполне вероятно, что нет.
Если основная часть операций — это выборка по первичному ключу, СУБД справляются хорошо. Однако, в кэш можно «запихнуть» практически всё и хранить в key-value, а в базе данных не всё реализуется через point select, поэтому могут возникнуть ситуации, которые приведут к просадке производительности.
Можно ли всё это гарантировать на практике? Увы, нет, нельзя гарантировать, что не случится неожиданный поток апдейтов или неоптимизированный сканирующий запрос. Если вы готовы изолировать свою базу данных так, что туда точно не провалится ничего плохого, то флаг вам в руки. Но, честно говоря, я как СТО на это бы не решился. Даже если я всем командам запрещу делать те или иные вещи, наступит момент, когда какой-то запрос провалится, и мы получим очевидную проблему в продакшене.
Мы находимся в эпохе «миллионов RPS», что легко достигается при должной сноровке и сравнительно недорогом железе → почти.
Около миллиона RPS можно выжать на оборудовании, которое обошлось нам в 30 тысяч рублей в месяц в аренде — это не так много. В реальности речь идёт о проектах с многомиллионным и даже миллиардным трафиком, где затраты в тысячи и десятки тысяч долларов в месяц на традиционное облако с возможностью обрабатывать миллион RPS — вполне оправданы.
Предвосхищая вопросы, расскажу, чего не хватало в тестах и что можно было сделать (и будет сделано в будущих исследованиях) лучше:
Смешанная нагрузка R/O vs R/W
Конечно, стоило протестировать смешанную нагрузку и её влияние на производительность. Для кэшей она, скорее всего, почти ничего бы не дала, так как это всё равно память, и перформанс останется на прежнем уровне (чуть ниже из-за необходимости обеспечивать конкурентный доступ к изменяемым данным). А вот у баз данных многое зависит от соотношения запросов на чтение и запись.
Redis/Valkey cluster
Интересно, как ведёт себя кластер. Сразу скажу, кластер не показал супер-результатов в тестировании. Я связываю это с тем, что он сам по себе является некоторым усложнением, добавляющим косты. Плюс нужно проверить, происходят ли в кластерном режиме постоянные редиректы между нодами. Тем не менее, такой график было бы здорово построить.
PostgreSQL + Odyssey вместо bouncer
До тестирования Postgres вместе с bouncer руки не дошли, но это интересный вариант.
Из того, что я видел на практике, bouncer сильно ограничивает общий throughput, так как изначально не скейлится по ядрам. Есть Odyssey, который скейлится. Возможно, связка PostgreSQL + Odyssey будет скейлиться лучше, но всё равно миллионов RPS в таком сценарии вряд добьёшся. Если один PostgreSQL на 1000 одновременных соединений делал миллион RPS, то при добавлении посередине bouncer, throughput и latency сразу падал почти вдвое, хотя скейлится такая «связка», конечно, лучше.
PostgreSQL + compile optimisations
Компиляция MySQL с определёнными флагами привела к интересным результатам. Мы не делали этого для PostgreSQL, но PostgreSQL написан на C, а MySQL на C++. Это одна из причин, почему за последние несколько лет именно MySQL начал деградировать. Можно ли выжать что-то за счёт флагов из более простого C-кода Postgres, не знаю. Кажется, шансов немного, но протестировать стоит.
Другие кэш-сервисы и key-value СУБД: Tarantool, DragonFly, KeyDB и др.
Как минимум эти три продукта известны как быстрые key-value хранилища, которые активно используются и как кэш, и как база данных. В наше тестирование они не вошли, но их сравнение с Redis/Valkey/Memcached могло бы дать интересные графики и выводы.
Если вы дочитали до конца и считаете, что мы что-то забыли, пишите в комментариях. А мы продолжим исследование.
Результатами делился Алексей Рыбак — автор телеграм-канала про System Design и Highload, основатель DevHands.io — проекта по продвинутому обучению программистов и teamwork360.io — сервиса для автоматизации HR-процессов и оценки сотрудников.