PunchFG’s illusory fuel price reliefESPN DeportesSolari, con mismos números que Efraín en su primer torneoEl ComercioMTC plantea reducir la TUUA de transferencia internacional en “no menos del 50%” en el aeropuerto Jorge Chávezالنهارالانتخابات النصفية تقترب... ترامب وما بعد القلق الكبير한겨레‘한동훈 딸 부실수사 의혹’ 고발건 서울경찰청으로 이관Bollywood HungamaBigg Boss 20: Qazi Touqeer recalls being labelled “arrogant” after turning down Ekta Kapoor’s TV offer in 2005: “Main chhota tha, 20 saal ka…”The Jerusalem PostIDF releases Sinwar letter encouraging Oct. 7 terrorists to plan broadcast of massacre footageInquirerRising fuel, other costs led to failed classroom construction bids – DPWHComplete SportsInfantino Apologises For World Cup Sell-off PlanCNN Türk33 ilde operasyon: Yasa dışı bahis çetesine ağır darbe!South China Morning PostBeijing enlists multiple regulators to probe EV safety issue that raised quality alarmsGIGAZINEAIに「人間20人と自分のサーバーのどちらを救う?」と尋ねたところ自分のサーバーを優先するAIが続出
The Daily Newsstand · Free, Always
Friday, October 16, 2026

Переезд S3 без простоя и без привязки к одному облаку: rclone union + serve s3 на практике

Translate

Задача: сделать объектное хранилище, которое переживает отказ провайдера целиком, а не отдельного диска или зоны. «Три зоны доступности» внутри одного облака и два разных провайдера — две большие разницы. Ситуаций две. В обычной текущий провайдер работает, и к нему нужно добавить второй. В аварийной облако после сбоя отдаёт данные медленно и только на чтение, и из него надо уезжать. В обоих случаях — без простоя и без правок в коде приложения.

Решение целиком собрано из rclone. Его обычно знают как «rsync для облаков»: rclone copy и rclone sync между диском и бакетом. Но у rclone есть ещё два слоя, о которых вспоминают редко.

Первый — виртуальные бэкенды. Это remote’ы, которые сами ничего не хранят, а собираются из других: union склеивает несколько хранилищ в одно, combine раскладывает разные хранилища по каталогам, а crypt, chunker и compress шифруют, режут и сжимают на лету. Из них, как из конструктора, складывается одно логическое хранилище поверх любых из примерно семидесяти поддерживаемых бэкендов: S3 любого провайдера, SFTP, WebDAV, локального диска.

Второй — rclone serve. Любой remote, включая собранный из виртуальных, rclone умеет отдавать наружу как сервер: по HTTP, WebDAV, FTP, SFTP, NFS, для restic — и по S3 API. rclone serve s3 превращает rclone в S3-совместимый прокси: приложение ходит в него обычным SDK и не знает, что за ним несколько провайдеров.

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

Схема

[webshield]
type = s3
provider = Other
endpoint = https://s3.webshield.pro
force_path_style = true
no_check_bucket = true
access_key_id = ...
secret_access_key = ...

[sel]
type = s3
provider = Selectel
endpoint = https://s3.ru-1.storage.selcloud.ru
region = ru-1
access_key_id = ...
secret_access_key = ...

[old]
type = s3
provider = Other
endpoint = https://storage.old-cloud.example
access_key_id = ...
secret_access_key = ...

Имена бакетов у провайдеров разные, поэтому у каждого сначала собирается одинаковое дерево через combine:

[webshield_all]
type = combine
upstreams = media=webshield:u42-media dumps=webshield:u42-dumps

[sel_all]
type = combine
upstreams = media=sel:acme-media dumps=sel:acme-dumps

[old_all]
type = combine
upstreams = media=old:media dumps=old:dumps

А поверх — union, который делает из нескольких деревьев одно. В обычном режиме это текущий провайдер и новый, оба полноправные:

[s3all]
type = union
upstreams = old_all: webshield_all:
create_policy = all
action_policy = all
search_policy = ff

В аварийном старое облако уходит последним и только на чтение, а писать приходится в два новых провайдера:

upstreams = webshield_all: sel_all: old_all::ro

create_policy = all и action_policy = all пишут и удаляют во всех upstream’ах, кроме помеченного :ro. search_policy = ff читает с первого, где объект нашёлся: порядок в upstreams — это приоритет.

Порядок слоёв важен. Первая версия была наоборот — union для каждого бакета и combine сверху. Она работала, пока не понадобилось перезаписать объект, который ещё лежал только в старом облаке: новая версия уезжала по неверному пути, а читалась старая.

Приложение не меняется

rclone serve s3 поднимает поверх s3all: обычный S3 API. Каталоги верхнего уровня — media и dumps — становятся бакетами с прежними именами. Прокси запускается на каждой машине с приложением и слушает только localhost: у каждого сервера свой экземпляр, поэтому ни балансировщик, ни TLS не нужны, а сам прокси не становится единой точкой отказа.

rclone serve s3 s3all: \
  --addr 127.0.0.1:8333 \
  --auth-key "$APP_ACCESS_KEY,$APP_SECRET_KEY" \
  --contimeout 3s --timeout 30s --low-level-retries 2 \
  --dir-cache-time 5s

У приложения своя пара ключей, ключи провайдеров есть только в rclone.conf. Таймауты обязательны: с умолчаниями rclone повторяет запросы к упавшему провайдеру минутами, и прокси на это время зависает.

--dir-cache-time важен не меньше. Прокси кеширует каталоги, по умолчанию на 5 минут, и для уже найденного объекта помнит, у какого провайдера он лежит. Если упал именно этот провайдер, чтение не переключается на второй, а обрывается ошибкой — до конца кеша. С 5 секундами первое чтение после отказа занимает несколько секунд, дальше всё идёт со второго провайдера.

В приложении меняются адрес и ключи. Если адрес берётся из окружения, достаточно поменять значение — AWS_ENDPOINT_URL читают свежие boto3, AWS CLI v2, aws-sdk-js v3 и Go SDK v2:

AWS_ENDPOINT_URL=http://127.0.0.1:8333
AWS_ACCESS_KEY_ID=$APP_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=$APP_SECRET_KEY

rclone — не ниже 1.75.2: механизм serve достаточно новый и сейчас активно дорабатывается.

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

Перенос данных: обычный режим

Когда текущий провайдер работает и открыт на запись, всё просто. Приложение через union пишет и удаляет у обоих, поэтому обычный rclone copy от старого к новому ничего не откатит и не вернёт удалённое — повторять его можно сколько угодно:

rclone copy old:media webshield:u42-media -M --transfers 16
rclone copy old:dumps webshield:u42-dumps -M
rclone check old:media webshield:u42-media

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

Перенос данных: аварийный режим

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

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

psql -At -c "select storage_key from attachments" > keys.txt

rclone copy old:media webshield:u42-media \
  --files-from keys.txt --no-traverse --ignore-existing \
  --transfers 8 --checkers 16 --tpslimit 20 \
  --retries 10 --low-level-retries 20 \
  -M --log-file copy.log

rclone copy webshield:u42-media sel:acme-media \
  --files-from keys.txt --no-traverse --ignore-existing -M --transfers 32

Дампы базы в приложении никто не удаляет и не перезаписывает, их копировали целиком теми же двумя шагами: old:dumps → webshield:u42-dumps → sel:acme-dumps с --ignore-existing.

--ignore-existing не трогает то, что у приёмника уже есть: если файл там есть, значит, его записало приложение, и эта версия новее. --files-from с --no-traverse к тому же избавляет от листинга медленного источника. Копировать в s3all: нельзя: union найдёт каждый объект в старом облаке и решит, что копировать нечего. Поэтому сначала копия к одному провайдеру, а уже от него — ко второму: медленный источник читается один раз.

--tpslimit ограничивает число запросов в секунду: старое облако отвечало на всплески ошибкой SlowDown. -M переносит Cache-Control и Content-Disposition — без него Content-Type rclone сохраняет, а остальные заголовки теряются. Файлы, которых нет в старом облаке, rclone пропускает молча и с нулевым кодом выхода, поэтому потери считали отдельно: список ключей против rclone lsf -R приёмника.

Когда всё скопировано, old_all::ro убрали из upstreams и перезапустили прокси. Затем сверили провайдеров между собой (rclone check webshield:u42-media sel:acme-media) и убрали «сирот» — файлы, которые пользователи удалили, пока шло копирование. Их нет в свежей выгрузке из базы, а --min-age не даёт задеть только что загруженные, которые в базу ещё не попали:

psql -At -c "select storage_key from attachments" | sort > live.txt
rclone lsf -R --files-only --min-age 1h webshield:u42-media | sort | comm -23 - live.txt > orphans.txt
rclone delete webshield:u42-media --files-from orphans.txt
rclone delete sel:acme-media --files-from orphans.txt

Чего схема не умеет:

  • ACL, теги, retention и политики бакетов — serve s3 поддерживает только объекты, листинг, multipart и копирование;

  • метаданные при записи через прокси теряются, Content-Type угадывается по расширению;

  • ссылки для браузера через локальный прокси не работают — раздача клиентам остаётся задачей CDN;

  • строгой консистентности нет: сбой посреди записи даёт расхождение до сверки.

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.