Темы



Мы только что добавили поддержку самой уродливой части HTTP: Vary

Коротко сгенерировано ИИ по тексту статьи
  • Cloudflare добавила поддержку HTTP-заголовка Vary в Cache Rules на всех тарифных планах.
  • Для заголовков из Vary доступны три действия: нормализация значений, сквозная передача без изменений или полный обход кэша.
  • Анализ 120 миллионов ответов с 50 000 сайтов показал, что около 3 000 ресурсов варьируют контент по четырем и более полям.

Почему это важно: Это позволяет выдавать корректные варианты ответов с одного URL и не допускать фрагментации кэша на множество одноразовых записей.

Заголовок ответа Vary называют »самой уродливой частью HTTP, которую мы до сих пор не улучшили.» В той же публикации он описывается как «ужасный, неуклюжий механизм» с «весьма удручающей совместимостью» между промежуточными узлами. Обычно именно в этот момент здравомыслящие инженеры начинают медленно отступать с поднятыми руками.

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

У одного URL может быть несколько правильных вариантов ответа. Например, сервер может передавать разные форматы изображений в зависимости от браузера. Если кэш игнорирует Vary, он рискует выдать запросу не те байты. Но если он считает каждое сырое значение заголовка уникальным, горстка похожих запросов может превратиться в тысячи едва ли пригодных для повторного использования записей кэша. Vary указывает кэшу, какие поля запроса могут влиять на ответ, но он не подсказывает кэшу, какие различия действительно имеют значение.

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

Как работает Vary

Vary — это стандартный заголовок ответа HTTP, который сообщает промежуточным кэшам (таким как Cloudflare), какие поля запроса могут повлиять на ответ, отправленный исходным сервером. Сайты используют Vary для выдачи разных языков, форматов изображений, схем сжатия или регионального контента с одного и того же URL.

Возьмем один URL, который формирует два допустимых представления. Браузер запрашивает веб-страницу:

GET /catalog HTTP/1.1
Host: example.com 
Accept: text/html

Исходный сервер возвращает HTML и указывает Accept в качестве поля, которое может повлиять на ответ:

HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=3600
Vary: Accept

API-клиент может запросить тот же URL с другими предпочтениями:

GET /catalog HTTP/1.1 
Host: example.com 
Accept: application/json

На этот раз правильным ответом является JSON. Заголовок Vary: Accept сообщает кэшу, что одного URL недостаточно для выбора между ответами. Необходимо также учитывать значение Accept в запросе.

Без Vary тот ответ, который попадет в кэш первым, может быть отправлен обоим клиентам. Если победит HTML, API-клиент получит разметку, и его JSON-парсер выдаст ошибку. Если победит JSON, браузер, ожидавний веб-страницу, получит ответ API.

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

Когда правильное кэширование становится бесполезным

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

Accept-Language: en-US, fr;q=0.8

В то время как другой клиент может запросить:

Accept-Language: fr;q=0.8, en-GB

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

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

Эта проблема усугубляется, когда ответ варьируется по нескольким полям. Десять возможных значений в одном поле создают десять вариантов. Десять значений в трех полях могут породить 1000 комбинаций. Реальные заголовки могут иметь гораздо большую кардинальность: значений User-Agent множество, файлы cookie могут быть уникальными для каждого отдельного посетителя, а заголовки предпочтений могут различаться порядком, форматированием (пробелы и табуляция имеют значение!) и коэффициентами качества.

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

Анализ более чем 120 миллионов ответов почти с 50 000 популярных сайтов показал, что почти 3 000 сайтов варьируют контент по четырем или более полям. Некоторые варьировали его по 10, 23 или даже 47 полям. Мы хотим убедиться, что у клиентов есть необходимые инструменты для использования Vary там, где это уместно, но не до такой степени, чтобы создавать бесполезный кэш.

Некоторая вариативность с высокой кардинальностью является преднамеренной. CDN или обратные прокси могут внедрять значения, например географический регион, для предсказуемого разделения контента. Это работает, когда возможные значения контролируются и все компоненты согласны с их значением. Без этих ограничений кэш фрагментируется на варианты, которые он, возможно, никогда больше не использует.

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

Как правила кэширования (Cache Rules) управляют Vary

У клиентов Cloudflare уже было несколько способов обработки согласованного контента, похожего на Vary. Они могли обойти кэш и переложить эту задачу на свой исходный сервер, воспроизвести логику согласования источника в пользовательском ключе кэша или другом правиле, использовать Worker или применить такие функции, как Vary для изображений.

Эти варианты по-прежнему полезны, но они либо отказываются от кэширования, либо дублируют логику приложения, либо требуют написания дополнительного кода, либо подходят для более узких сценариев использования. Vary in Cache Rules может заполнить пробел между этими существующими функциями, разделив поддержку на два решения:

  1. Источник использует Vary для определения заголовков запроса, которые могут повлиять на ответ.
  2. Правило кэширования (Cache Rule) определяет, как Cloudflare обрабатывает значение каждого заголовка.

Правило кэширования не заставляет каждый ответ использовать vary. Если источник не возвращает Vary, Cloudflare кэширует ответ обычным образом, хотя правило все равно может перезаписать Accept и Accept-Language перед пересылкой запроса на источник.

Когда источник возвращает Vary, Cloudflare использует настроенное действие для каждого названного им заголовка. Для заголовков без индивидуальных настроек применяется действие по умолчанию для этого правила. Доступны три действия:

Действие

Что делает Cloudflare

Лучше всего подходит для

normalize

Нормализует заголовки запроса перед выбором закэшированного варианта, помогая эквивалентным запросам разделять один кэшированный ответ. Применяет правила для конкретных заголовков к Accept, Accept-Language и Accept-Encoding. Для других заголовков он удаляет необязательные пробелы и объединяет повторяющиеся строки заголовков в их исходном порядке, сохраняя регистр и внутренние пробелы.

Рекомендуемая отправная точка для заголовков согласования, где множество значений запроса сопоставляются с небольшим набором ответов.

passthrough

Использует необработанные байты заголовка запроса для сопоставления с кэшем, сохраняя регистр, пробелы, порядок и дублирующиеся значения. Если заголовок отображается на нескольких строках, Cloudflare объединяет эти строки по порядку с помощью запятых для сопоставления с кэшем. Режим сквозной передачи (passthrough) оставляет исходящие строки заголовков без изменений. Cloudflare по-прежнему может перезаписывать Accept-Encoding, если отключена функция Respect Strong ETags.

Заголовки с контролируемым набором значений, где точное значение изменяет ответ.

bypass

Не сохраняет ответ, если источник указывает этот заголовок в Vary. Существующие записи в кэше не удаляются, поэтому очистите их вручную, если это необходимо.

Используется для персонализированных, высококардинальных или неожиданных заголовков, таких как Cookie или User-Agent.

Мы рекомендуем использовать normalize (нормализацию) в качестве значения по умолчанию. Для отдельных заголовков с персональными или неограниченными значениями используйте bypass (обход). Используйте passthrough (сквозную передачу), когда точное значение изменяет ответ.

Например, passthrough сохраняет различия в регистре, пробелах, порядке и дублирующихся значениях, даже если источник считает их эквивалентными. С Vary: X-View и passthrough эти три значения создают отдельные ключи кэша:

X-View: compact,full

X-View: Compact,full

X-View: compact, full

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

Независимо от настроенных действий, Vary: * всегда обходит кэш. Это означает, что любой аспект запроса, даже информация вне сообщения HTTP (например, IP-адрес клиента), может повлиять на то, какой ответ выберет источник. Следовательно, Cloudflare не может повторно использовать ответ для последующего запроса без обращения к источнику.

Как ответ перемещается по кэшу

Давайте проследим за одним из запросов /catalog из примеров выше через Cloudflare.

При первом запросе у Cloudflare нет сохраненных данных Vary для этого ресурса, поэтому происходит промах кэша (cache miss). Соответствующее правило кэширования может нормализовать настроенные поля до того, как Cloudflare свяжется с источником.

Это может произойти до того, как Cloudflare узнает, будет ли итоговый ответ содержать Vary. Правило кэширования определяет разрешенную нормализацию; ответ впоследствии определяет, станут ли эти поля частью закэшированного варианта.

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

Источник отвечает следующим образом:

Vary: Accept, Accept-Language

Cloudflare регистрирует эти имена заголовков и сохраняет ответ в качестве закэшированного варианта. Значения заголовков, обработанные в соответствии с правилом кэширования, отличают этот вариант от других для того же ресурса.

Когда поступает другой запрос для /catalog, Cloudflare начинает с базового ключа кэша ресурса: как правило, это URL плюс любые другие настроенные ключевые поля. Затем он считывает сохраненные поля Vary и применяет правило кэширования к этим заголовкам в новом запросе, чтобы найти подходящий закэшированный вариант.

Предположим, они нормализуются до:

Accept: text/html

Accept-Language: en,fr

Cloudflare использует эти значения для прямого поиска подходящего закэшированного варианта. Он не сравнивает запрос с каждым сохраненным вариантом по очереди.

Если подходящий вариант существует и является актуальным (свежим), происходит попадание в кэш (cache hit). Если нет, Cloudflare отправляет запрос на источник и может сохранить полученный ответ как еще один вариант.

Ответ источника замыкает цикл. Для каждого заголовка, указанного в Vary, Cloudflare использует действие, настроенное для этого заголовка, или действие правила по умолчанию, если заголовок не указан индивидуально:

  • Если он не содержит Vary, Cloudflare кэширует его обычным образом.
  • Если каждый указанный заголовок разрешается в normalize или passthrough, Cloudflare может сохранить ответ в качестве закэшированного варианта.
  • Если какое-либо указанное поле использует bypass, Cloudflare не сохраняет ответ.
  • Если ответ содержит Vary: *, Cloudflare его не сохраняет.

Это возлагает большую ответственность на исходный сервер. Каждый кэшируемый ответ, который может отличаться в зависимости от полей запроса, должен стабильно возвращать соответствующий заголовок Vary, включая ошибки и запасные (fallback) ответы. Если один из ответов опускает его, Cloudflare может закэшировать этот ответ без вариативности, необходимой для поддержания его изоляции.

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

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

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

Нормализация объединяет эквивалентные запросы

Помните запросы на английском и французском языках из примера выше?

Accept-Language: en-US, fr;q=0.8

Accept-Language: fr;q=0.8, en-GB

Оба запроса отдают предпочтение английскому языку, но режим passthrough будет рассматривать их как разные варианты. Если правило кэширования разрешает en, fr и de, нормализация (normalize) сводит оба варианта к en, fr, позволяя им использовать один и тот же кэшированный ответ.

Для этого Cloudflare приводит значения в Accept, Accept-Language и Accept-Encoding к нижнему регистру, а затем сортирует их по коэффициенту качества (самый высокий — в начале), используя алфавитный порядок для разрешения споров при равных коэффициентах. Таким образом, порядок следования у клиента не влияет на ключ кэша. После сортировки Cloudflare удаляет параметры из записей с ненулевым коэффициентом качества. Он также может отбрасывать q=0 («неприемлемо») при сокращении языковых тегов или фильтрации до настроенных форматов и языков. Например, en-US;q=0 может превратиться в en. Используйте passthrough для Accept или Accept-Language, если источнику необходимо видеть эти исключения.

Вы также можете настроить правило так, чтобы в Accept и Accept-Language сохранялись только указанные медиатипы или языки. Региональные языковые теги, такие как en-US, сокращаются до базового языка (en), если не настроен полный тег. Это позволяет сопоставить нормализацию с форматами и языками, которые реально обслуживает ваш исходный сервер.

Чтобы обеспечить соответствие выбора источника сопоставлению с кэшем, Cloudflare пересылает нормализованные значения Accept и Accept-Language на исходный сервер. Он также пересылает нормализованные значения Accept-Encoding, когда включена функция Respect Strong ETags. Другие заголовки нормализуются только для целей сопоставления с кэшем.

Настройка Vary в правилах кэширования

В панели управления Cloudflare перейдите в раздел Caching > Cache Rules (Кэширование > Правила кэширования), создайте или отредактируйте правило, сделайте ответ подходящим для кэширования и добавьте настройку Vary. Установите поведение по умолчанию, а затем добавьте заголовки, которые должен указывать ваш исходный сервер.

Такая же конфигурация доступна через API наборов правил (Rulesets API) на этапе http_request_cache_settings. Настройка по умолчанию выбирает резервное действие для заголовков, названных вашим источником в Vary, для которых вы не задали индивидуальные настройки.

В этом примере Accept и Accept-Language нормализуются до заданного набора форматов и языков. Действие нормализации по умолчанию также применяется к другим заголовкам, указанным в Vary:

{
  "rules": [
    {
      "ref": "vary_negotiated_content",
      "description": "Cache bounded negotiated representations",
      "expression": "(http.host eq \"example.com\" and http.request.uri.path eq \"/catalog\")",
      "action": "set_cache_settings",
      "action_parameters": {
        "cache": true,
        "vary": {
          "default": {
            "action": "normalize"
          },
          "headers": {
            "accept": {
              "action": "normalize",
              "media_types": ["text/html", "application/json"]
            },
            "accept-language": {
              "action": "normalize",
              "languages": ["en", "fr", "de"]
            }
          }
        }
      }
    }
  ]
}

Это полный текст запроса для PUT к точке входа этапа http_request_cache_settings. Операция PUT заменяет каждое правило в этой точке входа. Если у вас уже есть правила кэширования, включите их в массив rules или используйте соответствующую операцию создания или обновления для одного правила.

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

Информацию об ограничениях, дополнительных примерах и настройке через Terraform см. в документации по Vary.

Почему бы не использовать пользовательский ключ кэша?

В этот момент напрашивается очевидный вопрос: «Почему бы не добавить Accept и Accept-Language в пользовательский ключ кэша?»

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

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

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

Используйте Vary в правилах кэширования уже сегодня!

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

Vary в правилах кэширования объединяет эти две точки зрения. Источник определяет поля запроса, которые могут повлиять на ответ. Вы решаете, следует ли нормализовать значения, использовать passthrough для точных различий или исключить ответ из кэша.

Vary никогда не был слишком уродливым, чтобы быть полезным. Но ручная настройка поддерживаемых форматов и языков может подойти не для каждого приложения. Мы оцениваем, могут ли идеи из устаревшего черновика Availability Hints сократить эту работу, позволяя источникам напрямую описывать предоставляемые ими варианты представлений.

Vary в правилах кэширования доступен уже сегодня на тарифных планах Free, Pro, Business и Enterprise через панель управления Cloudflare, API наборов правил и Terraform.

© Cloudflare Blog