Привет, меня зовут Борис Литвиненко, я занимаюсь SRE и DevOps в Yandex Infrastructure. Такие задачи я решаю уже очень давно, последние 10 лет — в Яндексе.

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

Сегодня я расскажу о нашей специфике обслуживания базовой части инфраструктуры и причинах, которые привели к необходимости всё стандартизировать, а также выбрать облачный подход и запуститься в k8s. Но давайте всё по порядку.

Скрытый текст

Про нашу инфраструктуру

Что представляет из себя сетевая инфраструктура, о которой сегодня пойдёт речь:

  • Это файерволы, L3/L4-балансировщики нагрузки, разные виды туннеляторов.

  • Наши дата‑центры IPv6-only, так что на границе дата‑центра туннеляторы представляют большое разнообразие: NAT64, tun64, декапсуляторы и некоторые другие.

  • Это не маленькое количество железа, а сотни серверов.

  • Каждый сервер обслуживает до 200 гигабит. Можно и больше, но такой сейчас типовой конфиг.

  • У нас 10+ дата‑центров и точек присутствия. Я написал «дата‑центры», но на самом деле крупных дата‑центров здесь не такое большое количество. В то же время серверы стоят и в точках обмена трафиком, которых может быть очень много.

Одним словом, инфраструктура очень разнообразная.

Для лучшего понимания используемых сегодня технологий рассмотрим картинку:

Многие знают, что есть ядерные модули, которые уже давно реализуют нужную функциональность: это IPVS, netfilter, разнообразные туннели. Следующий этап по ускорению — это eBPF. Здесь мы забираем у ядра часть функциональности, за счёт чего может получиться быстрее, особенно если написать хорошо, — такой сейчас тренд.

Но существует и DPDK, когда мы у ядра забираем практически всю работу. Берём пакеты максимально близко у сетевой карты и получаем максимальную производительность. Для перекладывания сетевых пакетов DPDK подходит лучше всего.

Теперь рассмотрим, где находятся наши сервисы. Справа у нас дикий интернет, где есть всё: IPv6, IPv4, BGP и многое другое. И есть дата‑центры, которых может быть несколько. Внутри них у нас IPv6-only сеть, а на границе все эти компоненты что‑то должно соединять. Это и есть железо, о котором идёт речь.

Это не чёрные коробки и не вендороское железо, на самом деле, это серверы, такие же типовые, как и все остальные. Просто в них вставлены дополнительные сетевые карты, в наших серверах сетевых карты три‑четыре:

  • Две из них занимаются непосредственно перекладыванием продакшн‑трафика.

  • Остальные используются для менеджмента хоста. Хост сам должен уметь сходить куда‑то во внутренние сервисы, скачать файервольный рулсет, запушить куда‑то метрики. В конце концов, по SSH нам тоже надо уметь зайти.

  • Расположены сетевые карты могут быть по‑разному, у них может быть разный конфиг. Могут быть двухсотгиговые сетевухи, может быть меньше. Соответственно, и роли у них разные. Мы редко их миксуем: балансировщик чаще всего не будет параллельно и туннелятором. Это разделение упрощает жизнь.

Если заглянуть глубже на железку, то сейчас там всё запущено прямо на хосте: в systemd какие‑то дикие зависимости, всё стартует в непонятной последовательности. И если что‑то пошло не так, достаточно сложно сходу понять что. Иногда даже проще починить ребутом.

Естественно, используются все вещи, которые любят в монолитах:

  • Есть unix‑сокеты для межсервисного взаимодействия, которые не так уж удобно ложатся на контейнерную жизнь.

  • Есть CLI, причём CLI бинарно несовместимая от версии к версии. Это тоже хотелось бы как‑то пошарить между контейнерами в будущем.

  • Разумеется, всё конфигурируется тоже тысячей разных способов. Где‑то ставится пакет, где‑то метапакет, где‑то Salt накатили, кусочек Ansible… А ещё своя самописная система конфигурации для непосредственно сетевой части, которая настраивает анонсы и знает что‑то о самих сетях.

Думаю, понятно, почему YaNet (именно так мы назвали свой продукт) требовал перемен. Казалось бы, мы всего лишь перекладываем трафик, однако помимо Data Plane, Control Plane и парочки агентов у нас появились десятки точек синхронизации, кучка дополнительных сервисов, и всё выглядит уже не так легко, как мы ожидаем.

Здесь даже сами разработчики могут не помнить, кто кому делает reload. Естественно, это приводит нас к частому ручному вмешательству из‑за кучи способов конфигурации. Мы не можем нормально гарантировать стейт, потому что применение может быть непоследовательное.

Как это выглядит на хосте для системного администратора:

Data Plane грузит ядра на полную. На самом деле он в лупе ждёт пакетики, но со стороны это выглядит как стопроцентная загрузка CPU. А ещё: турбобуст отключен, отключен гипертрединг, и произведены некоторые другие манипуляции непосредственно в BIOS. Также прибиты ядра, память используется активно для файервольного рулсета.

В итоге YaNet использует почти всю железку, и для агентов и прочего остаётся не так уж много.

Всё ли так плохо? Конечно, нет, оно же уже работает. И если бы в работе YaNet случились крупные факапы, об этом было бы давно известно. Софт перекладывал пакетики, релизный цикл был не такой частый: раз в полгода обновляли все хосты руками за пару недель. Хорошие инженеры могут это сделать.

Можно догадаться, что это железо обслуживается не только классическими SRE и DevOps, здесь участвуют и сетевые инженеры. За счёт высокой квалификации всех инженеров и очень аккуратных действий — вся эта конструкция нормально существует. Есть и плюсы в том, что каждый хост относительно независим, поломать всё достаточно сложно.

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

Давайте поговорим о том, что хотелось бы получить в идеальном мире.

  1. Мы хотим унификации с остальной инфраструктурой. У нас не только эти коробки, есть и остальные сервисы, которые уже давно живут в Kubernetes. Пару лет назад я рассказывал, как мы переезжали с LXC на Kubernetes. Это тоже выделенное железо во избежание кольцевых зависимостей с остальной инфраструктурой компании.
    К тому же нам внезапно понравилось жить на Kubernetes, захотелось так везде, поэтому почему бы и нет?

  2. Хотим применить все эти новомодные DevOps‑практики: иметь удобный релизный цикл, не бояться катить что‑то, с тестами и прочим. Катиться плавно и автоматически, где это возможно.

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

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

Ну и отдельного разговора заслуживает монолит. С одной стороны, в нём нет никаких проблем, но с другой — кто‑то съел всю память, и мы уже пOOMились и, возможно, не там, где хотелось.

Также мы выехали в опенсорс: YaNet представлен на Гитхабе. Нам хочется поставлять свой продукт наружу, и чтобы это сделать, нужен нормальный способ запуска и конфигурирования. Нельзя выложить какие‑то рандомные ansible‑скрипты, непонятные пакеты с конфигами и странный способ наливки. Никто не сможет это воспроизвести, и даже в соседнюю команду сложно поставить такое решение.

В идеале хотелось переехать в managed‑сервисы, чтобы на нашем железе за нас сделали облако, а мы туда переехали, и всё работало. Большинство сервисов сейчас решают озвученные выше проблемы и хотелки именно так.


К сожалению, за нас облако делать никто не хотел!

Но если всё‑таки помечтать: что мы хотим от облака, над которым можно жить?

Понятно, что это должен быть Kubernetes, который уже безальтернативно победил. Можно было бы использовать какие‑то внутренние наработки, но они относительно сложно отчуждаемые и рассчитаны всё‑таки под другие паттерны. Опенсорс‑решения рассчитаны под большее количество паттернов и переиспользовать их проще.

Это сразу избавляет от многих мук выбора: если Kubernetes, то деплоиться будем Helm, это тоже стандарт де‑факто. Нужен какой‑то CI/CD, чтобы эти хелмы применить, а уже из опыта мы используем ArgoCD. А всё, что мы не сможем запустить стандартными средствами в k8s, мы будем запускать оператором, а не каким‑то костылём. И это тоже стандартный способ для жизни в Kubernetes.

Ладно, была не была, сделаем облако сами, но как это сделать?

Я думаю, многие это пробовали, есть несколько вариантов создания самого кластера.

  • Kubeadm от самих создателей.

  • Kubespray и куча ansible‑рецептов.

  • Hardway (понятно, что для продакшена он нам не нужен).

  • ClusterAPI, когда мы клепаем кластеры по кнопке. Наверное, нам это ещё рано, по крайней мере, не нужно столько кластеров.

Так сложилось, что мы выбрали Kubeadm. С ним есть одна маленькая неприятность. Он лишь инициализирует кластер, но не ставит пакеты и не настраивает sysctl, налиться полностью им не выйдет.

Пойдём по порядку

  1. Исторически мы использовали SaltStack. Итого: мы наливаем хост, а наливать хосты мы умели и до этого. При наливке уже ставится нужный пресет BIOS, доналиваем хост солтом (буквально поставили пакеты, пару sysctl, репы). Kubeadm»ом инициализируем кластер.

  2. У нас есть кластер, но, как мы знаем, кластер из коробки ничего не может, даже запустить обычный под. Нам нужна сеть. Так что следующим пунктом идёт Calico VXLAN. Оверлей нам нужен из‑за того, что железки очень разнообразные, у них может быть разная сеть, и это самый простой способ жизни в разнородной сети. А Calico мы используем, потому что Cilium не умел работать с IPv6 для VXLAN.

  3. У нас есть какая‑то сеть, у нас есть кластер. Теперь нам надо научиться в нём жить, так что ставим в него Argo CD.

  4. Дальше применим концепцию IAC. Мы используем Bootstrap‑приложение, всего лишь один Helm Chart, в котором куча applications для ArgoCD.

  5. Нажимаем много раз синк, и…

  6. Наш кластер готов!

В итоге мы можем клепать кластеры, хоть и не по кнопке, но достаточно легко и непринуждённо.

Следующий вопрос более сложный: сколько кластеров нам делать? Для ответа нужно снова подумать о том, что хочется получить:

  • Единица отказа у нас это дата‑центр. Без одного ДЦ плюс‑минус мы можем как‑то жить.

  • Мы хотим минимизировать риски, дата‑центр всё равно не хотелось бы положить полностью.

  • Слишком много кластеров тоже не делаем, нам же это обслуживать: обновлять, поддерживать. А у нас не managed cloud, где кластеры обслуживают за нас. Так что избавляемся от лишней работы.

  • Нужна высокая доступность: мастера должно быть три в каждом кластере.

Что мы делаем, исходя из этого?

  • Мастера на LXC, потому что эти железки уже были. Это другое железо, оно стоит в дата‑центре, удобно сделать мастера там и не делать их на боевых коробках, которые занимаются тем, что используют CPU для обработки трафика.

  • В крупных дата‑центрах будем делать по два кластера. Опять же, у нас есть запас железа, и потерю половины мощностей в одном дата‑центре без пиковых нагрузок никто не заметит. Так мы минимизируем риски потери даже одного дата‑центра.

  • Для мелких площадок, где не имеет смысла делать отдельный кластер, делаем один большой MultiDC‑кластер.

  • Все кластеры должны быть полностью независимые, у них в том числе должны быть свои ArgoCD.

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

Облако мы сделали, но надо ещё научиться в нём жить. А ведь мы ещё не переехали даже на контейнеры, живём на пакетах и systemd.

Простое заканчивается, начинается переезд

Очевидные тезисы.

  1. При переезде не выйдет перенести сразу всё в k8s, пока оставим салту махинации с железом (пиннинг ядер, настройка количества HugePages, конфигурация BIOS и прочее).

  2. Агенты уезжают в DaemonSet. Для запуска основных воркеров, которые занимаются трафиком: Data Plane, Control Plane, анонсер, bird2 — нужно использовать оператор. К этому ещё вернёмся позже.

  3. Для большинства озвученных воркеров будет использоваться hostNetwork True, потому что сеть используется напрямую. Для переезда так будет проще, потом будем отключать по одному.

  4. Также нам нужен слой совместимости. Переехать быстро не выйдет, даже просто переналить всё существующее железо аккуратно займёт не меньше года.

  5. Systemd dependency уезжает в initContainers: все эти мутные связи нам придётся реализовать силами k8s. Ниже рассмотрим, какие есть для этого варианты.

  6. Пробуем селить всё системное, что относится непосредственно к кластеру: операторы, ArgoCD и прочий обслуживающий софт, — на мастера. Мы ведь помним, что на боевых коробках не так много ресурсов, да и не стоит пугать сетевых инженеров воркерами ArgoCD на боевой коробке. Поэтому настраиваем Taints и Tolerations. Конфиги пока будем хранить на железе. Для совместимости так будет проще, потом, после полного переезда уберем в ConfigMaps.

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

Кроме главного инструмента в виде initContainers, также у нас есть PostStart‑хук, назначение которого сходу непонятно. Есть ещё и PreStop, но с ним всё более‑менее ясно из названия: он вызывается до SIGTERM, чтобы за собой что‑то почистить.

Возьмём первый пример: initContainer, который создаёт HugePages до старта основного приложения.

initContainers:
 - args:
  - -c
  - "cat /etc/yanet/hugepages |tee /sys/devices/system/node/node*/hugepages/hugepages1048576kB/nr_hugepages" 
  command:
  - /bin/sh
  image: busybox

Пример ожидания соседних сервисов. 

initContainers:
 - args:
  - -c
  - "while [ $(/usr/bin/yanet-cli version | grep controlplane | wc -l) -ne 0 ]; do
    echo \"controlplane already running...\"; sleep 1;
   done;
   [ -e /run/yanet/dataplane.sock ]; do
     echo \"dataplane.sock waiting...\"; sleep 1;
   done; sleep 20;"
  command:
  - /bin/sh 

Видно, что в Kubernetes намного меньше способов сделать зависимости, с другой стороны, они куда более гибкие, написать можно что угодно.

Вернёмся к PostStart. Казалось бы, зачем он нужен? Вот странный, на первый взгляд, пример, но жизненный. После старта приложения мы перемонтируем конфиг и дёргаем reload.

poststart = []string{
	"/bin/sh", 
	"-c",
	`sleep 60;
	/bin/mountpoint -q /etc/yanet/controlplane.conf;
	/bin/mount -o ro,bind /etc/yanet/controlplane.slb.conf /etc/yanet/controlplane.conf;
	/usr/bin/yanet-cli reload`, 
}

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

~# cat /usr/bin/yanet-cli 
#!/bin/bash 
set -e 
sudo crictl exec $(crictl ps --state Running --name controlplane -q | tail -n 1) /usr/bin/yanet-cli $@ 2>/dev/null 

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

Запускаем оператор

Мы поняли, как перейти на контейнеры, а зачем нам нужен оператор? Примитивно: оператор — это фактически наш демон, который общается с kubeapi.

Но почему нам нужен оператор? Попытаемся понять, что мы хотим от наших основных воркеров YaNet.

  • Три‑четыре версии в продакшене — уже звучит странно, но это факт. Инфраструктура очень критичная, и если что‑то упадёт, будет боль и унижение. Поэтому мы никогда не обновляемся полностью, это даёт нам больше гарантий.

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

  • У нас устроено внешнее управление, но опять же это наследие прошлой жизни. Как минимум, на переходный период нам нужно это учитывать. Внешний управлятор должен прийти и смочь обновить софт на конкретной железке.

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

  • Конфиги per host разные, то есть оператор должен понимать, какая роль у хоста, и использовать нужную.

Почему нельзя было сделать просто кучку деплойментов с селекторами? На самом деле можно, но вам нужно их где‑то генерировать и хранить. Можно сделать это в Helm Chart, и даже на крупных конференциях я слышал такие идеи. Мы можем сделать в Helm генератор, в нём всё обмазать темплейтами, и оно будет работать. Это так, но получится реально сложное решение. Оператор — это упрощение этой истории, он знает намного больше про кластер, и управлять кластером изнутри более естественно.

Какие у операторов есть паттерны использования? Для простоты я выделяю всего два: Watch и Webhook. Watch просто подписывается на изменения каких‑либо ресурсов через kubeapi, а Webhook вызывается в момент какой‑то жизненного цикла самим k8s. По факту это очень похожие вещи, но у них разный триггер запуска.

Рассмотрим, в какой момент вызывается Webhook.

Упрощённо мы имеем два типа вебхуков: ValidatingWebhook и MutatingWebhook. Через них мы можем, например, блокировать появление новых подов или переписывать их спеку. К примеру, kyverno использует оба типа вебхука.

Наверное, самый известный всем паттерн — это подселение сайдкара.

Большинство операторов, которые подселяют container в под, работают именно так. Они смотрят аннотацию каждого пода при создании, именно через Webhook, видят, что там есть нужная им аннотация, и подселяют через MutationWebhook свой сайдкар. Просто берут и патчат спеку пода, причём каждого.

Что с подпиской? Подписаться можно на любой объект, причём это может быть даже воркер‑нода, как в данном примере.

func (r *YanetReconciler) SetupWithManager(mgr ctrl.Manager) error { 
	return ctrl.NewControllerManagedBy(mgr).
		For(&yanetv1alpha1.Yanet{}).
		Watches(&v1.Node{}, &handler.EnqueueRequestForObject{}). 
		Owns(&appsv1.Deployment{}). 
		Complete(r) 
}

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

Про операторы можно говорить очень много, зачем вообще я углубился в эту тему? Мне кажется, многие админы всё ещё боятся писать операторы. Давайте докажем, что это максимально легко: точно проще, чем написать какой‑то страшный Helm Chart или ansible‑плейбук с кучей jinja2.

В примере ниже мы скачиваем оператор SDK, инитим свой оператор и создаём в нём API без кастомного ресурса, максимально простое, которое просто делает watch на воркер‑ноды.

$ wget https://github.com/operator-framework/operator-sdk/releases/download/v1.34.1/operatorsdk_linux_amd64
$ chmod +x operator-sdk_linux_amd64 
$ ~/operator-sdk_linux_amd64 init --domain borislitv.net --repo github.com/borislitv/node-operator
$ ~/operator-sdk_linux_amd64 create api --version=v1 --kind=Node --controller --resource=false 

В результате за нас уже нагенерировали целый проект, мы никогда не писали на GO, но фактически нам всё это и не надо, нам нужно лишь дописать логику в Reconcile loop а остальной код можно и не смотреть.

$ cat internal/controller/node_controller.go
 …..... 
func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 _ = log.FromContext(ctx)
 // TODO(user): your logic here
 return ctrl.Result{}, nil 
} 

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

Ну и простенький пример возможной логики:

func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)


	node := &v1.Node{}
	err := r.Client.Get(ctx, req.NamespacedName, node)
	if err != nil {
		return ctrl.Result{}, nil
	}
	for label := range node.Labels {
		if label == "node-role.kubernetes.io/control-plane" {
			return ctrl.Result{}, nil
		}
	}
	// TODO: gen zone from hostname, push it into node-operator/zone
	if node.Labels["node-operator/dc"] != node.Name {
		node.Labels["node-operator/dc"] = node.Name
		if err := r.Client.Update(ctx, node); err != nil {
			return ctrl.Result{}, err
		}
	}
	return ctrl.Result{}, nil
}

Получаем воркер‑ноду, на которую нам пришло уведомление. Проверяем, мастер это или не мастер. Если это мастер, мы ничего не делаем, если это не мастер, мы добавляем свой лейбл.

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

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

Поэтому в YaNet тоже появился свой оператор, и у него два своих кастомных ресурса.

  1. Один я условно назвал global config: он будет один на кластер, и он заменяет глобальную конфигурацию. Можно было бы сделать конфиг‑мапу, но в операторе её бы пришлось всё время поллить, и у неё не было бы спеки с валидацией. А свой кастомный API как раз решает за нас обе эти проблемы.

  2. Per host — отдельный конфиг для управления тем, что оператор поднимет на конкретном хосте. Для каждого хоста оператор генерирует три‑четыре деплоймента с replicacount: 1. Почему? Потому что поды создавать не особо принято, это не гибко. У деплоймента мы можем, например, сделать replicacount: 0 и просто посмотреть, какая спека сгенерировалась, ничего не повредив.

Почему не statefullset? Statefullset подходит хуже из‑за иммутабельности. Его сложнее править, а ведь у нас внешнее управление в виде оператора, править нам надо, и мы сами рулим всем, поэтому деплоймент удобнее. Это best practice в написании операторов.

Автодискавери тоже есть. Что имеется в виду? При появлении новой воркер‑ноды мы имеем возможность сходить во внешние ручки, узнать, какая роль у нового хоста, узнать его конфиги и сгенерить для него кастомный ресурс.

Давайте посмотрим, как это выглядит в реальной жизни. Вот у нас YanetConfig, который один на кластер.

~/$ kubectl -n yanet get yanetconfig/config -o yaml
apiVersion: yanet.yanet-platform.io/v1alpha1
kind: YanetConfig
Metadata:
 labels:
  argocd.argoproj.io/instance: yanet-operator
 name: config
 namespace: yanet
spec:
 autodiscovery:
  enable: false
  namespace: yanet
  registry: dockerhub.io
 stop: false

Он не такой большой. На примере stop можно понять, зачем он вообще нужен. Выставляя stop в true, мы заставим оператор прекратить любые деструктивные действия в эту же секунду. Фактически, это аналог стоп‑крана.

Больше примеров можно найти на Гитхабе.

Теперь per host, кастомный ресурс.

В нём можно отключать разные компоненты, можно включать, можно управлять их версиями.

  • Можно выставить replicacount: 0 и просто посмотреть, что сгенерировал оператор, ничего не запуская, даже для старого хоста без k8s.

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

  • Внешним управлятором можно легко через kubectl patch обновить версию, оператор перегенерирует все деплойменты, и дело сделано.

Также оператор пишет статус в наш кастомный ресурс.

status: 
 pods:
  Running:
  - controlplane-test.yandex.net-79c7764784-k7rwv
  - bird-test.yandex.net-cbd56c6cd-69jp6
  - dataplane-test.yandex.net-6bcdf78589-tcznp
  - announcer-test.yandex.net-6fb4b59497-7vwln

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

Какие проблемы ждут нас на пути переезда в светлое будущее

Контейнеризация. Казалось бы, в 2K24 это уже классика, однако в конкретных ситуациях всё ещё вылезает интересное.

  • Unix‑сокеты, cli‑тулзы — это неудобно, нужно монтировать себе что‑то в контейнер, нужно вызвать CLI из соседнего контейнера. В итоге можно дойти до монтирования сокета containerd к себе в контейнер и вызова CLI через crictl exec из соседнего. Всё это может быть из‑за бинарно несовместимого CLI и отсутствия возможности узнать версию протокола в соседнем контейнере. Такое надо чинить на стороне разработки, однако это может затянуться.

  • Есть проблемы с cgroup. Сgroup v1 сами разработчики признали кривой и стали пилить v2. Но для v2 они запилили не всё, только часть, поэтому версии существуют параллельно.
    Есть cgroup‑провайдеры, которых тоже несколько и которые эти cgroup создают. Например, есть свой systemd‑провайдер и некоторые другие. Они могут использовать разные версии cgroup, где‑то v1, где‑то v2.

    Например, HugePages, который мы используем, нет в v2, они нужны от v1. И на это можно наступить.

  • С cgroup также связана версия ядра. Вы жили на своём четвёртом, всё вас устраивало, а тут оказывается, что вам нужны новые фичи, и в итоге завтра вы проснулись на шестом ядре.

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

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

  2. Разнородная инфраструктура генерирует проблемы для самого кластера. Сделать кластер из разрозненных машин с разным качеством менеджмент‑сети может быть нетривиальной задачей.

  3. На первый взгляд, внедрение k8s сильно усложняет схему. Вроде у нас стояли коробки, долбили трафик, всё хорошо, а тут мы сделали кластер, облако. Зачем всё это надо? Такие вопросы могут возникнуть, и на них надо приготовить валидные ответы.

  4. Отказоустойчивость, ведь у нас теперь кластер, а не отдельные хосты.

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

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

Если в ДЦ апокалипсис. Если мастера умерли, это не проблема, хост продолжает работать как раньше: поды рестартит kubelet в случае падения. Вы лишь теряете возможность обновления.

Но что если у нас пропало питание, всё ребутнулось, кластеры развалились? Мы хотим жить даже в таких условиях, эти ваши облака не должны испортить нам малину. И решение есть — static‑поды.

Многие знают, что базовые компоненты кубера (etcd, kubeapi и прочее) часто запущены на мастерах именно static‑подами. Как это работает: берём YAML‑спеку пода, кладём в /etc/kubernetes/manifests/ и kubelet поднимает его без всякого мастера локально. Казалось бы, вот оно идеальное решение. Оператором можно было бы генерить эти спеки, подкладывать, и всё круто, мастер нам больше не нужен. Но это не так гибко и неудобно.

Однако решение есть, это openshift/pod-checkpointer-operator, который наши коллеги из YDB пропатчили и оживили. В чём суть оператора? Он запущен daemonset»ом и при нормальной работе синкает спеки нужных подов на каждом хосте в отдельную папочку, сохраняет конфиг‑мапы в виде файликов и так же сохраняет Secret‑мапы. Если он видит, что kubeapi пропал, он берёт эти файлики и копирует в боевую директорию для static‑подов. Всё поднимается как раньше, однако это уже будут static‑поды. При этом можно даже попатчить эти YAML‑файлы руками, например, поменять версию.

Переходный период. Это большая проблема, потому что инфраструктура критичная, никто не будет быстро всё менять. Счёт идёт на года. Эти года придётся жить на двух стульях, в том числе поддерживать врапперы и костыли для совместимости.

Давайте подведём итоги

Вспомним всё, что успели здесь рассмотреть:

  • Надеюсь, мы немного пролили свет на сетевую инфраструктуру, и теперь это не просто какие‑то чёрные коробки, а хотя бы серверы.

  • Примерно поняли, как это работает, как это устроено.

  • Осознали, почему облачный подход побеждает.

  • Обсудили, как перейти на контейнеры на примере нашего неординарного случая.

  • Разобрались, как готовить Kubernetes. Слава богу, это становится всё проще и проще.

  • Рассмотрели концепцию операторов, поняли, насколько легко их писать, что их надо использовать в любом непонятном случае.

  • Рассмотрели проблемы, с которыми вы можете столкнуться, если захотите повторить этот путь.

От себя хотел бы добавить: очень радует происходящая прямо сейчас стандартизация в мире облаков и инфраструктуры в целом. Kubernetes уже победил и от этого развивается ещё быстрее, становится надёжнее, гибче и понятнее. Уже сейчас можно клепать целые кластеры по кнопке через ClusterAPI. Whitebox‑свитчи с docker‑compose под капотом уже не удивляют. Думаю, кубер скоро будет в каждой стиралке, ведь у нас есть minikube/kind и подобные проекты, замена ими docker‑compose — это вопрос времени.

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