В этом тексте я не раскрываю понятия Service Level Indicators (SLI) и Service Level Objectives (SLO). О том, для чего служат и как рассчитываются, я писал в своей предыдущей статье на Хабре. Прочитайте ее, если эта тема вам не знакома.

Постановка проблемы

В последние годы перед инженерами нередко ставят задачу спроектировать SLI для тех компонентов, которые они поддерживают. Мне кажется, что одна из главных причин этого в том, что SLI/SLO позволяют оперировать всего несколькими цифрами, интуитивно понятными каждому. Вместо десятков технических метрик, которые еще нужно правильно проинтерпретировать, глядя на SLI можно сразу сказать, идут ли дела хорошо или плохо.

Идея авторов Google SRE Book состоит в том, что SLIs должны измерять «счастье» пользователей. Так, если пользовательский сервис недоступен, пользователи будут недовольны. Правильно спроектированный SLI в таком случае должен идти вниз и оказываться ниже целевого значения (SLO). Таким образом, SLI и SLO должны позволять сделать вывод о том, насколько хорошо работал сервис в выбранном интервале времени.

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

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

Более или менее понятно, какие SLIs следует применять для пользовательских сервисов и как их рассчитывать. Однако для инфраструктурных команд попытка внедрить SLI сопряжена с рядом сложностей.

Я столкнулся с ними пока внедрял SLI/SLO в своей команде, зоной ответственности которой являются Kubernetes-кластеры. Далее я расскажу несколько историй об этом и поделюсь выводами, к которым я пришел.

«Плохие» ответы от kube-apiserver

Когда вас просят сконструировать SLI для Kubernetes, самым очевидным кажется начать с Availability SLI для kube-apiserver. Именно этот компонент отвечает за все внешние взаимодействия, и мы ожидаем, что он всегда будет отвечать на запросы клиентов. Обычно Availability SLI рассчитывается в виде доли времени за выбранный интервал, когда сервис был здоров. Но что значит «здоров»?

Для пользовательских сервисов общепринято считать, что сервис здоров, если он не отдает ошибки в ответ на валидные запросы. Мы можем применить тот же подход к kube-apiserver, подсчитывая процент ответов с `http status code != 5xx` от общего числа запросов. Но если вы попытаетесь замерить такой SLI в большом высоконагруженном кластере, то с высокой вероятностью обнаружите, что ваши kube-apiservers почти никогда не отдают 0% ошибок. По крайней мере, такую картину я набюдал в большинстве наших продовых кластеров.

Мне стало любопытно, откуда берутся эти ошибки. С помощью Vector я преобразовал аудит логи kube-apiserver в метрики и построил дашборд, на котором в реальном времени можно видеть сервисы-виновники. В результате я обнаружил, что огромную долю ошибок порождают запросы от компонентов service mesh. У нас service mesh относится к зоне ответственности другой команды.

Дашборд с сервисами, генерирующими наибольший фон ошибок kube-apiserver
Дашборд с сервисами, генерирующими наибольший фон ошибок kube-apiserver

Я пришел к коллегам и рассказал, что 65,5% ошибок kube-apiserver в продуктовых кластерах вызваны сервисами, за которые они отвечают. Выяснилось, что ошибки генерировались только некоторыми из реплик. После того, как они перезапустили соответствующие поды, ошибки ушли, но уже через несколько дней ситуация повторилась. Тогда я создал алерт, направляющий сообщение владельцам сервисов, которые создавали наибольший фон ошибок. Но и это не сработало. Коллеги не спешили искать причину их возникновения.

Поразмыслив, я понял, что их вполне можно понять. Для того, чтобы найти и устранить причину, вероятно потребовалось бы приложить немало усилий. При этом один из проблемных компонентов уже был объявлен legacy, и от него планировали в скором времени отказаться. Было ли оправданным тратить усилия на поиск причины? Несмотря на генерируемые ошибки как сервис, так и сам kube-apiserver работали исправно, и никто на них не жаловался. Единственное, что страдало от этих ошибок, это наш новенький SLI.

Так я сформулировал для себя два требования к проектированию инфраструктурных SLI:

  • если система работает не идеально, но никто этого не замечает, состояние системы следует признать «хорошим». Иными словами, надо стремиться создавать такие SLI, падения которых пользователи способны заметить в ходе обычного пользования системой. Эта идея не нова – она в том или ином виде содержится в Google SRE Book.

  • если уж создавать SLI в отдельных командах, значения индикаторов должны коррелировать с качеством их работы. Мы хотим, чтобы SLI зависел от того, что полностью или хотя бы большей частью находится в зоне нашей ответственности. Если у нас что-то отказало по нашей недоработке, и SLI снижается – это нормально. Но если SLI больше реагирует на действия других команд, значит стоит подумать над тем, чтобы использовать другой SLI.

Руководствуясь этими требованиями, я сделал два наблюдения:

  • большая часть ошибок kube-apiserver возвращается на запросы GET и LIST, что остается не замеченным пользователями

  • небольшой фон ошибок на запросы типа CREATE, UPDATE и т.д. не свидетельствует о проблемах самого kube-apiserver

Раздел SLI-дашборда про control-plane после небольшого инцидента
Раздел SLI-дашборда про control-plane после небольшого инцидента

В результате сейчас мы используем SLI, который учитывает только запросы на создание и изменение ресурсов, и снижается только тогда, когда процент «плохих» ответов превышает 99,9%. Мы видим падение SLI, когда проблемы возникают на стороне самого kube-apiserver, но не видим его, когда ошибки вызывают клиенты apiserver. Разумеется, при этом мы время от времени поглядываем, какие сервисы генерируют наибольшее число ошибок, чтобы при необходимости дать знать об этом их владельцам.

Доступность worker nodes

Kube-apiserver играет ключевую роль в работе кластера, однако рабочая нагрузка запускается на worker nodes, и вне всякий сомнений для них тоже требуется какой-то SLI.

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

Во-первых, это более всего справедливо для кластеров, в которых запущены stateless-сервисы.

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

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

С другой стороны, разработка и запуск приложений в распределенных системах были бы затруднены, если бы им не предоставлялись некоторые гарантии. Так, находясь в Kubernetes, приложение может рассчитывать на автоматический перезапуск при провале liveness probes, балансировку трафика между «живыми» репликами и т.д. Пока предусмотренные гарантии выполняются, мяч находится на стороне разработчиков сервисов.

В-третьих, capacity кластера должно хватать для переноса запущенной нагрузки на здоровые ноды.

При соблюдении перечисленных требований, SLI, падающий при переходе всего одной ноды в состояние NotReady, вряд ли можно признать годным. Kubernetes «из коробки» умеет работать с отказом нод. Как известно, при обнаружении отказа он сам переводит ноду в статус NotReady и выселяет с нее рабочую нагрузку. До тех пор, пока capacity кластера хватает для заселения подов, выселенных с «плохих» нод, все должно быть хорошо. Таким образом, если мы знаем, что потеря 20% нод в кластере, не приводит к каким-либо негативным последствиям, такое состояние все еще можно признать «хорошим». В практическом смысле это означает, что SLI должен падать только, если в кластере недоступно более 20% нод (разумеется, при необходимости указанный порог можно попробовать рассчитывать динамически).

Часть SLI-дашборда про worker nodes, видна текущая и историческая доступность пулов нод
Часть SLI-дашборда про worker nodes, видна текущая и историческая доступность пулов нод

Но это решение также имеет один серьезный изъян – оно не учитывает частичные деградации нод. Большинство из частичных деградаций не обнаруживаются Kubernetes, поэтому нагрузка с них не снимается автоматически. При этом они могут крайне негативно влиять на приложения, запущенные в кластере. Если мы не будем этого учитывать, то получим SLI, слабо корелирующий со «счастьем» пользователей.

Для того, чтобы решить эту проблему, необходимо научиться автоматически обнаруживать симптомы частичной деградации. Для этого можно использовать Node Problem Detector. Кастомные проверки, написанные нами для него, проверяют сетевую связность ноды, работоспособность ключевых компонентов и ищут в логах паттерны, сигнализирующие о проблемах. В результате мы получили возможность учитывать в SLI не только ноды в статусе NotReady, но и ноды с обнаруженными автоматикой проблемами.

К сожалению, этот вариант не решает проблему полностью. Опыт показывает, что даже одна нода с частичной деградацией может вызывать заметные проблемы. Это не позволяет нам положиться на то, что 80% нод в кластере без выявленных деградаций, означает достаточное качество обслуживания. Порог в 80% удовлетворял бы нас, если бы частичные деградации приводили к выселению приложений с ноды, но, как я уже сказал, «из коробки» этого не происходит.

Это привело нас к поиску механизмов fail-fast. При обнаружении проблемы, мы хотим, чтобы автоматика выводила ноду из-под нагрузки, и по возможности пролечивала ее. Достичь этого позволило внедрение механик Auto Healing. В результате при обнаружении известных деградаций, мы научились автоматически снимать нагрузку. Это позволяет нам использовать пороговый SLI – теперь мы уверены, что если 80% нод в строю, этого хватает для нормальной работы сервисов.

Гарантии и состояния

Если обобщить, то в основе моих решений, принятых при проектировании конкретных SLI, лежит идея гарантий и состояний.

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

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

Скажем, разработчик может прийти к инженеру инфраструктуры с жалобой на слишком долгие ответы kube-apiserver. Внешние пользователи никаких проблем не замечают, однако разработчик недоволен. Мы можем предоставлять гарантии относительно длительности ответа kube-apiserver, а можем не предоставлять их. Иными словами, гарантии могут быть выбраны так, чтобы реагировать только на те деградации, которые становятся заметны внешним пользователям, либо быть более строгими. В каждом конкретном случае команда инфраструктуры (возможно в результате согласования с «внутренними» пользователями) может принять то или иное решение.

Для prod-окружения мне кажется более верным проектировать SLI, ориентированные только на «счастье» внешних пользователей. Чем больше гарантий предоставляет система, тем сложнее поддерживать ее в «хорошем» состоянии, и тем меньше времени и сил остается у команды инфраструктуры для ее развития. Разумеется, это не означает, что потребности внутренних пользователей следует игнорировать. Речь, конечно, идет только о расчете SLI.

С другой стороны, в staging-окружении мы вполне можем предоставить разработчикам отдельный набор, возможно менее строгих чем в prod-окружении, гарантий. Здесь сложно предложить общие подходы, и отталкиваться следует от потребностей и болей внутренних пользователей.

Часть SLI-дашборда, посвященная Error Budget. Можно видеть периоды, когда кластер не соответствовал установленным гарантиям
Часть SLI-дашборда, посвященная Error Budget. Можно видеть периоды, когда кластер не соответствовал установленным гарантиям

Выводы

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

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