Павло Щербуха

Logo

Персональна освітня сорінка

Розробка на Node.js, VUE.js, Python, IBM Integration Bus (App Connect Ent) , ORACLE PL/SQL
10 November 2026

Використання локального S3 Object Storage SeaweedFS у звичайній розробці

by Pavlo Shcherbukha

Використання локального S3 Object Storage SeaweedFS у звичайній розробці

1. Вступ

Якось спробував Object Storage SeaweedFS для побудови SPARK + Delta Lake (правда поки не вдало, бо там проблема з адаптацією Python + Java Spark). Але ідея мені сподобался. Тепер я зацікавився архітектурними шаблонами: Redis, Rabbit MQ, Node.js/Flask web service чи backend worker з використаням локального Object Storage SeaweedFS.

SeaweedFS — це інструмент для локального та розподіленого S3-сховища (плюс у нього є власний Filer із POSIX-інтерфейсом). Його головна фішка — швидкий старт (один бінарник на Go) та сумісність з S3 API, що робить його ідеальним моком (тест замінником) для хмарних AWS S3 у локальному середовищі або k3s/Docker-пісочницях і транзакційність для файлів.

Для того, щоб зрозуміти, чому я звернувся до цієї теми, потрібно сказати, що мені не одноразово приходилося писати обробники для пакетного файлового обміну. В таких випадках є класичною проблема, коли одна програма пише файл, а друга його починає читати.Останній раз надійна обробка файлів у системах пакетного обміну файлами у мене получалася десь в кооперації з Sybase Power Builder та Novel Netware на початку 2000-них. Там була якась реальна файлова транзакційність і доступ до файлів з блокуванням. А от як пішли мережі Microsoft і всякі гібриди: давайте віртуалку linux поверх VmWare та розшаримо там диск… Воно все стало таким не надійним….

У Novell NetWare файлова система (NWFS) і блокування на рівні записів/файлів (File Locking / Record Locking) були реалізовані на рівні ядра мережевої ОС. Коли програма на PowerBuilder чатала файл чи писала, було присутє ексклюзивне блокування і ніхто у всій мережі не міг туди залізти до завершення транзакції - тобто до завершення роботи з файлом.

А потім прийшов епохальний «прогрес»: шари віртуалізації, мережеві диски (NFS, SMB/CIFS), які намагаються емулювати локальні файлові системи поверх нестабільних мереж, і різноманітні гібридні схеми. У результаті ми отримали класичні болі:

Використання S3-сумісного об’єктного сховища (як-от SeaweedFS) у парі з чергами повідомлень і кешем — це, по суті, сучасний реінкарнований патерн транзакційного зберігання для розподілених систем.

Він повертає ті самі гарантії надійності, але адаптовані під сучасні хмарні та контейнеризовані реалії:

Це не стільки стаття «про SeaweedFS», скільки спроба описати практичний шаблон інтеграції з системами, які диктують файловий інтерфейс:

Ідея статті — не в тому, що S3 магічно забезпечує транзакційність, а в тому, що об’єктне сховище може стати спільною точкою передачі, а файлова інфраструктура — замінитися простішим контрактом: завантажити об’єкт, передати його ідентифікатор, обробити, і далі видалити або зберегти.

Як на мене , то формулювання цього всього у вигляді чітких архітектурних шаблонів для корпоративних систем пакетного обміну — надзвичайно цінна річ, особливо коли доводиться пояснювати молодшим розробникам, чому не треба пхати файли в NFS-шари та писати кастомні сокети.

Далі я спробував розібрати різні сценарії, які мені зустрічалися. Можливо, я десь перебільшив - тут все залежить від конкретної задачі.

2. Проблема 1: «Програмна вертушка» від вендора -> Архітектурний шаблон: «S3-Sidecar / Push Gateway для Legacy-вертушо

Часто вендори видають своїм клієнтам “програмі вертушки” (це якась закрита Java-програма або Legacy-бінарник ще з нульових, написана під Windows Server або забутий Linux) зазвичай ставиться за принципом “поставте на віртуалку, хай туди пише, а далі робіть що хочете”. Це класичний анекдот із життя інтеграцій з вендорами (і особливо з різноманітними процесингами, банківськими клірингами та банкоматними мережами, платіжними боксами … ). І за звичай, обслуговувати таку прогаму дають наймолодшому інфраструктурнику. Керівники ІТ не розумію всієї глибини катастрофи отаких вертушок.

І тут починається традиційний біль:

Спробую описати вирішення проблеми за допомогою SeaweedFS через архітектурний шаблон «S3-Sidecar / Push Gateway для Legacy-вертушок»

Архітектурний шаблон: «S3-Sidecar / Push Gateway для Legacy-вертушок»

Ми можемо за допомогою банальнгого curl копіювпти чи переносити файли на SeaweedFS і це буде надійніше, простіше і зрозуміліше ніж складні bash скрипти. Та і взагалі, перенести цю задачу на локальний (або віддалений) S3-ендпоїнт (SeaweedFS) за допомогою банального curl — це геніальне у своїй простоті й надійності рішення. Воно повністю нівелює людський фактор.

Як це працює на практиці:

# Приклад відправки файлу транзакцій однією командою одразу після закриття сесії
curl -X PUT -T "/var/processor/out/transactions_2026-11-09.dat" \
  "http://seaweed-s3.internal:8333/atm-processing-bucket/incoming/transactions_2026-11-09.dat"

Чому це кардинально змінює надійність:

Це ідеальний приклад того, як сучасні легкі хмарні патерни (S3 endpoint) елегантно вирішують хронічні болі класичного корпоративного IT-зоопарку.

Ну, тут правда, знайдуться розумники, що на тій віртуалці піднімуть ftp чи sftp і скажуть забирайте через нього. Любителі піднімати vsftpd чи OpenSSH sftp-server на віртуалці вендора зазвичай щиро вірять, що «це ж стандартний протокол». Але на практиці вони отримують цілий букет класичних архітектурних граблів:

Коли зовнішня система або внутрішня вертушка заливає файл через FTP/SFTP, він спочатку з’являється на диску в процесі передачі (або під якимось тимчасовим ім’ям на кшталт .inprog_filename). Якщо ваш збирач (ETL або воркер) почне читати цей файл за розкладом до того, як передача завершилася — ви отримуєте биті дані. Щоб цього уникнути, вендори змушені налаштовувати трюки з перейменуванням (rename після завершення), але «недодевопси» про це часто забувають або налаштовують криво.

Якщо файл великий (наприклад, багатогодинний потік транзакцій чи знімок стану банкоматів), а воркер намагається забрати його паралельно з тим, як FTP-сервер його дописує, починаються стандартні системні блокування файлів на рівні ОС (file locking). Процеси починають чекати один одного, зависати або падати з таймаутами.

Якщо мережевий обрив стався посередині FTP-сесії, на диску віртуалки залишаються напівзавантажені гігабайтні шматки. Вони висітимуть там роками, поки хтось вручну їх не почистить (бо автоочищення ніхто не написав).

Заведення окремих системних користувачів, налаштування chroot для ізоляції каталогів, ротація SSH-ключів або підтримка старих алгоритмів шифрування (бо у вендора стара бібліотека) — усе це витрати часу, які не додають жодної бізнес-цінності.

Чому перехід на S3-шлюз (SeaweedFS) виграє у цьому порівнянні:

Як на мене, то формування такого підходу в чіткі архітектурні шаблони — це чудовий спосіб раз і назавжди позбавити команду головного болю з інтеграцією Legacy-систем.

3. Проблема 2. Вендорська програма через кожні 15 хвилин пише в один і той же файл з номером банкомату його стан. Ну , скажемо, залишок купюр а касетах -> Архітектурний шаблон: «Time-Series S3 Ingestion & Append-Only Log»

Уявіть картину: раз на 15 хвилин кілька сотень (або тисяч) банкоматів по всій країні одночасно кидають у спільну папку (або штовхають через кривий FTP) свої state-файли на кшталт atm_status_100500_20261109_2100.dat.

Що робить стандартна «недодевопс»-архітектура на віртуалці:

Архітектурний шаблон: «Time-Series S3 Ingestion & Append-Only Log»

Замість того, щоб змушувати вендорський софт боротися з файловими блокуваннями на диску, цю задачу ідеально закриває S3-сумісне сховище (SeaweedFS) у парі з легкою асинхронною обробкою:

Налаштовуємо скрипт-транспорт так, щоб він формував назву файлу з унікальним штампом часу та суфіксом (наприклад, atms/100500/2026-11-09T21:00:00Z.json). Жодного перезапису одного й того самого файлу! Кожне опитування банкомату — це новий незмінний (immutable) об’єкт у сховищі.

Банкомат (або його шлюз) може заливати свій статус хоч щомиті. Об’єкти лягають у SeaweedFS паралельно, бо в об’єктних сховищах немає поняття «перезапис рядка в середині файлу» — є лише створення чи заміна ключа.

RabbitMQ повідомляє воркерам (Node.js / Python Flask), що з’явився новий пакет статусів за такий-то період. Воркер забирає файл з S3, робить швидкий INSERT у часову базу даних (TimescaleDB) чи відправляє алерти, а оригінальний JSON залишається в сховищі як історичний аудиторський слід.

Це повністю позбавляє від головного болю з ротацією старих статусів, проблемою забитих дисків та блокувань файлів на віртуалках.

4. Проблема 3. За допомогою IBM ACE запхати великий бінарний об’єкт в IBM MQ -> В чергу пишемо тільки метадані (URL великого бінарного об’єкту), а сам об’єкт зберігається на S3-сумісному сховищі SeaweedFS

Північноамериканське «пхати великі бінарники в MQ» — це класичний архітектурний біль enterprise-систем. Усі брокери повідомлень (і IBM MQ, і RabbitMQ, і Kafka) оптимізовані під швидку доставку легких подій та метаданих, а не під зберігання файлів. Коли через MQ починають ганяти гігабайти, брокер швидко забиває пам’ять, падає лог транзакцій, а решта черг стають «у пробку».

Тобто класична проблема для всіх черг, не зважаючи на вендора: В чергу пишемо тільки метадані (URL великого бінарного об’єкту), а сам об’єкт зберігається на S3-сумісному сховищі SeaweedFS.

5. Проблема 4. В IBM ACE є вузол для читаня файлів з диска. Але там треба примапити той диск до сервера IBM ACE, потім дати права власнику IBM ACE. І всерівно, при дуже великих файлах чи великій кількості малих файлів виникають проблеми -> Зовнішнє завантаження/вивантаження: Клієнт чи попередній сервіс завантажує файл напряму в SeaweedFS по S3 API (отримуючи швидкий локальний S3-ендпоїнт

  1. Проблема з прямим читанням файлів з диска в ACE (FileNodes / FileInput)
  1. Як S3-патерн (SeaweedFS) вирішує це в архітектурі з IBM ACE

Замість того, щоб змушувати IBM ACE «тягнути» файл по складних CIFS/NFS-шляхах з локальних дисків, потоковий потік перетворюється на чисту ізольовану шину:

Це робить архітектуру надзвичайно стійкою до пікових навантажень (коли летить багато великих файлів одразу).

  1. У Node-RED дуже привабливо виглядає ідея просто кинути файл у папочку через file out, а інший потоковий потік Node-RED підхопить його через file in чи watch. Але у реальному житті (особливо в контейнерах або кластерах) це тягне за собою низку проблем:
  1. Як S3-підхід (SeaweedFS) рятує Node-RED

У Node-RED є чудова екосистема нод для роботи з S3 (наприклад, пакети на кшталт node-red-contrib-s3 або банальний http request / стандартні axios/fetch усередині ноди function).

Коли ви використовуєте SeaweedFS як локальний S3-ендпоїнт для Node-RED:

7. Проблема 6. Використання “сирих сокетів» та кастомних TCP/UDP-серверів усередині Node-RED -> Атомарність замість «напівзаписаних файлів» та Ідеальний поділ шин (Control Plane vs Data Plane)

Це, мабуть, класика жанру, яка зазвичай закінчується написанням власного протоколу поверх TCP, де починаються проблеми з фрагментацією пакетів, обривами зв’язку, відсутністю ретраїв та падіннями пам’яті при переповненні буферів.

Перехід на S3-ендпоїнт (як-от SeaweedFS) вирішує ці проблеми на рівні архітектурних гарантій:

  1. Атомарність замість «напівзаписаних файлів»

У класичній файловій системі або при передачі через сирий сокет файл дописується шматками. Якщо воркер читає його паралельно (або процес обірвався посередині), ви отримуєте битий JSON або зіпсований бінарник. У протоколі S3 операція PUT є атомарною: об’єкт з’являється у сховищі тільки тоді, коли він повністю завантажений і пройшов перевірку контрольної суми (MD5 / ETag). До цього моменту його для споживачів просто не існує. Жодних блокувань і гонок (race conditions).

  1. Ідеальний поділ шин (Control Plane vs Data Plane)

Control Plane (Redis + RabbitMQ): Передає лише легкі метадані та інструкції керування (що зробити, де взяти, який статус). Це швидко, надійно і не навантажує мережу.

Data Plane (SeaweedFS S3): Приймає важкий контент натомість через стандартний, оптимізований HTTP/S3-протокол, який має вбудовані таймаути, коннект-пули, кешування та можливість докачки (range requests).

В результаті код і флоу стають чистими, передбачуваними і повністю втрачають потребу в будь-яких кастомних мережевих костилях.

8. Ще кілька архітектурних шаблонів, без прив’язки до конкртеної больвої точки

Шаблон 1. Обгортка над великими блобами

Проблема: Передавати важкі корисні навантаження (великі JSON-документи, бінарні файли, PDF, скомпільовані звіти, дампи) через RabbitMQ — антипатерн. Це забиває брокер, роздуває пам’ять і гальмує споживачів. Як вирішити через SeaweedFS:

{
  "task_id": "uuid-1234",
  "storage_bucket": "temp-processing",
  "storage_key": "incoming/payload_uuid-1234.json",
  "checksum": "sha256:..."
}

Шаблон 2. Кешування метаданих та S3-ідентифікаторів у Redis

Проблема: Звертатися до S3/SeaweedFS за списком об’єктів або перевіряти їх наявність під час високого навантаження — зайві мережеві затримки. Як вирішити через SeaweedFS + Redis:

HSET doc:uuid-1234 status "processing" s3_path "seaweed://bucket/path/file.bin" updated_at "1762725600"

Шаблон 3. Ізольоване інтеграційне тестування (Testcontainers / Local S3 Mock)

Оскільки SeaweedFS підтримує стандартний AWS S3 API (Endpoint http://localhost:8333), його неймовірно зручно використовувати в автоматизованих інтеграційних тестах (Jest для Node.js або Pytest для Flask).

Шаблон 4. «Файловий шлюз для Воркерів» через SeaweedFS Filer (POSIX mount)

SeaweedFS має унікальну особливість — Filer, який окрім S3 дозволяє монтувати сховище через WebDAV, gRPC або навіть через FUSE (локальна папка).

9. Готовий стек для локальної розробки та інтеграційного тестування

Ось готовий стек для локальної розробки та інтеграційного тестування. Він включає повний docker-compose.yml, де підняті SeaweedFS (Master + Volume + S3 API), Redis, RabbitMQ (разом із системою керування), а також приклад Node.js сервісу, який демонструє патерн S3-Offloading (коли важкий payload лежить у SeaweedFS, а в RabbitMQ лежить лише посилання).

version: '3.8'

services:
  # 1. Redis для стейтів, кешування та статусів задач
  redis:
    image: redis:7-alpine
    container_name: local_redis
    ports:
      - "6379:6379"
    restart: unless-stopped

  # 2. RabbitMQ для черги повідомлень
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    container_name: local_rabbitmq
    ports:
      - "5672:5672"     # AMQP порт
      - "15672:15672"   # Management UI (http://localhost:15672, guest/guest)
    restart: unless-stopped

  # 3. SeaweedFS S3-сумісне сховище (Master + Volume)
  seaweedfs:
    image: chrislusf/seaweedfs:latest
    container_name: local_seaweedfs
    command: "server -dir=/data -s3 -s3.port=8333"
    ports:
      - "9333:9333"     # Master port
      - "8080:8080"     # Filer port (опціонально)
      - "8333:8333"     # S3 API port
    volumes:
      - seaweed_data:/data
    restart: unless-stopped

volumes:
  seaweed_data:

Цей приклад реалізує базовий сервіс на Express.js та amqplib із використанням офіційного AWS SDK (@aws-sdk/client-s3), налаштованого на локальний ендпоїнт SeaweedFS.

Структура пректу

app/
├── package.json
└── index.js

package.json

{
  "name": "seaweed-rabbitmq-redis-demo",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.500.0",
    "amqplib": "^10.3.0",
    "express": "^4.19.2",
    "redis": "^4.6.13",
    "uuid": "^9.0.1"
  }
}

index.js - програмний код

import express from 'express';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import amqp from 'amqplib';
import { createClient } from 'redis';
import { v4 as uuidv4 } from 'uuid';

const app = express();
app.use(express.json());

// --- 1. ПІДКЛЮЧЕННЯ ДО СЕРВІСІВ ---

// Конфігурація SeaweedFS S3 API (Endpoint: http://localhost:8333)
const s3 = new S3Client({
  region: 'us-east-1',
  endpoint: 'http://localhost:8333',
  forcePathStyle: true, // Обов'язково для SeaweedFS/MinIO
  credentials: {
    accessKeyId: 'any_key',
    secretAccessKey: 'any_secret',
  },
});
const BUCKET_NAME = 'app-payloads';

// Redis клієнт
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();

// RabbitMQ підключення та ініціалізація черги
let rabbitChannel, rabbitConnection;
async function initRabbitMQ() {
  rabbitConnection = await amqp.connect('amqp://localhost:5672');
  rabbitChannel = await rabbitConnection.createChannel();
  await rabbitChannel.assertQueue('task_queue', { durable: true });
  console.log(' Connected to RabbitMQ & Queue asserted');
}
await initRabbitMQ();


// --- 2. ENDPOINT: СТВОРЕННЯ ЗАДАЧІ (Producer) ---
app.post('/api/tasks', async (req, res) => {
  try {
    const taskId = uuidv4();
    const largePayload = {
      taskId,
      createdAt: new Date().toISOString(),
      data: req.body.data || "Heavy payload data simulation...",
    };

    const s3Key = `tasks/${taskId}/payload.json`;

    // Крок А: Зберігаємо важкий об'єкт у SeaweedFS S3
    await s3.send(new PutObjectCommand({
      Bucket: BUCKET_NAME,
      Key: s3Key,
      Body: JSON.stringify(largePayload),
      ContentType: 'application/json',
    }));

    // Крок Б: Записуємо початковий стан у Redis
    await redisClient.hSet(`task:${taskId}`, {
      status: 'queued',
      storage_key: s3Key,
      updated_at: Date.now(),
    });

    // Крок В: Відправляємо легке посилання в RabbitMQ чергу
    const message = { taskId, storageKey: s3Key, bucket: BUCKET_NAME };
    rabbitChannel.sendToQueue('task_queue', Buffer.from(JSON.stringify(message)), {
      persistent: true,
    });

    res.status(202).json({
      success: true,
      message: 'Task accepted and offloaded to SeaweedFS',
      taskId,
      s3Key,
    });
  } catch (error) {
    console.error('Error creating task:', error);
    res.status(500).json({ error: error.message });
  }
});


// --- 3. ФОНОВИЙ ВОРКЕР (Consumer) ---
async function startWorker() {
  console.log(' Worker started, waiting for messages...');
  rabbitChannel.consume('task_queue', async (msg) => {
    if (!msg) return;

    const { taskId, storageKey, bucket } = JSON.parse(msg.content.toString());
    console.log(`\n[Worker] Processing task: ${taskId}`);

    try {
      // 1. Оновлюємо статус у Redis
      await redisClient.hSet(`task:${taskId}`, { status: 'processing', updated_at: Date.now() });

      // 2. У реальному житті тут можна завантажити файл із SeaweedFS за потреби:
      // const getObj = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: storageKey }));
      // const bodyString = await getObj.Body.transformToString();

      // Імітуємо корисну роботу (наприклад, 1.5 секунди обробки)
      await new Promise((resolve) => setTimeout(resolve, 1500));

      // 3. Завершуємо задачу, оновлюючи Redis
      await redisClient.hSet(`task:${taskId}`, { status: 'completed', updated_at: Date.now() });
      console.log(`[Worker] Task ${taskId} successfully completed!`);

      // Підтверджуємо обробку повідомлення для RabbitMQ
      rabbitChannel.ack(msg);
    } catch (err) {
      console.error(`[Worker] Failed to process task ${taskId}:`, err);
      // У разі помилки повертаємо у чергу або в DLQ
      rabbitChannel.nack(msg, false, false);
    }
  });
}

startWorker();

// Запуск HTTP сервера
const PORT = 3000;
app.listen(PORT, () => {
  console.log(` Server running on http://localhost:${PORT}`);
});
tags: