Разбираемся, что такое S3 и делаем простое объектное хранилище на Go

от автора

Привет, Хабр! С вами снова Матвей Мочалов из cdnnow!, и в этом посте мы не будем разбираться с FFmpeg — в этот раз наша рубрика «Эээээксперименты!» будет затрагивать объектные хранилища. Разберёмся, чем S3 отличается от S3, а также почему не всё то S3, что называется S3. А заодно эксперимента ради сделаем своё собственное простенькое объектное хранилище на любимом языке всех DevOps и SRE-инженеров — Go.

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

Объектные хранилища — это способ хранения данных, созданный для работы с большими объемами неструктурированной информации. В отличие от обычных файловых систем, где файлы структурированы по папкам, они могут ссылаться друг на друга различными путями, иметь разные уровни доступа и владельцев и в целом имеют строгую иерархию. Объектные хранилища максимально просты и представлены собственно только «объектами», где каждый — это минимальный набор метаданных, главный из которых — это уникальный для объекта идентификатор. 

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

Отличие объектных хранилищ от традиционных файловых систем

Привычные файловые системы NTFS, EXT4 и т.д. организуют файлы в виде иерархической структуры папок. Такая модель хорошо работает для небольших объемов данных и простых сценариев использования, когда количество файлов относительно невелико, к примеру, на личном устройстве или домашнем файловом сервере. Но, когда объём данных начинает переваливать за терабайты, а количество пользователей — это не несколько членов вашей семьи, подключающихся по WiFi к NAS, чтобы посмотреть вечером кино, пролистнуть фотки или скачать сканы загранпаспортов, уже начинаются заметные трудности по скорости работы и затрате ресурсов на выполнение операций.

Без жёсткой иерархической структуры все операции к доступу данных сводятся просто к использованию ключа для поиска нужного объекта: никаких долгих поисков по подпапкам подпапок и проверок прав доступа. К тому же, когда основное требование к объекту — это его уникальный идентификатор, система легко масштабируется: просто докидывайте объекты, пока хватает места на накопителе. А если не хватает, то расширьте на лету — капризов, как с файловыми системами, которые потребуют зачастую для подобного финта ушами форматирование или хотя бы отмонтировать раздел — не возникает. 

Особенности объектных хранилищ

Из преимуществ можно отметить:

Масштабируемость. Как уже было ранее упомянуто, легко масштабируются горизонтально путем добавления новых узлов и увеличения емкости хранилища. Только поспевайте подносить новые диски или стойки с СХД в серверную комнату.

Гибкость. В объектных хранилищах можно хранить любые типы данных: от небольших текстовых файлов до крупных видеороликов с тем, как 24 часа режут ножницами воду из-под крана. Всё в равной мере легко и просто контролируется через API.

Простота управления. Никаких заморочек с иерархией. Папки, подпапки, автор, права доступа, настраиваемые через chmod, — это всё не про объектные хранилища. Метаданные хранятся вместе с объектами, что позволяет их легко и быстро индексировать.

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

Но есть у объектных хранилищ и недостатки — там, где требуется максимально быстрое выполнение операций или работа с большим количеством маленьких файлов, лучше присмотреться к блочным хранилищам.
Кроме того, объектные хранилища по определению не подходят для работы с данными, где нужна строгая иерархия, различные права доступа, ссылки и т.п. Что, впрочем, никого не останавливает забивать гвозди микроскопом и превращать объектные хранилища в файловые системы. Зачем — история умалчивает.

Применение объектных хранилищ

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

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

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

Облачные приложения. Облачные сервисы и приложения по модели SaaS или PaaS зачастую используют объектные хранилища для хранения пользовательских данных, логов, отчетов и других неупорядоченных данных, которые, как правило, будут лежать бесхозно до второго пришествия.

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

Что такое S3?

С основами мы разобрались, а теперь поговорим о S3 — сервисе, протоколе и технологии, который, по сути, стал синонимичным для словосочетания “объектные хранилища”.

S3 предлагает пользователям простой и масштабируемый способ хранения данных через веб-интерфейс. Он поддерживает различные протоколы доступа, включая REST API, и интегрируется с другими сервисами AWS, такими как: EC2, Lambda и RDS. Благодаря своей надежности, доступности и гибкости, S3 стал стандартом де-факто для облачного хранения данных.

Интересно отметить, что, появившись 14 марта 2006 года, S3 в итоге стал не просто очередным сервисом от AWS, а эталоном для всех объектных хранилищ. Это привело к тому, что многие компании и разработчики начали создавать свои решения совместимые с S3 API, чтобы обеспечить пользователям возможность использовать те же инструменты и приложения, что и с S3, но на других платформах.

Как S3, но не S3

Когда мы говорим «S3», то чаще всего подразумевается не сервис от AWS. S3 постепенно превратилось в то, чем стал «ксерокс» для сканеров или «гугл» для поисковых систем — именем нарицательным. Термин «S3» стал обозначать целый класс объектных хранилищ совместимых с оригинальным стандартом API от Amazon.

Причина тому простая: S3 появился достаточно давно, уже 18 лет прошло, то есть сыграл эффект первопроходца. И ко всему прочему первопроходец в лице Amazon: мало того, что это одна из самых богатых мегакорпораций, так она и на облачном рынке без устали претендует на то, чтобы поджать весь рынок под себя. Из плюсов для простых смертных —API S3 оказался крайне простым и понятным в освоении, что способствовало его широкой адаптации. В результате S3 API превратился в своего рода универсальный язык для взаимодействия с объектными хранилищами.

Однако по сравнению с «ксероксом», где термин просто стал синонимом любого сканера, в случае с S3 — ситуация посложнее. S3-совместимые хранилища следуют общему стандарту. Они реализуют тот же API, что и оригинал от Amazon. То есть, будучи знакомым с оригиналом в экосистеме AWS, вы сможет с легкостью работать и с любыми другими S3-совместимыми хранилищами, будь-то Ceph, MinIO и т.п.

В результате эта стандартизация объектных хранилищ по лекалу S3 привела к интересному эффекту на рынке. Компании, не желавшие полностью зависеть от Amazon, чего, кажется, не хочет даже сама Amazon, или ищущие более экономичные альтернативы, чего также, кажется, хочет и сама Amazon, начали разрабатывать свои собственные объектные хранилища совместимые с S3, но которые просто называют S3-хранилищами. Хотя, если бы речь была не о IT, а о пищевых продуктах, то такую историю бы назвали скорее S3-продукт идентичный натуральному, либо S3-продукт имитация. Это, как если бы производители сканеров не просто назывались «ксероксами», а в значительной мере опирались на документацию и стандарты, используемые в оригиналах от самой Xerox.

Ceph, например, через свой RADOS Gateway (RGW) так хорошо имитирует S3, что большая часть приложений, изначально заточенных для AWS, может спокойно работать с Ceph, как с родным. MinIO пошел еще дальше и сделал совместимость с S3 API своим основным преимуществом, делая миграцию из AWS на собственное self-host решение или к провайдеру, использующим MinIO для S3-хранилищ ещё более бесшовным.

Но стоит понимать, что пусть все эти имитации и используют общий API и стандарт S3, они могут серьёзно различаться в своём бэкенде. Ceph и MinIO — это две совершенно разные истории, с как минимум различной производительностью и уровнем потребления ресурсов.

Рубрика Эээксперименты

Теория — это, конечно, здорово, но давайте перейдем к практике и попробуем написать своё объектное хранилище на Go. Почему? А почему бы и да?

Шаг 1: Создание и настройка проекта

Начнем с основ. Создадим директорию для нашего проекта и инициализируем Go-модуль:

```bash mkdir go-object-storage cd go-object-storage go mod init go-object-storage ```

Шаг 2: Написание кода

Теперь самое интересное. Создадим файл main.go и приступаем к написанию кода:

Архитектура приложения

Наше приложение представляет собой простое объектное хранилище. Давайте разберем его ключевые компоненты:

1 Структура Storage:

``` type Storage struct {     mu    sync.Mutex     files map[string][]byte } ```

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

2 — Методы Save и Load:

```go func (s *Storage) Save(key string, data []byte) func (s *Storage) Load(key string) ([]byte, bool) ```

Эти методы отвечают за сохранение и загрузку объектов. Save сохраняет данные как в оперативной памяти, так и на файловой системе, обеспечивая персистентность данных. Load загружает данные сначала из памяти, а при отсутствии данных там — с диска.

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

3 — HTTP-обработчики:

```go func HandleUpload(w http.ResponseWriter, r *http.Request, storage *Storage) func HandleDownload(w http.ResponseWriter, r *http.Request, storage *Storage) func HandleList(w http.ResponseWriter, r *http.Request, storage *Storage) ```

Эти функции обрабатывают HTTP-запросы для загрузки, скачивания и листинга объектов:

  • HandleUpload загружает данные на сервер и сохраняет их в хранилище.

  • HandleDownload предоставляет клиенту данные из хранилища по запросу.

  • HandleList возвращает список всех объектов, хранящихся в системе.

4 — Функция main:

```go  func main() {...} ```

Инициализирует хранилище и запускает HTTP-сервер.

Само приложение

```go package main  import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" "sync" )  const ( STORAGE_DIR         = "./storage"        // ДИРЕКТОРИЯ ДЛЯ ХРАНЕНИЯ ОБЪЕКТОВ UPLOAD_PREFIX_LEN   = len("/upload/")    // ДЛИНА ПРЕФИКСА ДЛЯ МАРШРУТА ЗАГРУЗКИ DOWNLOAD_PREFIX_LEN = len("/download/")  // ДЛИНА ПРЕФИКСА ДЛЯ МАРШРУТА ЗАГРУЗКИ )  // Storage — структура для хранения объектов в памяти type Storage struct { mu    sync.Mutex            // Мьютекс для обеспечения потокобезопасности files map[string][]byte      // Хэш-таблица для хранения данных объектов }  // NewStorage — конструктор для создания нового хранилища func NewStorage() *Storage { return &Storage{ files: make(map[string][]byte), } }  // Save — метод для сохранения объекта в хранилище func (s *Storage) Save(key string, data []byte) { s.mu.Lock()         // Захватываем мьютекс перед записью defer s.mu.Unlock() // Освобождаем мьютекс после записи  // Сохраняем данные в памяти s.files[key] = data  // Также сохраняем данные на диск err := ioutil.WriteFile(STORAGE_DIR+"/"+key, data, 0644) if err != nil { log.Printf("Ошибка при сохранении файла %s: %v", key, err) } }  // Load — метод для загрузки объекта из хранилища func (s *Storage) Load(key string) ([]byte, bool) { s.mu.Lock()         // Захватываем мьютекс перед чтением defer s.mu.Unlock() // Освобождаем мьютекс после чтения  // Проверяем наличие объекта в памяти data, exists := s.files[key] if exists { return data, true }  // Если объект не найден в памяти, пытаемся загрузить его с диска data, err := ioutil.ReadFile(STORAGE_DIR + "/" + key) if err != nil { return nil, false }  // Если загрузка с диска успешна, кэшируем объект в памяти s.files[key] = data return data, true }  // HandleUpload — обработчик для загрузки объектов func HandleUpload(w http.ResponseWriter, r *http.Request, storage *Storage) { if r.Method != http.MethodPost { http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed) return }  // Получаем ключ (имя объекта) из URL key := r.URL.Path[UPLOAD_PREFIX_LEN:]  // Читаем тело запроса (данные объекта) data, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Ошибка чтения данных", http.StatusInternalServerError) return }  // Сохраняем объект в хранилище storage.Save(key, data)  // Отправляем ответ клиенту w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Объект %s успешно сохранен", key) }  // HandleDownload — обработчик для загрузки объектов func HandleDownload(w http.ResponseWriter, r *http.Request, storage *Storage) { if r.Method != http.MethodGet { http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed) return }  // Получаем ключ (имя объекта) из URL key := r.URL.Path[DOWNLOAD_PREFIX_LEN:]  // Загружаем объект из хранилища data, exists := storage.Load(key) if !exists { http.Error(w, "Объект не найден", http.StatusNotFound) return }  // Отправляем данные объекта клиенту w.WriteHeader(http.StatusOK) w.Write(data) }  // HandleList — обработчик для вывода списка всех объектов func HandleList(w http.ResponseWriter, r *http.Request, storage *Storage) { if r.Method != http.MethodGet { http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed) return }  // Захватываем мьютекс для доступа к хэш-таблице объектов storage.mu.Lock() defer storage.mu.Unlock()  // Создаем список ключей (имен объектов) keys := make([]string, 0, len(storage.files)) for key := range storage.files { keys = append(keys, key) }  // Кодируем список ключей в формат JSON и отправляем клиенту w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(keys) }  func main() { // Проверяем наличие директории для хранения объектов if _, err := os.Stat(STORAGE_DIR); os.IsNotExist(err) { err := os.Mkdir(STORAGE_DIR, 0755) if err != nil { log.Fatalf("Ошибка создания директории %s: %v", STORAGE_DIR, err) } }  // Создаем новое хранилище storage := NewStorage()  // Настраиваем маршруты для обработки HTTP-запросов http.HandleFunc("/upload/", func(w http.ResponseWriter, r *http.Request) { HandleUpload(w, r, storage) }) http.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) { HandleDownload(w, r, storage) }) http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) { HandleList(w, r, storage) })  // Запускаем HTTP-сервер на порту 8080 log.Println("Сервер запущен на порту 8080") log.Fatal(http.ListenAndServe(":8080", nil)) } ```

Компилируем и тестируем

Теперь скомпилируем наше приложение:

```bash go build -o object-storage ```

И запустим наш свежеиспечённый сервер:

```bash ./object-storage ```

Давайте проверим, как работает наше творение. Используем curl для тестирования:

  1. Загрузка объекта:

```bash curl -X POST -d "Hello, World!" http://localhost:8080/upload/hello.txt ```
  1. Скачивание объекта:

```bash curl -O http://localhost:8080/download/hello.txt ```
  1. Получение списка всех объектов:

```bash curl http://localhost:8080/list ```

Вуаля! Мы создали простое объектное хранилище на Go. Конечно, это лишь базовая реализация, и в реальном мире вам понадобится гораздо больше дополнительного функционала поверх. Но это достаточно хорошая отправная точка для дальнейших экспериментов и изучения объектных хранилищ в рамках пэт-проекта.

P.S. Как можно заметить, в проекте активно используется файловая система Linux, хотя ранее весь пост я распинался про то, как объектные хранилища отличаются от файловых систем и вообще «Это другое»(тм). Всё дело в том, что здесь есть нюанс. То была теория, а на практике, объектные хранилища — это *барабанная дробь* – абстракция. Да, не опять, а снова. И если заглянуть в их суть, основу и базу, на нулевом уровне они будут использовать файловые системы для хранения информации.

Итог

История S3 и объектных-хранилищ в целом наглядно показывает старую и регулярно повторяющуюся историю, как технология за счёт возникновения в нужное время и в нужной компании, за счёт эффекта первопроходца и вкупе с монструозными размерами рынка, становится отраслевым стандартом и именем нарицательным для своих сородичей по цеху.
Впрочем, это было бы принижением достижений S3. Успех как сервиса, так и стандарта в значительной мере был также обеспечен простотой и понятной API и экосистемы в целом, что позволило легко и быстро даже новичками на лету интегрировать его в инфраструктуру своих сервисов и приложений. В итоге и без того укрепив позиции S3, как де-факто стандарта облачных хранилищ.

В cdnnow!, как можно догадаться после прочтения этой статьи, мы предоставляем клиентам доступ к различным хранилищам, включая S3-совместимые, на основе нашей реализации с помощью Ceph. Это позволяет гибко управлять данными, используя знакомые инструменты и процессы. А также не бояться, что из-за очередного пакета санкций придётся сказать “пока” вашему S3 хранилищу в рамках экосистемы AWS.


ссылка на оригинал статьи https://habr.com/ru/articles/840874/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *