
Все знают, что такое AWS S3, но немногие осознают масштабы, в которых он работает, и те усилия, которые понадобились, чтобы этого добиться.
По сути, это масштабируемый сервис многопользовательского хранилища с API для сохранения и извлечения объектов, обеспечивающий крайне высокую доступность1 и надёжность2 по относительно низкой цене3.
Масштабы
400+ триллионов4 объектов
150 миллионов запросов в секунду
> 1 ПБ/с пикового трафика
Десятки миллионов дисков
А что лежит в основе всего этого?
Жёсткие диски.
Способы достижения S3 таких масштабов — это настоящее инженерное чудо. Чтобы понять и оценить систему, нужно сначала оценить её базовый строительный блок — жёсткий диск.
Жёсткие диски (HDD) — это старая, уже выходящая из моды технология, во многом вытесненная SSDs. Жёсткие диски хрупки физически, ограничены по IOPS и имеют высокие задержки.
Однако благодаря им возможно то, на что пока неспособны флэш-диски: крайне дешёвая экономика хранения.

За срок нашей жизни HDD улучшились экспоненциально:
Цена: в 6 миллиардов дешевле за байт (с учётом инфляции)
Ёмкость: увеличилась в 7,2 миллиона раз
Размер: уменьшился в 5 тысяч раз
Вес: уменьшился в 1,2 тысячи раз
Но от одной проблемы никак не удавалось избавиться — они ограничены по IOPS. В последние тридцать лет они застряли на отметке 120 IOPS. Задержки тоже уменьшались не так быстро.

Из-за этого HDD становятся медленнее на байт.
Почему HDD медленные?
HDD медленные из-за физики.
Для считывания данных они требуют реального физического перемещения. (В отличие от SSD, использующих электричество, которое движется со скоростью ~50% от скорости света). Вот хорошая визуализация:
Источник: https://animagraffs.com/hard-disk-drive/ (похоже, на момент написания статьи сайт поломан)
Пластины вращаются на шпинделе с частотой примерно 7200 оборотов в минуту (RPM)5.
Механический рычаг (устройство позиционирования головок) с головкой чтения/записи физически движется вдоль пластины и ожидает, пока она провернётся до точного адреса LBA, где находятся данные.
Следовательно, для получения данных с диска требуется две механических и одна электрическая операции.
Физические перемещения:
Поиск — устройство позиционирования головок движется влево или вправо на нужную дорожку пластины
Время поиска по всей пластине: ~25 мс6
Время поиска по половине пластины (среднее): ~8-9 мс7
Вращение — ожидание поворота шпинделем диска, пока оно не будет соответствовать точному адресу на дорожке пластины
Полная задержка вращения: ~8,3 мс8
Половинная задержка вращения (средняя): ~4 мс
И электрический процесс:
Скорость передачи — процесс передачи головкой битов с пластины через шину в память (внутренний кэш диска)
Чтение 0,5 МБ: в среднем ~2,5 мс9
Последовательный ввод-вывод
Жёсткие диски оптимизированы под паттерны последовательного доступа.
Чтение/запись байтов, расположенных на диске последовательно, выполняется быстро. Естественное вращение пластины проходит через блок байтов, и избыточные операции поиска выполнять не нужно (устройство позиционирования остаётся неподвижным).
Простейшая и самая популярная структура данных с последовательным доступом — это журнал (лог). Популярные распределённые системы наподобие Apache Kafka построены на его основе, и благодаря паттернам последовательного доступа они выжимают из дешёвого оборудования отличную производительность.
Неудивительно, что сам бэкенд хранилища S3 (ShardStore) основан на журнально-структурированном дереве со слиянием (LSM).
По сути, операции записи для S3 просты. Так как запись на диск выполняется последовательно, сервис пользуется преимуществами скорости HDD. (Аналогично с Kafka: уверен, что его разработчики группируют ожидающие PUT, чтобы выжать из диска больше последовательной производительности при помощи добавления к логу.)10
Однако с чтением всё сложнее. AWS не может контролировать, какие файлы запросит пользователь, поэтому при их передаче должен перемещаться по диску.
Произвольный ввод-вывод
В среднем случае чтение из произвольной части диска требует половину полного физического перемещения.
Средняя задержка чтения — это сумма обоих средних физических перемещений плюс скорость передачи. В целом получается примерно 16 мс на считывание 0,5 МБ произвольного ввода-вывода с диска. Это очень медленно.
Так как секунда состоит из 1000 миллисекунд, мы достигнем всего около 32 МБ/с произвольного ввода вывода с одного диска.
Поскольку узким местом стали физические перемещения, уже почти тридцать лет у дисков сохраняется приблизительно одна и та же задержка произвольного ввода-вывода.
Они попросту неэффективны при работе с паттернами произвольного доступа. Именно в этом случае выбирают SSD. Но если нужно хранить огромные объёмы данных, то стоимость SSD становится неподъёмной11.
И это становится проблемой в случае S3 — системы с произвольным доступом12, в то же время хранящей огромные массивы данных.
Однако разработчики S3 нашли способ решения — сервис обеспечивает приемлемые задержки13 и выдающуюся14 пропускную способность, компенсируя при этом физические ограничения.
Необходимость параллелизма
S3 решает эту проблему при помощи масштабного параллелизма.
Сервис распределяет данные на множество (огромное!15) жёстких дисков, чтобы можно было обеспечить высокую пропускную способность чтения при параллельном использовании каждого диска.
Хранение файла на 1 ТБ на одном HDD приводит к ограничению скорости чтения максимальной пропускной способностью этого единственного диска (~300 МБ/с16).
Разбиение того же файла на 1 ТБ на 20 тысяч HDD позволяет считывать его параллельно с суммарной пропускной способностью всех HDD (порядка ТБ/с).
Это реализуется при помощи кодирования со стиранием (Erasure Coding).
Erasure Coding
Схемы с избыточностью часто применяются в системах хранения.
Чаще всего они связаны с устойчивостью данных — защитой от утери данных в случае аппаратных сбоев.
S3 использует Erasure Coding (EC). Оно разбивает данные на K частей (шардов) с M избыточных «контрольных» шардов . EC позволяет воссоздавать данные из любых K шардов по K+M шардам.
Команда S3 сообщила, что она использует схему «5 из 9». Она разбивает каждый объект на 9 частей — 5 обычных шардов (K) и 4 контрольных шарда (M)
При таком подходе приемлемы до 4 потерь. Для доступа к объекту требуется 5/9 шардов.

Эта схема помогает S3 найти срединный баланс — она отнимает не так много лишней ёмкости дисков, однако обеспечивает гибкий ввод-вывод.
Благодаря EC сохраняется в 1,8 раза больше исходных данных.
Наивной альтернативой была бы трёхсторонняя репликация, увеличивающая размер данных втрое. Когда речь идёт об экзабайтах данных, лишние 1,2 раза становятся важными.
EC предоставляет 5 из 9 возможных источников чтения, и это серьёзная защита от узких мест узлов
При трёхсторонней репликации было бы всего три источника, то есть устойчивость обеспечивалась бы только максимум для двух запаздывающих узлов. Если горячими оказываются все три узла, будет страдать производительность. EC «5 из 9» устойчива к четырём отстающим узлам (в два раза больше).
Кроме того, 5 из 9 источников обеспечивают гораздо более быстрый ввод-вывод благодаря параллелизму и шардингу данных17
Недооценённый аспект EC заключается именно в его способности распределять нагрузку. Такие схемы распределяют горячие точки системы и позволяют им гибко и сбалансированно управлять трафиком чтения. А поскольку шарды малы, выполнение хеджированных запросов18 для избегания тормозящих узлов гораздо малозатратнее, чем полные реплики.
Параллелизм в действии
В S3 есть три основных способа использования параллелизма:
С точки зрения пользователя: загрузка/скачивания файла блоками.
С точки зрения клиента: отправка запросов множеству разных фронтенд-серверов.
С точки зрения сервера: сохранение объекта на множестве серверов хранения.
Любая часть сквозного маршрута может оказаться узким местом, поэтому важно всё оптимизировать.
1. Между фронтенд-серверами
Вместо выполнения запросов всех файлов через одно соединение с одной конечной точкой S3, пользователей стимулируют открывать необходимое количество соединений. Это происходит незаметно для них в коде библиотек при помощи внутреннего пула HTTP-соединений.
При таком подходе используется множество различных конечных точек распределённой системы, благодаря чему ни одна отдельная точка в инфраструктуре не становится слишком горячей (например, фронтенд-прокси, кэши и так далее)
2. Между жёсткими дисками
Вместо хранения данных на одном жёстком диске система при помощи EC разбивает их на шарды и распределяет по множеству бэкендов хранения.
3. Между операциями PUT/GET
Вместо того, чтобы отправлять один запрос через один поток и HTTP-соединение, клиент разбивает его на десять частей и загружает каждую параллельно19.
Запросы PUT поддерживают многосоставные загрузки, которые AWS рекомендует применять для максимизации пропускной способности благодаря множественным потокам.
Запросы GET поддерживают HTTP-заголовок, обозначающий, что ведётся считывание только определенного диапазона объекта (byte-ranged GET). AWS тоже рекомендует его для достижения повышенной общей пропускной способности вместо одиночного запроса чтения объекта.
Загрузка 1 ГБ/с на один сервер может быть проблематичной, однако загрузка 100 блоков по 10 МБ/с на 100 разных серверов очень практична.
Эта простая идея даёт широкие возможности.
Избегание горячих точек
При этом S3 сталкивается со сложной проблемой. У сервиса есть десятки миллионов жёстких дисков, сотни миллионов параллельных запросов в секунду и сотни миллионов запрашиваемых за секунду шардов EC.
Как эффективно распределять эту нагрузку, чтобы избежать перегрева отдельных узлов/дисков?
Как мы говорили выше, один диск может обеспечить около 32 МБ/с произвольного ввода-вывода. Понятно, что это запросто может стать узким местом. Не говоря у же о том, что дополнительная работа по обслуживанию системы, например, перебалансировка данных для более эффективного распределения, тоже снизит драгоценный уровень ввода-вывода диска.
Образование горячих точек в распределённой системе опасно, потому что оно с лёгкостью может привести к эффекту домино и деградации всей системы20.
Не стоит и говорить, что S3 стремится крайне тщательно распределять данные. Его решение тоже выглядит обманчиво простым:
Рандомизируем расположение данных при их потреблении
Постоянно перебалансируем их
Масштабируемся без каких-либо проблем
Шардинг перетасовыванием и степень двойки
Вопрос изначального расположения данных очень важен для производительности. Их дальнейшее перемещение более затратно.
К сожалению, в момент записи мы не можем надёжным образом понимать, насколько часто будет выполняться доступ к сохраняемым данным.
При таких масштабах невозможно и с идеальной точностью узнать наименее нагруженный HDD для размещения данных. Нельзя иметь синхронную общую картину, если обслуживаешь сотни миллионов запросов в секунду к десяткам миллионов дисков. При таком подходе также возникает риск корреляции нагрузки — размещения схожих нагрузок вместе и их одновременного пикового доступа к ним.
Важнейшим выводом здесь стало то, что в таком сценарии лучше подходит случайный выбор.
Именно так AWS намеренно встраивает декорелляцию в свои системы:
Запрос PUT выбирает произвольный набор дисков
Следующий PUT, даже если он нацелен на тот же ключ/бакет, выбирает другой набор почти случайных дисков.
Это выполняется при помощи так называемого случайного выбора по степеням двойки:
Случайный выбор по степеням двойки (Power of Two Random Choices): хорошо изученное явление в области балансировки нагрузок — выбор между наименее нагруженными из двух совершенно случайных узлов обеспечивает гораздо лучшие результаты, чем случайный выбор лишь всего одного узла.
Перебалансировка
Ещё одно важное наблюдение заключается в том, что более новые блоки данных горячее, чем старые.
Доступ к свежим данным выполняется чаще. С их устареванием частота снижается.
Поэтому использование всех жёстких дисков постепенно охлаждается в процессе заполнения данными и устаревания этих данных. В результате ёмкость дисков заполняется полностью, а ёмкость ввода-вывода остаётся незадействованной.
AWS вынужден проактивно выполнять перебалансировку, извлекая холодные данные с дисков (чтобы освобождать место), и записывая их (чтобы использовать «бесплатный» ввод-вывод).
Перебалансировки данных также нужны при добавлении в S3 новых стоек дисков. Каждая стойка содержит 20 ПБ21, а каждый диск в ней абсолютно пуст. Система должна проактивно распределять нагрузку по новой ёмкости, поэтому S3 постоянно перебалансирует данные.
Спокойное масштабирование
Последнее наблюдение, вероятно, самое неочевидное: чем больше становится система, тем она предсказуемее.
В процессе роста S3 сервис AWS столкнулся с так называемой декорелляцией нагрузок. Это явление сглаживания нагрузок после их накопления в достаточно большом масштабе. Хоть их пиковый спрос растёт в размерах, дельта пика и среднего снижается.
Это вызвано тем, что нагрузки хранения по природе импульсные — они требуют многого за раз, а потом долгое время (месяцами) остаются в покое.

Так как моменты импульсов независимых нагрузок не совпадают, чем больше становится нагрузок, тем сильнее заполняются эти спокойные области и тем предсказуемее становится накопление данных системой.
Подведём итог
AWS S3 — масштабный многопользовательский сервис хранения данных. Это гигантская распределённая система, состоящая из множества по отдельности медленных узлов, в сумме позволяющих получать доступ к данным быстрее, чем один узел. S3 обеспечивает это благодаря:
Сильной сквозной параллелизации (пользователь, клиент, сервер)
Трюкам с балансировкой нагрузки, например, случайный выбор по степени двойки
Распределению данных при помощи erasure coding
Снижению хвостовой задержки при помощи хеджированных запросов
Экономике многопользовательского хранения в мировых масштабах
Изначально S3 был сервером, оптимизированным под хранение резервных копий, видео и изображений онлайн-магазинов, но постепенно получил поддержку, позволившую стать основной системой хранения для аналитики и машинного обучения.
Сегодня нарастают тенденции построения целых проектов инфраструктур данных на основе S3. Это даёт им преимущества узлов без хранения состояния (упрощается масштабирование, требуется меньше управления) и аутсорсинга сервису S3 различных проблем надёжности, репликации и балансировки нагрузок. К тому же это снижает затраты на облако22.
Примечания
1. За всё время своего существования S3 никогда не был офлайн больше 5 часов. Инцидент, произошедший 8 лет назад, затронул всего один регион AWS (из 38). Он считается одной из самых важных аварий AWS за всё время.
2. S3 позиционирует себя как спроектированный с расчётом на 11 девяток надёжности. Здесь нужно быть аккуратными с формулировками — сервис юридически не гарантирует надёжности на 99,999999999%. На самом деле, Amazon юридически не даёт никакого SLA надёжности.
3. Под относительно низкими ценами я подразумеваю стоимость относительно других хранилищ, которые можно купить в AWS. S3 по-прежнему стоит около $21,5-$23,5 за ТБ хранилища. На самом деле, S3 не снижал свои цены в течение 8 лет, несмотря на то, что за это время стоимость HDD упала на 60%. Приблизительно посчитав, сколько мне стоило бы создать свой голый S3, я получил цену примерно $0,875 за TB хранилища (в 25 раз дешевле). Хостинг на Hetzner стоил бы около $5,73 за ТБ.
4. 400 000 000 000 000
5. Первым диском на 7200 RPM стал Seagate Barracuda, выпущенный в 1992 году. Сегодня показатель RPM в основном остался таким же. Существуют большие диски с примерно 15 тысячами RPM, но они не очень распространены.
6. Во многом зависит от диска; 20-30 мс — это диапазон для диска на 7200 RPM.
7. Стоит отметить, что это не половина максимального времени поиска, а ближе к 1/3. Реальный показатель примерно равен 0,32-0,35 (интересная исследовательская статья на эту тему).
8. 7200 оборотов в минуту == 7200 оборотов за 60000 миллисекунд == 8,33 мс на оборот.
9. В среднем HDD имеют скорость передачи около 170-200 МБ/с; 200 МБ/1000 мс == 0,2 МБ/мс; 0,5 МБ == ~2,5 мс; они просто не оптимизированы под такой произвольный доступ.
10. Платформа Kafka любит группировать элементы. Она выполняет группировку на клиенте (ожиданием), в протоколе (объединяя элементы) и на сервере (храня кэш страницы и используя асинхронный сброс данных операционной системы). Это обеспечивает настолько очевидный прирост производительности, что S3 обязан был реализовать что-то подобное в своих системах бэкенда и хранения.
11. Однако ситуация начинает медленно, но верно меняться для определённых пороговых значений данных. Всего за последние 15 лет SSD существенно подешевели.
12. В целом, S3 демонстрирует произвольный доступ. Пользователь при помощи PUT/GET может работать с блобами любого размера. Поэтому среднестатистический диск S3 состоит из блобов тысяч пользователей. Если все они одновременно попытаются получить доступ к своим данным, до диск попросту не сможет обслужить каждый запрос.
13. Бенчмарков в этой сфере не очень много. Тестируя файлы на 0,5 МБ, я получал показатели записи ~140 мс p99 и 26 мс p50, чтения 86 мс p99. Файлы большего размера, предположительно, имеют больший p99, а показатели могут варьироваться в течение недели.
14. Как минимум, одна только Anthropic перемещает десятки терабайт в секунду. Вероятно, существуют и гораздо большие нагрузки отдельных заказчиков, ведь S3 передаёт больше петабайта в секунду!
15. AWS рассказала, что данные десятков тысяч её пользователей распределены по более чем миллиону дисков. Это прекрасный пример того, как многопользовательская система при достаточно больших масштабах может превратить нечто финансово невозможное в приемлемое по стоимости. Один пользователь не смог бы самостоятельно развернуть миллион HDD, но при общем доступе в многопользовательской системе это становится на удивление дешёвым.
16. Например, современный дешёвый HDD на 20 ТБ может обеспечить передачу данных в максимум 291 МБ/с: https://www.westerndigital.com/products/internal-drives/wd-gold-sata-hdd?sku=WD203KRYZ; учтите, что это тоже маркетинговые показатели.
17. Объясню это подробнее. Допустим, у нас есть объект на 100 МБ, и мы хотим записывать/считывать его за 1 секунду (для простоты). При трёхкратной репликации понадобилось бы три узла, обеспечивающих операции чтения или записи на 100 МБ/с. В случае EC «5 из 9» достаточно 9, обеспечивающих чтение и запись на 20 МБ/с. (Каждый шард содержит 20 МБ (100МБ/5), потому что данные разделены на 5 обычных шардов, а остальные 4 — это контрольные «копии» тоже по 20 МБ каждая.)
18. Концепция хеджированного запроса (hedge request) получила популярность благодаря статье Google «The Tail at Scale». По сути, в ней говорится о том, что разветвляющиеся запросы (когда корневой запрос приводит ко множеству субзапросов, например, как в S3 выполняются запросы GET к множественным шардам) могут существенно снизить их хвостовую задержку благодаря спекулятивной отправке дополнительных запросов (например, если для построения объекта нужно 5 субзапросов, то отправляем 6). Этот дополнительный запрос отправляется только в том случае, когда один из субзапросов превышает обычную задержку p95. Эта концепция также используется в клиентских библиотеках S3.
19. Интересная деталь заключается в том, что каждую часть многосоставной загрузки тоже можно подвергнуть Erasure Coding «5 из 9». То есть один объект, загружаемый через многосоставную загрузку, может состоять из сотен шардов.
20. Если за один момент времени к одному диску обращается слишком много запросов, то диск начинает простаивать из-за исчерпания его ограниченного ввода-вывода. Это накапливает хвостовую задержку запросов, зависящих от диска. В свою очередь, эта задержка влияет на другие операции, например, на запись. Всё это усиливается вверх по стеку в других компонентах за пределами диска. Если оставить проблему без внимания, то она может вызвать каскадный эффект, существенно замедляющий всю систему.
21. У меня нет опыта работы в дата-центрах, поэтому очень радуюсь, когда Amazon публикует фотографии физических дисков. Ниже показан пример такой стойки дисков. Она состоит из 1000 дисков по 20 ТБ каждый. Говорят, что эта стойка весит больше, чем автомобиль, поэтому Amazon вынуждена укреплять полы в своих дата-центрах.

22. Для Apache Kafka (с которой я знаком больше всего) возник так называемый тренд «Diskless», при котором путь записи использует S3 вместо локальных дисков. Это обеспечивает компромисс: повышение задержек в обмен на снижение затрат (на 90% [!]). Существуют и другие подобные проекты — Turbopuffer (Vector DB на основе S3), SlateDB (встраиваемая LSM на S3), Nixiesearch (Lucene на S3). В целом, похоже, что все проекты инфраструктур данных стремятся максимально переложить нагрузку на хранилища объектов. (Clickhouse, OpenSearch, Elastic). До Diskless Kafka похожим образом использовала двухуровневый подход, при котором холодные данные сгружались в S3 (обеспечивая десятикратную экономию затрат на хранение)
