CloudFlare + nginx, или экономим при помощи «кофеварки» (upd2: сверкороткий кеш динамики!)

  • Tutorial

Добрый пятница, уважаемый %username%, жадный читатель и борец за справедливость в интернетах!

Все мы помним (гугл точно помнит!), что была такая статья CloudFlare + nginx = кешируем всё на бесплатном плане. В которой рассматривались основные принципы экономии на тарифах и серверах, путем всеядного кеширование на стороне CloudFlare файлов до 512Мб.

В данном материале мы поиграем с кодами ответов нашего сервера, чтобы съекономить еще больше золота чтобы построить зиккурат и не переходить на «enterprise plan» которые нам «offer» похожий результат в своих «offers».

На бесплатном тарифе TTL составляет 2 часа. Это то, что у них написано в бесплатном тарифном плане. Если же копнуть глубже, то получаем:

  • 200 301 120m;
  • 302 303 20m;
  • 403 1m;
  • 404 5m;
  • any 0s;

Более развернуто: редиректы кешируются по 20 минут, ошибки авторизации на минуту и классические 404 по 5 минут. Прочие коды не кешируются.

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

К примеру:

  • Простой сервис, который дергает EXIF из фоток, которые были залиты. Пример ссылки http://example.com/api/image/exif/1.txt — что означает выдать EXIF как TXT из фотки с id=1.

Предположим, что у вас там 1336 фоток. Сие значит, что 1, 2..1336ая выдатут 200й ответ, который будет закеширован на стороне cloudflare.

Решение #2, без костыля с 418 кодом (следствие из этого коммента).


Решение #1, костыль через 418 «i'm teapot» код.



Вот наша эмуляция (nginx):

location ~* "^/api/image/exif/(.*?)\.txt$" {
	default_type text/plain;
	set $testimageid $1;
	##backend
	if ($testimageid ~ 1337){
		return 404 "Image $testimageid not found";
	}
	##backend
	return 200 "EXIF for $testimageid image";
}



И так далее…



Пока не дойдем до фото с айди 1337, которого еще на сервере нет. Что сделает наша динамика? Логично, что она выдаст некий текст про ошибку и код 404.


Который следом будет закеширован. И все последующие запросы к этому URL выдадут ответ из кеша CF на следующие 5 минут.

Критично ли это? И да и нет. Зависит того, сколько юзеров вы сделаете несчастными и сколько съекономленных на CDN (благодаря мне) миллионов вы потеряете.

Что же делать? Смотрим оффициальный материал и понимаем, что надо менять код… код ответа сервера в случае 404.

Но не все так просто. Нельзя_просто_так_взять_и_сменить_код_ответа.jpg, так как каждый код несет какую-то смысловую нагрузку, которая будет интерпретирована или на нее просто забьют!

Вот для этого и был придуман код, который никакой смысловой нагрузки не несет, это код 418, который описан в rfc2324.

Меняем код нашей эмуляции (и очищаем кеш для этой ссылки в панели CF):

location ~* "^/api/image/exif/(.*?)\.txt$" {
	default_type text/plain;
	set $testimageid $1;
	##backend
	if ($testimageid ~ 1337){
		return 418 "Image $testimageid not found";
	}
	##backend
	return 200 "EXIF for $testimageid image";
}

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



Сие знаменует факт, что до тех пор, пока картинки нет, CF не кеширует ответ от нашего сервера, передавая клиентам каждый раз свежий ответ с ошибкой 418 [от нашего сервера]. Следом, как картинка появляется на сервере, и наша динамика отдает ответ 200, CF кеширует этот ответ на 2 часа.

Код 418 относится к 4хх группе кодов. И даже, если у конечного пользователя нет определения этого кода в браузере (или в самописном софте), то там есть как минимум (должна быть!) проверка на 4хх как на скриншотах этой статью (firefox, красненький квадратик).

Вот так вот просто можно можно сообщить об ошибке (4хх) и попросить cloudflare не кешировать данный ответ при политике, когда мы все кешируем. Сие уберегает от ложных 404 ответов от сервера, когда кто-то запрашивал будущий материал до момента публикации.

upd2: Решение #2, без костыля с 418 кодом. Следствие из этого коммента.


В начале задаем условие для Browser Cache Expiration:


Затем создаем вот такое Page Rule:



Главный момент: отсутствие Browser Cache TTL и наличие Cache Level: Cache Everything!

Затем, если у вас TTL для валидных ответов составляет 315360000, то надо добавить
add_header Cache-Control "public, max-age=315360000";

в location'ы, без добавление оного в server!
например
location / {
	add_header Cache-Control "public, max-age=315360000";
}


Location с динамикой имеет вид:
location ~* "^/api/image/jexif/(.*?)\.txt$" {
	##backend
	proxy_pass http://localhost:8080/headerstest/headerstest.jsp?imgid=$1;
	proxy_set_header Host $host;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	##backend
}

Следом заставляет нашу динамику отдавать ошибки с кодом 404 вот с таким хедером (на примере JSP)
response.setHeader("Cache-Control", "private, max-age=0");

В итоге Cloudflare на указанных выше настройках не кеширует ошибки, но и это еще не всё!

Если вам нужны частые обновления кеша для 200 ответов вашей динамики, то вам надо указать (снова на примере JSP)
response.setHeader("Cache-Control", "public, max-age=20");

И тем самым вы получаете 20 секундный кеш ответов вашей динамики! Что меньше чем на тарифе ентерпрайз!

Фактически, пока такое работает, можно полностью убрать сайт на cloudflare и скрыть IP сервера (при условии что записи, отличные от A/AAAA/CNAME/TXT расположены на других ip).

Да прибудет с вами сила!

З.Ы, set $testimageid $1; используется потому, что nginx не может сравнивать $1 в if выражении (но может оперировать $1 в конфиге или в контенте), для этого надо вводить переменную, но YourChief показал более красивое решение.. Для второго решения надо убрать
add_header из server и оставить в location, чтобы первый не «клеился» nginx ко второму (вот такое чудо в ответе «public, max-age=20, public, max-age=315360000»).
Щербатые котлы картинки выравнены между собой и сжаты в png-8, перфекционисты на диалапе могут улыбнуться!
  • +21
  • 9,6k
  • 7
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 7
  • 0
    Не знаю насколько отдача заголовка 418 заслуживает описания на целую статью, возможно новичкам будет полезно, но есть одно «но» — данную проблему можно решить еще проще, переключив тип кеширования прям в интерфейсе cloudflare, если конечно не используется «уж очень разный» подход к кешированию (что бывает достаточно редко):
    • 0
      В статье ссылка на другую статью, которая относится к нестандартному кешированию.
      Сей подход реализован через Page Rules: Cache level->cache everything.

      Статья не о 418, а о том, как решить конкретную проблему (=побочный эффект от такого кеширования) через 418 код.
    • +1
      А еще можно поиграться с этим:
      • 0
        В общем, рабочий вариант:
        image
        +
        image
        Если брать тесты, то:

        add_header Cache-Control "public, max-age=315360000";
        В директиве сервер, следом
        эмулятор
        	location ~* "^/api/image/exif/(.*?)\.txt$" {
        		default_type text/plain;
        		set $testimageid $1;
        		##backend
        		if ($testimageid ~ 1337){
        			add_header Cache-Control "private, max-age=0" always;
        			return 404 "Image $testimageid not found";
        		}
        		return 200 "EXIF for $testimageid image";
        		##backend
        	}
        

        Основной момент, это always в add_header Cache-Control "private, max-age=0" always;, так как nginx по умолчанию не хедеры с 4хх ответами.

        и реальный бекенд
        	location ~* "^/api/image/jexif/(.*?)\.txt$" {
        		##backend
        		proxy_pass http://localhost:8080/headerstest/headerstest.jsp?imgid=$1;
        		proxy_set_header Host $host;
        		proxy_set_header X-Real-IP $remote_addr;
        		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        		##backend
        	}
        


        Вот код JSP:
        <%@ page contentType="text/plain;charset=UTF-8" language="java" trimDirectiveWhitespaces="true" session="false" %>
        <%
            String imgId = request.getParameter("imgid");
            if (imgId != null){
                if(imgId.equalsIgnoreCase("1337")) {
                    response.setStatus(404);
                    response.setHeader("Cache-Control", "private, max-age=0");
                    out.print("file not found");
                }else{
                    out.print("Exif for image "+imgId);
                }
            }
        %>
        


        Что в итоге?

        В случае Cloudflare статья стала реальным костылем, пока лавочку не прикрыли. Так как если убрать add_header Cache-Control "public, max-age=315360000"; из директивы server, и оставить только в location:
        например
        	location / {
        		add_header Cache-Control "public, max-age=315360000";
        	}
        

        И затем добавить к нашей динамике (в случае с JSP)
        response.setHeader("Cache-Control", "public, max-age=20");
        

        То мы получаем 20 секундный кеш ответов с динамики, вообще любой. Только что протестировал, и при такой раскладке бесплатно имеем главное преимущество более дорогих планов (30сек TTL и ниже только на ентерпрайзе).

        Спасибо за наводку, я добавлю в статью.
      • +1
        От директивы set можно красиво избавиться — достаточно использовать в location именованные выделения:
        location ~* "^/api/image/exif/(?P<testimageid>.*?)\.txt$" {
        	default_type text/plain;
        	##backend
        	if ($testimageid ~ 1337){
        		return 418 "Image $testimageid not found";
        	}
        	##backend
        	return 200 "EXIF for $testimageid image";
        }
        

        Тогда сразу по виду регулярки понятно, что куда идёт.
      • 0
        Добавил в статью решение без костыля.

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