Привет, Хабр! С вами снова Матвей Мочалов из 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 для тестирования:
-
Загрузка объекта:
```bash curl -X POST -d "Hello, World!" http://localhost:8080/upload/hello.txt ```
-
Скачивание объекта:
```bash curl -O http://localhost:8080/download/hello.txt ```
-
Получение списка всех объектов:
```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/
Добавить комментарий