Container image: доставить это немедленно

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

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

Я расскажу:

Статья написана по мотивам моего выступления на 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 секунд, или более десяти часов в день. Это метрика, с которой нам пришлось сражаться.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В результате у нас получилось два равноценных 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 в наших операционных системах, и поблагодарить Саргуна.

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

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

Мы не останавливаемся и продолжаем работу, перерабатываем 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 бонусных рублей.

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

Source habr.com