Привет, меня зовут Дмитрий Светляков, я руководитель группы эксплуатации облачной платформы ВКонтакте. Занимаюсь администрированием 12 лет, и более 6 из них — контейнерными технологиями.

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

Я расскажу: 

  • о базовом курсе 101 по OCI-образам;
  • о проблемах, с которыми мы столкнулись;
  • что такое Peer-to-Peer OCI-дистрибьюция;
  • как повысить отказоустойчивость доставки;
  • в качестве бонуса — как ускорить распаковку образов;
  • чего мы добились и как планируем развиваться дальше.

Статья написана по мотивам моего выступления на VK Kubernetes Conference, вы можете посмотреть его в записи.


OCI Image 101


Стандартная процедура доставки образа выглядит так: есть контейнерный движок и registry. Все, что нужно, — запросить образ и получить его. Но в частном облаке для этого необходима авторизация. В случае отдельного движка команда выполняется прямо в консоли, так что чувствительные данные остаются в файловой системе.


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

Секреты в Kubernetes — очень скользкая тема, так как эта абстракция не предоставляет на самом деле никакого встроенного метода для шифрования чувствительных данных (Прим. автора: На самом деле, такая возможность появилась благодаря EncryptionConfiguration). Мы не используем встроенную абстракцию секретов и предпочитаем для получения каких-либо чувствительные данных систему Hashicorp Vault. Но, к сожалению, нет возможности хранить в Vault учетку для registry.

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

Проблемы больших масштабов


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

Приложения, которые запущены в облаке, отвечают за различные подсистемы сайта, начиная с формирования страниц и заканчивая выполнением различных API-методов, таких как загрузка и обработка фотографий. Также мы помогаем улучшать пользовательский опыт с помощью ML-приложений. Используем baremetal, и помимо х86-й архитектуры, у нас есть ARM, а также GPU и FPGA-акселераторы.

На таких масштабах мы сталкивались с трудностями при использовании классической модели получения образов. У нас используются сотни различных приложений с разной кодовой базой. Но сегодня в качестве примера я хотел бы остановиться на одном из них, который вам известен — kPHP. Думаю, вы знаете, что ядро сайта написано на PHP и компилируется в сишный бинарник благодаря компилятору kPHP. В нашем облаке этот бинарь запускается в режиме сервиса, у которого scope задач ограничен единичным RPC-запросом. Мы прошли долгий путь эволюции и сейчас активно выделяем различную функциональность в более легковесные сервисы.

Сейчас это выглядит так:

  1. Весь сайт обновляется каждые полчаса, или 48 раз в день. Обновление включает в себя обычные bare metal-серверы и такие системы оркестрации, как Kubernetes. Пока вы читаете эту статью, сайт ВКонтакте обновится.
  2. После сборки из бинаря удаляются дебаг-символы, но даже после этого он занимает около 2 Гбайт.
  3. Мы используем около 500 машин в различных пулах облака для исполнения kPHP-сервисов.
  4. Пропускная способность registry — 10 Гбит в секунду. Но ее не всегда можно задействовать целиком, так как registry используется и для других нужд. 

На основе этих значений мы можем получить метрику времени, которое нам потребуется, чтобы скачать двухгигабайтный blob на 500 машин из конечного registry. Конечный результат получился страшным и составляет 13 минут и 20 секунд, или более десяти часов в день. Это метрика, с которой нам пришлось сражаться.

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

  • Сохранить источник правды в виде текущего registry, так как у нас много автоматизаций и интеграций на его основе.
  • Сохранить совместимость конечного клиента вне зависимости от характера его использования в Kubernetes или альтернативной технологии.
  • Отказоустойчивость и failover. Во всем конвейере доставки сам registry можно обозначить черным ящиком, который подвержен человеческими, программными и техногенными проблемам. Он может выйти из строя целиком, что приведет к недоступности образов. Конечно, есть документ для disaster recovery, но если рассматривать самый трагичный исход, то восстановление сервиса займет время, и хотелось бы его пройти в спокойном режиме без использования реактивной тяги инженера.
  • Оптимизировать граф передачи данных. В классической модели и при наших масштабах — это сотни серверов, которые запрашивали один и тот же образ — и мы бессмысленно гоняли одинаковый набор байтов между дата-центрами.
  • Ускорить возможность доставки, чтобы сравняться по метрике с остальным сайтом. Это позволит нам продолжать перевод stateless-приложений во внутреннее облако.


Особенности Peer-to-Peer-архитектуры


У нас есть механизм, который использует весь остальной сайт для доставки бинарников на baremetal — это наш внутренний движок copyfast. Из названия понятно, что его цель — быстро распространять бинарные blob. В основе движка лежит Gossip-репликация, и за прошедшие годы мы ни раз убедились в эффективности этого метода, поэтому выбрали P2P в качестве архитектуры для распространения образов.



Напомню вам основы P2P (пиринга). Это одноранговая децентрализованная сеть, в которой отсутствуют выделенные серверы — каждый узел является клиентом и выполняет функции сервера. В пиринговой сети для передачи данных агент может запрашивать у соседей какие-либо данные, а также отдавать их, если они у него есть. Чем больше агентов — тем больше возможных связей между ними, а с ростом количества увеличивается и суммарная пропускная способность P2P-облака.

Наш собственный движок copyfast мог принести нам OCI-образы в виде архивов, но контейнерному движку потребовалось бы импортировать их, что не удовлетворяло всем нашим начальным требованиям. Мы решили посмотреть на решения, которые предлагает open source.

Хотелось бы отметить, что решение мы выбирали почти два года назад, и на тот момент существовало два продукта:

  • Alibaba Dragonfly v1.
  • Uber Kraken.

Первым мы рассмотрели Dragonfly v1 и обратили внимание, что для передачи любого чанка между агентами требовалась обязательная координация со стороны суперноды. Это означало, что вся пропускная способность сети линейно зависела от производительности координатора. Как следствие, она падала при увеличении размера облака или передаваемого артефакта.

Сегодня Dragonfly первой версии уже считается устаревшим. Разработчики предлагают использовать вторую версию продукта, но, согласно документации, супернода все еще координирует передачу между конечными узлами.

Так что мы выбрали Uber Kraken: каждый из его компонентов — это unix-way, или как говорят сейчас, микросервисы.


Схема работы Uber Kraken. Каждый компонент — маленький, но важный кирпичик всей системы

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

  • Nginx  — расположен на той же машине, что и движок контейнера. Зачем он нам нужен, я расскажу вам чуть позже. На текущий момент все, что вам нужно знать, — он просто пересылает запросы процессу Kraken Agent.
  • Kraken Agent — процесс, который выполняется на каждом нашем контейнеровозе, будь то сервер Kubernetes или что-то другое. Он по запросу передает и/или получает бинарные blob.

Вы можете иначе разместить Nginx и Kraken Agent в зависимости от ваших потребностей.

Агенту для начала скачивания потребуется понять, что именно он хочет получить. Для этого он обращается к сервису BuildIndex.

  • Задача BuildIndex — построить граф слоев образа, который мы хотим скачать. Для этого BuildIndex обращается к авторитарному registry, а затем «рассказывает» агенту, какие слои необходимы для образа.
  • Далее в дело вступает Kraken Tracker — компонент, который показывает агенту, откуда скачать слои нужного образа. Агент передает ему список необходимых слоев, а трекер возвращает список конечных узлов, где их может получить агент. Трекер хранит топологию в базе данных Redis типа «ключ-значение». 

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

Пару слов о том, как наш агент строит связи между другими агентами. После получения списка раздающих мы можем также отранжировать их по критерию близости. Для этого каждый агент содержит сервисную информацию о местоположении нашего агента. В качестве примера: вы используете серверный ряд или просто метку дата-центра. На основе метки расположения агент отдаст предпочтение соседу, что оптимизирует передачу данных в облаке и позволит организовать желаемую data-locality.

  • Kraken Origin необходим, если в P2P-сети нет какого-то слоя. Агент запрашивает у него недостающий слой, а Kraken Origin запрашивает его у авторитарного registry и кэширует для будущих запросов. В этой схеме Kraken Origin — суперсид, который сохраняет как можно больше слоев и раздает всем желающим. 

Примечание. Можно хранить кэш Origin непосредственно на сервере или использовать удаленное хранилище по протоколу S3. Мы выбрали локальное хранение, чтобы уменьшить задержку доступа к кэшу.

Весь управляющий контур можно запускать в режиме master-master в любом количестве. Мы выбрали для себя оптимальным запуск по одной реплике всех головных компонентов в каждом нашем дата-центре. Авторизационные данные хранятся не в секретах Kubernetes, а в файлах конфигурации тех компонентов, которые обращаются к registry.

  • Эту архитектуру можно дополнить необязательным компонентом — Kraken Proxy. Допустим, есть сборщик, который собирает ваши образы и отправляет их в авторитарный registry. Если необходимо дополнительно «прогреть» P2P-облако, то можно воспользоваться Kraken Proxy. Он разобьет образ на граф слоев и на сами слои и добавит в компоненты BuildIndex и Origin.

Однако при всех преимуществах Uber Kraken не лишен недостатков. Сегодня проекту уделяется все меньше времени, и количество активностей вокруг него падает. Поэтому я не исключаю ситуацию, что его может ожидать судьба Uber Mikasu. Думаю, некоторые из вас использовали этот проект у себя.

Еще есть обязательное требование — использовать уникальные теги для образов, чтобы система без обращения к авторитарному registry знала, что образ актуален. На мой взгляд, это не недостаток, а самая настоящая фича, которая: а) учит хорошему тону; б) позволяет установить политику скачивания в режиме «Не скачивать, если есть локально».

Как-то в одном из внутренних чатов прозвучала фраза, что ImagePullPolicy IfNotPresent не является безопасной. Условный злоумышленник может подменить локальный image на машине, что будет проигнорировано движком. Ну, мне кажется, у нас будут более серьезные проблемы, чем подмененный image, если злоумышленник уже на нашей машине. Подмена образа — это уязвимость цепочки поставок и тема для отдельного разговора.

Отказоустойчивость доставки 


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

В первую очередь нам требуется написать модуль «Антивымыватель», он обеспечивает сохранность образов от других модулей, который хотят удалить образы и освободить место в registry. Модуль разбит на компоненты:

  • Первый опрашивает все источники, чтобы создать список всех активно используемых образов на сайте.
  • С его помощью второй модуль назначает образам метку иммунабельности в registry, чтобы их нельзя было удалить во время их эксплуатации.
  • Третий компонент постоянно и по расписанию запрашивает все образы из списка у Kraken Origin. «Прогрев» гарантирует нам, что все необходимые образы продублированы в P2P-облаке.
  • ???
  • PROFIT.

В результате у нас получилось два равноценных registry, у каждого из которых есть минимально необходимый нам набор образов. Чтобы переключаться между ними, мы создали отдельный модуль «Переключатель».



Помните, я обещал вам рассказать про nginx?

Разработчики специально не делали агент сложным и не включали в него логику веб-сервера. Для этого агент самостоятельно поднимает дочерний процесс nginx с необходимым конфигом.

Чтобы модуль мог работать вне зависимости от состояния Uber Kraken, нам потребовалось добавить в исходный код дополнительный аргумент запуска. С его помощью агент самостоятельно не создает процесс nginx, это позволило нам изолировать процессы друг от друга.

Теперь мы можем настраивать Nginx, так как нам требуется. И мы добавили возможность проверки upstream: если Kraken Agent отвечает «OK» на наличие образа Pause, то мы считаем, что наше P2P-облако доступно.

Наконец, на случай провала проверки нужен upstream, который обслуживал бы резервный способ доставки образов. Мы сдули пыль с забытого всеми проекта и взяли официальный Docker Registry. У него есть режим проксирования с возможностью кэшировать все полученные слои у себя. Кроме того, он хранит авторизационные данные для общения с авторитарным registry, что сохраняет совместимость с текущей моделью Uber Kraken. И, конечно, мы можем прогревать его нашим модулем-антивымывателем. Для проверки резервного способа некоторые машины на постоянной основе используют его как основной upstream. Получился этакий Kraken на минималках.

У всех модулей, о которых я рассказал, есть собственные проверки работоспособности, и наша система мониторинга сообщает нам, если мы сбиваемся с курса. Но мы привыкли резервировать все, даже собственные проверки. Поэтому используем проект k8s-image-availability-exporter.



Этот экспортер проверяет доступность всех образов и отдает метрики в формате Prometheus, в проекте представлены правила для alertmanager. Таким образом, если один источник информирования откажет, второй останется в строю. 

Пара слов о распаковке


Кажется, что распаковка по сравнению с остальным не стоит и упоминания: получили, распаковали, и дело с концом. Иногда мы забываем, какая работа за нас проделана, но стоит сказать спасибо Саргуну Дилону (Sargun Dhillon), инженеру из Netflix. Он сделал патч в проект moby в 2017 году, который бэкпортирован в проекты containerd и другие. Дилон добавил проверку на наличие бинарника unpigz в операционной системе. Если он присутствует, то патч позволяет распаковывать архив при помощи мультипоточности. А если бинарник не найден, то применяется простая однопоточная распаковка. Так что все, что нужно, — проверить, установлен ли pigz в наших операционных системах, и поблагодарить Саргуна.

Результаты и планы


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

  • Почти в 5 раз ускорилась фаза Pending, которая включает скачивание и распаковку. Решение доставки показывает одинаковые результаты с устоявшимся решением, которое используется у нас для baremetal. Это позволяет нам дальше перевозить сайт в свое облако.
  • Мы стали эффективнее строить граф передачи данных и добились желаемой data-locality.
  • Нам требуется обслуживать наш авторитарный registry и Kraken, за последний год зафиксировано только четыре простоя, включая аварии. Ни один из них не повлиял на доставку образов до конечных узлов.

Мы не останавливаемся и продолжаем работу, перерабатываем open source-решения с учетом наших реалий. Например, мы обнаружили в Kraken интересный режим Endspiel. В шахматах эндшпилем называют заключительную часть партии, когда на доске остается мало фигур, и можно рассчитать все возможные комбинации для завершения. Режим Endspiel в Kraken включается, когда агент почти скачал весь слой, и ему нужна лишь пара чанков. Тогда агент отправляет запрос на эти чанки сразу нескольким узлам. Этот режим позволяет улучшить 95 перцентиль скорости доставки. Мы хотим перенести эту идею в наш copyfast. 

Кроме того, недавно мы внесли правки, которые позволяют запускать все компоненты Kraken в rootless-режиме. Теперь планируем скрыть все наши секреты, добавив в код Kraken возможность получать авторизационные данные из безопасного хранилища Hashicorp Vault и также обновить библиотеку для работы с Redis, которая позволит перейти на Redis Cluster вместо Redis Sentinel.

Все принципы прозрачности и совместимости, которые мы взяли в начале внедрения, позволят нам легко сменить решение и отказаться от Kraken. Такая возможность пригодится, если мы заскучаем появится улучшенная версия Dragonfly или новый крутой проект. При этом мы сможем и дальше работать с Kraken, зная, что спецификация OCI уже устаканилась, и это решение можно спокойно адаптировать под наши нужды.

FAQ


Еще я хочу ответить на вопросы, которые мне задавали после выступления на VK Kubernetes Conference.

— Вы доставляете через Kraken только образы с kPHP?

— Нет, мы доставляем вообще все наши образы через облако Kraken: начиная с образов, где файловая система — только бинарь на Go, и заканчивая образами ML-приложений вместе с ML-моделями. Скорость доставки увеличивается, но не до таких значений, как в случае с kPHP. Отмечу, что скорость — второй по значимости фактор после надежности.

— У меня очень много образов, зачем мне хранить их на каждом агенте?

— Агенту для работы не требуется хранить все ваши образы локально. По запросу он получает от контейнерного движка только нужные, а соседям раздает лишь то, что у него уже есть. За то, чтобы сохранить как можно больше образов, отвечает Kraken Origin.

— У вашего решения нет авторизации. Значит ли это, что вы проигрываете в безопасности?

— Мы лишь изменили место хранения авторизационных данных, не отказываясь от них. Да, доступ к агенту без авторизации, но он доступен только на loopback-интерфейсе. Если злоумышленник уже находится внутри вашей системы, то скачивание и/или подмена образов — не самое худшее, что он может сделать.

Команда Kubernetes aaS VK Cloud Solutions развивает собственный Kubernetes aaS, о нем рассказывали в этой статье. Будет здорово, если вы его протестируете и дадите обратную связь. Для тестирования всем новым пользователям начисляем при регистрации 3 000 бонусных рублей.


Что почитать по теме:

  1. Устранение неполадок в Kubernetes: в каком направлении двигаться, если что-то идет не так
  2. Запуск проекта в Kubernetes за 60 минут
  3. Наш Telegram-канал с новостями о Kubernetes

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


  1. amarao
    11.02.2022 18:07
    +4

    Где-то в районе бинарника на 2Гб, я понял, что что-то не то. Бинарь? 2Гб?


    1. OkunevPY
      12.02.2022 15:01

      Ну как бы сам проект что-то не то. До этого попадалась статья, как они перформили картинки и мультимедия тут же зашол в вк на компе музыку послушать и вижу что картинки тупо не грузяться, а я не жалуюсь на канал у меня чистые 100 мб, могу любую картинку затащить)))


  1. vitalvas
    11.02.2022 18:20

    1) у вас в конфиге nginx есть отсылка к nginx plus. Это действительно так, или какой-то свой или opensource модуль?

    2) какова конфигурация vault? Один для всех или на каждый дц свой? Если на каждый дц свой, то как поддержываете консистентность секретов меджу дц? Как построеный HA, и что может произойти, если волт перестанет быть доступен?


  1. Slach
    11.02.2022 18:42

    EncryptionConfiguration это же шифрование только на уровне etcd
    внутри кубов как были get secrets в нешифрованные так и останутся? разве нет?


    1. gecube
      12.02.2022 00:27
      +1

      все так, как Вы говорите. Поэтому очень странно, что автор вообще сделал такую ремарку. Толку от шифровании в etcd, если мы получили доступ к kube api, то и смогли вытащить все секреты


  1. Daemonic
    12.02.2022 12:53

    Попробуйте bitnami SealedSecrets для безопасной доставки секретов


    1. OkunevPY
      12.02.2022 15:08
      -2

      Я бы посоветовал вообще больше времени уделять Research и меньше странным пабликам на хабре. Всё описанное в статье решаеться гораздо более простыми великами. Для компании которая имеет такой штат разработчиков не написать своё кастомное решение для доставки образов стыд и срам, работать с шифрованными секретами можно, для этого надо чуть чуть расширить кубик своими плагинами и всё. Опять же если компания позиционрует себя как опытного потребителя кубика то не иметь таких кастомных расширений она просто не может.

      И да, я понимаю что поддержка любой кастомщины сложное дело, но мы ведь говорим не о команде из 2-х разрпботчиков, а о целом огромном подразделении которое отвечает за эксплуатацию всего этого хозяйства. Мы у себя закастомили очень многое, и мы не ВК, мы как раз скорее амбициозный стартап, и нам это оказалось по силам))))


      1. gecube
        12.02.2022 19:50

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

        Далеко не всегда. Всегда надо оценивать трейдоффы от поддержки своего велосипеда и внедрения чужого.

        Мы у себя закастомили очень многое, и мы не ВК, мы как раз скорее амбициозный стартап, и нам это оказалось по силам))))

        Молодцы. И сколько у вас при этом людей, которые могут фуллтайм инфрой заниматься? Я замечаю, что во многих компаниях - продуктовая разработка в почете и она может творить любую дичь, никакой унификации нет, а выделенной команды девопсов нет (да и не работает она).


  1. evg_krsk
    12.02.2022 22:00
    +2

    Тема доклада актуальная, и не только в масштабах VK. Сам сталкивался с желанием иметь p2p-сидирование образов когда надо было на небольшой кластер (10-16 воркеров) тянуть время от времени многогигабайтные образы (кажется, гугловский tensorflow). Нашел те же варианты что и докладчик, но для тех масштабов это всё оверкилл, хотелось чего-то более легковесного чем кракен, но увы. Выкрутились кэширующим registry:2 в кластере.


    Может, с тех пор появилось лёгкое p2p-кэширование для самых маленьких, знает кто?


  1. gecube
    12.02.2022 22:09

    Как-то в одном из внутренних чатов прозвучала фраза, что ImagePullPolicy IfNotPresent не является безопасной. Условный злоумышленник может подменить локальный image на машине, что будет проигнорировано движком. Ну, мне кажется, у нас будут более серьезные проблемы, чем подмененный image, если злоумышленник уже на нашей машине. Подмена образа — это уязвимость цепочки поставок и тема для отдельного разговора.

    там основная проблема в другом. Предположим, что у нас есть мультитенантный кластер. В случае ImagePullPolicy=Always - всегда идет проверка того, что конкретный под из конкретного неймспейса обладает соответствующим regcred, а когда ImagePullPolicy=IfNotPresent - эта проверка байпассится. К сожалению, я не смогу подтвердить это опасение исходным кодом (честно, не смотрел), но могу подтвердить ссылкой на CIS или на сайт Fairwinds: https://www.fairwinds.com/blog/kubernetes-how-to-ensure-imagepullpolicy-set-to-always Как будто в случае указания полного имени образа с sha-суммой это не беда, но давайте будем честны - кто так делает ? :-)

    то скачивание и/или подмена образов — не самое худшее, что он может сделать.

    не согласен - это может быть шагом к более серьезным проблемам.

    Еще очень интересно (не совсем понял) - как решается проблема двойного расходования места на образа. Т.е. если мы делаем локальный кэш слоев на базе docker registry или nginx, то контейнерный движок оттуда все равно будет перекладывать слои к себе во внутренние каталоги /var/lib. Хотелось оптимизировать метрику потребления дискового пространства, т.к. у нас есть некоторые образа и по 10ГиБ, а места на /var/lib попросту не напасешься. Что по этому поводу думает уважаемый автор?