Кэш, хэш и няш-меш

    UPD0 (2016-07-19 23-31): судя по всему, первая половина моей статьи — успешно изобретённый велосипед. Спасибо хабравчанам за ссылку на спецификацию
    Статья ценна не более, чем вольное описание уже придуманной технологии.


    Предыстория


    Июльский субботний вечер подходил к концу. Нарубив дров на шашлык, я повесил USB-модем на багету, скомандовал sudo wvdial, развернул браузер и обновил вкладку с открытым гитхабом. Вернее, попытался обновить. Скорость не радовала, и в итоге страница-то обновилась, но явно не хватало какого-то из стилевых файлов; и дело было не в блокировке, поскольку аналогичные проблемы я наблюдал и с другими сайтами, и зачастую они решались просто многократным обновлением страницы. Во всём был виноват перегруз 3G-сети.


    Стоп! А как же кэш?


    Недолгое гугление привело на официальный гугловский мануал. Целиком пересказывать его не буду; скорее всего, дело было в том, что браузер прилежно ждал, когда сервер передаст ETags, а ответ сервера затерялся в переполненных триджунглях.


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


    Суть предложения


    Добавить ко всем тэгам для подключения подчинённой статики (стилей, скриптов, изображений) атрибут checksum, который бы хранил хэш (например, SHA-1, как в git) требуемого файла:


    <link href="//habracdn.net/habr/styles/1468852450/_build/topic_form.css" rel="stylesheet" media="all" checksum="ef4547a3d5731e77f5a1a19e0a6d89915a56cd3a"/>

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


    Обратная совместимость предлагаемого решения очевидна.


    Какие проблемы это решает?


    Пресловутая угадайка: актуален ли файл в кэше?


    • Больше не нужно отправлять запрос и сличать полученные ETags.
    • Даже если файл в кэше вроде как устарел, но хэш совпадает — его можно смело использовать.
    • Чистка кэша как средство решения проблем частично теряет актуальность.

    Дилемма: jQuery со своего домена или с CDN?


    Владельцам малых сайтов часто приходится выбирать: либо подключать jQuery и/или подобные ей библиотеки с CDN (гугловского, например), или со своего домена.
    В первом случае уменьшается время загрузки сайта (в том числе первичной, т.е. при первом заходе посетителя на сайт) за счёт того, что файл с серверов Гугла с большой долей вероятности уже есть в кэше браузера. Но, например, разработчики WordPress придерживаются второго варианта, ставя во главу угла автономность. И в условиях, когда CDN падают, блокируются и т.д., их можно понять.
    Теперь от такой проблемы можно будет избавиться навсегда: не всё ли равно, откуда получен файл, если его содержимое — это ровно то, что нужно html-странице, и она это удостоверяет? Можно смело указывать свой домен, и если библиотека есть в кэше (неважно, загруженная с этого сайта, другого "малого" сайта или из какого-нибудь CDN) — она подхватится.


    Смешанный HTTPS/HTTP-контент


    Одна из причин запрета загрузки HTTP-ресурсов на HTTPS-страницах — возможность подмены HTTP-контента. Теперь это больше не преграда: браузер может получить требуемый контент и сверить его хэш с хэшем, переданным по HTTP. Отмена запрета на смешанный контент (при наличии и совпадении хэша) позволит ускорить распространение HTTPS.


    Косвенное определение истории по времени загрузки статики


    Известно, что владелец некоторого сайта evilsite.org может (с некоторой долей вероятности) определить, был ли посетитель на другом сайте goodsite.org, запросив, например, изображение goodsite.org/favicon.ico. Если время загрузки иконки ничтожно мало — то она в кэше, следовательно, посетитель был на сайте goodsite.org. Теперь эта атака усложнится: околонулевое время отклика будет лишь обозначать, что посетитель был на сайте с таким же фавиконом. Это, конечно, не решает проблему целиком, но всё же несколько усложняет жизнь определяющему.


    На что это не влияет?


    • На html-страницы
    • На изображения, стили и скрипты, открываемые по непосредственной ссылке, а не служащие вспомогательными элементами страницы.
    • На изображения, стили и скрипты, которые не предполагаются неизменными, например, когда подключается самая новая версия некоторой библиотеки с CDN этой библиотеки.

    Идеология


    Как обычно (математик я, что уж тут поделать) сформулируем аксиомы, которые вкладываются в предложение:


    1. Все передаваемые файлы делятся на главные (в основном html-страницы) и подчинённые (скрипты, изображения, стили и т.д.).
      В идеологии, заложенной в стандарты HTTP-кэширования, все файлы равноправны. Это, конечно, толерантно, но не отвечает современным реалиям.
    2. Неважно, откуда получен подчинённый файл. Важно, что его содержимое удовлетворяет нужды главного.
      В существующей идеологии даже сама аббревиатура URI — Uniform Resource Identifier — предполагает, что идентификатором ресурса является его адрес в сети. Но, увы, для подчинённых файлов это несколько не соответствует действительности.

    Перспективы


    Обещанный няш-меш


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


    Доверенные устройства


    Например, в офисе работают программисты, ЭВМ которых объединены в локальную сеть. Программист Вася приходит рано утром, открывает гитхаб и получает в кэш стили от нового дизайна, который выкатили ночью (у нас — ночь, там — день). Когда в офис приходит программист Петя и тоже загружает html-код гитхабовской странички, его ЭВМ спрашивает у всех ЭВМ в сети: "А нет ли у вас файла с таким-то хэшем?" "Лови!" — отвечает Васина ЭВМ, экономя тем самым трафик.
    Потом наступает перерыв, Вася и Петя лезут смотреть котиков и пересылают фотографии друг другу. Но каждый котик скачивается через канал офиса только один раз...


    Анонимный разделяемый кэш


    Аня едет в трамвае с работы и читает новости… например, на Яндекс-Новостях. Встретив очередной тэг <img>, Анин телефон со случайного MAC-адреса спрашивает всех, кого видит: "Ребят, а ни у кого нет файла с таким-то хэшем?". Если ответ получен в разумное время — профит, Аня сэкономила недешёвый мобильный трафик. Важно почаще менять MAC-адрес на случайный да не "орать", когда в поле видимости слишком мало узлов и спрашивающего можно идентифицировать визуально.
    Разумность времени ответа определяется исходя из стоимости трафика.


    Дальнейший переход к няш-мешу


    Фотография в соцсети может быть представлена как блоб, содержаший хэш и адрес собственно изображения (возможно, в нескольких различных размерах), а также список комментариев и лайков. Этот блоб тоже можно рассматривать как вспомогательный файл, кэшировать и передавать друг другу.
    Более того, альбом фотографий тоже легко превращается в блоб: список хэшей изображений + список хэшей блобов-фотографий (первое нужно, чтобы при добавлении лайка/комментария показывать фотографии сразу, а метаинформацию — по мере её получения).
    Останется только реализовать электронную подпись и поля вида "замещает блоб такой-то" — и готова няш-меш-социалочка.


    Компактизация хэша


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


    Поведение при несовпадении


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


    Файлы-альтернативы


    В некоторых случаях можно использовать любой из нескольких файлов с разными хэшами. Например, на сайте используется минифицированная jQuery, но если в кэше браузера есть неминифицированная — что мешает использовать её?


    Превентивное кэширование


    Многие устройства работают в двух режимах: когда интернет условно-безлимитен (например, мобильный телефон в вай-фай сети) и когда интернет ограничен (лимит по трафику или узкий канал). Браузер или расширение к нему может, пользуясь безлимитным подключением, заранее скачивать популярные библиотеки (наподобие jQuery и плагинов к ней), также по мере необходимости их обновлять. Это ли не мечта многих, чтобы jQuery была включена в браузер?


    Заключение


    Выдвигаемое рацпредложение актуально, так как борьба за оптимизацию загрузки сайтов идёт полным ходом. Более всего выиграют малые и средние сайты за счёт разделяемых библиотек (и, может быть, некоторых часто используемых изображений) в кэше. Уменьшится потребление трафика мобильными устройствами, что важно с учётом ограниченной пропускной способности каналов сотового интернета. Крупные сайты также могут уменьшить нагрузку на свои серверы в случае, если будут внедрены mesh-технологии.
    Таким образом, поддержка предлагаемой технологии выгодна и вебмастерам, чьи сайты будут грузиться быстрее, и производителям браузеров, которые тоже будут быстрее отображать страницы, и провайдерам, у которых уменьшится потребление полосы (пусть и не столь значительно, но от провайдеров активных действий и не требуется).


    P.S.
    Мне было бы очень приятно услышать мнение Mithgol, Shpankov и BarakAdama.


    P.P.S.
    Хабр всезнающий, в какое спортлото отправлять рацпредложение?

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 34

      +4
      «Аня едет в трамвае с работы и читает новости…»
      А рядом сидит сурового вида парень в толстовке с капюшоном, который ворует чужие сессии через раздачу заражённого jquery с подогнанным хешем.
        0
        Как вариант, локальные версии файлов популярных библиотек мог бы периодически загружать сам браузер из гарантированно надёжных источников.
          +1
          Тогда теряется вся няш мешность.
          0
          Взять алгоритм, коллизии в котором неизвестны и их вероятностью можно пренебречь — и все дела.
            0
            Указывать также и размер файла, или же указывать связку из двух хэшей. Или и то и другое вместе. Задача найти коллизию в таком случае становится более интересной.
            +2
            Хабр всезнающий, в какое спортлото отправлять рацпредложение?
            Все предложения, касающиеся веб-разработки, W3C в настоящее время рекомендует публиковать на WICG (хотя де-факто толку в этом мало).
              +4
              Кстати, аналогичный атрибут, хранящий хэш скрипта или файла таблицы стилей, уже предусмотрен и называется integrity, правда, предназначен он сейчас исключительно для проверки целостности файла с целью предотвращения подмены файла по пути от сервера к пользователю.
                +2
                Насколько мне известно, проблема в реализации указанного в статье только одна — cache poisoning.
                Именно из-за этого хэш использется только для проверки целостности и никак иначе
                оф дока: www.w3.org/TR/2014/WD-SRI-20140318/#risks-1
                  0

                  С описанным по приведённым ссылкам (ещё раз спасибо!) вполне можно бороться. Там два типа атаки:
                  а) Обход CSP (расписан на примере XSS). По крайней мере тот пример, что там есть — смешно читать. Нейтрализуется простым правилом: наличие same-origin CSP влечёт запрет загрузки из хэш-кэша. Всё.
                  б) Брутфорс ожидаемых хэшей (например, некий сервер по GET-запросу (!) отдаёт пользователю идентификационные данные (!!)). Нейтрализуется тоже достаточно просто: если сервер разрешает хранить передаваемый субресурс в хэш-кэше, то он отдаёт специальный заголовок, например, Allow-Subresource-Sharing (сокращённо — ASS-заголовок). Делается тремя строчками в htaccess, обратную совместимость не ломает, сделать такое случайно… Надо постараться. Хотя с тем, что всякие wp-admin.css в хэш-кэш лучше не класть, спорить сложно.


                  Видимо, буду пытаться направить эти соображения туда… кстати, порекомендуйте, а куда лучше и через что?

                0

                1) Для нормальной работы кэша достаточно указать правильные заголовки — браузер не будет ничего грузить
                2) Более сложные сценарии (в том числе работа offline) уже успешно решены с помощью WebSockets

                  0
                  Не WebSockets, а WebWorkers)
                    0

                    На самом деле ServiceWorkers, опечатался, да

                  +1
                  Это плохо, т.к.:
                  1. Обновил один файл стилей
                  2. Пересобрал 10 страниц сайта, т.к. в теле каждой изменилось значение одного хеша.

                  Итого: вместо 1 файла, браузер должен будет обновить 11.
                  Имхо: если интернет настолько медленный, что даже ETags не затянуть, то можно считать, что его нет вообще.

                    0
                    ETags отваливался не у всех файлов, а с вероятностью примерно процентов 10.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      jQuery со своего домена или с CDN?


                      Замечательный CDN от Cloudflare: https://cdnjs.com

                      Если же какой-то модифицированный jquery, то со своего сервера через тот же CF (на бесплатном тарифном плане).
                        +2
                        > Больше не нужно отправлять запрос и сличать полученные ETags.
                        А ведь достаточно просто не прописывать ETags, используя вместо него старый добрый Expires, и не нажимать F5, а просто щёлкать по ссылкам.
                        И тогда браузер вообще не будет отправлять запрос в сеть, а будет использовать кеш, если дата в Expires не истекла.
                          0

                          Всё уже придумано за нас: DHT

                            +1
                            или еще вот ipfs.io
                              0
                              И так как NickKolok желал знать моё мнение, то вот оно: я также считаю, что для распределённого хранения данных с адресацией по контенту более всего подходит IPFS.

                              Рекомендую к прочтению блогозапись «Почему Интернету нужен IPFS, пока ещё не поздно» и её обсуждение на Хабрахабре.
                              +1
                              > Владельцам малых сайтов часто приходится выбирать: либо подключать jQuery и/или подобные ей библиотеки с CDN (гугловского, например),
                              > или со своего домена.
                              > В первом случае уменьшается время загрузки сайта (в том числе первичной, т.е. при первом заходе посетителя на сайт) за счёт того,
                              > что файл с серверов Гугла с большой долей вероятности уже есть в кэше браузера.

                              Шанс того, что кеш браузера, заваленный добром от недавно просмотренных десятков сайтов, будет содержать именно тот файл с CDN, который лично данному сайту необходим, крайне невелик. Но пусть это не 1%, пусть целых 5% — т.е. у одного из 20 человек именно этот файл будет доступен на 0.2 сек быстрее.

                              Зато у 19 из 20 будут лишние задержки на DNS-поиск, возможное установление https-соединения с дополнительным хостом, плюс имеем риск недоступности файла с CDN — в общем, 19 из 20 посетителей имеют шанс получить ухудшение вместо улучшения.

                              Как по мне, так не та статистика, за которую надо владельцу малого и/или малопосещаемого сайта держаться. Лучше положить файлы js и стилей себе на сервер, объединить их и минифицировать, настроить сжатие (в идеале — gzip_static), настроить кеширование — положительный эффект будет куда большим!

                              А версионирование файлов легко и непринужденное делается через указание версии в URI файла (а не в GET-параметре, как это любят неотключаемо делать мамонты вроде Битрикса). Хоть в виде http://domain.tld/files/js/VERSION/file.js, хоть в виде http://domain.tld/files/js/file-VERSION.js — настроить веб-сервер на отдачу в каждом случае файла, лежащего по факту в /files/js/file.js (т.е. на игнорирование часто VERSION) несложно, а польза безо всяких расширений языка разметки (и реализации поддержки расширения в браузере) несомненна.

                              И, да, заголовок etag как раз и придуман для реализации того, что вы в своем предложении упоминаете. Только куда более прозрачно и с меньшим влиянием на процесс создания html-кода страницы.
                                0

                                Спасибо за развёрнутый комментарий!
                                Начну с конца.
                                etag:
                                а) требует запроса к серверу. Это время и трафик (пусть и мизерный). И нагрузка на сервер (тоже небольшая).
                                б) не позволяет разделять субресурсы (картинки, библиотеки и т. д.) между сайтами. А в этом есть определённая выгода. Некоторые библиотеки (и в первую очередь jQuery) действительно очень популярны и с очень большой долей вероятности окажутся в кэше. Размер jQuery достаточно невелик (в пределах 200 кБ, сжатая — и вовсе 32 кБ). Или, например, типовые смайлики на phpbb-форумах — за каждым отдельный запрос слать и etags сверять, учитывая, что такое добро наверняка уже в кэше валяется?

                                  +1
                                  действительно очень популярны и с очень большой долей вероятности окажутся в кэше

                                  Вы знаете, сколько различных версий одной jQuery? Плюс минифицированные и полные варианты.
                                  Или, например, типовые смайлики на phpbb-форумах

                                  Часто заменяются на альтернативные.
                                    0
                                    Вы знаете, сколько различных версий одной jQuery? Плюс минифицированные и полные варианты.

                                    По одной версии на каждую актуальную версию вордпресса/джумлы и все остальные версии.


                                    А вообще вопрос интересный, тянет на отдельную тему для исследования. Спасибо.


                                    Часто заменяются на альтернативные.

                                    Часто, но не всегда. Хотя согласен, что году эдак в 2007-м подобных повторяющихся элементов оформления было больше.

                                    0
                                    Про смайлики — помните, был анекдот про порутчика Ржевского, мол, все шутки пронумеровать и говорить только номер той, что хотите отпустить? Так вот и все смайлики мира, все библиотеки, все даже фразы в блоках и твитах пронумеровать, и придумать расширение для html, которое вместо ссылки на графический смайлик или вместо указания фразы текстом просто будет на номер в метатаблице ссылаться! Мы не победим, пока не возьмемся за дело всерьез, это же понятно — так что старые браузеры нужно будет удалить, а вот тогда новые уже будут заменять контент «по номерам» как надо )))

                                    А если серьезно, вы поизучайте свой кеш. Да, jquery невелика (хотя как посмотреть!), но и экономия, кроме лишнего запроса, будет не смертельна. А вот вы посмотрите, каков размер кеша в вашем ПК, а каков в вашем телефоне, потом прикиньте, какой из вариантов jqeury туда надо поместить, чтобы многие сайты им воспользовались, и осознайте, сколько, глядя на объем запрашиваемых файлов любого современного сайта, вы каждым кликом по ссылке в вебе в кеше заменяете/удаляете.

                                    CDN только Гугла отдает 54 (!!) версии jquery, да еще умножьте на 2 (протоколы поддерживаются и http, и https), да еще, наверное, у них есть и «просто», и «min»-версии. Таким CDN далеко не один, а минимум штуки 4-5 (я про самые популярные). Итого, если я зашел на один сайт, где с CDN грузилась jquery, а потом сразу на другой (где тоже jquery и тоже с CDN), то имеем вероятность примерно 1/500 — 1/1000, что библиотека jquery там и там — одна и та же, и на втором сайте не будет грузиться, т.к. загрузилась на первом. Это при условии, что кеш вообще объемен, и что-то хранит. Ну и да, теперь давайте подумаем, сколько в вашем кеше только под разные версии jqeury отведено места ))

                                    Скажем прямо: идея унификации хороша, но в реальном мире проще не городить огороды. Вы боретесь с лишним http-запросом? Попросите сначала Яндекс с Гулом в своих счетчиках посещений сайта не делать столько http-перенаправлений :) — пользы для людей будет больше!
                                      0

                                      Про старые браузеры оговорено отдельно — едва ли их смутит новый атрибут. Хотя вот за IE не ручаюсь...


                                      А если серьёзно, то спасибо, Вы меня окончательно убедили в том, что необходимо серьёзное исследование. С цифрами и фактами.


                                      Понимаете, если у меня из-за нестабильного мобильного интернета отвалится счётчик Яндекса или Гугла — я переживать не буду, даже вряд ли замечу. А вот если jQuery — скорее всего, не смогу нормально пользоваться сайтом.


                                      Сейчас унификация малоактуальна — нет контентной адресации. Вы совершенно верно указали на зоопарк CDN'ов. Теперь представим, что зачатки контентной адресации в виде предлагаемого хэш-кэша внедрены и начинают использоваться на некоторых сайтах. И вот в один прекрасный момент производитель некоторого мобильного браузера публикует список ресурсов (основных библиотек и шрифтов, например), которые будут захардкожены в кэш, основываясь на статистических данных. Естественно, что сайты, использующие именно эти библиотеки/шрифты, будут работать быстрее (в данном браузере), что создаёт стимул к унификации. Например, к переходу на несколько более новую (и распространённую) версию jQuery.

                                        0

                                        А также напрочь останавливает какой-либо прогресс.

                                          0

                                          Отнюдь. Вышла новая jQuery — прилетело обновление браузеру. В чём проблема?

                                            0

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

                                              0

                                              Не сломаются. Хэш (в том числе ради обратной совместимости) страхуется классическим урлом субресурса.

                                                0

                                                А это тут при чём?

                                                  0

                                                  Процитирую статью:


                                                  <link href="//habracdn.net/habr/styles/1468852450/_build/topic_form.css" rel="stylesheet" media="all" checksum="ef4547a3d5731e77f5a1a19e0a6d89915a56cd3a"/>

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


                                                  Или, в нашем случае,


                                                  <script src="//somecdn.net/libs/jquery-9.9.9-max.js" checksum="004547a3d5731e77f5a1a19e0a6d89915a56cd3a"/>

                                                  (контрольная сумма — рандом).


                                                  Если у браузера нет в кэше файла с хэшем 004547a3d5731e77f5a1a19e0a6d89915a56cd3a, то браузер честно идёт по указанному урлу, выкачивает оттуда файл и кэширует его. Если файл в кэше есть, он берётся оттуда.


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

                                                    0

                                                    Преференции для определённых версий определённых библиотек приводят либо к замедлению прогресса, либо к бесполезности предзагрузки.

                                    0

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


                                    А фишка-то именно в разделении.


                                    Кстати, а что Вы имеете против GET-параметра?

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

                                  Самое читаемое