k8s-logo

Пусть вы – разработчик, который пришел в компанию, где весь процесс разработки крутится вокруг непонятной штуки под названием Kubernetes. Или, другой сценарий: ваш коллектив только готовится к такому переходу. Я расскажу о том, как будет выглядеть быт разработчика и что ему надо знать, этакий курс молодого бойца. Я делаю предположение, что читателю знакома идея контейнеров и имеется практический опыт работы с ними. Если это не так – ссылочки из предыдущего предложения в помощь. Как и в случае докера – рассказ про кубер будет состоять из двух статей: теоретической и практической.

Введение

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

Разве контейнеров не достаточно?

Docker – замечательная технология контейнеризации приложений, значительно упростившая жизнь командам разработки. Она дает возможость забыть о проблемах зависимостей, требованиях к окружению, специфике сборки в различных языках и фреймворках, а также – доставки кода в боевые и тестовые окружения. Докер позволяет относиться к какому-либо приложению как к функции в языке программирования. Ведь что из себя представляет функция? Это оформленная единица полезной функциональности, у которой четко определены входы и выходы, а внутри – черный ящик. Как не нужно знать внутреннее устройство хорошо написанной функции, чтобы эффективно ею пользоваться – так и внутренние особенности контейнера не важны до тех пор, пока соблюдены “соглашения по входам-выходам”. Контейнеры закрыли определенный класс проблем, и мы двинулись дальше, в светлое контейнерное будущее… Но на самом деле – как всегда, навстречу новым, более высокоуровневым проблемам :)

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

  • Как распределить сотню-другую контейнеров по серверам?
  • Как следить за “здоровьем” каждого из них?
  • Как они будут друг с другом общаться?
  • Как масштабировать приложение, когда один контейнер не справляется с нагрузкой?
  • Как пользователям попадать на правильный сервер при обращении к определенному приложению?

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

В Cloud Native тусовке есть мнение, что оркестраторы являются естественным развитием микросервисной идеи и в конечном итоге накроют индустрию веб-разработки такой же волной, как в свое время это сделали контейнеры.

Что такое Kubernetes?

Kubernetes – изначально разработанный в Гугле, а ныне опенсорсный, контейнерный оркестратор. Из всех существующих является, скорее всего, самым фичастым и с большим запасом – самым популярным.

Оффтоп: Причина такой популярности кроется в том, что кубер – это не готовое, отлитое в бронзе коробочное решение, а комплект апишечек, предоставляющих набор примитивов, необходимых для работы кластера, в котором крутятся контейнеры. Такая абстрактность и модульность дает большой простор для кастомизации и расширения. Любой сторонний разработчик может прикрутить к куберу “свою сеть” или “свой сторедж” просто реализовав IngressController или StorageClass.

Кубер в своей основе является системой кластеризации. Основной задачей такого класса программных систем является объединение неких ресурсов в абстрактный пул и предоставление конечным потребителям удобного интерфейса для доступа к этому пулу ресурсов.

Если добавить контейнерной специфики, то задачи можно записать следующим образом:

  • объединение нод (виртуальных машин или физических серверов) в единый кластер
  • распределение контейнеров по нодам
  • запуск контейнеров и поддержание их работоспособности
  • обеспечение сетевой связности между контейнерами
  • обеспечение доступа к контейнерам из внешнего мира
  • сохранение состояния stateful-контейнеров

Ближе к делу

Устройство Kubernetes

С точки зрения пользователя, основными примитивами являются:

  • Pod – минимальная единица оркестрации в кубере (да , он занимается распределением именно подов, а не контейнеров). В поде, как горошины в стручке, размещаются один или несколько контейнеров с общим локахлостом и вольюмами (поэтому все контейнеры пода запускаются на одной ноде). В 99% случаев это именно один контейнер, но есть ситуации, когда объединение нескольких контейнеров в один под чрезвычайно полезно.

  • ReplicaSet – контроллер, позволяющий создать набор одинаковых подов и работать с ним, как с единой сущностью. Это наш основной механизм масштабирования приложений. ReplicaSet поддерживает заданное количество запущенных реплик. Например, если ReplicaSet заметит, что вместо сконфигурированных 5 копий пода есть всего 4 (например умерла нода, где находился пятый под) – он попросит кубер создать недостающий под.

  • Deployment – контроллер, управляющий ReplicaSet’ами. Именно с ним обычно непосредственно работают пользователи. Дополнительный уровень абстракции нужен для процесса обновления. При обновлении на новый образ контейнера создается новый ReplicaSet, в который “перекачиваются” поды из старого. Таким образом мы можем выкатить новую версию приложения без даунтайма. Плюс деплой хранит историю конфигураций и позволяет автоматически делать откат, если в процессе релиза что-то пошло не так.

  • Service – сетевое имя пода для связи внутри кластера. Если нужно, чтобы до пода могли достучаться другие поды – на него должен указывать сервис. В голом докере эту функцию выполняет имя контейнера: типичная ситуация, когда контейнер приложения обращается к базе через mongo://db:27017. В кубере сетевое имя отделено в отдельную сущность. Это необходимо для того, чтобы сервис мог указывать не на один под, а на целый комплект – ReplicaSet или StatefulSet, тем самым давая возможность балансировать запросы. Дополнительная приятность – общение через сервисы работает даже если контейнеры находятся на разных нодах.

  • Ingress – Напарник сервиса в сетевой области, но на этот раз – в вопросе внешнего доступа пользователей. Здесь разработчик говорит:

    Если на кластер пришел запрос на домен foo.com и роут /bar – отправь его вот на этот Service.

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

  • Job/CronJob – контроллер, запускающий (разово или по расписанию) короткоживущий под, который должен сделать какую-то работу и умереть. Миграции, бекапы, скрейперы – это все вот сюда. Контроллер будет пытаться добиться успешного завершения пода, перезапуская его в случае если он завершится с ошибкой. Настойчивость рестартов и другие параметры – контролируемы.

  • PersistentVolumeClaim – запрос на вольюм (он же PersistentVolume) для нужд пода. Здесь разработчик говорит:

    “Моему приложению нужно 50 гигов места, куда я смогу складывать свои данные.”

    Кубер должен расшибиться и найти поду этот вольюм, иначе он не будет запущен.

  • StatefulSet – тот же ReplicaSet, но специально для stateful-приложений, как правило – кластеризуемых баз данных или систем очередей. Кубер понимает, что каждому поду в этом сете нужен свой собственный вольюм, а не один на всех. Плюс у самих подов будут предсказуемые порядковые имена вида db-0, db-1, db-2…, вместо псевдослучайных хешей, чтобы можно было автоматизировать формирование кластеров.

  • Namespace – весь кластер можно поделить на изолированные неймспейсы с изоляцией по именам сущностей, ресурсам, правам доступа и т.д. Практически все примитивы, кроме некоторых общесистемных, относятся к какому-то неймспейсу.

Пример того, как может выглядеть приложение с фронтендом и бекендом, хранящим данные в кластеризованной базе. k8s-app

Ради технической дотошности, пара подкапотных моментов, не отображенных на схеме (можно смело пропустить):

  1. Оба деплоймента создают реплика сеты с количеством реплик, равное трем, которые в свою очередь поддерживают заданное число реплик.
  2. Путь от стейтфул сета до вольюмов в контейнерах следующий:
    • в спецификации стейтфул сета в форме PersistentVolumeClaim указано, что каждому поду нужно хранилище определенного объёма
    • облачный провайдер удовлетворяет этот запрос и создает PersistentVolume‘ы по количеству реплик (создает и подключает виртуальные диски к нодам кластера)
    • каждый под подключает эти PersistentVolume‘ы в виде вольюмов

Возможности и плюшки

Из простого списка примитивов не всегда складывается стройная картина того, какие плюшки приносит использование кубера. А это, ни много ни мало – реализация идеи Infrastructure-as-a-Code, доступная разработчикам, не искушенным в вопросах серверного администрирования. По сути у автора программы появляется возможность сказать куберу:

“Вот, что я знаю о потребностях моего приложения – учти их и разверни его, как считаешь нужным.”

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

Продолжая тенденцию, разработчик лучше всех знает:

  • Что значит “приложение работает”?
  • Как устроен роутинг приложения?
  • Можно ли его масштабировать?
  • и т.д…

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

Есть только описание приложения и его потребностей, а также уверенность в том, что стоит скормить это описание куберу – и вскоре можно будет вбить в браузере нужный адрес и увидеть результат.

Интерфейс пользователя

Кубер является декларативной системой, поэтому все операции с ним происходят посредством написания и скармливания ему YAML-определений описанных выше (и не только) ресурсов. Формат похож на docker-compose, но в силу большей декомпозиции – гораздо более многословный. Вот пример простенького приложения, похожего на схему выше. Оно обслуживает роут /demo и хранит свои данные в монге.

Родная консольная утилита kubectl позволяет:

  • скармливать куберу определения через kubectl apply -f myapp.yaml
  • изучать созданные ресурсы через kubectl [get|describe|logs] {resource}
  • изменять и удалять ресурсы через kubectl [edit|delete] {resource}

Как правило, kubectl используется только для отладки. В обычном рабочем процессе используются более высокоуровневые инструменты. Как в мире докера docker-compose по большей части заменяет консольную утилиту docker, так в мире кубера люди используют различные больше-чем-просто-шаблонизаторы. Де-факто стандартом является Helm.

Подробнее о возне с ямликами – в следующей статье.

Новый быт разработчика

Итак, допустим по матчасти поверхностное представление появилось. Как все это повлияет на ежедневную работу?

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

Второй закономерный момент: приспособить приложение для работы в подобной среде. Мы должны пользоваться открывающимися возможностями, а не бороться с системой. По большей части необходимые модификации вносятся в приложение еще на этапе контейнеризации. Эти моменты хорошо раскрыты в знаменитом чеклисте 12 factor app.

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

Хелсчеки

Одна из таких возможностей – это возможность следить за здоровьем нашего приложения. Это реализовано через livenessProbe и readinessProbe – они же в простонародье “хелсчеки”. Это некоторые определенные пользователем действия, которые кубер периодически выполняет по отношению к поду. Например это может быть запрос httpget: /health для нашего веб-приложения или exec: redis-cli ping для контейнера редиса.

livenessProbe отвечает на вопрос:

“Живо ли приложение внутри пода?”

В случае отрицательного ответа на этот вопрос планировщик перезапустит под.

readinessProbe же отвечает на вопрос:

“Готово ли приложение принимать трафик?”

В случае отрицательного ответа на этот вопрос контроллер Service исключит этот под из балансировки.

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

“Почему этим должен заниматься разработчик, а не “кто-то из обслуживающих серваки”? У меня локально все работает, а на проде сами уж там следите за всем!”

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

  1. Он без задней мысли вкрутит какой-нибудь httpget: / на свое усмотрение. Окажется, что конкретно в этом приложении этот роут рисует статичную страницу и не стучится в базу, а подключение к ней как раз-таки неправильно настроено или оборвалось в процессе работы. В итоге “Ой, чет ничего не работает” вам скажут пользователи, а не мониторинг.
  2. Он более отвественный и дотошный. Он допытает разработчика по этому вопросу, выудит у него из головы тайное знание о наиболее показательном способе проверять здоровье приложения и задокументирует его в виде правильного хелсчека.

Очевидно, что рано или поздно все придут ко второму варианту. Дак почему бы не миновать допрос от админа и не написать одну строчку в YAML-файл самостоятельно? Более того – зачастую нет какого-то роута, который одновременно показателен в плане определения здоровья и который можно безопасно дергать каждые несколько секунд. В таком случае переходят от колхозного поиска наиболее репрезентативного роута к практике создавать технический роут /health, который не крутит реальную бизнес-логику, а делает внутренние проверки своего состояния. Это в любом случае будет реализовывать разработчик.

Ресурсы

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

“Как распределить сотню-другую контейнеров по серверам?”

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

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

Нижний предел называется requests. Это то, сколько контейнер запрашивает ресурсов у кластера. Планировщик должен выбрать ноду, на которой есть минимум столько ресурсов, сколько указано в реквестах.

Верхний предел называется limits. Это предел потребления, который контейнер не может превысить. В случае, если контейнер превышает лимит CPU – он троттлится (у него принудительно отнимается процессорное время, как итог – он замедляется). Если он превышает потребление памяти – он попросту прибивается.

Пример указания ресурсов:

resources:
  requests:
    cpu: 100m
    memory: 100Mi
  limits:
    cpu: 200m
    memory: 200Mi

(100m – это “100 миллиядер” или 0.1 ядро)

Опять может возникнуть возмущение: “Почему опять разработчики?!”. И опять ничего нового: разработчик – главный знаток своего приложения. Казалось бы:

“Запустить контейнер, посмотреть потребляемые ресурсы и записать их в YAML-файл – одинаково легко для разработчика и админа. Это займет всего несколько минут, не отрывайте разработчиков на всякую фигню!”

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

  • Почему мой тривиальный микросервис со старта потребляет гигабайт памяти?!
  • Почему потребление памяти стабильно растет, хотя мое приложение не должно накапливать никакого состояния?
  • Почему у моего однопоточного приложения реквест в 5 ядер?

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

Нарисовать остальную сову

owl

Последний шаг на пути к успеху – написать оставшиеся 95% ямликов. Почему такое внимание к 5% и пренебрежение ко всему остальному?

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

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