Pull to refresh
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Как мы сэкономили 2000 USD на трафике из Amazon S3 с помощью nginx-кэша

Reading time6 min
Views18K

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

Проблема и как её решать

Имеются следующие вводные:

  • Объем статики — около 1 ТБ;

  • Исходящий трафик — около 15 ТБ в месяц;

  • Текущий ежемесячный счет — около 2000 USD.

Как известно, в случае с S3 главная статья расходов — это исходящий трафик, а не сам хранимый объем. Соответственно, задача состоит в том, чтобы перевести этот трафик в более дешевое место.

Что с этим можно сделать?

  1. воспользоваться услугами различных CDN-провайдеров;

  2. реализовать собственный кэширующий прокси.

От CDN’ов отказались по двум причинам:

  • В CDN как таковом не было потребности: у проекта единственный целевой регион пользователей. К тому же, точек присутствия известных CDN’ов в данном регионе не было (и даже близко).

  • Стоимость. Все популярные решения выходили в среднем от 300 USD до тысяч у крупных провайдеров.

Примерная стоимость услуг популярных CDN для нашего случая получалась такая:

  • Selectel — 250 USD;

  • Akamai — 350 USD;

  • Amazon CloudFront — 1000+ USD;

  • Google CDN — 1000+ USD.

По этим причинам оптимальным выходом виделся self-hosted кэширующий прокси, т.к. он целиком удовлетворяет потребности с многократным выигрышем в стоимости решения.

Реализация

Архитектура

Основная проблема, которая возникала при локальном кэширующим прокси, — отказоустойчивость. Так как данных много и отдавать их надо быстро, под хранение требуется сервер на быстрых NVMe-дисках с достаточным объемом. Главная забота в таком случае — потеря самого сервера: ведь тогда мы полностью теряем статику, что никуда не годится.

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

Итоговая схема выглядит так:

Детали

Весь трафик мы получаем в единую точку входа: nginx-балансировщики. После этого запросы уходят в Kubernetes и попадают по назначению в nginx на выделенном сервере-хранилище. Если по какой-то причине существуют проблемы в получении ответа от нашего прокси, включается backup upstream — в его роли origin.

Могут появиться резонные вопросы: «Зачем делать этот прокси узлом Kubernetes? Это же не приносит никакой пользы в данной схеме!». Всё довольно просто: мы сторонники подхода IaC и для нас есть вполне ощутимая польза: унификация конфига и управления новой сущностью. В случае, когда это отдельный хост (живущий сам по себе), им надо как-то управлять, ставить туда nginx, конфигурировать его и т.д. А потом забудут о нём и о том, из какого репозитория он управляется (если он вообще появится)… Kubernetes в наших реалиях автоматически решает все эти боли и не приносит проблем или накладных расходов, а управляется IaC-манифестами из Git-репозитория.

Как выглядят конфиги для этой схемы?

Вот что в Nginx LB:

##################

server {
    listen 80;
    server_name MAIN.IMG.DOMAIN;

    location / {
        proxy_set_header Host $http_host;
        proxy_pass http://s3-upstream;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    }
}

##################

upstream s3-upstream {
    server k8s-ingress-ip-1:30080 max_fails=1 fail_timeout=5s weight=100;
    server k8s-ingress-ip-2:30080 max_fails=1 fail_timeout=5s weight=100;
    server k8s-ingress-ip-3:30080 max_fails=1 fail_timeout=5s weight=100;

    server 127.0.0.1:8888 backup;
}

###################

server {
    listen 127.0.0.1:8888 default_server;
    server_name DOMAIN.NAME;

    location / {
        resolver 8.8.8.8 valid=10s;
        set $bucket "BUCKET_NAME.s3.eu-central-1.amazonaws.com";
        proxy_set_header Host $bucket;
        proxy_pass http://$bucket;
    }
}

Здесь есть пара очень важных моментов, на которые стоит обратить внимание:

  1. Конструкция из нескольких server'ов. В чем её смысл? Если в качестве backup upstream указать напрямую адрес бакета, то рано или поздно мы столкнёмся с проблемой того, что адрес будет преобразован в IP во время загрузки конфигурации nginx и закэширован. Но IP в URL'е бакета — динамический, а следовательно, nginx ничего не узнает о том, когда адрес изменится, поэтому в момент переключения на backup upstream начнет отправлять трафик на неправильный IP.

    Чтобы избежать этой фатальной ошибки, приходится делать промежуточный internal server_name, в котором указываем resolver и время жизни полученного IP. Также стоит сделать его default_server, чтобы не получить проблем с Host-заголовком, что пробрасывается из origin'а.

  2. Директива proxy_next_upstream. Если вы её не укажете, переключение на backup upstream будет происходить почти наверняка не так, как вы того хотите.

Далее конфиг самого прокси-кэша (точнее, только значимые его части):

proxy_cache_path /var/cache/nginx/s3-cache levels=1:2 keys_zone=s3-cache:1280m max_size=1050g inactive=43200m;

location / {
    add_header 'Access-Control-Allow-Origin' '*';
    proxy_cache s3-cache;
    proxy_cache_key $scheme$request_method$host$request_uri;
    proxy_cache_valid  200 301 302 60d;
    proxy_cache_valid  403 404     1m;
     proxy_cache_revalidate on;
     proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_403 http_404 http_429;
     proxy_cache_background_update on;
     proxy_cache_lock on;

    expires 60d;

    proxy_http_version     1.1;
    proxy_hide_header      x-amz-id-2;
    proxy_hide_header      x-amz-request-id;
    proxy_hide_header      x-amz-version-id;
    proxy_ignore_headers   "Set-Cookie" "Expires" "Cache-Control";

    proxy_pass http://BUCKET_NAME.s3.eu-central-1.amazonaws.com/;
}

Тут всё должно быть понятно.

NB: Кстати, существует более сложный и неочевидный вариант конфигурации nginx при использовании приватного S3-бакета — мы его уже описывали в другой статье.

Ingress-ресурс не прилагаю, т.к. он выглядит совершенно буднично.

Результат

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

Получается, что вся связка стоила нам лишь добавления одного сервера стоимостью 60 евро и бесплатного трафика, который отдается с него, в текущих объемах. При этом есть ещё двукратный запас по росту трафика до того момента, когда он может начать оплачиваться. А про прежний счёт в 2000 USD можно забыть благодаря тому, что с разовым «прогревом» кэша S3-трафик пропадает (а стоимость хранения данных в S3 минимальна).

Выводы

Что мы поняли после эксплуатации подобного решения на протяжении некоторого времени? Эта реализация имеет простую схему отказоустойчивости и подходит даже для больших объемов и нагрузок. Однако надо понимать ее потенциальные недостатки и что стоит учесть перед боевым внедрением.

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

Второй момент — объем трафика. Учитывайте особенности тарификации своего провайдера. Поскольку точка раздачи по сути единственная, объемы могут быть значительными и «неожиданно» могут привести к немалой трате средств. Тогда уж лучше задуматься про подключение CDN.

В-третьих, отказоустойчивость. Вам почти наверняка не удастся воспроизвести отказоустойчивость, которую сможет предложить популярный CDN. Если вы сейчас работаете в рамках одного ЦОДа, то точек отказа — великое множество. Даже если вы продумаете хорошую схему с механизмом failover’а входных балансировщиков, в машинном зале ЦОДа может отказать БП, а трактор перед ЦОДом обязательно перекопает магистральный канал… Очевидно, что ради отказоустойчивости одной только раздачи статики нет смысла городить межЦОДовую архитектуру — тогда тоже проще подключить CDN. Однако если у вас уже такая инфраструктура, почему бы и нет?.. (И опять же, не забудьте про регион доставки контента.)

Такие потенциальные проблемы, их следствия и пересечения разнятся от ситуации к ситуации, поэтому критично учитывать узкие места архитектуры в целом и, конечно, понимать, какую цель вы хотите достичь и технически, и для бизнеса.

P.S.

Читайте также в нашем блоге:

Tags:
Hubs:
Total votes 45: ↑43 and ↓2+53
Comments27

Articles

Information

Website
flant.ru
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Александр Лукьянов