1. 소개

이 사용방법(예제)에서는 계속해서 Java Kubernetes API를 살펴보겠습니다. 이번에는 Watches 를 사용하여 클러스터 이벤트를 효율적으로 모니터링하는 방법을 보여드리겠습니다.

2. Kubernetes Watch란 무엇입니까?

Kubernetes API 를 다루는 이전 기사 에서 주어진 리소스 또는 리소스 모음에 대한 정보를 복구하는 방법을 보여주었습니다. 우리가 원하는 것이 주어진 시간에 해당 리소스의 상태를 가져오는 것이라면 괜찮습니다. 그러나 Kubernetes 클러스터가 본질적으로 매우 동적이라는 점을 감안할 때 일반적으로 이것만으로는 충분하지 않습니다.

대부분의 경우 이러한 리소스를 모니터링하고 발생하는 이벤트를 추적하려고 합니다 . 예를 들어 포드 수명 주기 이벤트 또는 배포 상태 변경을 추적하는 데 관심이 있을 수 있습니다. 폴링을 사용할 수는 있지만 이 접근 방식에는 몇 가지 제한 사항이 있습니다. 첫째, 모니터링할 리소스 수가 증가함에 따라 제대로 확장되지 않습니다. 둘째, 폴링 주기 사이에 발생하는 이벤트를 잃을 위험이 있습니다.

이러한 문제를 해결하기 위해 Kubernetes에는  watch 쿼리 매개 변수 를 통해 모든 리소스 수집 API 호출에 사용할 수 있는  Watches  개념이 있습니다. 값이  false 이거나 생략된 경우 GET 작업은 평소와 같이 작동합니다. 서버는 요청을 처리하고 지정된 기준과 일치하는 리소스 인스턴스 List을 반환합니다. 그러나 watch=true 를 전달 하면 동작이 크게 바뀝니다.

  • 이제 응답은 수정 유형 및 영향을 받는 개체를 포함하는 일련의 수정 이벤트로 구성됩니다.
  • 긴 폴링이라는 기술을 사용하여 이벤트의 초기 배치를 보낸 후 연결이 열린 상태로 유지됩니다.

3.  시계 만들기

Java Kubernetes API 는 Watch 클래스 를 통해  Watches 를 지원 하며 여기에는 단일 정적 메서드인  createWatch가 있습니다. 이 메서드는 세 가지 인수를 사용합니다.

  • Kubernetes API 서버에 대한 실제 REST 호출을 처리 하는  ApiClient
  • 감시할 리소스 컬렉션을 설명하는 Call  인스턴스
  • 예상 리소스 유형이 포함 된  TypeToken

listXXXCall() 메서드 중 하나를 사용하여 라이브러리에서 사용할 수 있는 xxxApi 클래스에서 Call  인스턴스를 만듭니다 . 예를 들어 Pod 이벤트 를 감지 하는 Watch 를 만들려면 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 을 사용합니다. 즉, timeoutwatch  라는 두 가지 예외를 제외하고 "기본값 사용"을 의미합니다 . 후자는 watch 호출에 대해 true 로 설정되어야 합니다 . 그렇지 않으면 이것은 일반적인 휴식 호출이 될 것입니다. 이 경우 타임아웃 은 감시 "time-to-live"로 작동합니다. 즉, 서버가 이벤트 전송을 중지하고 연결이 만료되면 연결을 종료합니다 .

초 단위로 표현되는 timeout 매개 변수 에 대한 적절한 값을  찾으려면 클라이언트 응용 프로그램의 정확한 요구 사항에 따라 달라지므로 약간의 시행 착오가 필요합니다. 또한 Kubernetes 클러스터 구성을 확인하는 것이 중요합니다. 일반적으로 시계에는 5분이라는 엄격한 제한이 있으므로 그 이상을 전달하면 원하는 효과를 얻을 수 없습니다.

4. 이벤트 수신

Watch 클래스 를 자세히 살펴보면 표준 JRE에서 IteratorIterable 을 모두 구현 하므로 for-each 또는 hasNext()-next() 루프 에서 createWatch() 에서 반환된 값을 사용할 수 있음을 알 수 있습니다.

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);
    }
}

각 이벤트 의 유형 필드는 개체(이 경우 Pod)에 어떤 종류의 이벤트가 발생했는지 알려줍니다. 모든 이벤트를 소비한 후에는 Watch.createWatch() 를 새로 호출하여 이벤트 수신을 다시 시작해야 합니다. 예제 코드 에서는 Watch 생성 및 결과 처리를  while 루프 로 둘러쌉니다 . ExecutorService 또는 이와 유사한 것을 사용하여 백그라운드에서 업데이트를 수신하는 것과 같은 다른 접근 방식도 가능합니다 .

5. 리소스 버전 및 북마크 사용

위 코드의 문제는 새 Watch  를 만들 때마다 주어진 종류의 기존 리소스 인스턴스가 모두 포함된 초기 이벤트 스트림이 있다는 사실입니다. 이것은 서버가 그들에 대한 이전 정보가 없다고 가정하기 때문에 발생합니다. 그래서 그냥 모두 보냅니다 .

그러나 이렇게 하면 초기 로드 이후에만 새 이벤트가 필요하므로 이벤트를 효율적으로 처리하려는 목적에 어긋납니다. 모든 데이터가 다시 수신되는 것을 방지하기 위해 감시 메커니즘은 리소스 버전과 책갈피라는 두 가지 추가 개념을 지원합니다.

5.1. 리소스 버전

쿠버네티스의 모든 리소스에는  메타데이터에 resourceVersion 필드가 포함되어 있습니다. 이는 무언가가 변경될 때마다 서버에서 설정하는 불투명한 문자열입니다. 또한 리소스 컬렉션도 리소스이므로 연결된 resourceVersion  이 있습니다. 컬렉션에서 새 리소스가 추가, 제거 및/또는 수정되면 이 필드가 그에 따라 변경됩니다.

컬렉션을 반환하고 resourceVersion 매개변수 포함 하는 API 호출을 만들 때 서버는 해당 값을 쿼리의 "시작점"으로 사용합니다. Watch API 호출의 경우 이는 알림 버전이 생성된 시간 이후에 발생한 이벤트만 포함됨을 의미합니다.

그러나 호출에 포함 할 resourceVersion 을 어떻게 얻 습니까? 단순: 초기 동기화 호출을 수행하여 컬렉션의 resourceVersion 이 포함된 리소스의 초기 List을 검색 한 다음 후속  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;
        }
    }
}

이 경우 예외 처리 코드는 다소 중요 합니다. 어떤 이유로 요청된 resourceVersion  이 존재하지 않는 경우 Kubernetes 서버는 504 또는 410 오류 코드를 반환합니다. 이 경우 반환된 메시지에는 일반적으로 현재 버전이 포함됩니다. 안타깝게도 이 정보는 구조화된 방식이 아니라 오류 메시지 자체의 일부로 제공됩니다.

추출 코드(못생긴 해킹이라고도 함)는 이 인텐트에 대해 정규식을 사용하지만 오류 메시지는 구현에 따라 달라지는 경향이 있으므로 코드는 null 값으로 돌아갑니다. 이렇게 하면 메인 루프가 시작점으로 돌아가서 새로운 resourceVersion 으로 새로운 List을 복구하고 감시 작업을 재개합니다.

어쨌든 이 경고에도 불구하고 요점은 이제 모든 시계에서 이벤트 List이 처음부터 시작되지 않는다는 것입니다.

5.2. 북마크

북마크는 Watch 호출 에서 반환된 이벤트 스트림에서 특별한 BOOKMARK 이벤트 를 활성화하는 선택적 기능입니다 . 이 이벤트는 후속 Watch 호출에서 새 시작점으로 사용할 수 있는 resourceVersion  값을 메타데이터에 포함 합니다.

이는 옵트인 기능이므로 API 호출 에서 allowWatchBookmarks 에 true 전달하여 명시적으로 활성화해야 합니다. 이 옵션은 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을 얻을 때마다 전체 List을 가져와야 합니다. BOOKMARK 이벤트는 다른 이벤트와 동일한 객체 유형을 가지므로 여기서는 특별한 캐스팅이 필요하지 않습니다. 그러나 우리가 관심을 갖는 유일한 필드는  다음 Watch 호출 을 위해 저장하는 resourceVersion 입니다.

6. 결론

이 기사에서는 Java API 클라이언트를 사용하여 Kubernetes Watch를 생성하는 다양한 방법을 다루었습니다 . 늘 그렇듯이 예제의 전체 소스 코드는  GitHub 에서 찾을 수 있습니다 .

Generic footer banner