Создание микросервисов — страница 63 из 69

Кроме инструкций cache-control и использования Expires-заголовков, в нашем арсенале полезных свойств HTTP есть еще один вариант — метки объектов (Entity Tags, или ETags). ETag используется для определения того, изменилось значение ресурса или нет. Если я обновляю запись клиента, URI ресурса остается прежним, но значение становится другим, поэтому я буду ожидать изменения ETag. Эффективность этого приема проявляется при использовании условных GET-запросов. При отправке GET-запроса можно указать дополнительные заголовки, сообщая сервису о необходимости отправки нам ресурса только в том случае, если он отвечает ряду критериев.

Представим, к примеру, что мы извлекаем клиентскую запись и ее ETag возвращается в виде o5t6fkd2sa. Чуть позже, возможно, из-за того, что инструкция по управления кэшированием данных предписывает нам, что ресурс должен рассматриваться как несвежий, мы хотим убедиться в том, что получаем самую свежую версию. При выдаче последующего GET-запроса мы можем передать условие об извлечении данных в случае несовпадения ETag — If-None-Match: o5t6fkd2sa. Тем самым серверу сообщается, что нам нужен ресурс по указанному URI, если у него уже имеется ETag, значение которого не соответствует указанному. Если уже есть новейшая версия, сервис отправляет нам ответ 304 Not Modified, обозначающий, что мы уже располагаем самой последней версией. Если доступна более свежая версия, мы получаем ответ 200 OK с изменившимся ресурсом и новой меткой ETag для него.

Тот факт, что эти средства управления встроены в столь широко применяемую спецификацию, означает, что мы можем воспользоваться преимуществом целого арсенала уже существующих программных средств, управляющих кэшированием. Такие обратные прокси-серверы, как Squid или Varnish, могут незаметно размещаться в сети между клиентом и сервером и сохраняя кэшируемое содержимое, и устанавливая для него срок истечения годности. Эти системы предназначены для очень быстрого обслуживания огромного количества параллельных запросов и являются стандартным средством масштабирования общедоступных сайтов. Такие сети доставки контента (CDN), как имеющаяся в AWS сеть CloudFront или Akamai, могут обеспечить маршрутизацию запросов к средствам кэширования, расположенным ближе к осуществившему вызов клиенту, гарантируя тем самым, что трафик, когда он понадобится, не проделает половину кругосветного путешествия. И что еще прозаичнее, справиться за нас с этой работой смогут клиентские библиотеки и клиентские средства кэширования, относящиеся к технологии HTTP.

ETags, Expires-заголовки и cache-control могут перекрывать функции друг друга, и если не проявить осторожность и принять решение о применении всех этих средств, то можно столкнуться с получением весьма противоречивой информации! Получить более глубокое представление о достоинствах тех или иных средств можно, прочитав книгу REST In Practice (O’Reilly) или изучив раздел 13 спецификации HTTP 1.1, где описывается, как эти различные управляющие средства реализуются как на клиентской, так и на серверной стороне.

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

Кэширование, проводимое для операций записи

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

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

Кэширование в целях повышения отказоустойчивости

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

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

Скрытие источника

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

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

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

Рис. 11.7. Скрытие источника от клиента и заполнение кэша в асинхронном режиме

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

Не нужно ничего усложнять

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

Отравление кэша: предостережение

При кэшировании мы думаем, что при его неправильном применении самое худшее, что может быть, заключается в кратковременном использовании устаревших данных. А что, если вам в итоге придется всегда пользоваться только устаревшими данными? Ранее я уже упоминал о проекте, над которым работал, где мы использовали приложение со strangler-шаблоном, чтобы легче было перехватывать вызовы к нескольким устаревшим системам с прицелом на постепенный отказ от их применения. Наше приложение вполне эффективно работало в качестве прокси-сервера. Трафик направлялся через приложение к устаревшим приложениям. На обратном пути выполнялись несколько служебных действий. Например, мы убеждались в том, что к результатам, полученным от устаревших приложений, применялись надлежащие HTTP-кэш-заголовки.

Однажды, почти сразу после обычного планового выпуска, начали происходить весьма странные вещи. Была допущена ошибка, при которой к небольшому поднабору страниц в коде вставки кэш-заголовка применялось логическое условие, в результате чего заголовок вообще не изменялся. К сожалению, незадолго до этого изменили и само нижестоящее приложение и в него был вставлен HTTP-заголовок постоянной свежести Expires: Never. Ранее это не производило никакого эффекта, поскольку заголовок переписывался. А теперь никакой перезаписи не происходило.