«Кабанчик» и консистентность кэша

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Листал на днях «Высоконагруженные приложения» Мартина Клеппмана — хорошую книгу, которую стоит прочитать всем современным разработчикам, которые имеют дело с программированием и поддержкой производительных приложений.

Не смотря на то, что книга написана еще в 2014-2016 годах (первое издание в O’Reilly вышло в 2018-м), «Кабанчик» не теряет, а только приобретает актуальность. Разрабатывая высоконагруженные приложения для финтеха и управляя разработкой таких систем, я нахожу много полезного в книге Клеппмана. При этом если отобрать дюжину разработчиков, то «Кабанчика» читали от силы несколько из них.

Причина — в сложном и обстоятельном подходе автора (и в том, что книга насчитывает почти 700 страниц). Читать «Кабанчика» непросто, не так трудно как Хофштадтера, но и не так просто, как Вольфрама. Но не смотря на то, что книга написана давно, в ней неплохо отзываются некоторые современные проблемы.

Репликация по Клеппману

Во втором разделе «Кабанчика» Клеппман пишет о репликации данных и особенностях кэширования данных. Несмотря на то, что любая современная СУБД поддерживает репликацию «из коробки» (включая даже MySQL), в случае высоконагруженных приложений всё может быть не так просто.

Клеппман пишет, что репликация может быть синхронной или асинхронной; это очень сильно влияет на поведение системы в случае сбоя. Хотя асинхронная репликация может выполняться быстрее в случае нормальной работы системы, важно понимать, что произойдет при росте задержки репликации и отказах серверов. В случае отказа ведущего узла и возведения одного из ведомых узлов в ранг ведущего существует вероятность потери недавно зафиксированных данных.

Когда размер набора более не позволяет хранить и обрабатывать его на одной машине, требуется секционирование данных — распределение нагрузки по данным и запросам равномерно по нескольким машинам. Это требует выбора подходящей для набора данных схемы секционирования и перебалансировки секций при добавлении или удалении узлов из кластера.

  • Секционирование по диапазонам значений ключа, при котором ключи сортируются и секция содержит все ключи, начиная с определенного минимума до определенного максимума. Преимущество сортировки состоит в возможности выполнять эффективные запросы по диапазонам; но если приложение часто обращается к расположенным близко (в соответствии с сортировкой) ключам, то возникает риск возникновения горячих точек. При этом подходе полезно проводить динамическую перебалансировку секций с помощью разбиения диапазона на два поддиапазона в случае, когда секция становится слишком велика.

  • Хеш-секционирование — это метод, при котором вычисляется хеш-функция каждого ключа и к каждой секции относится определенный диапазон хешей. Этот метод нарушает упорядоченность ключей, делая запросы по диапазонам неэффективными, но позволяет более равномерно распределять нагрузку. При хеш-секционировании часто заранее создается фиксированное количество секций, по нескольку для каждого узла, а при добавлении или удалении узлов между узлами часто перемещаются целые секции. При этом также можно использовать динамическое секционирование.

Возможны и гибридные подходы, например с составным ключом: применение одной части ключа для идентификации секции, а другой — для определения порядка сортировки.

Кэш и Redis

В последнее время популярным инструментом хэширования для приложений и сайтов стал Redis — особенно если мы говорим про реляционную базу, которая не очень хорошо держится под высокой нагрузкой. В таком случае между кэшированием на уровне приложения и уровнем хранения данных используют прослойку быстрого кэша на Redis в оперативной памяти. В таком случае достаточно кэшировать примерно 20% данных, к которым обращаются в 80% случаев — закон Парето никто не отменял.

Окей, мы добавили кэш, производительность повысилась. Но случилась проблема, которой Клеппман посвятил полсотни страниц — один и тот же набор данных теперь хранится в разных местах: в базе данных и кэше на Redis. Как обеспечить согласованность таких данных?

При работе с данными хочется обеспечить 100% согласованности, однако в большинстве случаев можно добиться 99% согласованности и остановиться — например, если у вас нет большого количества «грязных» данных. Иными словами, сложное решение не всегда самое лучшее.

Самая простая логика будет такой:

  • Для операций чтения данные берём из Redis, в базу не обращаемся. В случае кэш промаха забираем данные из базы и перезаписываем в Redis.

  • Для операций изменения (записи) данные пишем в базу данных, а кэш в Redis — удаляем (именно удаляем, а не обновляем).

Такой подход в том числе используют в Facebook*. В большинстве сценариев такой метод он может гарантировать конечную согласованность.

Допустим, процесс A пытается обновить существующее значение. В определенный момент A успешно обновил значение в базе. Прежде чем удалить запись в Redis, другой процесс B попытается прочитать то же значение. В этом случае B получит попадание кэша (поскольку запись еще не была удалена в Redis). Поэтому B прочитает устаревшее значение. Однако старая запись в Redis в конце концов будет удалена, и другие процессы в итоге получат обновленное значение.

В сложных случаях такой метод не может гарантировать окончательную согласованность. Если процесс A будет убит до того, как он попытается удалить запись в Redis, эта старая запись никогда не будет удалена. Следовательно, все последующие процессы будут продолжать считывать старое значение.

Но и в простом случае согласованность может нарушиться. Допустим, процесс C пытается прочитать значение и получает промах в кэше. Затем C запрашивает базу и получает результат. Внезапно C зависает — в этот момент другой процесс D пытается обновить то же самое значение. D обновляет запись в базе и удаляет запись в Redis. После этого C возобновляет работу и сохраняет результат своего запроса в Redis. Следовательно, C сохраняет старое значение в Redis, и все последующие процессы будут считывать «грязные» данные.

Другие подходы

Есть также модель двойного удаления:

  • При чтении при попадании смело забираем данные из Redis, при промахе — из базы.

  • При записи сперва удаляем кэш, после перезаписываем данные в базу, а после еще раз удаляем кэш. При этом между записью в базу и вторым удалением полезно подождать около 500 мс, «отлавливая» процессы.

Есть даже вариант обратной записи, который используют, например, в Alibaba.

При таком подходе репликацию выполняют в другом направлении. Вместо того, чтобы реплицировать изменения из Redis в базу, он подписывается на binlog базы и реплицирует его в Redis. Это обеспечивает гораздо лучшую долговечность и согласованность, чем оригинальный алгоритм. Поскольку binlog является частью технологии RDMS, мы можем предположить, что он долговечен и устойчив к катастрофам.

В любом случае, ни один из вышеперечисленных подходов не может гарантировать строгую согласованность. Но тут важно понимать, что строгая согласованность и высокая производительность с кешированием — это антонимы. Тут уж или одно, или другое!

Бонус-трек — как добиться 100% согласованности

Я пытался найти способ, как гарантировать 100% записи в кэш баланса банковской карты после его обновления — с использованием метода обратной записи.

Принцип такой:

  • Кэш стал базой. При уменьшении или увеличении значения мы брали исходное значение из кэша, производили вычисление — и записывали уже в базу с optimistic locking.

  • Если значение из базы было неактуальным, то мы считывали его снова из кэша. Или считывали из базы, залогировав исключительную ситуацию для мониторинга количества кэш промаха.

В результате к кэшу обращались все, кому было нужно. А база с optimistic locking была истинной копией — но обращаться к кэшу всё равно было быстрее и удобнее.

И, конечно, такой подход позволял не только ускорить работу с балансом, но и позволял достичь практически 100% попаданий. Вот так вот!

*Meta, Facebook - запрещённые в РФ организации

Источник: https://habr.com/ru/articles/788574/


Интересные статьи

Интересные статьи

В этой статье Антон Колесов, технический руководитель группы по эксплуатации платформ и системных решений, расскажет, как команда разработчиков Nexign настраивала альтернативное решение для кэша одной...
Поговорим сегодня про выбор, перед которым встают разработчики всех распределённых систем: обеспечивать ли консистентность данных или доступность системы при различных внешних условиях —  поломка...
В современной веб разработке сложно переоценить значение такого инструмента как кэш. Мы сохраняем результаты выполнения длительных, дорогостоящих или часто выполняемых операций в некое хранилище, обра...
Подмена сервера доменных имен (DNS) — это кибератака, с помощью которой злоумышленник направляет трафик жертвы на вредоносный сайт (вместо легитимного IP-адреса). Злоумышленники испол...
В Яндексе я руковожу службой общих интерфейсов. О них и поговорим. О том, как трудно (но приходится) делать что-то для всех. Позволю себе аналогию: сидишь, пишешь код и захотел пить....