Инвалидация кеша на стороне сервера добавлена ​​в первое решение Deno для кэширования GraphQL

Введение

Obsidian — это первое решение Deno для кэширования GraphQL, обеспечивающее кэширование на стороне клиента для компонентов React с использованием кеша браузера и кэширование на стороне сервера для маршрутизаторов Oak с использованием кеша Redis.

Эффективное кэширование в уме

Поскольку ответы GraphQL могут быть произвольно большими и вложенными, эти вызовы API могут быть дорогостоящими. Внедрение стратегии кэширования GraphQL в архитектуру системы обеспечивает более дешевую альтернативу вызову GraphQL: проверку кеша.

Время запроса = проверка кэша (частота попаданий) + время вызова службы (1 — частота попаданий) + запись в кэш (1 — частота попаданий)

При попадании в кеш время запроса эквивалентно времени проверки кеша.

При промахе кеша время запроса эквивалентно времени вызова GraphQL и времени записи кеша.

Чтобы иметь производительный кеш, приоритеты Obsidian заключаются в следующем:
1) Свести к минимуму дорогостоящие вызовы службы GraphQL
2) Максимально увеличить частоту попаданий в кеш.

Компромиссы кэширования

Управление как пространством кэша, так и согласованностью кэша — самый сложный аспект кэширования, поскольку приоритизация одного над другим сопряжена с компромиссами. Obsidian 4.0.0 представляет надежную стратегию аннулирования кеша и дополнительную стратегию вытеснения кеша для кэширования на стороне сервера.

Инвалидация кеша

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

Мутации создают проблему: если кеш статичен, не только невозможно узнать, что устарело, но вполне возможно, что все в кеше устареет, если произойдут правильные мутации.

Удаление кеша

Когда кэшу не хватает памяти, необходимо принять решение о том, что вытесняется, чтобы освободить место. Были изучены две стратегии: наименее часто используемые и наименее часто используемые. Хотя реализация обоих функций тривиальна со встроенными функциями Redis, а Obsidian Router не имеет мнения о том, какую стратегию выбирает пользователь, было принято решение использовать одну из них по умолчанию.

Obsidian 4.0.0: Реализация и обоснование

В компьютерных науках есть только две сложные вещи: аннулирование кеша и присвоение имен — Фил Карлтон

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

Инвалидация кеша

Вместо того, чтобы хранить статические ответы GraphQL, они преобразуются в ссылки перед кэшированием. При чтении фрагмент кэша детрансформируется, а затем возвращается. Если запись в кеш и чтение из кеша становятся более дорогими, что является компенсационным компромиссом?

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

Чтобы сделать кеширование ответов GraphQL динамическим, был реализован нормализованный кеш. Ответы GraphQL нормализуются, и каждый компонент добавляется в кэш со ссылочным именем. Эти ссылки заменяют соответствующие значения в ответе GraphQL перед кэшированием.

Запросы на мутацию и их ответы никогда не кэшируются. Однако они бегают. Абстрактное синтаксическое дерево GraphQL запроса проверяется, чтобы определить, является ли это мутацией или нет. Кроме того, если ответ мутации найден в кеше, Obsidian делает вывод, что это мутация удаления, и удаляет эту ссылку из кеша. Во всех остальных случаях добавления или обновления мутаций ссылка записывается в кеш.

Удаление кеша

LFU — отличный кандидат для большинства стратегий выселения. Это гарантирует, что фрагменты кеша с более высокой вероятностью попадания будут сохранены, и вытеснит те, которые имеют более высокую вероятность промаха. Поскольку LFU оптимизируется с точки зрения частоты попаданий и простоты реализации в Redis, LFU является базовым вариантом. Причина, по которой Obsidian 4.0.0 решила вместо этого использовать LRU по умолчанию, заключается в том, что он лучше дополняет новую стратегию инвалидации кеша.

Поскольку ссылки будут считываться чаще, чем сами ответы GraphQL, по умолчанию используется LRU, чтобы предотвратить их вытеснение, поскольку они будут использоваться реже, чем их ссылочные компоненты. Хотя у пользователя есть возможность изменить этот параметр по умолчанию, настоятельно рекомендуется оставить его равным LRU.

Реструктурированная логика кэширования на стороне сервера

Возможны пять сценариев:
1) Запрос кэширован, и ни одна ссылка не была вытеснена или удалена
2) Запрос кэширован, и по крайней мере одна ссылка была вытеснена или удалена
3) Запрос не кэшируется, и это запрос на чтение
4) Запрос не кэшируется, и это мутация удаления
5) Запрос не кэшируется, и это мутация создания или обновления

В лучшем случае запрос кэшируется, а ссылки не удаляются. Это избавляет пользователя от вызова GraphQL и заменяет операцию дешевым чтением кэша. Все остальные сценарии требуют вызова GraphQL.

Логика кэширования на стороне сервера была переработана, чтобы включить аннулирование кэша. Первоначально проект предусматривал два независимых пути: один для попадания в кеш и один для промаха в кеше. После дальнейшего рассмотрения стало очевидно, что вытесненная или удаленная ссылка потребует выполнения запроса. Поскольку альтернативой было бы возвращение неполного ответа, этот компромисс для вызова GraphQL был оправданным.

В случае попадания в кеш и всех ссылок в кеше процесс детрансформации динамически перестраивает ожидаемый ответ GraphQL из кешированных ссылок.

В случае попадания в кеш и в кеше не найдена хотя бы одна ссылка (т. е. она была вытеснена или удалена), выполняется вызов службы GraphQL, его ответ прикрепляется к телу ответа Obsidian, а значения ссылок перезаписываются. кешировать.

В случае промаха кеша выполняется вызов службы GraphQL, и его ответ прикрепляется к телу ответа Obsidian. Последующие операции выполняются для кэширования. Ответ GraphQL нормализуется, и выполняются отдельные операции для запроса чтения, удаления мутации и всего остального.

В случае запроса на чтение ответ GraphQL преобразуется в объект ссылки и кэшируется вместе со строкой запроса в виде хэша Redis. Кроме того, каждый ссылочный объект кэшируется со своей ссылочной строкой в ​​виде хэша Redis.

В случае мутации удаления ссылки абстрагируются от ответа GraphQL и становятся недействительными из кеша посредством удаления.

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

Нормализация, трансформация и детрансформация

Чтобы проиллюстрировать, следующий ответ GraphQL будет проходить через основные функции для подготовки к кэшированию.

Основой новой логики кэширования на стороне сервера является алгоритм рекурсивной нормализации. Произвольно вложенный ответ GraphQL сводится к объекту уникальных ссылочных строковых ключей и соответствующих им объектов за линейное время.

Эти ссылочные строки создаются уникальными идентификаторами, которые по умолчанию представляют собой массив «id» и «__typename». Объект считается хешируемым, если он содержит все уникальные идентификаторы в качестве свойств. Это вырезается и добавляется к нормализованному объекту до точки любого вложения. Вложенность преодолевается посредством рекурсивных вызовов функции.

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

Методы условной инвалидации кэша

Строки запроса GraphQL преобразуются в абстрактные синтаксические деревья, чтобы проверить, относится ли операция к типу мутации. Если это так, вызывается мутация и нормализуется ответ GraphQL. Тип мутации выводится путем чтения ссылок из Redis.

Если Redis возвращает нулевое значение, предполагается созданная мутация. Новая ссылка записывается в кеш.

Если значение Redis и ответ GraphQL полностью совпадают друг с другом, предполагается мутация удаления. Ссылка удаляется из кеша.

Если равенство ложно, предполагается мутация обновления. Ссылка перезаписывается обновленным значением.

Разработка через тестирование

Для обеспечения надежности был выбран подход к разработке через тестирование. Каждая из основных частей описанной выше функциональности (нормализация, преобразование, детрансформация и аннулирование кеша) должна была пройти предопределенные тесты, чтобы быть объединенной и выпущенной.

Соображения относительно будущих функций и улучшений

Горизонтальное масштабирование кеша с помощью шардинга

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

Недействительные ответы GraphQL в кеше: избегайте недостаточного ответа после добавления мутаций

В настоящее время нет способа аннулировать ответы GraphQL. Недействительными становятся только ссылки. Если есть запрос на чтение из базы данных, за которым следует мутация, которая добавляет, возможно, что тот же запрос на чтение ответит из кэша с неполными данными. Исходный запрос для чтения не будет включать эту недавно добавленную ссылку в преобразованное кэшированное значение. Хотя вызов GraphQL избегается, Obsidian не отвечает.

Недействительные ответы GraphQL в кеше: избегайте чрезмерных ответов

Хотя чрезмерный ответ не так проблематичен, как недостаточный ответ, это стоит отметить. Если запрос кэшируется, а последующий запрос запрашивает меньшее количество полей из первого, процесс преобразования обманывает Obsidian, заставляя думать, что два ответа должны быть одинаковыми. Это связано с тем, что Obsidian не может отличить ссылку «~7~Movie», которая включает свойство «Год выпуска», и ссылку, которая его не включает. То, что он всегда кэшировал, - это тот, у которого больше полей.

Хотя аннулирование ответов может решить эту проблему, следует сначала изучить другие методы, прежде чем аннулировать дорогостоящие ответы GraphQL из кэша. Пользователю может быть лучше просто знать об этом нюансе.

Распараллелить ответ Obsidian и запись в кэш

Ответ GraphQL прикрепляется к ответу Obsidian при его получении. Кэширование выполняется позже. Поскольку они не зависят друг от друга, эти пути можно распараллелить. Это принесет преимущества в производительности.

Выборочный запрос исключенных или удаленных ссылок

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

Если в Redis не найдено более одного ключа, было бы здорово сгруппировать запросы, которые возвращают искомый ответ, и отправить его как один запрос, чтобы уменьшить количество сетевых запросов, которые мы отправляем к GraphQL.

Автоматизированное сквозное тестирование

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

Дополнительные тесты, в которых выполняются различные комбинации запросов и мутаций, также улучшат время выполнения заказов в будущем.

Начиная

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

Obsidian — продукт с открытым исходным кодом, выпускаемый в рамках технологического ускорителя Open Source Lab. Мы приветствуем вклады и отзывы через GitHub.

Другие статьи об Obsidian:

Обсидиан 1.0
Обсидиан 2.0
Обсидиан 3.0
Обсидиан 3.1
Обсидиан 3.2
Обсидиан 4.0
Обсидиан 4.0 Техническое описание

В соавторстве с Team Obsidian:
Сардор Ахмедов| GitHub
Майкл Чин| Гитхаб| LinkedIn
Дана Флури| GitHub
Йоги Патуру| Гитхаб| ЛинкедИн

Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.