Все заметки

Перенос статических файлов в Cloudflare R2: экономия на трафике

Ко мне обратился клиент со старым мобильным приложением. Он хотел сократить расходы на серверы: в зависимости от объёма трафика они составляли от $108 до $1000 в месяц.

У проекта было два сервера. Файловый стоил $96 в месяц и отдавал файлы мобильного приложения для Android и iOS. На втором работали сайт, API и админка.

В тарифный план его файлового сервера входило 10 ТБ трафика, но скачивания выходили за этот лимит, и к стоимости сервера добавлялась плата за превышение.

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

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

Почему я не мог просто поменять адрес

В приложении пользователи скачивали и слушали аудиозаписи. Они создавали основную нагрузку на файловый сервер. На диске лежало около 43.5 ГБ аудио, а за месяц сервер отдавал больше 10 ТБ. Проблема была в объёме скачиваний, а не в размере коллекции.

Файлы хранились на сервере в Лондоне. Скорость скачивания зависела от местоположения пользователя: все обращались к одному серверу.

В приложениях из Google Play и App Store был прописан технический хост провайдера, обозначу его как legacy.hosting.example. Это имя принадлежало Linode, и своей DNS-записи для него у проекта не было.

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

Для хранения я выбрал Cloudflare R2. У него нет платы за исходящий трафик; отдельно считаются хранение и операции с объектами. Схема получилась такой: приложение запрашивает файл по старому адресу, сервер отвечает редиректом, а само содержимое клиент скачивает с нового CDN-домена.

Так большие файлы переставали проходить через Linode, хотя для приложения всё начиналось с привычного URL.

Сначала я разобрался с клиентами

Прежде чем включать редирект, я проверил, что делают опубликованные клиенты.

На Android файлы скачивались через OkHttp. Он автоматически следовал редиректам, в том числе с HTTP на HTTPS. Проверил и обработку редиректов на iOS: файлы скачивались через NSURLSession. Отдельно нужно было учитывать новую iOS-версию со стримингом через AVPlayer и Range-запросами.

При разборе нашлось неприятное отличие: приложение на iOS не проверяло HTTP-статус перед сохранением скачанного файла. Если вместо аудио приходила страница ошибки, она могла сохраниться как mp3. Приложение считало бы файл скачанным, хотя воспроизвести его было бы нельзя.

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

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

Запросы Apache записывались в отдельный файл логов. По нему я следил за переключением клиентов.

Переносил по каталогам, начиная с одного файла

Для аудио я подключил к R2 домен cdn.example.com. Custom domain позволял использовать кеш Cloudflare перед бакетом. Соответствие старых и новых путей задавалось в конфиге веб-сервера.

Файлы заливал через rclone copy, затем запускал rclone check. Эта команда сравнивает размеры и доступные хэши файлов. Отдельно проверял HTTP-ответы CDN и размеры объектов.

Первое правило я включил только для одного аудиофайла. На Android проверил полный сценарий: приложение получило редирект, скачало файл и воспроизвело его. В серверном логе появился и запрос опубликованного iOS-клиента с ответом 302. Это была первая победа, но праздновать было рано.

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

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

Я использовал 302, чтобы обозначить временное перенаправление. Постоянный 301 может кешироваться клиентом и осложнить возврат к прежней схеме; обещать ему окончательный новый адрес на этапе выкатки я не хотел.

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

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

В результате Linode вместо содержимого mp3 отвечал коротким редиректом. Большой объём скачиваний ушёл с файлового сервера.

Теперь можно было уменьшить сервер, но сохранить его имя

После переноса аудио держать отдельную большую машину ради файлов больше не требовалось.

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

Но файловый сервер работал на Ubuntu 14.04 с Apache 2.4.7. Для сайта требовалось современное окружение. Я рассмотрел обновление системы на месте и запуск Docker на старом окружении, но выбрал rebuild того же Linode на Ubuntu 24.04. Это означало стирание диска, зато не требовало переносить старую систему через несколько поколений обновлений.

Перед rebuild оставался вопрос с именем legacy.hosting.example. Оно было связано с IP и легаси-rDNS провайдера. Сохранить просто «какой-нибудь сервер» было недостаточно: старые приложения продолжали зависеть от конкретного имени.

Я написал в саппорт Linode и спросил, сохранится ли старое имя сервера после переустановки системы и перехода на более дешёвый тариф. В первом ответе речь шла о проверке нового формата имён. Мне же было нужно подтверждение для старого формата служебных доменов.

После уточнения саппорт подтвердил, что rDNS следует за IP и сохраняется при rebuild и resize.

Переезд я начал с восстановления редиректов

Перед стиранием диска завершил сверку оригиналов с R2 без расхождений, сохранил конфиги и логи, осмотрел сервер на предмет других сервисов, баз и cron-задач.

Редиректы заранее перенёс в nginx-конфиг и проверил локально с разными значениями Host. После rebuild на Ubuntu 24.04 первым делом поднял временный nginx с этими правилами. Старые приложения не должны были ждать, пока я закончу перенос сайта.

Затем уменьшил диск до 24 ГБ и перевёл сервер на тариф с 2 ГБ памяти. Размер диска выбрал с запасом для следующего уменьшения: он помещался и в план с 1 ГБ RAM.

Сайт в это время продолжал работать на втором сервере. Я перенёс приложение и PostgreSQL, проверил новый сервер через curl --resolve, до переключения DNS. После этого направил домены сайта на новый адрес и проверил сайт, API, админку и старые файловые маршруты. Отдельно убедился, что техническое имя по-прежнему разрешается, а PTR-запись сохранилась.

После осмотра и архивирования оставшихся данных второй сервер удалил. В итоге сайт и совместимость со старыми приложениями остались на одной машине.

Чтобы хватало 1 ГБ памяти, я убрал сборку с сервера

Сборку приложения я перенёс в GitHub Actions. Благодаря этому серверу хватило 1 ГБ RAM вместо 2 ГБ, и я перевёл его на тариф за $5 в месяц.

Что получилось

Раньше клиент содержал два сервера и платил за них от $108 до $1000 в месяц в зависимости от трафика. Теперь сайт, API, админка и редиректы работают на одном сервере за $5 в месяц. Сейчас за R2 клиент платит ещё $0.53 в месяц.

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

В R2 Standard первые 10 млн операций чтения класса B в месяц бесплатны. Считаются обращения к самому хранилищу, а не скачивания из кеша CDN. По текущей нагрузке я оценивал запас до лимита примерно в сто раз, если соотношение скачиваний и обращений к R2 останется прежним.

Даже если бесплатный лимит будет превышен, каждый следующий начатый миллион операций чтения в Standard стоит $0.36. Это небольшая доплата по сравнению с прежними пиками расходов до $1000 в месяц.

Ещё до включения редиректов проверил скорость. В замере с одного Mac из США медианная скорость для большого фрагмента файла составила 8.57 МБ/с через CDN против 4.02 МБ/с со старого сервера. В этом тесте разница получилась около 2.1 раза.

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