Спидтест показывает 900 Мбит, а бэкап на удалённое хранилище льётся сутки
Спидтест на сервере честно показывает 900 Мбит/с, а ночной бэкап в облачное хранилище всё равно не укладывается в отведённое окно и расползается на сутки. Разница в 900 Мбит/с канала против фактических единиц мегабит на приёмке — это не обман провайдера и не испорченный канал, а следствие того, что сам процесс заливки бэкапа устроен так, что просто не даёт этой полосе раскрыться. Ниже — какие причины специфичны именно для загрузки бэкапа в объектное или облачное хранилище, и что с этим реально можно сделать без апгрейда тарифа.
Содержание
- Почему спидтест не имеет отношения к вашему бэкапу
- Тысячи мелких файлов: цена одного запроса на объект
- Один поток вместо параллельной заливки
- Лимиты самого хранилища: rate limiting на стороне провайдера
- CPU в потолке: шифрование и сжатие «на лету» на слабом сервере
- Что делать: батчинг, параллельность и правильный инструмент
Почему спидтест не имеет отношения к вашему бэкапу
Стоит сразу развести два вопроса: «какая пропускная способность у канала» и «почему бэкап заливается медленно». Спидтест (Speedtest, iperf3 или встроенный тест панели хостера) отвечает на первый — он гонит синтетический поток, обычно в несколько параллельных TCP-соединений под максимальную скорость, и показывает, на что физически способен канал между двумя точками. Чем именно такой тест отличается от одиночной HTTP-загрузки одного файла — разница в числе потоков, TCP-окне, диске и лимитах веб-сервера — подробно разобрано в статье про то, чем iperf3 отличается от реальной загрузки файла. Здесь речь о другом сценарии: не об одной большой закачке одним потоком, а о бэкапе — процессе со своей архитектурой, обычно совсем не похожей на честный тест одного файла.
Бэкап на удалённое хранилище (S3-совместимый бакет, облачный backup-сервис, объектное хранилище другого провайдера) почти никогда не выглядит как «взять один большой файл и залить его по одному соединению». Это либо множество отдельных объектов (конфиги, вложения, снапшоты БД, логи), либо архив, заливаемый инструментом, который сам решает, сколько потоков открывать, как часто обращаться к API хранилища и сколько CPU тратить на подготовку данных. Именно в этих решениях — не в канале — чаще всего и теряется полоса, которую честно показал спидтест. Дальше — четыре причины, характерные конкретно для заливки бэкапа в удалённое хранилище.
Тысячи мелких файлов: цена одного запроса на объект
Если бэкап — это не один архив, а прямая заливка каталога с тысячами (а тем более миллионами) мелких файлов конфигов, вложений или логов, каждый файл улетает в объектное хранилище отдельным HTTP-запросом (PUT на S3-совместимом API). У каждого такого запроса есть накладные расходы, не зависящие от размера файла: установление соединения (если оно не переиспользуется), TLS-рукопожатие, обмен заголовками, ожидание ответа. На файле в несколько мегабайт это почти незаметно на фоне времени передачи. На файле в несколько килобайт — а среди «мелкого мусора» бэкапа именно такие преобладают — накладные расходы могут занимать больше времени, чем сама передача, и объявленная спидтестом полоса просто нечем заполнить: канал стоит без дела между запросами.
Ключевой фактор — переиспользуются ли соединения между последовательными запросами к хосту хранилища. Если клиент (или самописный скрипт на curl/aws s3 cp в цикле) открывает новое TCP+TLS-соединение на каждый файл вместо того, чтобы держать одно открытое — цена лишнего round-trip умножается на число файлов. На сотне файлов это не заметно. На ста тысячах — это уже часы, потраченные не на передачу данных, а на рукопожатия. Подробный разбор того, сколько стоит новое соединение и как проверить его переиспользование (через DevTools Connection ID или curl -v с Re-using existing connection) — в статье про keep-alive и цену нового соединения; тот же принцип применим к запросам, которые backup-клиент шлёт к API хранилища.
Проверить это на своём бэкапе просто — посчитать, сколько объектов реально уходит за прогон:
find /data/backup-source -type f | wc -l
find /data/backup-source -type f -printf '%s\n' | \
awk '{sum+=$1; n++} END {print sum/n, "байт в среднем на файл,", n, "файлов всего"}'
Если файлов десятки тысяч, а средний размер — единицы-десятки килобайт, дело почти наверняка не в канале, а в количестве отдельных запросов. Что с этим делать — в разделе про практические меры ниже.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверОдин поток вместо параллельной заливки
Вторая частая причина — backup-скрипт (особенно самописный, на bash с циклом по файлам или на однопоточном вызове aws s3 cp/curl без флагов параллелизма) заливает данные строго последовательно: файл за файлом, один запрос за другим, ни разу не открывая второе соединение одновременно с первым. Спидтест при этом честно замерял суммарную ёмкость канала через несколько параллельных потоков — а backup-скрипт использует ровно один.
У одного TCP-соединения есть собственный потолок пропускной способности, зависящий от размера TCP-окна и произведения полосы на задержку (RTT) до хранилища. На хранилище в той же локации, где сервер, задержка небольшая и потолок одного потока обычно не заметен. А вот если бэкап улетает в хранилище другого региона или континента — тот же принцип, что ограничивает единичный поток scp или rsync на длинном канале, ограничивает и заливку бэкапа: окно успевает «провернуться» за секунду не так много раз, и один поток физически не выбирает всю полосу, даже если канал способен на большее. Разбор этого эффекта и bandwidth-delay product — в статье про TCP-окно и длинный толстый канал.
Практическая проверка — сравнить один поток и несколько параллельных на реальном хранилище, если инструмент это позволяет. Для rclone это делается флагом --transfers:
# один поток — потолок одного TCP-соединения до хранилища
rclone copy /data/backup-source remote:bucket/backup/ --transfers 1 --progress
# несколько параллельных потоков — суммарная ёмкость канала
rclone copy /data/backup-source remote:bucket/backup/ --transfers 16 --progress
Если второй прогон кратно быстрее первого — вы упирались не в канал и не в хранилище, а именно в однопоточность самого способа заливки. Это самая дешёвая и часто самая результативная правка: не апгрейд тарифа, а замена самописного цикла на инструмент, который умеет параллелить запросы из коробки.
Лимиты самого хранилища: rate limiting на стороне провайдера
Даже если ваш канал свободен и заливка идёт в несколько параллельных потоков, у самого объектного хранилища почти всегда есть собственные ограничения — на число запросов в секунду на аккаунт или на префикс ключей, на суммарную полосу для одного клиента, иногда отдельно на PUT и на GET. Это ограничения инфраструктуры провайдера хранилища, и они действуют независимо от того, какая скорость указана в вашем тарифе на сервер.
Внешне это проявляется как ответы 429 Too Many Requests или 503 Slow Down от API хранилища, иногда с заголовком Retry-After, а иногда без него — тогда клиент сам решает, сколько ждать перед повтором. Хорошие backup-клиенты (rclone, restic, официальные SDK) умеют распознавать такие ответы и делать экспоненциальный backoff — повторять запрос с растущей паузой вместо того, чтобы долбить хранилище на полной скорости и получать всё больше отказов. Самописный скрипт на голых HTTP-запросах часто этого не делает вообще: либо падает на первой же ошибке, либо зависает в цикле мгновенных повторов — со стороны это выглядит как «бэкап висит», хотя на самом деле идёт бесполезная борьба с лимитом.
Точные цифры лимитов у каждого провайдера свои и часто зависят от тарифного плана — их нужно смотреть в документации конкретного сервиса, а не переносить с одного хранилища на другое. Универсальный практический шаг — не гнать параллелизм на максимум «чтобы было быстрее», а сознательно ограничить частоту запросов и посмотреть, исчезают ли ошибки 429/503 в логах клиента:
rclone sync /data/backup-source remote:bucket/backup/ \
--transfers 16 --checkers 8 \
--tpslimit 10 --tpslimit-burst 20
--tpslimit — потолок запросов в секунду, который клиент сам себе назначает, --tpslimit-burst — сколько запросов можно отправить одним всплеском сверх потолка. Смысл не в том, чтобы угадать точное число (оно зависит от конкретного хранилища и тарифа), а в том, чтобы иметь управляемый параметр и подбирать его по факту — по логам с ошибками 429 и фактической скорости заливки.
CPU в потолке: шифрование и сжатие «на лету» на слабом сервере
Четвёртая причина не имеет отношения ни к сети, ни к хранилищу — она в самом сервере, откуда бэкап уходит. Если backup-инструмент шифрует и/или сжимает данные «на лету» перед отправкой (что почти всегда правильно с точки зрения безопасности и объёма трафика), эта работа требует CPU, а не сетевой полосы. На сервере с одним-двумя vCPU, где ресурсы и так поделены между приложением и бэкапом, однопоточное сжатие или шифрование легко становится узким местом — сетевой канал и хранилище при этом простаивают заметно ниже своих возможностей, просто потому что процессор не успевает готовить для них данные.
Быстрая проверка — посмотреть на загрузку CPU по ядрам во время реальной заливки, а не на усреднённую цифру:
mpstat -P ALL 2
Если одно ядро стабильно на 100% в %usr, а остальные простаивают — это почти всегда однопоточный компрессор или шифратор (gzip, xz -9, openssl enc без явного распараллеливания), упирающийся в одно ядро вне зависимости от того, сколько их на сервере. Полная методика разделения бэкапа на четыре звена (диск-источник, CPU, сеть, приёмник) и диагностика каждого через iostat, mpstat и pv в конвейере — подробно разобрана в статье про предел скорости бэкапа: диск, шифрование или канал; здесь же важен частный случай для заливки в облако — CPU-потолок легко спутать с сетевым, оба выглядят одинаково: «бэкап течёт медленно, хотя канал вроде свободен».
Практическое решение — заменить однопоточный инструмент на многопоточный:
tar cf - /data | zstd -T0 -o /tmp/backup.tar.zst
lscpu | grep -i aes
Если aes в выводе lscpu есть, аппаратное шифрование AES-GCM на современном CPU обходится дёшево — и виновник CPU-упора почти всегда сжатие, а не шифрование, особенно на высоких уровнях компрессии.
Что делать: батчинг, параллельность и правильный инструмент
Все четыре причины выше сходятся к одному выводу: способ, которым бэкап заливается в удалённое хранилище, определяет фактическую скорость сильнее, чем номинальная полоса канала. Три меры покрывают большинство случаев.
Батчить мелкие файлы в архивы вместо поштучной заливки. Если бэкап — каталог с тысячами мелких файлов и пофайловая инкрементальность не критична, проще собрать их в один архив локально и залить одним объектом:
tar cf - /var/www /etc | zstd -T0 -19 > /tmp/backup-$(date +%F).tar.zst
rclone copy /tmp/backup-$(date +%F).tar.zst remote:bucket/daily/ --progress
Это убирает проблему накладных расходов на запрос — их теперь не тысячи, а один. Плата — потеря пофайловой инкрементальности: следующий бэкап снова заливает архив целиком, если инструмент не умеет дельту поверх архивов. Для каталогов, где большая часть файлов между прогонами не меняется, это может оказаться дороже по трафику, чем пофайловая синхронизация — стоит сопоставить оба варианта на своих данных.
Использовать инструменты с параллельной загрузкой из коробки, а не самописные циклы. rclone (--transfers, --checkers), restic и borg устроены так, чтобы использовать несколько потоков и разумно группировать данные без ручного батчинга. У restic и borg есть дополнительное преимущество для множества мелких файлов: они не заливают каждый файл отдельным объектом, а упаковывают данные в более крупные pack-файлы, дедуплицируя повторяющиеся блоки — решают проблему мелких файлов на уровне архитектуры, а не требуют ручного батчинга поверх.
| Подход | Параллельность | Пофайловая инкрементальность | Когда оправдан |
|---|---|---|---|
Цикл for file in ...; do aws s3 cp ...; done | Нет | Да, ценой лишних запросов | Практически никогда при заметном числе файлов |
tar/zstd в один архив, заливка одним объектом | Не требуется | Теряется полностью | Разовый бэкап или каталог, меняющийся целиком |
rclone sync/copy --transfers N --checkers M | Да, настраиваемая | Сохраняется | Регулярный бэкап с нужной структурой на приёмнике |
restic/borg (chunking, pack-файлы, дедупликация) | Да, встроенная | Инкрементально на уровне блоков | Регулярный бэкап с историей версий и экономией места |
Проверять документацию хранилища на лимиты API до того, как гнать параллелизм на максимум. Каждое дополнительное соединение и запрос в секунду приближает вас к лимиту провайдера — оптимальная точка обычно не «максимум потоков», а разумный параллелизм, подобранный по факту отсутствия ошибок 429/503 в логах.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро понять причину, не перебирая все четыре по очереди?
Начните с подсчёта файлов и среднего размера (find ... | wc -l) — если файлов десятки тысяч, а средний размер мал, почти наверняка накладные расходы на запросы. Параллельно посмотрите mpstat -P ALL 2 во время заливки — если одно ядро на 100%, а сеть недогружена, дело в CPU. Эти две проверки за пару минут отсекают большую часть версий.
Батчинг файлов в архив всегда быстрее пофайловой заливки?
Не всегда. Для набора файлов, который между бэкапами почти не меняется, пофайловая инкрементальная синхронизация экономит трафик. Батчинг в один архив каждый раз заливает всё целиком, если инструмент не умеет дельту поверх архивов. Выбор зависит от того, как часто и насколько сильно меняются данные.
Стоит ли увеличивать --transfers до предела, который выдержит канал?
Нет, если у хранилища есть собственный лимит запросов в секунду — избыточный параллелизм увеличит долю запросов с 429/503, а не ускорит заливку. Подбирайте значение по факту отсутствия ошибок в логах, свериваясь с документацией хранилища.
Restic или borg сами решают проблему мелких файлов, или нужен батчинг вручную?
В большинстве случаев решают сами — упаковывают данные в более крупные объекты (pack-файлы) на уровне архитектуры, а не заливают каждый исходный файл отдельным объектом. Ручной батчинг через tar актуален для простых инструментов вроде голого rclone или самописных скриптов без встроенной упаковки.
Шифрование бэкапа перед заливкой действительно может быть узким местом само по себе?
Да, если оно однопоточное и сервер слабый по CPU — но на железе с AES-NI шифрование само по себе дёшево, а тормозит обычно сопутствующее сжатие на высоком уровне компрессии однопоточным инструментом. Проверяется через mpstat во время реальной заливки.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →