Всем привет! Я Александр Киров, технический менеджер Yandex Cloud, и за время работы с объектными хранилищами я встречал немало «подводных рифов» на пути к быстрому и эффективному хранению.

Мы уже рассказывали, как сервис Yandex Object Storage устроен изнутри. Поэтому в статье я лишь вкратце остановлюсь на внутреннем устройстве, и более подробно покажу, где чаще всего проседает производительность при работе с S3-совместимым хранилищем, — на примерах из реальных кейсов технической поддержки.

Немного об оптимизации работы на стороне провайдера и на стороне клиента

Мой коллега Павел Левдик рассказывал, как устроена наша собственная реализация S3. Для того чтобы не ограничивать пользователей искусственными лимитами и уметь обрабатывать сотни тысяч RPS ежесекундно, мы отмасштабировали инфраструктуру, продумали архитектуру, реализовали умное шардирование для хранения метаданных и многое другое. Но это не избавляет техподдержку от обращений на тему «тормозящего S3» — потому что на стороне провайдера мы можем контролировать не всё.

При этом логично предположить, что пользователям интересно именно то, как быстро их приложение работает с S3. Любой пользователь оценивает, за какое время его приложение выполнит запрос получения/записи данных к S3, — и время выполнения будет оцениваться именно на стороне приложения.

Но мы понимаем, что любой такой запрос проходит несколько этапов:

  • Отправка запроса.

  • Выход запроса в сеть.

  • Балансировка запроса.

  • Получение запроса S3-хранилищем.

  • Проверка прав доступа.

  • Запись объекта в хранилище.

  • Возвращение ответа.

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

Кейс первый: классический «медленный S3» 

Дано: Клиент жалуется на низкую скорость нашего S3 и прикладывает скриншот из CyberDuck, который показывает скорость загрузки в 32 MB/s. Клиент негодует и доказывает, что купил интернет на 1 Gbit, приводит математические факты, что скорость должна быть никак не меньше 100MB/s. Следовательно: медленным он считает именно наш сервис.

Что проверили в первую очередь. Плохие каналы связи — обычно довольно очевидная причина, которую клиенты, как правило, уже проверили сами, перед тем как обратиться в поддержку. А если клиенты работают внутри Yandex Cloud, то они пользуются высокоскоростной программно‑определяемой сетью без выхода в дикий интернет. Так что эту причину мы отмели быстро.

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

Менее очевидные проверки. Самыми распространёнными причинами низкой производительности в нашей практике с S3 является малое выделение ресурсов. Приложение, работающее с S3 на больших скоростях, прилично потребляет CPU и RAM.

В самом простом случае мы любим показывать клиентам, как увеличение с 2 до 8 vCPU улучшает показатели производительность больше, чем вдвое, — но это тоже довольно очевидно. Гораздо более интересны случаи, когда под виртуальную машину, работающую с S3, может быть выделено неполное количество CPU. Например, время CPU может разделяться при запуске приложения внутри контейнера. В практике было несколько случаев, когда ВМ по конфигурации выглядела идеально, но показывала плохую производительность работы с S3 при потоковой работе с данными. Оказывалось, что обработчик был запущен внутри контейнера, который получал только 50% одного процессорного ядра и тормозил.

Итого, с точки зрения CPU и памяти в этом кейсе мы вместе с клиентом проверили, что:

  • В ОС, где запущен тест, достаточно ядер CPU, и они не загружены другими приложениями. Если тестирование проводится внутри виртуальной машины, то лучше добавить ядра vCPU, чтобы исключить возможные проблемы на этой стадии. Для полноценного многопотчного тестирования желательно иметь не менее 8 vCPU.

  • Ядра, выделенные под машину с тестом, не разделяются с другими виртуальными машинами. Это поможет предотвратить возможное влияние соседних виртуальных машин на работу теста.

  • Запуск теста не осуществляется на прерываемых машинах, это является ещё одной частой проблемой. Если нагрузка запускается внутри контейнера K8s, то важно убедиться, что выделено 100% CPU.

  • Также важно обращать внимание не только на гостевую ОС (чтобы не было других потребителей CPU), но и вовне (чтобы сама виртуальная машина не останавливалась, не подтормаживала, её выполнение не прерывалось).

Но это ещё не всё. Поскольку S3 работает с данными, эти данные нужно откуда‑то взять, чтобы отправить в S3. Yandex Object Storage способен отдавать и принимать десятки гигабайт в секунду (именно гигабайт в секунду, а не гигабит). Но не каждое локальное хранилище будет готово переварить большой поток данных. Так что в случае значительных объёмов стоит посмотреть производительность локальной дисковой подсистемы, откуда берутся данные для передачи в S3-хранилище.

При этом важно убедиться, что локальный диск, где размещены тестовые данные, значительно быстрее, чем ожидаемая скорость S3. Например, если вы загружаете данные с HDD в S3, а S3 отрабатывает любые запросы мгновенно, то общий показатель результата теста будет не выше производительности HDD (~100 RPS, ~100 МБ/c).

В чём оказалась проблема. Итак, для работы с S3 важно использовать быстрые диски локальной инфраструктуры, особенно при работе с большим потоком данных. Для Yandex Cloud — это network-ssd-io-m3 (высокопроизводительный SSD‑диск). Для тестирования производительности S3 также можно использовать RAM‑диски, чтобы хранить данные для теста и считывать их в оперативную память, до того как вы будете выполнять замер производительности S3. Главное — выбирайте тип диска такой производительности, которая будет намного выше предполагаемого результата скорости работы с S3.

И несмотря на небольшой размер компании, в этом и крылась причина медленной работы в таком кейсе. Данные медленно считывались с флеш‑носителя, чья производительность последовательного доступа составляла ~30 MB/s.

Кейс второй: свой нагруженный FSS

Дано: Допустим, клиент создаёт своё приложение наподобие некоторого File Sync and Share (FSS), где есть несколько тысяч конечных пользователей. Например, это может быть фотосток. Его пользователи загружают данные в приложение, скачивают оттуда файлы и смотрят список объектов, которые им доступны.

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

  • пользователь загружает данные через приложение;

  • приложение состоит из бизнес‑логики, которая принимает эти данные;

  • балансировщики входного трафика будут направлять данные на бизнес‑логику;

  • аутентификация и авторизация пользователей тоже на уровне бизнес‑логики: они прописываются в правилах взаимодействия и зависимостей объектов и пользователей;

  • бизнес‑логика, в свою очередь, обращается к S3 от одного сервисного аккаунта.

Что здесь может пойти не так. В этом случае данные проходят через бизнес‑логику. Хорошо, если этих данных не очень много. А если мы имеем дело, например, с видеохостингом, и его конечные пользователи записывают в хранилище видеоконтент, то весь этот тяжелый трафик от тысяч пользователей будет идти через бизнес‑логику: придётся создавать больше виртуальных машин, будет сложнее балансировать нагрузку, а Control Plane будет страдать от перегрузки.

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

Как можно справиться с «узкими местами». Выгоднее и удобнее в этой ситуации разделить Control Plane и Data Plane. Тогда управляющие функции приложения, аутентификация и авторизация осуществляются по старой схеме с бизнес‑логикой: через балансировщики и систему управления учётными данными (Identity and Access Management, IAM). А уже затем после всех проверок пользователю предоставляются креды с возможностью загрузить данные напрямую в хранилище. В таком случае:

  • виртуальные машины и балансировщики будут менее нагружены;

  • производительность повысится.

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

4 способа повысить безопасность работы с данными. Самый простой подход к управлению правами доступа в S3: один конечный пользователь приложения — это один S3-бакет. Но количество бакетов ограничено и для каждого нужно выделить слой метаданных, а также вписать его во множество политик (Bucket policy, Life circle). Поэтому в реальном проекте данные нескольких конечных пользователей могут храниться в одном бакете. Такие бакеты будем называть коммунальными. Вот как можно обеспечить их безопасность.

  1. PUT и GET по pre-signed URL
    S3 поддерживает pre-signed URL — подписанные запросы, которые позволяют не только получать объекты, но и загружать их в хранилище. Бизнес-логика выписывает для каждого запроса специальную ссылку в формате KeyID + Signature. Приложение может сразу применить подписанную ссылку на размещение или скачивание файла. Поскольку приложение будет обращаться напрямую к S3, проксировать данные не потребуется.

    Для генерации подписи запроса нужны только ключи к S3, запросов в само хранилище не требуется. Пример создания подписи с помощью AWS CLI для получения ссылки объекта logo_full.svg из бакета yandex-cloud:

    aws --endpoint-url=https://storage.yandexcloud.net s3 presign --expires-in 2592000 s3://yandex-cloud/s3-useful-tips.pdf
    https://storage.yandexcloud.net/yandex-cloud/s3-useful-tips.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=qb7B3O9FWTzU079i_XAI%2F20240811%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20240811T181836Z&X-Amz-Expires=2592000&X-Amz-SignedHeaders=host&X-Amz-Signature=1c8eb7806ff7476b2f06c08f83fea8f192c18bf92f14938ef1a232e12b6c960d

    Более того, бизнес-логика сможет контролировать в подписанной ссылке на загрузку объекта, какие именно данные будут заливаться в S3. Можно не только создать подписанную ссылку, ограниченную для загрузки данных по определённому ключу, но и задать размер загружаемого файла. Можно даже задать MD5 от загружаемого контента.

    Например, вот эта ссылка позволит загрузить объект в мой бакет yandex-cloud до 10 сентября 2024 года, но только файл из предыдущего примера с MD5:

    curl -i -XPUT -H 'Content-MD5: IhLL6etET26a+oRIOh6h3A==' --upload-file s3-useful-tips.pdf 'https://storage.yandexcloud.net/yandex-cloud/habr/s3-useful-tips.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=YCAJEx9qlb5lxU6Sf56hJRDql%2F20240811%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20240811T182910Z&X-Amz-Expires=2592000&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bhost&X-Amz-Signature=efbe7b9f155b6ce28f6946a11453faeb4dc800d5f4cf271cb13c760b7825fa84'

    Вы можете попробовать позагружать объекты.

    К сожалению, такую ссылку нельзя получить через AWS CLI, но несложно это сделать в программном коде. Например в Python:

    response = s3.generate_presigned_url(
        ClientMethod='put_object',                     # загрузка объекта
        Params={
            'Bucket': 'yandex-cloud',                  # имя бакета
            'Key': 'habr/s3-useful-tips.pdf',          # ключ объекта куда загружаем
            'ContentMD5': 'IhLL6etET26a+oRIOh6h3A==',  # #openssl dgst -md5 -binary filename | base64
            'ContentLength': 3273649,                  # размер объекта
        },
        HttpMethod='PUT',                              # загрузка объекта методом PUT
        ExpiresIn=2592000,                             # Срок жизни ссылки 1 месяц
    )
  2. Secure token в CDN
    Этот способ работает на уровне CDN, а не S3. Он подойдёт для тех, кто планирует только раздавать контент и не собирается получать данные от пользователей.

    Механика такая же, как и для подписанных ссылок для получения данных, только на уровне CDN. CDN‑сервер получает токен и сверяет его по хешу с секретным ключом CDN‑ресурса и переданными в HTTP‑запросе данными, а потом разрешает или запрещает доступ к файлу.

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

  3. Бакетные политики
    Политики регулируют права доступа на бакеты, объекты и их группы, как для пользователей, так и для системных аккаунтов. Политика срабатывает, когда пользователь отправляет запрос к какому-либо ресурсу.

    С помощью политики можем ограничить доступ к бакету по IP или, наоборот, расшарить данные по префиксу в бакете и выдать определённым пользователям права на запись или чтение. Пример такого правила:

    {
      "Effect": "Allow",
      "Principal": {
        "CanonicalUser": "<идентификатор_пользователя>"
      },
      "Action": "*",
      "Resource": [
        "arn:aws:s3:::<имя_бакета>/*",
        "arn:aws:s3:::<имя_бакета>"
      ],
      "Condition": {
        "StringLike": {
          "aws:referer": "https://console.yandex.cloud/folders/*/storage/buckets/your-bucket-name*"
        }
      }
    }

    Тут есть несколько ограничений: размер пользовательских политик не может превышать 20 КБ, а пользователи и сервисные аккаунты должны существовать в IAM. Подход не очень удобный, если у приложения миллионы конечных пользователей — каждому придётся создавать аккаунт.

  4. STS — Security Token Service
    Обойти создание пользователей в IAM всё-таки можно. Для этого достаточно воспользоваться временными токенами, совместимыми с AWS S3 API. В этом случае мы создаём разрешения на получение или запись объекта для определённых префиксов. 

    Security Token Service — компонент сервиса Identity and Access Management. Он помогает объединить множество пользователей в одном сервисном аккаунте с помощью короткоживущих токенов. Временные ключи позволяют гранулярно разграничить доступы в бакеты Object Storage. 

Что ещё важно учесть 

Оптимизация на этапе получения запроса S3-хранилищем. Когда запрос поступает на прокси‑сервер (в нашем случае — nginx), он передаётся в компонент S3-proxy, который отвечает за разбор запросов протокола AWS S3. Важно, что прежде чем хранилище получит запрос, он должен полностью поступить на nginx.

В чём сложность: если устанавливать отдельное соединение с S3 на каждый запрос, то будут возникать множественные TLS‑сессии. Если вдобавок у сети высокая задержка, то это сильно увеличит время на каждый TLS‑handshake.

Чтобы избежать множественных TLS‑сессий, можно посылать несколько HTTP‑запросов внутри одного соединения TLS, используя keepalive‑опцию S3-клиента. Если требуется скопировать несколько мелких объектов, эта оптимизация позволяет значительно ускорить получение запроса хранилищем.

Ускорение работы S3 за счёт экономии потребления трафика и других фишек GeeseFS. AWS CLI — основной инструмент работы с S3, но не единственный. Вы можете выбрать другие решения для работы с хранилищем.

В прошлой статье мы уже упоминали GeeseFS — высокопроизводительный FUSE‑клиент для монтирования бакетов. Инструмент позволяет работать с хранилищем S3 как с обычной файловой системой. GeeseFS экономит потребление трафика, поддерживает частичное изменение объекта и дозапись. Кроме этого, у GeeseFS реализован механизм кеширования, который обеспечивает ускоренное получение данных за счёт параллелизации запросов.

С помощью частичного изменения можно хранить в бакете данные в виде единого файла и периодически дописывать в него информацию. Опция монтирования enable‑patch помогает увеличить производительность GeeseFS за счёт передачи в S3-хранилище только тех изменений, которые вы внесли в данные.

За счёт параллелизма GeeseFS может показывать лучшую производительность по сравнению с AWS CLI.

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

Эта функциональность — необязательный пункт стандарта S3-хранилища, но согласованность данных важна клиентам в сценариях синхронизации данных.

Такая синхронная запись в три зоны доступности требует времени. Вот пример стандартного времени, которое требуется для сохранения данных в Yandex Object Storage от момента, когда nginx proxy полностью получил PUT‑запрос от клиента и провёл проверку авторизации, до ответа клиенту.

Размер объекта

Медианное время

Комментарий

0 КБ

15 мс

Создание объекта. Запись об объекте и его метаданные будут храниться в трёх зонах доступности, поэтому даже на создание пустого объекта уходит время на синхронный коммит в PostgreSQL.

4 КБ

24 мс

Создание объекта и запись его данных на диски в трёх зонах доступности.

512 КБ

50 мс

Создание объекта и запись его данных на диски в трёх зонах доступности.

50 МБ

1.2–1.5 с

Большие объекты занимают больше времени из‑за передачи и записи данных на диски.

При размещении и хранении данных в одной зоне доступности этот процесс был бы быстрее, но хранение было бы менее надёжным. Так что при реализации сервиса мы отдали приоритет именно надёжности.

Наш топ рекомендаций по работе с производительностью S3

Итак, по нашей статистике, есть несколько основных этапов на пути запроса от клиента, где производительность страдает больше всего. Вот как избежать снижения скорости работы с S3-хранилищем:

  • Продумать, как можно избежать лишних нагрузок на бизнес‑логику, отделив Data Plane от Control Plane.

  • Выделять необходимые ресурсы CPU/RAM и сети.

  • Отслеживать скорость дисковой подсистемы с данными для загрузки в S3.

  • Минимизировать влияние сети и сетевых задержек.

  • Использовать keepalive для избежания пересоздания TLS‑сессий.

Буду рад ответить на вопросы и обсудить, какие способы повышения производительности S3 используете вы.

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


  1. iliks84
    13.08.2024 06:11

    Спасибо. По поводу последней таблицы: как-то не бьется с данными в начале статьи. Из последней строки мы видим, что скорость записи получается где-то ну 60 МБ в секунду на кластер из трех зон. То есть скорость hdd. При этом в начале вы пишете, какие космические скорости вы способны развивать и при тормозах затуп скорее где-то у вас как у клиента.

    Или в начале было сугубо про чтение?


    1. alkir Автор
      13.08.2024 06:11
      +1

      В таблице в конце статьи приведены характерные задержки (latency) при обработке единичного запроса PUT. С S3 хранилищем можно и нужно работать с параллельной нагрузкой. Например при загрузке объекта использовать multipart upload (многосоставную загрузку объекта во множество параллельных потоков). Суммарно несколько потоков загрузки одного объекта дадут производительность кратно выше. За счёт этого и достигается линейное масштабирование производительности S3. Кстати, тут можно посмотреть на эксперименты с параллельными запросами и как это влияет на скорость загрузки в S3 на реальных примерах: https://www.youtube.com/watch?v=nBYQqLUyYXo