Разбираемся, что такое 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 для тестирования:
Загрузка объекта:
```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.