TOPZNANIE

Цифровая практика без сложностей

Выберите город
MET Norway · CC BY 4.0
← На главную

· Полезные находки

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

class=»tm-article-presenter__content tm-article-presenter__content_narrow»>

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

TimurTukaev3 минуты назадКраткое содержание статьиЯnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)Время на прочтение27 минОхват и читатели0Яндекс API * Kubernetes * DevOps * Облачные сервисы *

Вчера звонил Сатана. Оказывается, в аду зачитываются фанфиком про Gachinetes. Они давно зотели улучшить систему своих пыток, сделать ее более клиентоориентированной и подстроенной под каждого посетителя. По словам Сатаны, им необходим был подходящий инструмент для пыток продуктовых команд Яндекса. Взамен он обещал, что после смерти найдет мне местечко в подземелье Вана, чтобы я не слишком страдал в аду, а даже радовался загробной гачи-жизни под Venus in Furs. Предложение было слишком привлекательным, чтобы отказываться. Да и Веельзевул мужичонка солидный, иметь таких в пуле должников весьма полезно.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Приступим. В Kubernetes есть довольно фундаментальный архитектурный недостаток: его придумали не в Яндексе. С этим, конечно, можно жить, но тогда для оркестрирования контейнеров придётся пользоваться чужим API-сервером, чужим планировщиком и какой-то etcd, хотя у тебя уже есть свои базы данных, свой фреймворк и люди, которые способны полтора часа объяснять на конференции, почему готовое решение не подходит для их масштабов.

Вообще, яндексовский подход к разработке давно заслуживает отдельного дистрибутива. Компания успела обзавестись собственной системой контроля версий, собственными контейнерами и собственным внутренним облаком, а некоторые разработки, вроде CatBoost, вполне успешно живут за пределами Я. Вот и мне захотелось собрать Kubernetes, в котором привычка делать всё по-своему доведена до логического завершения: снаружи можно оставить знакомые команды и объекты, а внутри написать заново всё, что решает, как кластер будет работать.

Однако для большинства людей Яндекс начинается с гораздо более приземленных вещей. Ты хочешь уехать домой, а спрос уже повышенный, так что ведьмаку заплатите чеканной монетой чеканной монетой. Ищешь что-нибудь, будь добр поглазей на рекламу. Собираешься устроиться на работу, пройди 85 этапов интервью, стань боссом качалки литкода, а потом еще подожди полгодика, когда тебе найдут подходящую команду. И где бы ты ни оказался, Плюс наверняка сделает твою жизнь немного лучше, и тебе об этом многократно будут напоминать. Так что было бы странно, если бы сегодншний дистрибутив Kubernetes унаследовал от Я только технический стек и проигнорировал кадровые и продуктовые решения, по отношению к пользователям мы так поступить не имеем права. Ну, все мы знаем этот ЛОР и мемы:) Они и стали основной материала.

В итоге получился Яnetes. В нём API-сервер написан на C++ с userver, объекты хранятся в YDB, а новые поды проходят собеседования и ждут, пока их возьмут в команду. Деплой надо согласовать через Трекер, место в выдаче зависит от ранжирования и рекламного размещения, а баллы Плюса можно потратить на CPU. Всё это работает на живом кластере. В ходе создания сего творения пришлось разбираться с довольно обычными проблемами: пропавшими полями protobuf, потерянными результатами нагрузочного теста и тем, сколько памяти съедает база, пока мы радуемся компактному API-серверу.

Реквитисрую использование Яnetes в Яндекс Клауде, а то чего там этот богомерзкий K8s делает.

Когда же ты наконец остановишься?!

В бэклоге осталась всего пара идей. Извините, скоро закончу(

Почему готовое решение не подошло

Для тех, кто не читал предыдущие статьи: я продолжаю эксперименты с Kubernetes, который уже успел пожить в Google Таблицах, заговорить по-татарски и научиться проводить документы на языке 1С. Каждый раз исходная идея выглядит несколько сомнительно, но дальше её приходится реализовывать всерьёз, потому что kubelet ничего не знает о задумке автора и ждёт от API вполне определённого поведения.

В случае Яндекса материала хватило бы на несколько таких проектов. Правда, если почитать историю его внутренней инфраструктуры, становится понятно, откуда взялись собственные системы. Общий пул серверов понадобился ещё в начале 2010-х, когда сервисов уже было много, а выделенные машины значительную часть суток простаивали. Затем появились Nanny, изоляция контейнеров через Porto и Yandex Planner. К моменту, когда Kubernetes стал привычным ответом на вопрос об оркестрации, у компании уже была своя инфраструктура, свои требования к сети и огромное количество работающих приложений. Просто выкинуть всё это и начать заново было бы довольно дорогим способом следовать отраслевым рекомендациям.

С Arc история тоже интереснее, чем «нам не понравился Git». Для большого монорепозитория сделали виртуальную файловую систему, которая подгружает файлы по мере необходимости, а часть тяжёлых операций перенесли на сервер. При этом разработчику оставили знакомые команды. То есть человек продолжает делать примерно то же самое, хотя под привычным интерфейсом уже работает совсем другая система.

Эту идею я и взял за основу. У Яnetes остаётся Kubernetes API, к которому подключаются обычные kubectl и kubelet, а за ним работают собственные компоненты. yapi принимает запросы и хранит состояние в YDB, nanny выполняет работу контроллеров, yplanner размещает поды на узлах. Kube-apiserver, kube-controller-manager, kube-scheduler и etcd в этой схеме вообще отсутствуют. При этом kubelet, kube-proxy, containerd, flannel и CoreDNS вполне стандартные, так что контейнеры запускаются и общаются по сети без каких-либо скидок на происхождение дистрибутива.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

У настоящего Yandex Planner, кстати, другое хранилище: в статье 2022 года описаны динамические таблицы и Cypress из YT. Для своего проекта я выбрал YDB. Наши nanny и yplanner тоже написаны здесь с нуля; от внутренних сервисов Яндекса им достались названия и некоторые идеи. Сам дистрибутив — независимый проект, к которому Яндекс отношения не имеет (но проект под pache 2.0 так что посмотрим).

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Чтобы масштаб проделанной работы можно было оценить без презентации, есть команда ya nih. Она считает исходники компонентов: сейчас это примерно 13 тысяч строк C++ у API-сервера, шесть тысяч строк Go у контроллеров и ещё тысяча у планировщика. В их число входят тесты, а сгенерированные схемы и данные модели исключены. Уже по этому соотношению можно догадаться, что некоторые возможности большого Kubernetes ещё только предстоит реализовать.

Что пришлось написать вместо API-сервера

Для API-сервера я взял userver, яндексовский фреймворк для асинхронных сервисов на C++. Он позволяет писать обработчик как нормальную последовательность действий: сначала проверить пользователя, потом разобрать запрос, сходить в базу и вернуть ответ. Пока база отвечает, поток может заниматься другой работой. Для сервера, который постоянно ждёт сеть или хранилище и одновременно держит открытые watch-соединения, это вполне подходящая основа.

Сами объекты лежат в четырёх таблицах YDB. В objects хранится текущее состояние, в revisions — счётчики версий, в events — изменения, которые должны увидеть наблюдающие клиенты, а в compactions — граница истории, к которой ещё разрешено обращаться. Когда меняется объект, запись о нём и соответствующее событие попадают в одну транзакцию. Иначе можно получить очень неприятный кластер, в котором новая версия уже есть, а контроллеру о ней никто не сообщил. Здесь пригодились сериализуемые транзакции YDB: несколько связанных записей должны либо измениться вместе, либо не измениться вовсе.

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

Для чтения у yapi есть watch cache. Сервер забирает изменения из таблицы events, обновляет объекты в памяти и после синхронизации с базой может отдавать оттуда списки и события наблюдателям. Журнал здесь собственный, и сервер периодически его опрашивает; changefeed YDB в этой реализации не используется. Когда список всё-таки приходится читать из базы целиком, он читается страницами в одной snapshot-транзакции. Это нужно, чтобы начало списка и его конец показывали одно состояние кластера, даже если за время чтения кто-нибудь успел создать или удалить под.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

На этом месте уже можно представить себе довольно убедительную демонстрацию: kubectl создаёт под, kubelet его видит, контейнер запускается. Дальше, правда, выясняется, что клиентам Kubernetes нужен не только JSON. Есть ещё protobuf, потоковые ответы watch, вложенные типы и поля, которые в Go выглядят совсем не так, как ожидаешь по схеме. Поэтому для сериализации я использовал схемы generated.proto из Kubernetes и дополнительные сведения, извлечённые из его Go-типов. Готовое protobuf-представление объекта сохраняется в кэше, чтобы при следующем LIST не кодировать заново десятки тысяч одинаковых объектов.

И всё равно часть полей сначала терялась. У встроенного EphemeralContainerCommon не оказалось ожидаемого name= в теге, из-за чего отладочный контейнер получал неполное описание. У ExtraValue, через который передаются дополнительные сведения о пользователе, нашлась другая особенность: значения там представлены срезами. На обычном поде таких проблем можно долго не замечать, зато тесты временных контейнеров, TokenReview и агрегированного API довольно быстро объяснили, насколько далеко нам ещё до совместимости.

С историей изменений тоже пришлось повозиться. В отличие от глобальной ревизии etcd, здесь счётчик свой у каждого вида ресурсов. Если какой-нибудь вид долго не менялся, его старая ревизия могла оставаться доступной, а токен продолжения списка — жить дольше положенного. Поэтому истечение срока старых ревизий теперь происходит отдельно от физического удаления записей. Само удаление идёт порциями по тысяче событий, с ограничением в тридцать секунд на один проход. Иначе сервер, занятый уборкой собственной истории, начинал слишком сильно забивать на текущую жизнь кластера.

В итоге yapi умеет проверять RBAC, выдавать токены сервисных аккаунтов, вызывать admission-вебхуки, обслуживать CRD с преобразованием версий, публиковать OpenAPI, подключать агрегированные API и исполнять политики с CEL. Ограничения тоже есть, и для дальнейших замеров они имеют значение: apply пока объединяет объекты без учёта владельцев отдельных полей, managedFields не ведутся, часть встроенной валидации написана вручную. Для входа используются bearer-токены; аутентификацию клиентов по x509 и первоначальное подключение узлов через CSR я пока не реализовал.

Кто следит за подами и решает, куда их поставить

Контроллеры собраны в процессе nanny, который написан на Go и использует client-go. Здесь я позволил себе некоторую слабость и взял готовую библиотеку. Через общие informer-кэши контроллеры получают изменения объектов, а потом каждый занимается своим участком работы: один создаёт репликасет для деплоймента, другой поддерживает нужное число подов, третий следит за выполнением задания, а сборщик мусора удаляет объекты, оставшиеся после удаления владельца. Здесь же работают контроллеры узлов, квот и адресов сервисов.

Набор пока заметно меньше, чем у kube-controller-manager. Например, в нём нет HPA, контроллера привязки PersistentVolume и подписания сертификатов. Это одна из причин, по которым сравнивать потребление ресурсов двух систем нужно с оглядкой на то, сколько работы каждой из них вообще поручено.

Обычный yplanner сначала отбрасывает узлы, на которых под нельзя разместить: проверяет доступные ресурсы, taints, affinity, занятые порты и другие ограничения. Затем выбирает из подходящих, учитывая свободные ресурсы. На большом кластере рассматривается выборка узлов, а привязки подов выполняются асинхронно. Сведения о кластере приходят в кэш через события, поэтому на каждый новый под не нужно заново опрашивать все машины.

Но было бы странно делать яндексовский дистрибутив и в таком важном деле обходиться без ранжирования. Поэтому рядом есть отдельный профиль yanetes-1922, который под может выбрать через schedulerName. Обязательные ограничения он проверяет так же, а подходящие узлы оценивает с помощью CatBoost. В событии о размещении после этого появляется YetiRank.

Скрипт обучает на синтетических примерах модель из 48 деревьев глубины четыре и сохраняет её в JSON-формате CatBoost. Для её исполнения написан небольшой вычислитель на Go, результаты которого сверяются с предсказаниями самого CatBoost. Поэтому запускать Python и библиотеку обучения для каждого решения планировщика не приходится.

На вход подаются 1922 признака. Первые восемь описывают готовность, свободные ресурсы, наблюдаемый трафик, количество меток, перезапуски, соответствие запросу и удаление объекта. Остальные получаются хешированием имён и меток. Из этого богатства нынешние деревья используют 50 признаков. Данные и модель у нас свои, довольно небольшие; промышленный поиск Яндекса за ними не скрывается. Позже эта же модель пригодится для поиска по подам, потому что заказывать ранжирование для одного только планировщика было бы расточительно.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Одно приложение для всех потребностей кластера

Пользователь может продолжать работать через kubectl, но все дополнительные услуги собраны в клиенте ya. Через него можно искать поды, согласовывать изменения, заказывать доставку образов, устанавливать чарты и проводить реорганизации. Для подключения достаточно выполнить ya enable в нужном неймспейсе. По умолчанию включаются найм, поиск, Такси, Лавка, Трекер и капча; Плюс подключается отдельно. Если такой объём заботы пока избыточен, можно выбрать только нужные сервисы.

Настройки хранятся в конфигмапе yanetes-policy, а обслуживает их процесс superapp: в нём находятся дополнительные контроллеры, admission-вебхуки и HTTPS API для клиента. Вебхуки выбирают только неймспейсы, которые подключили эти услуги. Для них ответ службы обязателен, поэтому при её недоступности запись с такой политикой будет отклонена. Остальные неймспейсы продолжают жить обычной жизнью.

Здесь довольно легко случайно сделать второй вход в кластер, через который пользователь получает больше прав, чем ему выдали в Kubernetes. Поэтому каждый запрос к дополнительному API проходит TokenReview и SubjectAccessReview. Чтобы искать поды, надо иметь право их читать; чтобы обратиться к сервису через прокси, нужно право services/proxy. Настройки и согласования доступны владельцу неймспейса или администратору, а контроллер работает со своим сервисным аккаунтом и обновляет его токен.

В примерах дальше kubeconfig, context, namespace и адрес службы уже настроены. При явном указании эти параметры у ya стоят перед именем команды. В сценариях проверок они записаны полностью, особенно там, где участвуют сразу два кластера: переносить сервис в случайно оставшийся контекст было бы слишком реалистичной реорганизацией.

Прежде чем под начнёт работать, ему надо пройти собеседование

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

При создании пода admission добавляет ему пять дополнительных условий готовности. Контроллер проводит его через Algorithms, AdvancedCode, Architecture, TeamMatching и Hired, а kubelet учитывает эти условия вместе с обычной готовностью контейнеров. Поэтому приложение уже может работать, но сервис ещё не направляет к нему пользовательский трафик. Сначала нужно закончить отбор.

Алгоритмическую секцию проводит настоящий Яндекс Танк. Он отправляет девять HTTP-запросов с частотой три в секунду, и каждый ответ должен быть успешным и укладываться в одну секунду. Танк обращается прямо к IP пода, иначе получилось бы, что для прохождения собеседования кандидату сначала нужно стать готовым, а на деле готовым он станет только после собеседования. Поэтому первому объявленному порту приложения в этом режиме нужен HTTP; для других нагрузок потребуется подходящий HTTP-обработчик для проверки или неймспейс без такого отбора.

Контроллер читает сохранённый отчёт и проверяет число запросов, ошибки и время ответа. Одного нулевого кода завершения процесса ему недостаточно. После технических секций кандидат доходит до TeamMatching и может сколько угодно хорошо отвечать на HTTP, пока кто-нибудь не возьмёт его к себе:

ya team candidate Search

После этого все пять условий становятся истинными, и под наконец получает Ready. Назначение относится к конкретному поду: если деплоймент создаст новую реплику, её тоже придётся принять в команду, если команда не задана заранее в шаблоне. Это, кстати, всплыло после conformance-тестов, когда пересозданные демонстрационные поды вполне исправно ждали нового назначения.

Если кандидат не справился с заданием, следующая попытка в нашем дистрибутиве разрешена через шесть месяцев. Месяц по умолчанию длится тридцать дней, но ждать полгода ради проверки контроллера я не стал и в тесте уменьшил его до десяти секунд. Через минуту под действительно получил новую попытку. Самостоятельно поправить себе результат собеседования через PATCH он не может: служебные аннотации защищены admission.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

На записи ниже под проходит секции, ждёт назначения команды и после него становится готовым. Между секциями в этой демонстрации выставлены секундные паузы, а нагрузочное упражнение Танк выполняет целиком. Все GIF и MP4 в статье сняты с живого кластера; при воспроизведении длинные промежутки без вывода сокращены до двух секунд. В исходных записях терминала сохранено настоящее время команд.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

С самим Танком тоже получилась небольшая история. На таком коротком испытании иногда оказывалось, что запросов было девять, а строк с результатами — восемь. Причина нашлась в BFG reader закреплённой версии: последние результаты могли потеряться при завершении, а при конкурентном чтении запись иногда попадала в буфер, который уже успели заменить. Пришлось поправить смену буфера и дочитывание после закрытия, а заодно адаптировать вызов rsplit к pandas 2. Исправление проверяется на 250 записях: каждая должна дойти ровно один раз. Восемь из девяти выглядело бы вполне приличным результатом собеседования, но для отчёта нагрузочного инструмента критерий всё-таки другой.

Узлы от этой системы тоже не освобождены. Команда ya node interview ставит на выбранный узел собственный taint и запускает проверочный под. После успешного отбора ограничение снимается. Если участие надо отменить, ya node withdraw уберёт проверочный под и свой taint, сохранив чужие ограничения узла.

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

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Найдётся всё, но сначала реклама

Обычный kubectl get pods довольно бесхитростен: просишь показать поды, он их показывает. В ya get pods запрос обслуживается с учётом накопленного опыта поисковой компании. Сначала идут рекламные размещения, затем колдунщик с ответом по кластеру, а уже потом органическая выдача, для которой пригодилась модель CatBoost из предыдущего раздела.

У выдачи есть и требования к качеству. Исторический Минусинск боролся с покупными SEO-ссылками, а Баден-Баден — с переоптимизированными текстами. В нашем случае Минусинск снижает оценку пода с платными ссылками владения, Баден-Баден обращает внимание на избыток меток, а АГС — на отсутствие наблюдаемого трафика за последние пятнадцать минут. Причины и величины ограничений можно посмотреть через ya webmaster.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

На снимке у sponsored действуют сразу три ограничения, поэтому итоговая оценка у него отрицательная. Рекламное место он при этом сохраняет. У candidate ситуация лучше: перед съёмкой я обратился к нему через ya proxy search/hostname, и он получил наблюдаемый трафик, а я — имя пода в HTTP-ответе.

Здесь трафик действительно проходит через прокси. Команда выбирает адрес готового пода с учётом рейтинга, отправляет запрос и записывает число обращений, ошибки и время ответа. Это даёт ранжированию обратную связь от работы приложения. Заодно можно проверить, как исчезает ограничение АГС после настоящего запроса, вместо того чтобы просто поставить красивое число в аннотацию.

Обычные сервисы продолжают обслуживаться через kube-proxy. Поисковое ранжирование участвует в маршрутизации, когда пользователь явно выбирает ya proxy; глобально перехватывать весь трафик кластера ради этой функции я не стал. Статистика прокси хранится в памяти службы, поэтому после её перезапуска отсчёт начинается заново.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

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

Ресурсы есть, но сейчас повышенный спрос

Заказывая такси, пользователь довольно быстро узнаёт, что наличие машин и привычная стоимость поездки — вещи не обязательно связанные. В Яnetes с ресурсами происходит похожая история: исходный CPU-бюджет у неймспейса есть, но доступная квота зависит от текущего спроса.

Спрос считается по доле подов, которые ещё ждут размещения: 1 + 1,7 × pending/total, максимум 2,7. Исходный бюджет делится на этот коэффициент, а результат записывается в настоящую квоту ресурсов. Например, при бюджете в четыре CPU и максимальном спросе остаётся 1481m. Под, который просит два CPU, после этого получает отказ по квоте, что мы и проверили на стенде. На следующем проходе контроллер снова берёт исходные четыре CPU: если делить уже уменьшенную квоту, после нескольких обновлений в кластере наступит экономическая катастрофа.

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

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Для тех, кого эконом уже не устраивает, есть Comfort+. Под получает node selector на выделенные узлы и класс приоритета со значением 2000 вместо обычных 1000. Это условия для самого планировщика, поэтому в проверке под действительно оказался на нужном узле.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Доставкой образов занимается Лавка. В её описании логистики быстрая доставка начинается задолго до заказа: товары уже лежат в небольших складах рядом с покупателями. Для контейнеров такая подготовка тоже полезна. Команда ya lavka IMAGE… создаёт демонсет, который заранее скачивает образы на рабочие узлы через OCI image volumes, доступные только для чтения. Запускать приложения из этих образов ради наполнения кэша не требуется.

Обещанное время запуска по умолчанию составляет пятнадцать минут. Контроллер сравнивает создание пода с фактическим стартом последнего контейнера приложения: если запустился только sidecar, заказ ещё не доставлен. При опоздании появляется Late, при успехе сохраняется время доставки. Последующий перезапуск приложения этот результат уже не переписывает, иначе опоздавший заказ можно было бы бесконечно переоформлять как новый.

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

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Без задачи в Трекере изменения не принимаются

Собственный Трекер у Яндекса вырос из внутреннего инструмента в систему для самых разных рабочих процессов. У нашего дистрибутива процесс пока один, зато очень понятный: если хочешь что-нибудь поменять, сначала получи согласование.

ya approve DEPLOY-1922 ya apply -f application.yaml —ticket DEPLOY-1922

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

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

С Helm всё оказалось чуть сложнее. Он хранит манифесты установленного релиза в собственном секрете, поэтому пометить только отправленные в API объекты было бы недостаточно. Команда ya helm —ticket=DEPLOY-1922 install … использует post-renderer, который добавляет согласование в манифесты и шаблоны, а admission проверяет общий тикет также в сохранённом релизе. Это проверялось вместе с остальными услугами: чарт устанавливался, его поды проходили секции и дожидались команды Platform.

Во время установки заодно появляются Элементы. Клиент заранее сообщает, что они выбраны по умолчанию, и создаёт отдельный деплоймент с небольшими запросами ресурсов. Элементы тоже проходят собеседование, получают команду Elements и занимают место в рекламной выдаче. Если прочитать сообщение до установки, можно передать —elements=false. Оба варианта проверены: дополнение действительно работает, а снятая галочка действительно его не устанавливает.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

С Плюсом будет удобнее

Когда в кластере уже есть выдача, тарифы и доставка, довольно быстро выясняется, что всё это можно объединить подпиской. Команда ya plus on подключает Плюс для неймспейса, после чего новые поды получают класс приоритета ya-plus со значением 3000. У эконома, напомню, было 1000, у Comfort+ — 2000. Теперь планировщик может учитывать степень участия приложения в экосистеме.

Есть и преимущество, которое видно без планировщика: ya logs перестаёт показывать рекламу. Без подписки предложение подключить Плюс выводится в stderr, а stdout остаётся выводом приложения. Поэтому обрабатывать журнал скриптом по-прежнему можно. Обычный kubectl logs тоже читает его напрямую; менять содержимое логов контейнера ради дополнительной услуги мы не стали.

За каждый готовый под начисляется один балл, который можно потратить на увеличение CPU-бюджета. Для этого пришлось завести небольшую бухгалтерию. Начисления привязаны к UID, потому что один и тот же под контроллер увидит много раз, а под с тем же именем после пересоздания будет уже другим объектом. Счёт хранится в конфигмапе yanetes-plus-ledger, изменять которую разрешено только контроллеру.

ya plus balance ya plus redeem —request-id cpu-budget-1 1

Один балл добавляет 100m к исходному CPU-бюджету, после чего к нему уже применяется коэффициент спроса. Каждое списание получает идентификатор. Если клиент не дождался ответа и отправил запрос ещё раз, тот же идентификатор не позволит списать балл повторно. Начисления и списания сохраняются в одном объекте, а квоту контроллер обновляет следом. Между этими действиями может быть задержка, но повторное обновление квоты не должно превращаться в повторную покупку процессора.

На стенде я дождался двух готовых подов, получил два балла, потратил один и повторил ту же команду. Квота стала 4100m, на счёте остался один балл. Затем я перезапустил службу: баланс сохранился, старые поды повторного вознаграждения не получили. Подделать счёт прямым запросом и потратить больше имеющегося тоже не удалось.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Отключение через ya plus off выполняется сразу. Баланс остаётся, прибавка к CPU перестаёт действовать, а новые поды снова получают обычный приоритет. Приоритет уже созданных подов сохраняется, потому что Kubernetes запрещает менять это поле. У нынешней бухгалтерии есть и вполне конкретный предел: 4096 уникальных подов в одном журнале. Для больших объёмов начислений пришлось бы выделить отдельное хранилище, потому что конфигмапа постепенно перестаёт справляться с ролью финансовой системы.

Докажите, что ваш контроллер не робот

Аккаунт, который отправляет слишком много запросов, получает HTTP 429 и требование решить капчу. Лимит относится к паре «неймспейс и аккаунт», поэтому активность одного пользователя не расходует запас другого. Команда ya captcha выдаёт арифметическое задание, а правильный ответ снимает ограничение для этого аккаунта на час.

Сервисные аккаунты тоже участвуют. В проверке робот сначала получал 429, затем решал задание и успешно создавал объект. Чужой ответ и повторное использование уже решённого задания не принимались. Запросы с dry run лимит не расходуют. Здесь у нас собственная локальная капча, состояние которой хранится в памяти службы; облачный SmartCaptcha в работе не участвует.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Если общаться с кластером хочется по-человечески, можно позвать Алису. Она понимает небольшой набор русских команд по правилам, без обращения к внешней языковой модели. Зато у неё есть —rudeness-level от нуля до десяти. Параметр меняет интонацию ответа, а полномочия пользователя и результат операции остаются прежними: право отказать в доступе выдаёт RBAC, а способ сообщить об этом можно настроить отдельно.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Продукт остаётся, название меняется

Ещё одна полезная способность большой экосистемы — продолжать обслуживать пользователя, пока вокруг него меняются названия сервисов. В Яnetes для этого есть ресурс «продукт» с двумя версиями API. Старый клиент видит Taxi и поле rides, новый — Go и trips. Вебхук преобразования версий переводит эти представления в обе стороны, сохраняя UID и данные объекта.

ya product ride Taxi ya rebrand ride Market

После второй команды новый клиент увидит Market, а старый — Beru. Сам kind менять между версиями одного CRD Kubernetes не разрешает, так что бренд пришлось положить в spec.brand. С точки зрения API это всё тот же продукт, которому просто обновили вывеску. На снимке ниже видно, что обе версии показывают один UID, а история в rides и trips сохраняется при преобразовании.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Если смены вывески недостаточно, есть ya split и ya sell. Первая команда разделяет сервисы на российскую и международную части в явно указанном целевом кластере. Общая конфигурация копируется в обе части, а связанные группы приложений распределяются согласованно. Вторая команда оформляет перенос со скидкой и, если передать —move, удаляет исходные декларации после проверки результата на другой стороне.

На уровне реализации за реорганизацией стоит довольно осторожное копирование конфигмап, обычных секретов, сервисных аккаунтов, деплойментов и сервисов. Поля, которые назначил исходный кластер, вроде UID, resourceVersion, status и выделенного ClusterIP, удаляются. У сервиса без кластерного IP-адреса при этом сохраняется clusterIP: None, иначе после переезда изменится способ обнаружения приложения. Если целевой объект уже существует, команда останавливается. Перед удалением источника снова проверяются UID и resourceVersion: за время продажи сервис могли успеть изменить, и стирать эти изменения вместе со старой копией было бы неправильно.

Такой перенос проверялся между двумя настоящими API: данные совпали, UID стали новыми, повторная попытка записать поверх существующей цели закончилась отказом. Процессы и содержимое томов этими командами не переезжают, поэтому бесшовную миграцию работающего приложения они пока не обеспечивают.

На записи ниже показаны разделение и продажа деклараций между кластерами. В целевом кластере нет рабочих узлов, поэтому скопированный деплоймент остаётся без готовых реплик; проверяются сами объекты и их данные.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Для продукта, который решили сохранить для истории, предусмотрена команда ya museum on. После неё декларации можно читать, но нельзя менять или удалять. Обновления status продолжают работать, поэтому оставшиеся приложения не выпадают из наблюдения контроллеров. Если у проекта снова появился смысл жизни, владелец может выполнить ya museum off и вернуть возможность записи.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Релизы играют в города

Яндекс называл версии поиска именами российских городов. Например, в анонсе «Магадана» 2008 года уже было объявлено, что следующая версия будет называться «Находкой». Для релизов Яnetes я тоже взял названия городов, а их порядок проверяется по правилу знакомой игры: следующий город должен начинаться на последнюю букву предыдущего.

В нашем кластере цепочка уже дошла до «Калининграда»: Магадан → Находка → Арзамас → Снежинск → Калининград. Поэтому следующему релизу нужно название на «д».

Команда ya release показывает эту историю, а ya release ГОРОД добавляет новое название, предварительно проверив первую букву и отсутствие повторов. История хранится в кластере. Сборку и публикацию релиза команда не выполняет, зато неправильное название до выпуска не пропустит.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

А Kubernetes во всём этом ещё работает?

После всех этих изменений я заново запустил полный conformance Kubernetes 1.37.1 на трёх настоящих узлах, с включённым RBAC. На окончательной сборке 9d633a5 прошли 433 теста из параллельной части и 29 из последовательной — всего 462 из 462. Первая часть заняла 15 минут 41 секунду, вторая — 16 минут 13 секунд.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Результаты я дополнительно сверил по JUnit-отчётам: в них 462 разных теста, нет ошибок, оба запуска завершились с нулевым кодом. До и после сохранились PID, время запуска и хеши бинарников пяти служб. Они совпали, поэтому разные части набора не могли незаметно пройти на разных версиях дистрибутива. В приложенный XML попали результаты без потоков вывода: исходные журналы Kubernetes иногда содержат временные токены и остаются на стенде.

Здесь надо пояснить, что именно проверялось. Conformance создаёт обычные неймспейсы. Дополнительные службы и вебхуки в этот момент установлены, но найм, Трекер и другие услуги подключаются только там, где их включил владелец. Поэтому к стандартному набору понадобились отдельные проверки самих услуг: отказ без согласованного тикета, защита результатов найма и баланса, капча для сервисных аккаунтов, повторное списание, запрос через ранжирующий прокси, преобразование версий продукта и перенос между кластерами.

Кроме отдельных сценариев прошла проверка, в которой все семь сервисов работали одновременно: Helm устанавливал чарт вместе с Элементами, Трекер проверял согласование, поды проходили найм и становились Ready, а остальные политики продолжали действовать. Для Плюса отдельно проверялось сохранение счёта после перезапуска процесса. В коде прошли 18 тестов C++, тесты всех Go-пакетов и проверки изменённых компонентов с race detector.

У нас свои масштабы, поэтому 30 001 узел

Оставалось разобраться с масштабом. В описании Yandex Planner 2022 года крупнейший кластер насчитывал чуть больше тридцати тысяч хостов. Для нашего опыта я выбрал 30 001 узел и десять тысяч подов. Правда, такого количества серверов у меня для статьи не нашлось, поэтому узлы создаёт KWOK: он имитирует их работу и меняет статусы подов, но контейнеры на них не запускает. Нагрузку от большого числа объектов управляющий слой получает, а сеть приложений, диски и поведение тридцати тысяч настоящих kubelet остаются за пределами этого опыта.

В другом углу ринга оказался обычный Kubernetes 1.37.1: kube-apiserver, kube-controller-manager и kube-scheduler, а для хранения состояния — etcd 3.6.4. У обеих виртуальных машин управляющего слоя было по четыре vCPU и восемь GiB памяти, модель процессора — AMD EPYC-Genoa. Хранилища работали в отдельных контейнерах, каждому выделены лимиты в восемь CPU и шестнадцать GiB и том на сто GiB с репликацией DRBD. Генератор нагрузки жил отдельно и в расход ресурсов серверов не включался.

До повторного сравнения пришлось привести в порядок сам сценарий. Иначе довольно легко получить прекрасные результаты, повторно создавая уже существующие узлы или останавливая таймер до того, как все объекты дошли до нужного состояния. Теперь старые KWOK-узлы и leases останавливают прогон ещё до начала, а ошибка запроса, неправильный формат ответа или неполное число объектов делают измерение неуспешным. Для каждой стороны создаётся новый неймспейс и сохраняется исходный отчёт.

Перед своим прогоном процессы управляющего слоя перезапускались, но базы оставались на уже использовавшихся томах, со своей историей и кэшами. Виртуальные машины находились на разных физических хостах. Ещё одно различие: у Яnetes оставались три настоящих узла и четырнадцать фоновых подов, а у upstream такого фона не было. Всё это записано в отчёте, потому что одинаковые лимиты CPU сами по себе ещё не делают два окружения полностью одинаковыми.

Дополнительные политики неймспейсов на время замеров выключены, superapp и отдельный планировщик с CatBoost остановлены. Яnetes использует обычный yplanner. То есть в этом опыте подам не приходится искать команду или ждать согласования задачи, прежде чем попасть в таблицу с результатами.

Сначала двести параллельных обработчиков создают узлы. Затем запускается KWOK, сценарий дожидается готовности всех узлов, выдерживает ещё три минуты и проверяет leases. После этого читаются списки в JSON и protobuf, причём сжатие явно выключено для обоих API. У каждого ответа сохраняются Content-Type и число байтов, чтобы случайно не сравнить разные форматы.

Для каждого формата отдельно измеряется первая пара LIST, а затем три серии по восемь пар с четырьмя параллельными обработчиками. В пару входят список 30 001 узла и список подов ещё пустого тестового неймспейса. Время включает передачу по сети и декодирование в client-go. Первое чтение здесь тоже нельзя автоматически считать холодным: до него API уже обслуживал KWOK и запросы проверки готовности.

Последний этап — создание десяти тысяч подов с selector на KWOK-узлы. Таймер до Running запускается перед их созданием, поэтому в результат входят приём запросов, размещение и имитация статусов. В обоих прогонах все объекты дошли до нужного состояния, ошибок запросов не было.

Измерение

Kubernetes + etcd

Яnetes + YDB

Создание 30 001 узла

11,08 с

7,69 с

Все узлы Ready после старта KWOK

81,53 с

79,66 с

Возраст Lease, p99

15,10 с

22,17 с

Таблица узлов kubectl, медиана двух чтений

12,39 с

11,14 с

Первая пара LIST, JSON

5,56 с

3,12 с

8 пар LIST, JSON, медиана трёх серий

23,15 с

7,01 с

Объём ответа JSON на пару LIST

94,61 MiB

56,78 MiB

Первая пара LIST, protobuf

1,40 с

3,33 с

8 пар LIST, protobuf, медиана трёх серий

10,98 с

3,61 с

Объём ответа protobuf на пару LIST

61,99 MiB

27,28 MiB

Создание 10 000 подов

14,81 с

27,90 с

Все поды Running от начала создания

266,35 с

100,28 с

Здесь хорошо видно, почему одной красивой цифры для сравнения недостаточно. Полный путь десяти тысяч подов до Running у Яnetes занял 100,3 секунды против 266,4 у Kubernetes, хотя сами объекты подов наш API создавал почти вдвое медленнее. Дальше свою роль сыграли контроллеры и планировщик: у nanny и yplanner другая реализация и клиентский лимит 500 QPS, а upstream работал с настройками по умолчанию.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Повторные серии LIST у Яnetes прошли примерно втрое быстрее, зато первая пара в protobuf оказалась медленнее. Ответы при этом заметно различаются по размеру. Kubernetes хранит и возвращает managedFields, а наша реализация пока этого не делает, поэтому ей приходится обрабатывать и передавать меньше данных. Приписывать весь выигрыш языку C++ или способностям YDB было бы слишком смело.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

С готовностью узлов получилась почти ничья, а возраст Lease в p99 у Яnetes хуже: 22,17 секунды против 15,10. Это один замер после трёхминутной выдержки, причём возраст считается на момент получения всего списка и включает время его чтения. Для выводов об отказоустойчивости такого опыта явно мало.

Куда ушли процессор и память

Одновременно с замером времени раз в секунду снимались показатели cgroup v2. Три процесса управляющего слоя учитывались вместе, контейнер хранилища — отдельно. Для памяти сначала складывались одновременные значения трёх процессов, а уже потом выбирался максимум; складывать их пики, случившиеся в разные моменты, было бы неправильно. Использовался memory.current, в который входит и файловый кэш.

Полные интервалы измерения составили примерно 695 секунд у Kubernetes и 445 у Яnetes, включая трёхминутную выдержку и все перечисленные этапы. Поэтому в таблице есть и среднее число занятых ядер, и CPU-секунды, то есть всё процессорное время за соответствующий прогон.

Ресурс за полный прогон

Kubernetes + etcd

Яnetes + YDB

CPU управляющего слоя

2680,77 CPU·с

1657,33 CPU·с

CPU хранилища

598,68 CPU·с

860,54 CPU·с

Средний CPU управляющего слоя

3,86 ядра

3,73 ядра

Средний CPU хранилища

0,86 ядра

1,94 ядра

Максимум памяти управляющего слоя

6870,08 MiB

2999,68 MiB

Максимум памяти хранилища

1636,90 MiB

13686,18 MiB

Управляющий слой Яnetes действительно потребовал заметно меньше памяти. Но если посмотреть на следующую строку, возникает некоторое затруднение для будущего рекламного буклета: контейнер YDB дошёл до 13,4 GiB, тогда как etcd — примерно до 1,6 GiB. Средний расход CPU у хранилища тоже выше. Так что экономия памяти в API-сервере ещё не означает, что вся система стала дешевле.

Яnetes — Kubernetes для тех, у кого свои масштабы (и для страданий, конечно)

Это результаты конкретных работающих экземпляров, которые до замера уже использовались. Базы имеют разную историю и состояние кэшей, а полный сравнительный прогон выполнен один раз; трижды повторялись только серии LIST внутри него. Вдобавок одновременно меняются язык API-сервера, хранилище, контроллеры, клиентские лимиты и объём работы с объектами. Чтобы отделить влияние одного решения от другого, понадобятся новые эксперименты.

Мне эти результаты интересны именно своей неоднородностью. У небольшой реализации получилось быстрее пройти несколько важных сценариев, но цена этого выигрыша видна очень четко: более узкая функциональность, другие настройки и большой расход ресурсов YDB. Отдельно ещё предстоит проверить отказы и более крупные кластеры. Тридцать тысяч объектов узлов в API сами по себе не дают права обещать эксплуатацию тридцати тысяч серверов.

Что в итоге можно с этим делать

Исходники и сценарии находятся в репозитории Яnetes, лицензия — Apache 2.0. Для повторения эксперимента сначала нужно подготовить стенд по документации.

После подготовки окружения скрипт собирает C++, запускает его тесты, проверяет Go-пакеты и собирает остальные компоненты. Каждый выпуск управляющего слоя выкладывается в отдельный каталог, после чего переключается символьная ссылка. К такому способу выкладки мы пришли после вполне конкретного SIGBUS: оказалось, что копировать разделяемые библиотеки поверх файлов работающего процесса было плохой идеей.

Работы впереди еще предостаточно. В API прежде всего нужно довести владение полями при apply, а в контроллерах — реализовать недостающие возможности. Для разговоров об эксплуатации понадобятся проверки отказов: пока и YDB, и управляющий слой работают в одном экземпляре, а реплицируемый диск не спасает от остановки единственного процесса.

Теперь можно зайти в кластер с обычным kubectl и увидеть знакомые объекты Kubernetes, а потом открыть ya и обнаружить, что у этих объектов довольно насыщенная корпоративная жизнь. Под уже умеет отвечать на запросы, но ещё ждёт команду; ресурсов вроде бы хватало, пока не вырос спрос; в выдаче над нужным приложением стоят Элементы, а заработанные баллы можно потратить только внутри экосистемы. Всё это пришлось выразить через реальные объекты, контроллеры и проверки доступа, и именно на этих стыках нашлось немало интересного. Кроме того, надо научить ya управлять гибридной инфрой и кластерами в Яnetes, Я Клауде и Декхаусе.

После «Калининграда» следующему релизу Яnetes нужен город на «д». По крайней мере, с этой частью продуктовой стратегии уже есть определённость.

Предыдущие статьи с «нетесами»

  • Sheeternetes: как мы запустили оркестратор контейнеров внутри Google Таблиц (а заодно написали ОС и создали фонд)

  • Tatarnetes: как мы научили Kubernetes говорить по-татарски, цитировать Габдуллу Тукая и останавливаться на чай с молоком

  • А что, если бы Kubernetes был человеком: Kyvernetria, Mansplainetes и Misogynetes

  • Автодополнение в tatarnetes, sheeternetes on-prem и новые фичи гендерных K8s

  • Assnetes (1Ж: Оркестратор 8), или как мы заставили Kubernetes подчиниться правилам Тамары Петровны из бухгалтерии

  • Gachinetes: три хундрет бакс, честная борьба за ресурсы и Билли в сердце (никакой пошлятины, только полезные фичи)

Теги:

  • яндекс
  • kubernetes
  • ydb
  • yandex cloud
  • on premise
  • bare metal
  • дистрибутив
  • платформа

Хабы:

  • Яндекс API
  • Kubernetes
  • DevOps
  • Облачные сервисы