Наша биг дата проанализировала Telegram-чаты, форумы и разговоры в кулуарах IT-мероприятий и пометила объектные хранилища как инструмент, который ещё не все осмеливаются использовать в своих проектах. Хочу поделиться с вами своим опытом в формате статьи-воркшопа. Если вы пока не знакомы с этой технологией и паттернами её применения, надеюсь, эта статья поможет вам начать использовать её в своих проектах. 

Зачем вообще говорить о хранении объектов?

С недавних пор я работаю Golang-разработчиком в Ozon. У нас в компании есть крутая команда админов и релиз-инженеров, которая построила инфраструктуру и CI вокруг неё. Благодаря этому я даже не задумываюсь о том, какие инструменты использовать для хранения файлов и как это всё поддерживать. 

Но до прихода в Ozon я сталкивался с довольно интересными кейсами, когда хранение разных данных (документов, изображений) было организовано не самым изящным образом. Мне попадались SFTP, Google Drive и даже монтирование PVC в контейнер! 

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

TL;DR

Объектное хранилище – это дополнительный слой абстракции над файловой системой и хостом, который позволяет работать с файлами (получать доступ, хранить) через API.

Объектное хранилище может помочь вам в кейсах, когда необходимо хранить файлы пользователей в ваших приложениях, складывать статику и предоставлять доступ к ней через Ingress или хранить кеши вашего CI

Все материалы к статье (исходники, конфиги, скрипты) лежат вот в этой репе

Что такое объектное хранилище

Хранить данные нашего приложения можно различными способами, от хранения данных просто на диске до блоба в нашей БД (если она это поддерживает, конечно). Но будет такое решение оптимальным? Часто есть нефункциональные требования, которые нам хотелось бы реализовать: масштабируемость, простота поддержки, гибкость. Тут уже хранением файлов в БД или на диске не обойтись. В этих случаях, например, масштабирование программных систем, в которых хранение данных построено на работе с файловой системой хоста, оказывается довольно проблематичной историей.

И на помощь приходят те самые объектные хранилища, о которых сегодня и пойдёт речь. Объектное хранилище – это способ хранить данные и гибко получать к ним доступ как к объектам (файлам). В данном контексте объект – это файл и набор метаданных о нём. 

Стоит ещё упомянуть, что в объектных хранилищах нет такого понятия, как структура каталогов. Все объекты находятся в одном «каталоге» – bucket. Структурирование данных предлагается делать на уровне приложения. Но никто не мешает назвать объект, например, так: objectScope/firstObject.dat .

Основное преимущество хранения данных в объектах – это возможность абстрагирования системы от технических деталей. Нас уже не интересует, какая файловая (или тем более операционная) система хранит наши данные. Мы не привязываемся к данным какими-то конкретными способами их представления, которые нам обеспечивает платформа. 

В этой статье мы не будем сравнивать типы объектных хранилищ, а обратим наше внимание на класс S3-совместимых стораджей, на примере MinIO. Выбор обусловлен тем, что MinIO имеет низкий порог входа (привет, Ceph), а ещё оно Kubernetes Native, что бы это ни значило

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

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

Disclaimer: под S3 я буду иметь в виду технологию (S3-совместимые объектные хранилища), а не конкретный коммерческий продукт. Цель статьи – показать на примерах, как можно использовать такие решения в своих приложениях. 

Кейс 1: прокат самокатов

В рамках формата статьи-воркшопа знакомиться с S3 в общем и с MinIO в частности мы будем на практике. 

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

Давайте перейдём к кейсу. Представим, что мы пишем сервис для проката самокатов и у нас есть user story, когда клиент фотографирует самокат до и после аренды. Хранить медиаматериалы мы будем в объектном хранилище.

Для начала развернём наше хранилище.

Самый быстрый способ развернуть MinIO – это наш любимчик Docker, само собой.

С недавнего времени Docker – не такая уж и бесплатная штука, поэтому в репе на всякий случай есть альтернативные манифесты для Podman. 

Запускать «голый» контейнер из терминала – нынче моветон, поэтому начнём сразу с манифеста для docker-compose.

# docker-compose.yaml
version: '3.7'

services:
 minio:
   image: minio/minio:latest
   command: server --console-address ":9001" /data/
   ports:
     - "9000:9000"
     - "9001:9001"
   environment:
     MINIO_ROOT_USER: ozontech
     MINIO_ROOT_PASSWORD: minio123
   volumes:
     - minio-storage:/data
   healthcheck:
     test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
     interval: 30s
     timeout: 20s
     retries: 3
volumes:
 minio-storage:

Сохраняем манифест и делаем $ docker-compose up в директории с манифестом.

Теперь мы можем управлять нашим хранилищем с помощью web-ui. Но это не самый удобный способ для автоматизации процессов (например, для создания пайплайнов в CI/CD), поэтому сверху ещё поставим CLI-утилиту:

$ go get github.com/minio/mc

И да, не забываем про export PATH=$PATH:$(go env GOPATH)/bin.

Cоздадим алиас в mc (залогинимся):

$ mc alias set minio http://localhost:9000 ozontech minio123

Теперь создадим bucket – раздел, в котором мы будем хранить данные нашего пользователя (не стоит ассоциировать его с папкой). Это скорее раздел, внутри которого мы будем хранить данные.

Назовем наш бакет “usersPhotos”:

$ mc mb minio/usersPhot


$ mc ls minio > [0B] usersPhotos

Теперь можно приступать к реализации на бэке. Писать будем на Golang. MinIO любезно нам предоставляет пакетик для работы со своим API. 

Disclaimer: код ниже – лишь пример работы с объектным хранилищем; не стоит его рассматривать как набор best practices для использования в боевых проектах.

Начнём с подключения к хранилищу:

func (m *MinioProvider) Connect() error {
  var err error
  m.client, err = minio.New(m.url, &minio.Options{
     Creds:  credentials.NewStaticV4(m.user, m.password, ""),
     Secure: m.ssl,
  })
  if err != nil {
     log.Fatalln(err)
  }

  return err
}

Теперь опишем ручку добавления медиа:

func (s *Server) uploadPhoto(w http.ResponseWriter, r *http.Request) {
  // Убеждаемся, что к нам в ручку идут нужным методом
  if r.Method != "POST" {
     w.WriteHeader(http.StatusMethodNotAllowed)
     return
  }

  // Получаем ID сессии аренды, чтобы знать, в каком контексте это фото
  rentID, err := strconv.Atoi(r.Header.Get(HEADER_RENT_ID))
  if err != nil {
     logrus.Errorf("Can`t get rent id: %v\n", err)
     http.Error(w, "Wrong request!", http.StatusBadRequest)
     return
  }

  // Забираем фото из тела запроса
  src, hdr, err := r.FormFile("photo")
  if err != nil {
     http.Error(w, "Wrong request!", http.StatusBadRequest)
     return
  }

  // Получаем информацию о сессии аренды
  session, err := s.database.GetRentStatus(rentID)
  if err != nil {
     logrus.Errorf("Can`t get session: %v\n", err)
     http.Error(w, "Can`t upload photo!", http.StatusInternalServerError)
     return
  }

  // Складываем данные в объект, который является своего рода контрактом
  // между хранилищем изображений и нашей бизнес-логикой
  object := models.ImageUnit{
     Payload:     src,
     PayloadSize: hdr.Size,
     User:        session.User,
  }
  defer src.Close()

  // Отправляем фото в хранилище
  img, err := s.storage.UploadFile(r.Context(), object)
  if err != nil {
     logrus.Errorf("Fail update img in image strorage: %v\n", err)
     http.Error(w, "Can`t upload photo!", http.StatusInternalServerError)
     return
  }

  // Добавляем запись в БД с привязкой фото к сессии
  err = s.database.AddImageRecord(img, rentID)
  if err != nil {
     logrus.Errorf("Fail update img in database: %v\n", err)
     http.Error(w, "Can`t upload photo!", http.StatusInternalServerError)
  }
}

Загружаем фото:

func (m *MinioProvider) UploadFile(ctx context.Context, object models.ImageUnit) (string, error) {
  // Получаем «уникальное» имя объекта для загружаемого фото
  imageName := samokater.GenerateObjectName(object.User)

  _, err := m.client.PutObject(
     ctx,
     UserObjectsBucketName, // Константа с именем бакета
     imageName,
     object.Payload,
     object.PayloadSize,
     minio.PutObjectOptions{ContentType: "image/png"},
  )

  return imageName, err

Нам надо как-то разделять фото до и после, поэтому мы добавим записи в базу данных:

func (s *PGS) AddImageRecord(img string, rentID int) error {
  // Получаем информацию о сессии аренды
  rent, err := s.GetRentStatus(rentID)
  if err != nil {
     logrus.Errorf("Can`t get rent record in db: %v\n", err)
     return err
  }

  // В зависимости от того, были загружены фото до начала аренды
  // или после её завершения, добавляем запись в соответствующее поле в БД
  if rent.StartedAt.IsZero() {
     return s.updateImages(rent.ImagesBefore, img, update_images_before, rentID)
  }

  return s.updateImages(rent.ImagesAfter, img, update_images_after, rentID)
}

Ну и сам метод обновления записи в БД:

func (s *PGS) updateImages(old []string, new, req string, rentID int) error {
  // Добавляем в список старых записей
  // новую запись об изображении
  old = append(old, new)
  new = strings.Join(old, ",")

  _, err := s.db.Exec(req, new, rentID)
  if err != nil {
     logrus.Errorf("Can`t update image record in db: %v\n", err)
  }

  return err
}

Также мы могли бы напрямую через сервис вытаскивать и отдавать фото по запросу. Выглядело бы это примерно так: 

func (s *Server) downloadPhoto(w http.ResponseWriter, r *http.Request) {
  if r.Method != "GET" {
     w.WriteHeader(http.StatusMethodNotAllowed)
     return
  }

  rentID := r.URL.Query()["rid"][0]
  if rentID == "" {
     http.Error(w, "Can`t get rent-id from request", http.StatusBadRequest)
  }

  img, err := s.storage.DownloadFile(r.Context(), rentID)
  if err != nil {
     logrus.Errorf("Cant`t get image from image-storage: %v\n", err)
     http.Error(w, "Can`t get image", http.StatusBadRequest)
  }

  s.sendImage(w, img.Payload)
}

Ну и само получение файла из хранилища:

func (m *MinioProvider) DownloadFile(ctx context.Context, image string) (models.ImageUnit, error) {
  reader, err := m.client.GetObject(
     ctx,
     UserObjectsBucketName,
     image,
     minio.GetObjectOptions{},
  )
  if err != nil {
     logrus.Errorf("Cant`t get image from image-storage: %v\n", err)
  }
  defer reader.Close()

  return models.ImageUnit{}, nil
}

Но мы можем и просто проксировать запрос напрямую в MinIO, так как у нас нет причин этого не делать (на практике такими причинами могут быть требования безопасности или препроцессинг файлов перед передачей пользователю). Делать это можно, обернув всё в nginx:

server {
   listen 8080;
   underscores_in_headers on;
   proxy_pass_request_headers on;

   location / {
       proxy_pass http://docker-samokater;
   }

   location /samokater {
       proxy_pass http://docker-minio-api;
   }

}

server {
   listen 9090;

   location / {
       proxy_pass         http://docker-minio-console;
       proxy_redirect     off;
   }
}

Получать ссылки на изображения мы будем через ручку rent_info:

func (s *Server) rentInfo(w http.ResponseWriter, r *http.Request) {
  if r.Method != "GET" {
     w.WriteHeader(http.StatusMethodNotAllowed)
     return
  }

  rentID, err := strconv.Atoi(r.Header.Get(HEADER_RENT_ID))
  if err != nil {
     logrus.Errorf("Can`t get rent id: %v\n", err)
     http.Error(w, "Wrong request!", http.StatusBadRequest)
     return
  }

  session, err := s.database.GetRentStatus(rentID)
  if err != nil {
     logrus.Errorf("Can`t get session: %v\n", err)
     http.Error(w, "Can`t rent info!", http.StatusInternalServerError)
     return
  }

  // Обогащаем поля ссылками на изображения
  session = enrichImagesLinks(session)

  s.sendModel(w, session)
}

И сам метод обогащения:

func enrichImagesLinks(session models.Rent) models.Rent {
  for i, image := range session.ImagesBefore {
     session.ImagesBefore[i] = fmt.Sprintf("%s/%s", ImageStorageSVCDSN, image)
  }

  for i, image := range session.ImagesAfter {
     session.ImagesAfter[i] = fmt.Sprintf("%s/%s", ImageStorageSVCDSN, image)
  }

  return session
}

Упакуем всё в docker-compose.yaml:

docker-compose.yaml
version: '3.7'

services:
 minio:
   image: minio/minio:latest
   container_name: minio
   restart: unless-stopped
   command: server --console-address ":9001" /data/
   ports:
     - "9000:9000"
     - "9001:9001"
   environment:
     MINIO_ROOT_USER: ozontech
     MINIO_ROOT_PASSWORD: minio123
   volumes:
     - minio-storage:/data
   healthcheck:
     test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
     interval: 30s
     timeout: 20s
     retries: 3
   networks:
     - app-network

 samokater:
   image: samokater:latest
   container_name: samokater
   build:
     context: ./
     dockerfile: samokater.Dockerfile
   restart: unless-stopped
   ports:
     - 8080:8080
   networks:
     - app-network
   environment:
     SERVERPORT: :8080
     DBPATH: user=postgres password=devpass dbname=postgres host=db port=5432 sslmode=disable
     MINIOHOST: minio:9000
     MINIOUSER: ozontech
     MINIOPASS: minio123
   depends_on:
     - db
     - minio

 initDB:
   image: mingration:latest
   container_name: init
   environment:
     DBPATH: user=postgres password=devpass dbname=postgres host=db port=5432 sslmode=disable
   build:
     context: ./
     dockerfile: mingration.Dockerfile
   networks:
     - app-network
   depends_on:
     - db

 db:
   container_name: db
   image: postgres
   restart: always
   environment:
     POSTGRES_PASSWORD: devpass
   volumes:
     - pg-storage:/var/lib/postgresql/data
   ports:
     - 5432:5432
   networks:
     - app-network

 nginx:
   image: nginx-custom:latest
   build:
     context: ./
     dockerfile: nginx.Dockerfile
   restart: unless-stopped
   tty: true
   container_name: nginx
   volumes:
     - ./nginx.conf:/etc/nginx/nginx.conf
   ports:
     - 8000:80
     - 443:443
   networks:
     - app-network
   depends_on:
     - samokater

networks:
 app-network:
   driver: bridge

volumes:
 minio-storage:
 pg-storage:

Протестируем работу нашего приложения:

# Создаём сессию аренды
$ curl -i -X POST --header 'user_id:100' http://localhost:8080/api/v1/rent
HTTP/1.1 200 OK

{"ID":100,"Name":"","RentID":8674665223082153551}

# Добавляем пару фото до начала аренды
$ curl -i  -X POST --header 'rent_id:8674665223082153551' --form photo=@/Users/ktikhomirov/image_1.png  http://localhost:8080/api/v1/upload_photo --insecure
HTTP/1.1 200 OK

# Начинаем сессию аренды
$ curl -i -X POST  http://localhost:8080/api/v1/rent_start  -H  "Content-Type: application/json" -d '{"ID":100,"RentID":8674665223082153551}'
HTTP/1.1 200 OK

# Завершаем сессию аренды
$ curl -i -X POST  http://localhost:8080/api/v1/rent_stop  -H  "Content-Type: application/json" -d '{"ID":100,"RentID":8674665223082153551}'
HTTP/1.1 200 OK

# Добавляем фото после завершения аренды
$ curl -i  -X POST --header 'rent_id:8674665223082153551' --form photo=@/Users/ktikhomirov/image_2.png http://localhost:8080/api/v1/upload_photo --insecure

# Получаем информацию об аренде
curl -i -X GET -H "rent_id:8674665223082153551"  http://localhost:8080/api/v1/rent_info
HTTP/1.1 200 OK

{"ID":100,"Name":"","StartedAt":"2021-10-21T08:10:31.536028Z","CompletedAt":"2021-10-21T08:19:33.672493Z","ImagesBefore":["http://127.0.0.1:8080/samokater/100/2021-10-21T15:15:24.png","http://127.0.0.1:8080/samokater/100/2021-10-21T08:06:15.png"],"ImagesAfter":["http://127.0.0.1:8080/samokater/100/2021-10-21T08:21:06.png"],"RentID":8674665223082153551}
Изображение полученное при переходе по URL от ответа сервиса
Изображение полученное при переходе по URL от ответа сервиса

Кейс 2: хранение и раздача фронта

Ещё одна довольно популярная задача, для решения которой можно использовать объектные хранилища, – хранение и раздача фронта. Объектные хранилища пригодятся нам тут, когда захотим повысить доступность нашего фронта или удобнее им управлять. Это актуально, например, если у нас несколько проектов и мы хотим упростить себе жизнь.

Небольшая предыстория. Однажды я встретил довольно интересную практику в компании, где в месяц релизили по несколько лендингов. В основном они были написаны на Vue.js, изредка прикручивался API на пару простеньких ручек. Но моё внимание больше привлекло то, как это всё деплоилось: там царствовали контейнеры с nginx, внутри которых лежала статика, а над всем этим стоял хостовый nginx, который выполнял роль маршрутизатора запросов. Как тебе такой cloud-native-подход, Илон? В качестве борьбы с этим монстром мной было предложено обмазаться кубами, статику держать внутри MinIO, создавая для каждого лендинга свой бакет, а с помощью Ingress уже всё это проксировать наружу. Но, как говорится, давайте не будем говорить о плохом, а лучше сделаем!

Представим, что перед нами стоит похожая задача и у нас уже есть Kubernetes. Давайте туда раскатаем MinIO Operator. Стоп, почему нельзя просто запустить MinIO в поде и пробросить туда порты? А потому, что MinIO-Operator любезно сделает это за нас, а заодно построит High Availability-хранилище. Для этого нам всего лишь надо три столовые ложки соды... воспользоваться официальной документацией.

Для простоты установки мы вооружимся смузи Krew, который всё сделает за нас:

$ kubectl krew update

$ kubectl krew install minio

$ kubectl minio init

Теперь надо создать tenant. Для этого перейдём в панель управления. Чтобы туда попасть, прокинем прокси:
$ kubectl minio proxy -n minio-operator

После прокидывания портов до нашего оператора мы получим в вывод терминала JWT-токен, с которым и залогинимся в нашей панели управления:

Интерфейс управления тенантами
Интерфейс управления тенантами

Далее нажимаем на кнопку «Добавить тенант» и задаём ему имя и неймспейс:

Интерфейс настройки тенанта
Интерфейс настройки тенанта

После нажатия на кнопку «Создать» мы получим креденшиалы, которые стоит записать в какой-нибудь Vault:

Теперь для доступа к панели нашего кластера хранилищ, поднимем прокси к сервису minio-svc и его панели управления:

# Поднимаем прокси к дашборду minio-svc
kubectl -n minio-operator  port-forward service/minio-svc-console 9090:9090

# Поднимаем прокси к API minio-svc
kubectl -n minio-operator  port-forward service/minio-svc-hl 9000:9000

И вуаля! У нас есть высокодоступный отказоустойчивый кластер MinIO. Давайте прикрутим его к нашему GitLab CI и сделаем .gitlab_ci, чтобы в пару кликов деплоить фронт.

Вот так у нас будет выглядеть джоба для CI/CD на примере GitLab CI (целиком конфиг лежит в репе):

# gitlab-ci 
deploy-front:
 stage: deploy
 image: minio/mc
 script:
  # Логинимся в MinIO
  - mc config host add --insecure deploy $CI_OBJECT_STORAGE $CI_OBJECT_STORAGE_USER $CI_OBJECT_STORAGE_PASSWORD
  # И всё собранное ранее переносим в наш бакет
  - mc cp dist/* deploy/static --insecure -c -r
 dependencies:
   - build-front

Для того чтобы отдавать статику, добавим Ingress-манифест:

# static.yaml
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
 name: example-static
 labels:
   app.kubernetes.io/name: example-static
   app.kubernetes.io/version: "latest"
 annotations:
   cert-manager.io/cluster-issuer: letsencrypt-worker
   kubernetes.io/ingress.class: nginx
   kubernetes.io/tls-acme: "true"
   nginx.ingress.kubernetes.io/proxy-body-size: 100m
   nginx.ingress.kubernetes.io/secure-backends: "true"
   nginx.ingress.kubernetes.io/ssl-redirect: "true"
   nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
   nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
 tls:
   - hosts:
       - "domain.ru"
     secretName: ssl-letsencrypt-example
 rules:
   - host: "domain.ru"
     http:
       paths:
         - backend:
             serviceName: minio-svc
             servicePort: 9000
           path: /(.+)
           pathType: Prefix

А если вдруг потребуется доступ из других неймспейсов, то мы можем создать ресурс ExternalName:

---
apiVersion: v1
kind: Service
metadata:
 name: minio-svc
 namespace: deploy
spec:
 ports:
   - port: 9000
     protocol: TCP
     targetPort: 9000
 sessionAffinity: None
 type: ExternalName

Вместо вывода

Объектные хранилища – это класс инструментов, которые позволяют наделить систему высокодоступным хранилищем данных. Во времена cloud-native это незаменимый помощник в решении многих задач. Да, на практике могут случаться кейсы, в которых использование объектного хранения данных будет избыточным, но вряд ли это можно считать поводом совсем игнорировать этот инструментарий в других своих проектах.

Отдельно я бы посоветовал обратить внимание на S3-совместимые решения, если вы занимаетесь машинным обучением или BigData и у вас есть потребность в хранении большого количества медиаданных для их последующей обработки.

Рассмотренное в статье MinIO – это не единственный достойный инструмент, который позволяет работать с данной технологией. Существуют решения на основе Ceph и Riak CS и даже S3 от Amazon. У всех инструментов свои плюсы и минусы. 

Желаю вам успехов в создании и масштабировании ваших приложений и надеюсь, что объектные хранилища вам будут в этом помогать!

Делитесь в комментариях о вашем опыте работы с объектными хранилищами и задавайте вопроы!

Комментарии (7)


  1. JacobL
    28.10.2021 15:54
    +1

    Вам спасибо за статью! Какие стоит учесть минусы при планировании подобных решений? Я могу предположить, что размер хранилища будет заметно больше чем размер хранимых данных. Но не уверен, интересно ваше мнение. Как обстоят дела со скоростью работы?


    1. worldbug Автор
      28.10.2021 16:11
      +1

      Можно уточнить вопрос, что имеется под "размером хранилища", оверхед на хранение данных + сами данные ? Если так, то думаю вопрос скорее стоит в выборе реализации хранилища (софт + железо). В таком случае Ceph будет хорошим решением, он может. Держать большое количество данных в себе с минимально деградацией (боюсь давать точные цифры). В том или ином случае, если стоит вопрос хранения файлов в сервисе/приложении то объектные хранилища это наверное в первую очередь инструмент создания границы (в ее роли выступит API выбранного вами решения) между файлами и бизнес логикой. 
Такое отделение позволит закладывать масштабируемость. Например у нас будет несколько инстансов приложения, которое ходит через сеть в наш сторадж за данными, причем они могут находится на разных железках.


    1. Casus
      30.10.2021 10:31
      +3

      Коллеги пробовали минио около года назад, не знаю поменялось ли что то с тех пор, но под нагрузкой (io) минио гарантированно разваливался, при этом статус в самом минио показывал что все хорошо. Востановление данных не работало (при реплика данных х 3).

      Тестируйте свои решения на базе минио очень тщательно или готовтесь потерять все данные.

      Для некоторых решений остановились на Rook Ceph, работает стабильно, хорошая производительность, рабочее автовостановление.

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


      1. worldbug Автор
        30.10.2021 12:59
        +1

        Поддерживаю позицию что для более серьезных решений Ceph будет лучшим выбором


  1. JacobL
    28.10.2021 15:58
    +1

    Баг у хабра :(


  1. Aytuar
    31.10.2021 22:59
    +1

    Мы внедрили seaweed. Достаточно удобен. Производительность на уровне Minio. Не разваливается. Гибкие настройки избыточности. Тестировали и сломать кластер довольно сложно.


  1. felix0id
    02.11.2021 16:25
    +2

    5 копеек, `go get $URL` уже давно считается устаревшим, нужно использовать `go install $URL`