Перейти к основному содержимому

Использование Watch с API Kubernetes

· 7 мин. чтения

1. Введение

В этом руководстве мы продолжим изучение Java Kubernetes API. На этот раз мы покажем, как использовать часы для эффективного мониторинга событий кластера.

2. Что такое часы Kubernetes?

В наших предыдущих статьях, посвященных Kubernetes API , мы показали, как восстановить информацию о заданном ресурсе или их наборе. Это нормально, если все, что нам нужно, — это получить состояние этих ресурсов в определенный момент времени. Однако, учитывая, что кластеры Kubernetes очень динамичны по своей природе, этого обычно недостаточно.

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

Для решения этих проблем в Kubernetes есть концепция наблюдения , которая доступна для всех вызовов API сбора ресурсов через параметр запроса наблюдения . Когда его значение равно false или опущено, операция GET ведет себя как обычно: сервер обрабатывает запрос и возвращает список экземпляров ресурсов, соответствующих заданным критериям. Однако передача watch=true резко меняет его поведение:

  • Теперь ответ состоит из серии событий модификации, содержащих тип модификации и затрагиваемый объект.
  • Соединение будет оставаться открытым после отправки начального пакета событий с использованием метода, называемого длительным опросом.

3. Создание часов

Java Kubernetes API поддерживает Watch через класс Watch , который имеет единственный статический метод: createWatch. Этот метод принимает три аргумента:

  • ApiClient , который обрабатывает фактические вызовы REST к серверу API Kubernetes .
  • Экземпляр Call , описывающий коллекцию ресурсов для просмотра
  • TypeToken с ожидаемым типом ресурса

Мы создаем экземпляр Call из любого из классов xxxApi , доступных в библиотеке, используя один из их методов listXXXCall() . Например, чтобы создать Watch , который обнаруживает события Pod , мы использовали бы listPodForAllNamespacesCall() :

CoreV1Api api = new CoreV1Api(client);
Call call = api.listPodForAllNamespacesCall(null, null, null, null, null, null, null, null, 10, true, null);
Watch<V1Pod> watch = Watch.createWatch(
client,
call,
new TypeToken<Response<V1Pod>>(){}.getType()));

Здесь мы используем null для большинства параметров, что означает «использовать значение по умолчанию», за двумя исключениями: тайм- аут и просмотр. Последний должен быть установлен в значение true для вызова часов. В противном случае это будет обычный вызов на отдых. Тайм - аут в этом случае работает как часы «время жизни», что означает, что сервер прекратит отправку событий и разорвет соединение, как только оно истечет .

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

4. Получение событий

Присмотревшись к классу Watch , мы увидим, что он реализует как Iterator , так и Iterable из стандартной JRE, поэтому мы можем использовать значение, возвращаемое createWatch() , в циклах for-each или hasNext()-next() :

for (Response<V1Pod> event : watch) {
V1Pod pod = event.object;
V1ObjectMeta meta = pod.getMetadata();
switch (event.type) {
case "ADDED":
case "MODIFIED":
case "DELETED":
// ... process pod data
break;
default:
log.warn("Unknown event type: {}", event.type);
}
}

Поле типа каждого события сообщает нам, какое событие произошло с объектом — в нашем случае с подом. Как только мы обработаем все события, мы должны сделать новый вызов Watch.createWatch() , чтобы снова начать получать события. В примере кода мы окружаем создание Watch и обработку результатов циклом while . Возможны и другие подходы, такие как использование ExecutorService или аналогичного для получения обновлений в фоновом режиме.

5. Использование версий ресурсов и закладок

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

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

5.1. Версии ресурсов

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

Когда мы делаем вызов API, который возвращает коллекцию и включает параметр resourceVersion , сервер будет использовать его значение в качестве «отправной точки» для запроса. Для вызовов Watch API это означает, что будут включены только события, которые произошли после того, как была создана информированная версия.

Но как нам получить resourceVersion для включения в наши вызовы? Просто: мы просто делаем первоначальный вызов синхронизации, чтобы получить исходный список ресурсов, который включает в себя resourceVersion коллекции , а затем используем его в последующих вызовах Watch :

String resourceVersion = null;
while (true) {
if (resourceVersion == null) {
V1PodList podList = api.listPodForAllNamespaces(null, null, null, null, null, "false",
resourceVersion, null, 10, null);
resourceVersion = podList.getMetadata().getResourceVersion();
}
try (Watch<V1Pod> watch = Watch.createWatch(
client,
api.listPodForAllNamespacesCall(null, null, null, null, null, "false",
resourceVersion, null, 10, true, null),
new TypeToken<Response<V1Pod>>(){}.getType())) {

for (Response<V1Pod> event : watch) {
// ... process events
}
} catch (ApiException ex) {
if (ex.getCode() == 504 || ex.getCode() == 410) {
resourceVersion = extractResourceVersionFromException(ex);
}
else {
resourceVersion = null;
}
}
}

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

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

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

5.2. Закладки

Закладки — это необязательная функция, которая включает специальное событие BOOKMARK в потоках событий, возвращенных из вызова Watch . Это событие содержит в своих метаданных значение resourceVersion , которое мы можем использовать в последующих вызовах Watch в качестве новой отправной точки.

Поскольку это необязательная функция, мы должны явно включить ее, передав значение true для параметра allowWatchBookmarks при вызовах API. Этот параметр действителен только при создании Watch и игнорируется в противном случае. Кроме того, сервер может полностью игнорировать его, поэтому клиенты вообще не должны полагаться на получение этих событий.

По сравнению с предыдущим подходом, использующим только resourceVersion , закладки позволяют нам в основном избежать дорогостоящего вызова синхронизации:

String resourceVersion = null;

while (true) {
// Get a fresh list whenever we need to resync
if (resourceVersion == null) {
V1PodList podList = api.listPodForAllNamespaces(true, null, null, null, null,
"false", resourceVersion, null, null, null);
resourceVersion = podList.getMetadata().getResourceVersion();
}

while (true) {
try (Watch<V1Pod> watch = Watch.createWatch(
client,
api.listPodForAllNamespacesCall(true, null, null, null, null,
"false", resourceVersion, null, 10, true, null),
new TypeToken<Response<V1Pod>>(){}.getType())) {
for (Response<V1Pod> event : watch) {
V1Pod pod = event.object;
V1ObjectMeta meta = pod.getMetadata();
switch (event.type) {
case "BOOKMARK":
resourceVersion = meta.getResourceVersion();
break;
case "ADDED":
case "MODIFIED":
case "DELETED":
// ... event processing omitted
break;
default:
log.warn("Unknown event type: {}", event.type);
}
}
}
} catch (ApiException ex) {
resourceVersion = null;
break;
}
}
}

Здесь нам нужно получить полный список только при первом проходе и всякий раз, когда мы получаем исключение ApiException во внутреннем цикле. Обратите внимание, что события BOOKMARK имеют тот же тип объекта, что и другие события, поэтому здесь нам не нужно никакого специального приведения. Однако единственное поле, о котором мы заботимся, — это resourceVersion , которое мы сохраняем для следующего вызова Watch .

6. Заключение

В этой статье мы рассмотрели различные способы создания часов Kubernetes с помощью клиента Java API. Как обычно, полный исходный код примеров можно найти на GitHub .