Привет, Хабр! Я — Сева, разработчик в Yandex Infrastructure. Уже больше десяти лет я занимаюсь разработкой внутреннего облака Яндекса, которое охватывает около 150 000 физических хостов и поддерживает все сервисы платформы. Каждый день мы выпускаем десятки релизов собственных компонентов, а каждую секунду происходят релизы сервисов, запускаемых в нашем облаке.

Сегодня я представлю вам практический кейс по обеспечению очень высокой надёжности комплексной системы на примере собственного облака Яндекса. Принципы обеспечения надёжности будут продемонстрированы на всех уровнях архитектуры системы, чтобы в итоге сложилась картина, как достичь наивысшей отказоустойчивости. Статья написана по мотивам моего доклада для HighLoad++.
За последние шесть лет у нас было лишь три серьёзных инцидента, связанных с компонентами внутреннего облака, и ни одного случая, когда всё полностью останавливалось. Это высокая планка, которой можно достичь, следуя правильным методам.
Мы стремимся описывать инфраструктуру большой распределённой системы так, чтобы всё работало качественно. Порой случаются ошибки, как, например, у Facebook (продукт Meta, деятельность которой признана экстремистской, запрещена на территории России по решению Тверского суда Москвы от 21.03.2022), когда неверная конфигурация сети привела к остановке всей инфраструктуры, включая офисные турникеты. В 2024 году Яндекс Станции положили NTP-серверы, так что мало кто застрахован от подобных ситуаций.
Если уж мем «Во всём виновата сеть» существует, то мы, как разработчики бэкенда, не хотели бы перетягивать на себя внимание и стараемся избегать таких событий.
Из чего состоит инфраструктура
Наша система включает множество связанных компонентов. На иллюстрации ниже — упрощённая схема внутреннего облака.

Я выделил несколько групп компонентов по назначению:
1. Базовые агенты на хостах (розовым внизу). Ключевой агент ISS получает целевой набор спек подов и отвечает за запуск контейнеров. Он ходит в наш рантайм контейнеров PORTO, поднимает там контейнеры с подовыми агентами, каждый из которых настраивает свой отдельный под. Вся эта связка напоминает Kubelet, если брать аналогии из Kubernetes®.
Также есть вспомогательные агенты: SSH-сервер для доступа в поды, агент, который конфигурирует файрволы для хоста и для сетей самих подов, сервис, который скачивает Docker®-образы, и так далее.
2. Оркестрация и шедулинг подов (тёмно-розовым повыше). В первую очередь это наша база YP на основе YTsaurus. Она хранит информацию о развёрнутых сервисах, подах, эндпоинтах и так далее. Это примерно то же самое, что в мире Kubernetes API-сервер и etcd® одновременно.
Над этой базой работает агент под названием StageCtl. Это контроллер, который берёт спеки сервисов, ходит в локации, шедулит раскатку сервиса в каждую локацию, делает какие-то действия в смежных системах и собирает статусы.
Внутри локации работает уже Replica Set Controller (RSC) — сервис, который шедулит конкретные коды в рамках локаций.
Есть ещё всякие дополнительные штуки типа сервиса DynresourceCtl, который доставляет динамические данные в поды без их перевыкатки, автоскейлер сервисов под нагрузкой, legacy-система управления сервисами Nanny и другие.
3. IaC. Дальше у нас должно было бы быть примерно 50–60 компонентов, которые составляют нашу Infrastructure as Code — систему управления конфигурацией с помощью какого-то кода и спек. Если вы незнакомы с термином IaC, то с Terraform или с Pulumi вы наверняка работали, это типичные представители такого рода систем.
Наш IaC построен на основе Kubernetes. Вы туда заливаете YAML-спеки, и дальше он уже применяет их к инфраструктуре.
4. Объединённые компоненты CI/CD. Формально, как любая подобная система, наш CI мог бы выполнять действия любого характера с любыми внутренними системами. Однако модельным мы считаем именно сценарий, когда спеки при появлении в репозитории, при прокатке релизов коммитятся в IaC и дальше уже IaC применяет их к инфраструктуре более декларативно.
Есть ещё тонна всяких вспомогательных сервисов, которые я не стал выделять. Например, мониторинг, артефактница, хранилище секретов и т. п.
Как повысить надёжность и снизить вероятность отказов
Нам надо как-то выжить и максимизировать UX для наших пользователей и для нас самих. Давайте посмотрим на такую систему: какая вообще вероятность, что всё развалится?

Можно вспомнить основы теории вероятностей. Если наивно предположить, что компоненты падают независимо друг от друга и каждый компонент может всё уронить, то это описывается формулой вероятности:

Тут видно, что чем больше будет компонентов, тем выше вероятность отказа. Если у нас связи сложнее и не описываются таким простым, наивным произведением, то формула будет отличаться, но суть остаётся.
Надёжность системы обычно описывается, наоборот, как вероятность безотказной работы. Или если говорить статистически — сколько времени на заданном временном интервале ваша система будет работать без сбоев.
В качестве примера возьмём какой-то минимальный набор компонентов в виде воркфлоу выкатки сервиса через CI:
CI коммитит спеки в IaC.
IaC доставляет их в нашу базу YP.
Дальше StageCtl шедулит их на локации.
В локации RSC шедулит поды на хосты.
ISS получает спеку этих подов, поднимает подовый агент (Pod agent).
Подовый агент поднимает ваш под в Porto.
Вот такой control flow из восьми компонентов:

Возьмём эти восемь компонентов: если мы считаем, что они достаточно надёжны, то есть надёжность одного компонента — 99,99%, то, по приведённой выше формуле, для восьми компонентов надёжность будет уже 99,92% — а это семь часов даунтайма в год.
Если перемножить вообще все компоненты, то получится примерно двое суток отказа в год. Это, конечно, много. Каждая минута простоя в Яндексе стоит дорого. Чтобы избежать финансовых потерь, можно использовать несколько вариантов.
Например, мы можем использовать резервирование. Это хорошо работает для железа. Если бы у нас отказывало только железо, мы могли бы потратить в три раза больше денег и вместо четырёх девяток получили бы примерно одиннадцать — это две миллисекунды простоя в год.

К сожалению, с софтом так не работает. Мы могли бы писать по три реализации каждой подсистемы, точно так же тратить в три раза больше денег: если одна отказывает, фолбэчиться на другую. Однако надёжность это сильно не повысит: люди склонны писать одни и те же баги, и системы будут отказывать одинаково. Ещё компоненты будут завязаны на какие-то общие вещи из внешнего мира, также возможен баг в коде фолбэка. Вдобавок всегда есть сценарий, что сервис формально не лежит, но на практике он делает какие-то неверные вещи, и сфолбэчиться тут уже нельзя.
Как мы можем потратить меньше средств, а получить больше результата? Делать надёжные компоненты — это хорошо и правильно, но все люди ошибаются, и все компоненты в неожиданный момент всё равно у вас когда-нибудь откажут. Поэтому мы, кроме надёжности, в первую очередь растим устойчивость.
Как быть устойчивее
Устойчивость — это концепция, которая говорит, что отказ одного компонента не должен приводить к отказу всей системы, она должна деградировать. Давайте посмотрим ещё раз схему с воркфлоу выкладки и поймём, что можно сделать.
Меньше связность компонентов

Нивелируем падение CI/CD
В классической системе выкладки, например Ansible, которая императивно последовательно применяет кубики, все связи между компонентами, как правило, были бы достаточно жёсткими. И если у вас отказывает какой-то шаг, то всё разваливается. Мы так не хотим. Поэтому у нас всё достаточно изолировано.
Например, падение CI/CD в нашей картине мира вообще не должно приводить к каким-то значимым последствиям. Дежурный всегда может взять и собрать и выполнить релиз у себя на ноутбуке, те же самые действия, которые прописаны в релизном flow CI/CD, прокатить сервис. Да, везде будет видно, что это был нештатный процесс релиза, но, когда вы чините прод, вам не до таких мелочей.
Выкидывая одну такую уже не очень необходимую зависимость, мы понижаем теоретическое время даунтайма с 7 до 6 часов в год и получаем час экономии.
Обходимся без IaC
Мы встречали разработчиков, которые говорят: «Мы вам дали Terraform provider, все должны работать через него. Зачем вам ещё какое-то API для похода в сервис?»
Мы считаем, что так неправильно. Потому что, когда у вас факап, дежурный, который бросается это всё чинить, побежит по максимально короткому пути. Чем ближе он сможет по вашему стеку, минуя все абстракции, спуститься к тому месту, которое можно починить и закидать костылями, тем лучше: во время факапа вам нужна оперативность. Нормальный процесс вы наладите потом.

Мы учитываем, что инженер внесёт изменения напрямую, и наш IaC даже специально поддерживает этот сценарий. В следующий раз, когда вы штатно покатите релиз, вам максимально подсветят, что у вас, кроме планируемых изменений из репозитория, есть ещё какие-то ручные изменения в спеке компонента. Вы должны будете либо закоммитить их в репозиторий, чтобы они попали в релиз, либо сказать: ОК, мы их перетрём, и они нам больше не нужны.
База данных
Следующей в нашем флоу идёт база данных. БД всегда просится быть единой точкой отказа, поэтому мы делаем так, чтобы база была не одна. У нас есть отдельная кросс-ДЦ-инсталляция базы данных, которая натянута на все дата-центры. В неё по дефолту пользователи заливают спеки сервисов.

Дальше сервис StageCtl берёт эти спеки и раскладывает по локациям. В каждой локации есть своя отдельная локационная база, и с ней уже работают все дальнейшие компоненты ниже по стеку.
Таким образом, если у нас отказывает кросс-ДЦ-инсталляция или StageCtl, то внутри локации всё продолжает работать: поды продолжают шедулиться. При большом желании даже можно пойти напрямую в локацию и в локационную базу данных закоммитить спеку вашего сервиса.
Если падает как раз уже локационная база данных или RSC в локации, то тут шедулинг пода встаёт, но только для этой локации. Разумеется, мы при этом ожидаем, что ничего не должно ломаться ни вниз по стеку (то есть на хостах все существующие спеки должны сохраняться, а поды работать), ни вверх по стеку. Кросс-ДЦ-база остаётся работать, StageCtl продолжает шедулить спеки по всем локациям, кроме упавшей, а когда она вернётся, ваши релизы докатятся. Поэтому это тоже довольно обособленный компонент.
ISS-агент
Ну и последнее, что хочется выделить, — это ISS-агент, который занимается доставкой спек на хосты. ISS-агент и подовый агент в сумме составляют примерно Kubelet. Изначально мы их разделили не просто так. У нас единое облако, в нём живут все сервисы, и на хосте могут спокойно сожительствовать поды абсолютно разных сервисов, разных бизнес-юнитов, хотелось минимизировать их импакт друг на друга.
Если у нас падал ISS-агент, то каждый подовый агент продолжал крутиться независимо, он поднимал свой собственный под, контролировал его, и таким образом мы тоже можем понизить сильную связанность.
Эта схема была актуальна на время доклада, но с развитием системы мы всё-таки объединили ISS- и подовый агент, одновременно с этим упростив их общую архитектуру. Однако для понимания нашей логики рассуждений и причин тех или иных решений сокращать этот момент не будем.
Меньше критических компонентов
Нам важно расставить приоритеты: определить, куда направить основные усилия, а что требует меньших затрат. Потому что, во-первых, невозможно всё сделать одинаково хорошо, а во-вторых, это очень дорого. При этом надёжность системы, очевидно, не может быть выше, чем надёжность самого ненадёжного незаменимого компонента.

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

Первый тир самый критичный — тир А. Мы требуем от сервисов в этом тире минимальной надёжности — четыре девятки. У этих сервисов должны быть организованы круглосуточные дежурства, для дежурных должны быть написаны подробные инструкции по деградации всех компонентов — что нужно делать по пунктам, если отказывает какая-то функциональность. Должны быть регулярные учения, разборы всех инцидентов и т. д. Это критические сервисы (такие, как Яндекс Поиск, Яндекс Go и т. п.), которые влияют на нашу прибыль.
Тир В — по сути, похожие сервисы, но они меньше влияют на прибыль. К ним применяются те же требования, но в более мягком формате: допустимы медленные реакции на инциденты и отсроченное решение проблем.
Тир С — это внутренние сервисы, которые не влияют на финансовые показатели. При падении внешние пользователи определённое время ничего не замечают.
Например, это может быть сервис подготовки данных для Поиска. Если он ложится, то Поиск продолжает работать, у всех всё хорошо, пользователи видят результаты.
Если же он лежит, скажем, сутки, то данные в Поиске начинают устаревать, мы начинаем терять долю поискового трафика. Поэтому для этих сервисов тоже есть определённые требования по доступности. Но, скажем, если поздно ночью происходит инцидент, уже можно не бросаться будить разработчиков, чтобы они срочно всё чинили, проблема ждёт рабочего времени. Таким образом мы просто экономим деньги.
Тир D — внутренние сервисы, которые используются нерегулярно. Например, сервис заказа железа на следующее полугодие. Если он ляжет и полежит неделю, ничего критичного не произойдёт.
В тот момент, когда мы классифицировали сервисы, нам становится гораздо проще следить за приоритетными для бизнеса системами. Когда мы что-то разрабатываем, мы точно знаем, на какие вещи можем полагаться, а на какие нет. Например, если вернуться к воркфлоу выкладки, то CI/CD и IaC в процессе выкладки для пользователя не являются максимально критичными вещами. Их можно безопасно потерять из воркфлоу. Мы можем предъявлять разные требования к разным компонентам, в зависимости от их критичности в разных сценариях.
До этого момента мы рассматривали всю систему в целом, но, чтобы всё хорошо работало, должны быть определённые подходы ещё и при разработке каждого отдельного компонента.
Пережить отказ ДЦ
Базовое требование для абсолютно любого яндексового сервиса — работоспособность в режиме «минус один ДЦ». Если сервис живёт в одном ДЦ, то он уходит в даунтайм вместе с этим дата-центром. Например, когда приезжает экскаватор и рвёт все входные линии в дата-центр (это реальная ситуация) или когда в регионе случается авария на подстанции (тоже не менее реальная ситуация).

Есть разные подходы, как можно обеспечить соответствие этому требованию. Можно сделать кросс-ДЦ-инсталляцию, которая будет натянута на несколько дата-центров. Или сделать локационные инсталляции по одной в каждом дата-центре.
У каждого подхода есть свои плюсы и минусы.
Локационность
Для некоторых сервисов переключения мастера при отказе дата-центра могут вызвать временный даунтайм, какие-то флапы, пятисотки. В этом случае мы используем отдельные инсталляции в каждой локации. Такие инсталляции будут знать и использовать только другие компоненты в этой же локации, ничего не знать про остальные и никак не реагировать на падение другого дата-центра.

Как я уже говорил, у нас в каждой локации есть своя отдельная база данных в YP. А ещё вся машинерия по работе с подами тоже действует в рамках локаций.
Есть очевидные минусы. Это надёжно, но неудобно для людей: чтобы катать сервисы раздельно по локациям, пришлось бы так или иначе копипастить спеки сервисов в каждую отдельную локацию, писать скрипты, циклы в том же Terraform, самостоятельно собирать общие статусы, поддерживать синк между версиями на разных локациях. Это не очень дружелюбно, поэтому над локационными инсталляциями мы имеем кросс-дата-центровую оркестрацию. Это позволяет балансировать UX с надёжностью.
Пользователь может залить свой сервис один раз в эту кросс-дата-центровую инсталляцию, описать в нём, как именно она должна реплицироваться по отдельным локациям, в каком порядке выкладываться. Дальше уже задача кросс-ДЦ-инсталляции и StageCtl — донести этот сервис до всех локаций. Это можно делать, даже когда у вас какой-то из дата-центров лежит, потому что пользователь закоммитит и уйдёт, а спека доедет потом. Не надо ждать окна, когда всё поднимется, чтобы катать свои спеки.
Конечно, у кросс-ДЦ-инсталляции есть свои проблемы в виде разовых пятисоток, в виде какого-то latency, но этим как раз можно пренебречь. В том же CI вы можете поретраить операции, они должны быть идемпотентны, никаких проблем в этом месте быть не должно.
Вот так это выглядит на нашей схеме. В красном прямоугольнике находятся все полокационные компоненты.

Это локационная база, replica set controller, ну и всё, что на хостах. А всё, что вне — кросс-ДЦ-инсталляция и IaC, CI/CD, StageCtl, — это кросс-дата-центровое.
Какие тут могли бы быть альтернативы?
Полностью кросс-локационная схема — это максимально просто разрабатывать. Не надо думать о том, что вы как-то разбиваете данные, раскладываете по разным инсталляциям, — вам надо следить только за одним продом. Но это максимально ненадёжно.

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

Поэтому мы используем гибрид: у нас есть локационная машинерия и кросс-локационная оркестрация.

На самом деле даже наш IaC имеет отдельные локационные инсталляции для критичных сервисов, которым нужна максимальная надёжность. Если вам очень надо, можно использовать отдельную инсталляцию IaC, которая оперирует только конкретным дата-центром.
Локальность данных
Ещё важная практика в плане борьбы с такими проблемами связанности — это уменьшение числа онлайн-зависимостей. Современный подход предполагает использование микросервисной архитектуры для доступа к данным. Это очень здорово, но, когда микросервис ложится, вы удивляетесь.
Поэтому хороший подход — приносить данные асинхронно. Пусть у вас будет отдельный процесс, который доставляет вам полный слепок данных, складывает их на под на диск, или в память процесса, или и туда и туда, что ещё лучше. Когда они потребуются, ваш сервис больше не будет думать, сложится ли ваша зависимость под нагрузкой, если нагрузка внезапно возрастёт, и не будет ли каких-то проблем сетевой связности. Он знает: либо у него данные сейчас есть локально, либо их нет. Это гораздо надёжнее.

Можно пойти ещё дальше: отказаться от каких-то зависимостей. Потому что у любого внешнего компонента будут свои планы, как и когда ломаться, — это всегда точка снижения надёжности.
Разумеется, я не предлагаю возвращаться в каменный век, пилить монолиты, которые будут содержать всё в себе. Лучше подумать, как архитектурно отказаться от каких-то вещей. Потому что лучший код — это код, который никто никогда не писал, а самые надёжные зависимости — те, которых тоже нет.
На конкретном примере: у нас есть меганадёжный компонент — наш SSH-сервер для доступа в поды, это не OpenSSH, а наша собственная разработка. Это последнее оружие дебага — когда все более современные средства уже не помогают. SRE начинают заходить по SSH в контейнер пода, читать логи, смотреть в /proc, аттачиться к процессам, смотреть, что происходит. Такой инструмент должен быть максимально безотказным, и в нашем SSH-сервере ноль онлайновых зависимостей. Мы не ходим ни в какой LDAP, у нас асинхронно на хост приезжают ACL, мы их кешируем, держим и в случае сбоя синхронизации долгое время работаем автономно.
Спеки подов тоже есть локально, но долгое время у нас это было узкое место. Как и многие, мы использовали стандартную ключевую аутентификацию, когда у пользователя есть приватный ключ, а на сервере — публичная часть этого ключа. Когда пользователь подключается к серверу, сервер просит его подписать какой-то маленький чанк данных приватным ключом и убеждается, что пользователь — тот, за кого себя выдаёт.
Тут есть проблемное место. Для аутентификации нужно, чтобы пользователь залил публичную часть ключа в хранилище, а сервис должен донести её до всех хостов. Если у вас есть сотрудник без ключа, который только к вам устроился, например, или перевыпускает отозванный ключ, или вам надо кого-то неожиданно подключить к починке инцидента, то у вас есть задержка на доставку этого ключа.
Из-за распределения нагрузки на 150 000 хостов задержка данных достигала 15 минут, что критично во время инцидента.
Мы переработали всю схему: перешли с ключевых пар на сертификатную авторизацию. Пользователь выписывает себе сертификат, подписанный удостоверяющим центром, на сервере лежит сертификат этого УЦ, и, когда пользователь приходит, надо только проверить, что его сертификат валиден: не истёк, не отозван и подписан удостоверяющим центром.
Как только пользователи выпускают себе сертификат, мы сразу можем их пустить на сервер. Задержки нет, система получается более устойчивая. Единственное, что нам теперь требуется, — это обновлять сертификат удостоверяющего центра. Делаем это раз в несколько месяцев, не зависим от пользователей, получается более плавно, и в этом месте мы намного лучше себя чувствуем.
Избегать кольцевых зависимостей
В моей практике часто встречается проблема кольцевых зависимостей, поэтому хочу особо выделить один подход.

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

У вас должен быть отработан сценарий, как вы будете всё это чинить или бутстрапить с нуля, но это не самое главное — проблему надо предотвращать.
Мы у себя для борьбы с такими проблемами используем отдельную инсталляцию, которую называем «админский контур». Это, по сути, ещё одно такое же облако, которое обновляется очень редко, только на самые стабильные версии, и занимается тем, что деплоит компоненты основного облака.

Так мы одновременно и занимаемся догфудингом, и избегаем необходимости изучать какую-то отдельную систему деплоя, чтобы деплоить само облако. И сразу избавляемся от риска всё взорвать, так как компоненты одного облака являются подами в другом и наоборот. Если мы выкатываем какой-то проблемный релиз, то ничего страшного, мы его точно так же без проблем откатываем стабильной машинерией другого облака.
Другой пример: у нас есть система управления правами под названием IDM. Она общая для всех наших систем. Это удобно и с точки зрения UX для пользователей, и с позиции безопасников, и для аудита. Но тут есть маленькая проблема, она тоже живёт в нашем облаке. Если вдруг IDM сломается, то как выдать кому-то права на её починку? Или, если в этот момент параллельно откажет какая-то критическая система, как её дежурным выдать права на починку?

Тут мы не придумали ничего оригинального: просто сделали большую кнопку в каждом сервисе, с нажатием которой дежурные получают абсолютный доступ в прод, минуя систему, то есть права выпишутся напрямую. Конечно, в этот момент уходят алерты в SOC, и возможно, потом придётся объяснять это безопасникам. Но в тот момент, когда у вас всё горит, самое важное — максимально быстро всё починить, а бюрократией можно заняться потом.
Сомневаться во всём
Я часто вижу, как люди в интернете пишут: «Яндекс, зачем вы опять изобретаете свои велосипеды? Есть прекрасные индустриальные практики, есть стандартные вещи. Почему вы просто не используете их?» В комментариях к этому посту, уверен, такое напишут тоже, и не раз.
Даже наши коллеги иногда так говорят: «Выдайте нам всем по Kubernetes, и мы будем как-нибудь сами с этим жить». К сожалению, дело в том, что все ошибаются и «все врут». Это касается и наших самых лучших и самых умных коллег из соседнего отдела, и опенсорсных решений, которые все используют, и софта, за который мы платим неприлично большие деньги.
Все такие вещи приходится валидировать. Например, когда мы начинали делать наш IaC, мы взяли Kubernetes как популярное решение для управления инфраструктурой. В общих чертах вот как выглядит управление инфраструктурой с помощью Kubernetes, в том числе в крупных облаках:

Для каждой сущности заводится свой тип объекта. В Kubernetes это называется CRD — custom resource definition. У него есть схема, пользователь заливает YAML со спекой в API-сервер. API-сервер помещает её в своё хранилище, и операторы Kubernetes применяют эти спеки уже к каким-то компонентам инфраструктуры.
У этого решения есть минус: его тяжело надёжно мейнтейнить и развивать. Дело в том, что в Kubernetes схематизация устроена странновато. Схема CRD — это один глобальный объект, который сразу содержит все версии и применяется сразу на всех. Это опасно, можно случайно сломать сразу все версии. С одной стороны, нельзя это легко раскатывать на отдельных пользователей, а с другой стороны, если мы хотим, чтобы пользователи начинали постепенно мигрировать на новые версии нашей схемы, нужно за этими пользователями бегать и говорить: «Ребята, давайте переходить». Пользователи должны проактивно предпринимать какие-то действия.
Мы посмотрели на вот этот стандартный flow, как обрабатываются объекты при заливке, и кое-что заметили: когда пользователь приносит свой объект, в первую очередь API-сервер валидирует его этой статической схемой.
После этого он дёргает по HTTPS какие-то динамические вебхуки, которые занимаются динамической валидацией объектов, и уже после этого заливает объект в хранилище. Первый этап для нас как раз является узким местом.

Мы перешли на концепцию, которую мы у себя внутри назвали Schemaless CRD — когда в схеме объекта прямо написано: здесь может лежать что угодно. Такую схему можно залить в Kubernetes один раз и больше никогда не трогать, никогда не версионировать. Таким образом, наш API-сервер находится в безопасности, и мы избавлены от ситуации, когда что-то резко всем сломаем.
Мы хотим эти схемы как-то обновлять и расширять и при этом ничего не ломать. Поэтому мы тут для описания схем взяли Protobuf, у которого есть гайды по совместимости. Мы ввели строгое требование соблюдать обратную совместимость наших схем. В момент релиза мы буквально идём в прод и проверяем, что все объекты с новой схемой будут работать. А валидацию схем объектов мы проводим на вебхуках. Вебхук — это простой HTTPS-эндпоинт, и это очень гибко. Мы можем тут поставить балансировщик, мы можем их масштабировать, применять новую версию в соответствии с какими-то правилами только к определённому множеству объектов или только к конкретным пользователям, катить изменения по процентам, использовать любые фича-флаги.
Узкое место в виде CRD в такой схеме становится просто тупым storage, чья задача — взять объект и сохранить. Этот CRD больше никто никогда не трогает, и это замечательно.

Гайдлайны Kubernetes говорят, что надо использовать стандартный подход, валидировать с помощью их довольно урезанной версии OpenAPI™ (она прямо страшная) или использовать более новый язык валидации CEL. Идём ли мы тут против них?
Да, мы, безусловно, нарушаем все эти гайды, и нас многие, наверное, осудили бы за это. Однако мы уверены, что прямо сейчас ни один из наших провайдеров инфраструктуры перед выходными не выкатывает новую версию схемы, которая испортит сразу все объекты. Это, кажется, довольно существенный бонус, и он даёт нам спать по ночам спокойно.
Выводы
Не забывайте валидировать и проверять всё, что вам предлагают, какими бы «стандартами» это ни называли. Это могут быть отличные решения, но бывают и просто индустриальные стандарты, потому что так сложилось.
Даже тому, что я рассказываю, не стоит слепо верить. Надо аккуратно примерять это на свою инфраструктуру и думать, подходит ли оно вам. Наши подходы помогли нам за шесть лет существования последней архитектуры облака избежать факапов и успешно выходить из инцидентов. Но они не универсальны.
Скрытый текст
А пока что мы приглашаем вас на Saint HighLoad++. В этом году мероприятие пройдёт в формате конференции развития. Это будет инструмент решения задач, а не потребления контента. Больше интерактивных форматов и нетворкинга, чтобы участники были не пассивными слушателями, а активными создателями решений, знаний, новых контактов и инсайтов.
