Мы недавно объявили, что WunderGraph теперь полностью открыт в исходном коде. Сегодня мы хотели бы объяснить, как вы можете использовать нашу платформу для разработчиков API, чтобы добавить кэширование на уровне Edge в ваши GraphQL API, не привязывая себя к конкретному поставщику.
Кэширование GraphQL на уровне Edge должно быть независимо от поставщика.
Сервисы, такие как Akamai и GraphCDN / Stellate, предлагают собственные варианты решения этой проблемы. Мы сравним различные подходы и их компромиссы.
Почему мы создали собственные решения CDN для GraphQL?
Хороший вопрос для начала - почему мы вообще создали собственные решения CDN для GraphQL?
Большинство реализаций GraphQL игнорируют, как работает веб
Проблема большинства реализаций GraphQL заключается в том, что они на самом деле не используют "платформу", на которой работают. Под платформой я подразумеваю веб, или, точнее, HTTP и ограничения REST.
Веб может предложить многое, если вы используете его правильно. Если бы запросы на чтение (Queries) использовали глагол GET, в сочетании с заголовками Cache-Control и ETag, браузеры, CDN, серверы кэша, такие как Varnish, Nginx и многие другие инструменты, могли бы обрабатывать кэширование из коробки. Вам действительно не понадобился бы сервис, который понимает GraphQL.
Однако реальность такова, что большинство GraphQL API заставляют вас отправлять запросы через HTTP POST. Заголовки Cache-Control и ETags в этом случае не имеют смысла, так как все участники веба думают, что мы пытаемся чем-то "манипулировать".
Таким образом, мы создали решения для кэширования на уровне Edge, которые переписывают HTTP POST на GET и способны инвалидировать очень умными способами. Анализируя запросы и мутации, они способны создавать кэш и инвалидировать объекты по мере прохождения мутаций через систему.
Ограничения и проблемы CDN GraphQL / Edge Caches
CDN GraphQL создает вторичный источник истины
Как обсуждалось в ограничении слоевой системы Филдинга, посредники могут использоваться для реализации общего кэша для уменьшения задержки запросов.
Проблема, которую я вижу с "умными" кэшами Edge GraphQL, заключается в том, что они смотрят на операции GraphQL, чтобы понять, что кэшировать и что инвалидировать. Некоторые реализации CDN GraphQL даже позволяют вам использовать их API для инвалидации объектов.
То, что звучит очень умно, создает огромную проблему, вы создаете второй источник истины. До использования CDN GraphQL все, что вам нужно было сделать, это реализовать ваши резолверы. Теперь вам нужно думать о том, как инвалидировать CDN.
Еще хуже, теперь вы программируете для одного поставщика и его реализации, связывая ваше приложение с одним поставщиком услуг. Вы не можете просто легко переключиться с одного поставщика CDN GraphQL на другого. В отличие от REST/HTTP API, нет стандарта, как кэшировать API GraphQL, поэтому каждая реализация будет отличаться.
Еще одна проблема заключается в том, что мы создаем вторичный источник истины. Представьте, что мы ставим CDN GraphQL перед API GraphQL GitHub, чтобы кэшировать вопросы для репозитория. Если мы используем "умное" инвалидирование кэша с помощью мутаций, мы не сможем обновить кэш, если пользователь обходит наш CDN и использует API GitHub напрямую.
CDN GraphQL действительно работает только в том случае, если 100% трафика проходит через систему.
Ваш кэш GraphQL Edge не будет работать на localhost или в вашем CI/CD
Тестирование занимает огромную часть разработки программного обеспечения, и мы определенно хотим иметь отличный опыт разработчика при создании наших API. Использование собственного, только облачного, решения для кэширования означает, что мы не можем тестировать его локально или в наших системах непрерывной интеграции.
Вы будете разрабатывать на своей локальной машине без CDN, только чтобы узнать, что нечто ведет себя странно, когда вы используете CDN в продакшене.
Если бы мы использовали стандартизированные директивы кэширования, мы могли бы использовать любой сервер кэша, такой как Nginx или Varnish, в нашем конвейере CI/CD для тестирования наших API.
Собственные CDN GraphQL создают проблему блокировки поставщика
Как упоминалось ранее, нет спецификации для того, как должно работать кэширование GraphQL. Каждая реализация отличается, поэтому вы всегда связаны с конкретным поставщиком. Они могут обанкротиться, их могут приобрести, они могут отменить свою услугу, они могут изменить свою ценовую модель.
В любом случае, это риск, которым нужно управлять.
CDN GraphQL может ввести кросс-доменные запросы
В зависимости от настройки, добавление CDN GraphQL в вашу архитектуру может означать, что ваши приложения, работающие в браузере, должны делать кросс-доменные запросы. То есть, когда ваше приложение работает на example.com
, а ваш CDN GraphQL работает на example.cdn.com
, браузер всегда будет делать дополнительный предварительный запрос.
Возможно, кэшировать предварительный запрос, но это все равно означает, что мы должны сделать по крайней мере один дополнительный запрос при начальной загрузке страницы.
CDN GraphQL может не работать хорошо для аутентифицированных запросов с серверным рендерингом (SSR)
Допустим, ваше приложение требует, чтобы ваши пользователи были в системе, но вы все еще хотите иметь возможность применять кэширование.
Кроме того, вы хотели бы реализовать серверный рендеринг (SSR). В идеальном сценарии вы бы заставили своих пользователей войти в ваш сервер аутентификации, который устанавливает cookie на вашем основном домене, так что вы вошли в систему на всех поддоменах. Если ваш CDN работает на другом домене, будет невозможно отрендерить страницу на стороне сервера, так как браузер не отправит cookie пользователя на домен CDN.
К счастью, некоторые поставщики предлагают пользовательские домены, поэтому аутентификация на основе cookie все еще может работать для вас.
Инвалидация глубоко вложенных операций GraphQL может не работать
Вот пример операции GraphQL, которая легко инвалидируется.
mutation UpdateProfile {
updateProfile(update: {id: 1, name: "Jannik"}) {
id
name
friendsCount
}
}
Если вы ранее запрашивали пользователя с id 1, вы можете инвалидировать все записи с этим id и инвалидировать следующий запрос:
query {
profile(id: 1){
id
name
friendsCount
}
}
Давайте усложним и запросим всех ваших друзей:
query {
me {
friends {
id
name
}
}
}
Теперь вы подружились на последней конференции. Давайте добавим их:
mutation AddFriend {
addFriends(where: {user:{hasConnection: {conferences: {id: {eq: 7}}}}}){
id
name
}
}
Теперь мы добавили всех пользователей в друзья, которые посетили конференцию с id равным 7. На этом этапе мы не получаем обратно все данные, когда снова запрашиваем всех ваших друзей, потому что кэш не может знать, что мутация addFriends
инвалидировала кэш для запроса friends
.
На этом этапе вам придется начать добавлять тегирование ответов, замещающие ключи или анализировать возвращаемые типы GraphQL, чтобы сделать вашу стратегию инвалидации "умнее".
Мы вернемся к этой теме после преимуществ.
Преимущества использования CDN GraphQL / Edge Cache
Когда есть стоимость, есть и преимущества!
Использование CDN GraphQL означает, что вам не нужно много менять в вашем приложении. В идеальном сценарии вы просто меняете URL сервера GraphQL, и все должно работать.
Вы также не должны развертывать и настраивать какие-либо инструменты. Вы покупаете готовый к использованию сервис, который просто работает.
Несмотря на ранее обсужденные проблемы, CDN GraphQL, вероятно, может улучшить производительность многих приложений.
Когда не стоит использовать CDN GraphQL
Как обсуждалось в разделе об инвалидации кэша, создание умного сервиса CDN, который ничего не знает о вашем бэкенде GraphQL, на самом деле очень сложно. Проблема заключается в том, что бэкенд является источником истины, но не делится всей этой информацией с кэшем.
На самом деле, если вы столкнулись с такими проблемами, когда инвалидация становится сложной, вы, возможно, захотите применить кэширование на другом уровне, внутри резолверов, или даже на одном уровне ниже, на уровне сущности, используя паттерн DataLoader.
Кроме того, если ожидается, что ваши данные будут часто меняться, вам, возможно, не имеет смысла кэшировать на уровне Edge.
Еще один большой недооцененный недостаток заключается в том, что инвалидация распределенного кэша является в конечном итоге согласованной. Это означает, что вы обслуживаете устаревший контент после мутации в течение короткого периода времени. Если вы не учитываете это в своей архитектуре, это может нарушить бизнес-логику клиентов.
Client (US) -> Query all posts to fill the cache
Client (US) -> Run mutation to update the post with ID:1
System fires a web-hook to a third-party service in FRA
Client (FRA) -> Query all posts -> stale content
Это не специфично для GraphQL, но с кэшированием HTTP семантика лучше понята. В общем, не используйте CDN GraphQL, если вам нужна последовательность записи после чтения.
Если ваше требование - поддерживать последовательность записи после чтения, есть несколько решений проблемы.
Одно из них - не кэшировать на уровне Edge, а скорее на уровне приложения. В этом случае вы обмениваете задержку на согласованность, что может быть хорошей сделкой.
Другой способ решения проблемы - распределение состояния по Edge и шардирование данных на основе местоположения. Эта модель шардирования не всегда возможна, но если общее состояние используется только группами пользователей в одном местоположении, это решение может работать очень хорошо.
Одним из примеров этого является Cloudflare Workers + Durable Objects, который предоставляет вам простое хранилище ключ-значение, которое сохраняется в определенном месте, что означает, что все пользователи, находящиеся близко к этому месту, могут иметь согласованное состояние при низкой задержке.
Когда кэш GraphQL Edge имеет наибольший смысл
Если у вас есть один GraphQL API, и этот API - единственный API, с которым общается ваш фронтенд, вы полностью владеете этим API, и никакой трафик не обходит ваш кэш, то такой CDN может действительно иметь смысл.
В противном случае я сомневаюсь, что вы получите ожидаемые результаты, особенно не учитывая дополнительных затрат, обсуждаемых ранее.
WunderGraph - альтернативный подход к кэшированию GraphQL Edge без привязки к поставщику
Мы обсудили плюсы и минусы CDN GraphQL, теперь я хотел бы предложить другой подход, который позволит вам оставаться независимым от поставщика.
Когда речь идет о решении проблем, иногда умно быть глупым. Я думаю, что кэш может быть намного умнее, когда он глуп и играет по правилам веба, и это именно то, что мы делаем с WunderGraph.
Наше решение - это открытый исходный код и его можно развернуть везде.
Как это работает?
Я работаю с GraphQL уже много лет, и я понял, что когда мы развертываем приложение, которое использует API GraphQL, я никогда не видел, чтобы приложение меняло операции GraphQL во время выполнения.
То, что приложения делают во время выполнения, - это изменение переменных, но операции обычно остаются статическими.
Так что то, что мы сделали, - это создали компилятор GraphQL в JSON-RPC, который рассматривает операции GraphQL как "подготовленные выражения" (Prepared Statements), вы, вероятно, слышали об этом термине при использовании базы данных.
Когда вы впервые используете SQL-выражение, вы отправляете его в базу данных и получаете обратно дескриптор для его "выполнения" позже. Последующие запросы теперь могут выполнять выражение, просто отправляя дескриптор и переменные. Это делает выполнение намного быстрее.
Поскольку мы не меняем операции GraphQL во время выполнения, мы на самом деле можем сделать этот шаг "компиляции" во время разработки.
В то же время мы заменяем HTTP POST для запросов на HTTP GET, отправляя переменные как параметр запроса.
Отправляя запросы GraphQL через JSON-RPC с HTTP GET, мы автоматически включаем возможность использования заголовков Cache-Control и ETags.
И это вся магия истории кэширования WunderGraph.
Мы не строим "умный" кэш. Мы не кэшируем объекты, и нам не нужно строить сложную логику инвалидации. Мы просто кэшируем ответ уникальных URL.
Для каждой операции вы можете определить, должна ли она быть кэширована и сколько времени. Если ответ может часто меняться, вы также можете установить время кэша на 0, но настроить "stale-while-revalidate" на неотрицательное число. Клиент автоматически отправит запрос к источнику с ETag, сервер либо отправит обновленный ответ, либо 304, если не изменен.
Это очень глупый подход. Если кэшированное значение истекло или устарело, мы спрашиваем у источника, есть ли у него обновление. В зависимости от конфигурации это может создать несколько дополнительных запросов к источнику, но мы также не создаем второй источник истины или не имеем дело со сложностью управления тегами инвалидации кэша для вложенных объектов.
Это похоже на использование Change Data Capture (CDC) в качестве источника для подписок против простого опроса. CDC может быть чрезвычайно сложным для правильной работы, в то время как простой опрос на стороне сервера может отлично работать большую часть времени. Это просто и надежно.
На самом деле мы здесь не многое изобрели, все это стандартное поведение кэширования, и любой сервис, такой как Cloudflare, Fastly, Varnish или Nginx, поддерживает его из коробки.
Есть стандарт, как все участники веба обрабатывают заголовки Cache-Control и ETag, мы просто реализовали этот стандарт.
Удаляя GraphQL из уравнения, мы сделали его совместимым с вебом.
Если вы создаете инструменты для веба, вы должны уважать то, как построен веб, иначе вы создаете больше проблем, чем решаете.
Дополнительные преимущества подхода WunderGraph к кэшированию GraphQL
Это не только CDN и серверы, которые понимают заголовки Cache-Control и ETag. Браузеры также автоматически кэшируют и инвалидируют ваши ответы, не добавляя ни одной строки кода.
Кроме того, поскольку мы удалили GraphQL из времени выполнения, мы автоматически уменьшили поверхность для атаки нашего приложения. Если наш фронтенд не меняет запросы GraphQL во время выполнения, зачем предоставлять API, которое позволяет это делать?
Ограничения подхода кэширования JSON-RPC
Одним из ограничений этого подхода является то, что мы больше не можем использовать обычные клиенты GraphQL, поскольку они ожидают, что мы будем отправлять операции GraphQL через HTTP POST, а подписки через WebSockets.
Это не проблема для новых проектов, но может быть препятствием для существующих приложений. Кстати, у нас есть внутренний RFC для добавления режима совместимости, позволяющего, например, клиенту Apollo / urql и т.д. работать с WunderGraph через адаптер. Если вам это интересно, пожалуйста, дайте нам знать.
Тем не менее, использование простого JSON-RPC было бы очень неудобно. Вот почему мы не просто компилируем GraphQL в JSON RPC, но также генерируем полностью типобезопасные клиенты.
Одной из таких клиентских интеграций является пакет NextJS, который очень удобен для использования WunderGraph с NextJS, включая серверный рендеринг, аутентификацию, загрузку файлов и т.д.
Еще одним ограничением, на данный момент, является то, что вы должны самостоятельно размещать WunderGraph. Мы работаем над размещенным серверлесс-решением, но на данный момент вам придется развернуть и запустить его самостоятельно.
Хотя это может быть не очень удобно, это также имеет преимущество: WunderGraph лицензирован по лицензии Apache 2.0, вы можете запустить его где угодно.
Как вы можете развернуть WunderGraph?
Теперь, когда мы обсудили плюсы и минусы подхода WunderGraph к кэшированию, давайте посмотрим, как мы можем развернуть WunderGraph для достижения хороших результатов кэширования.
Во-первых, вам не нужно развертывать WunderGraph глобально. Его можно запустить близко к вашему источнику, например, к (микро-) сервисам, которые вы хотели бы использовать. В этом сценарии можно запустить WunderGraph в качестве входа в ваши другие сервисы.
Архитектура этого сценария выглядит так:
Client -> WunderGraph Server -> Origin
Развертывание WunderGraph с Nginx или Varnish в качестве дополнительного слоя кэширования
Если вы развертываете WunderGraph близко к источнику, он автоматически добавит необходимые заголовки Cache-Control & ETag. Эта настройка уже может быть достаточной для вашего сценария.
Однако в некоторых сценариях вы захотите добавить еще один слой серверов кэширования. Это может быть, например, кластер серверов Nginx или Varnish, размещенных перед вашим сервером WunderGraph.
Архитектура, обновленная с этим сценарием:
Client -> Cache Server -> WunderGraph Server -> Origin
Развертывание WunderGraph с Cloudflare или Fastly в качестве Edge Cache
В зависимости от того, где находятся ваши пользователи, централизованный слой кэширования может быть недостаточным для вашего сценария.
В этом сценарии вы можете использовать такие сервисы, как Cloudflare (Workers) или Fastly, чтобы добавить глобально распределенный CDN GraphQL / Edge Cache.
Важно отметить здесь, что вы не привязаны к конкретному решению. Как уже упоминалось ранее, мы используем стандартизированные директивы Cache-Control, поддерживаемые всеми решениями сервера кэша, поэтому вы не привязываете себя к конкретному поставщику.
Обновленная архитектура:
Client -> CDN -> WunderGraph Server -> Origin
Развертывание WunderGraph прямо на Edge, используя fly.io
Еще одним вариантом является развертывание WunderGraph прямо на Edge, устраняя необходимость в дополнительном слое кэширования. Сервисы типа fly.io позволяют вам развертывать контейнеры как можно ближе к вашим пользователям.
Архитектура этого сценария выглядит следующим образом:
Client -> WunderGraph Server (on the Edge) -> Origin
Развертывание сервиса на Edge не всегда выгодно
Есть один важный момент, который я хотел бы подчеркнуть, и он относится ко всем решениям, упомянутым в этом посте.
Если мы развертываем рабочую нагрузку на Edge, этот сервис будет очень близко к пользователям, что в общем случае является хорошим решением.
В то же время, в зависимости от того, где на "Edge" мы обрабатываем запрос, может быть случайная задержка от ~0 до 300 мс до вашего исходного сервера(ов) за один раундтрип. Если нам нужно сделать несколько раундтрипов для получения данных для одного клиентского запроса, эта задержка может накапливаться.
Как вы можете видеть, наличие логики на Edge не всегда выгодно для общей производительности.
Есть еще одна вещь! Это решение работает для GraphQL, REST, gRPC и других протоколов!
Вы правильно слышали. WunderGraph - это не только о происхождении GraphQL. Мы поддерживаем широкий спектр протоколов вверх по потоку, таких как REST, gRPC (скоро будет!) и базы данных, такие как PostgreSQL, MySQL, Planetscale и т.д.
WunderGraph интегрирует все ваши сервисы и создает "Виртуальный граф", схему GraphQL, представляющую все ваши сервисы.
Описанный выше слой кэширования работает не только для происхождения GraphQL, но и для всех сервисов, которые вы добавили в свой виртуальный граф.
Это означает, что вы можете применить единый слой аутентификации, авторизации и, конечно, кэширования ко всем вашим API.
По нашему опыту, редко бывает так, что вы подключаете фронтенд к одному серверу GraphQL. Вместо этого вы, вероятно, будете подключать его к многим сервисам, что мы и пытаемся упростить, и кэширование - это часть этой истории.
Заключение
Мы обсудили различные варианты улучшения производительности приложения с помощью различных систем кэширования. Есть готовые к использованию решения, которые обеспечивают простоту, но также привязывают вас к конкретным поставщикам.
С WunderGraph мы пытаемся предложить альтернативу с открытым исходным кодом, которая основана на стандартах, реализованных многими инструментами и поставщиками, чтобы вы могли выбрать лучшее решение для вашей ситуации.
Если вы смотрите в сторону добавления кэширования в ваш стек, мы бы хотели поговорить! Присоединяйтесь к нам в Discord или свяжитесь с нами в Twitter.