1. Introduction
1.绪论
In this tutorial, we’ll continue to explore the Java Kubernetes API. This time, we’ll show how to use Watches to efficiently monitor cluster events.
在本教程中,我们将继续探索Java Kubernetes的API。这一次,我们将展示如何使用Watches来有效监控集群事件。
2. What Are Kubernetes Watches?
2.什么是Kubernetes手表?
In our previous articles covering the Kubernetes API, we’ve shown how to recover information about a given resource or a collection of them. This is fine if all we wanted was to get the state of those resources at a given point on time. However, given that Kubernetes clusters are highly dynamic in nature, this is usually not enough.
在涵盖Kubernetes API的前几篇文章中,我们展示了如何恢复有关某个给定资源或其集合的信息。如果我们只想获得这些资源在某一特定时间点的状态,这是很好的。然而,鉴于Kubernetes集群的性质是高度动态的,这通常是不够的。
Most often, we also want to monitor those resources and track events as they happen. For instance, we might be interested in tracking pod life cycle events or deployment status changes. While we could use polling, this approach would suffer from a few limitations. Firstly, it would not scale well as the number of resources to monitor increases. Secondly, we risk losing events that happen to occur between polling cycles.
最常见的是,我们还想监控这些资源,并在它们发生时跟踪事件。例如,我们可能对跟踪pod生命周期事件或部署状态变化感兴趣。虽然我们可以使用轮询,但这种方法会受到一些限制。首先,随着需要监控的资源数量的增加,它的规模不会很大。其次,我们有可能在轮询周期之间丢失碰巧发生的事件。
To address those issues, Kubernetes has the concept of Watches, which is available for all resource collection API calls through the watch query parameter. When its value is false or omitted, the GET operation behaves as usual: the server processes the request and returns a list of resource instances that match the given criteria. However, passing watch=true changes its behavior dramatically:
为了解决这些问题,Kubernetes拥有Watches的概念,Watch可通过watch查询参数用于所有资源收集API调用。当其值为false或省略时,GET操作的行为与平常一样:服务器处理请求并返回符合给定条件的资源实例的列表。然而,通过watch=true会极大地改变其行为:。
- The response now consists of a series of modification events, containing the type of modification and the affected object
- The connection will be kept open after sending the initial batch of events, using a technique called long polling
3. Creating a Watch
3.创建一个Watch
The Java Kubernetes API support Watches through the Watch class, which has a single static method: createWatch. This method takes three arguments:
Java Kubernetes API通过Watch类支持Watch,它有一个静态方法。createWatch。该方法需要三个参数。
- An ApiClient, which handles actual REST calls to the Kubernetes API server
- A Call instance describing the resource collection to watch
- A TypeToken with the expected resource type
We create a Call instance from any of the xxxApi classes available in the library using one of their listXXXCall() methods. For instance, to create a Watch that detects Pod events, we’d use listPodForAllNamespacesCall():
我们使用库中可用的任何xxxApi类的listXXXCall()方法来创建一个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()));
Here, we use null for most parameters, meaning “use the default value”, with just two exceptions: timeout and watch. The latter must be set to true for a watch call. Otherwise, this would be a regular rest call. The timeout, in this case, works as the watch “time-to-live”, meaning that the server will stop sending events and terminate the connection once it expires.
在这里,我们对大多数参数使用null,意味着 “使用默认值”,只有两个例外。timeout和watch。后者必须被设置为true,以便进行观察调用。在这种情况下,timeout,作为观察的 “生存时间”,意味着一旦过期,服务器将停止发送事件并终止连接。
Finding a good value for the timeout parameter, which is expressed in seconds, requires some trial-and-error, as it depends on the exact requirements of the client application. Also, it is important to check your Kubernetes cluster configuration. Usually, there’s a hard limit of 5 minutes for watches, so passing anything beyond that will not have the desired effect.
为timeout参数(以秒为单位)找到一个好的值,需要一些尝试和错误,因为它取决于客户应用程序的确切要求。此外,检查你的Kubernetes集群配置也很重要。通常情况下,手表有5分钟的硬性限制,所以超过这个时间的传递将不会有预期的效果。
4. Receiving Events
4.接收事件
Taking a closer look at the Watch class, we can see that it implements both Iterator and Iterable from the standard JRE, so we can use the value returned from createWatch() in for-each or hasNext()-next() loops:
仔细看看Watch类,我们可以看到它同时实现了标准JRE中的Iterator和Iterable,所以我们可以在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);
}
}
The type field of each event tells us what kind of event happened to the object – a Pod in our case. Once we consume all events, we must do a new call to Watch.createWatch() to start receiving events again. In the example code, we surround the Watch creation and result processing in a while loop. Other approaches are also possible, such as using an ExecutorService or similar to receive updates in the background.
每个事件的type字段告诉我们对象发生了什么样的事件–在我们的例子中是一个花苞。一旦我们消耗了所有的事件,我们必须重新调用Watch.createWatch()以再次开始接收事件。在示例代码中,我们将Watch的创建和结果处理置于一个while循环中。其他方法也是可行的,例如使用ExecutorService或类似的方法来在后台接收更新。
5. Using Resource Versions and Bookmarks
5.使用资源版本和书签
A problem with the code above is the fact that every time we create a new Watch, there’s an initial event stream with all existing resource instances of the given kind. This happens because the server assumes that we don’t have any previous information about them, so it just sends them all.
上述代码的一个问题是,每次我们创建一个新的Watch,都会有一个初始事件流,其中包含所有现有的给定类型的资源实例。发生这种情况是因为服务器假设我们没有任何关于它们的先前信息,所以它只是发送它们全部。
However, doing so defeats the purpose of processing events efficiently, as we only need new events after the initial load. To prevent receiving all data again, the watch mechanism supports two additional concepts: resource versions and bookmarks.
然而,这样做违背了有效处理事件的目的,因为我们只需要在初始加载后的新事件。为了防止再次接收所有数据,观察机制支持两个额外的概念:资源版本和书签。
5.1. Resource Versions
5.1.资源版本
Every resource in Kubernetes contains a resourceVersion field in its metadata, which is just an opaque string set by the server every time something changes. Moreover, since a resource collection is also a resource, there’s a resourceVersion associated with it. As new resources are added, removed, and/or modified from a collection, this field will change accordingly.
Kubernetes中的每个资源在其元数据中都包含一个resourceVersion字段,这只是一个不透明的字符串,在每次有变化时由服务器设置。此外,由于资源集合也是一种资源,所以也有一个resourceVersion与之相关联。当新的资源被添加、移除和/或从一个集合中修改时,这个字段将相应地改变。
When we make an API call that returns a collection and includes the resourceVersion parameter, the server will use its value as a “starting point” for the query. For Watch API calls, this means that only events that happened after the time where the informed version was created will be included.
当我们进行一个返回集合的API调用时,并且包括resourceVersion参数,服务器将使用其值作为查询的 “起点”。对于Watch API调用,这意味着只有在创建通知版本的时间之后发生的事件才会被包括在内。
But, how do we get a resourceVersion to include in our calls? Simple: we just do an initial synchronization call to retrieve the initial list of resources, which includes the collection’s resourceVersion, and then use it in subsequent Watch calls:
但是,我们如何获得一个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;
}
}
}
The exception handling code, in this case, is rather important. Kubernetes servers will return a 504 or 410 error code when, for some reason, the requested resourceVersion doesn’t exist. In this case, the returned message usually contains the current version. Unfortunately, this information doesn’t come in any structured way but rather as part of the error message itself.
异常处理代码,在这种情况下,是相当重要的。当由于某种原因,请求的resourceVersion不存在时,Kubernetes服务器将返回504或410错误代码。在这种情况下,返回的信息通常包含当前的版本。不幸的是,这个信息并不是以任何结构化的方式出现的,而是作为错误信息本身的一部分。
The extraction code (a.k.a. ugly hack) uses a regular expression for this intent, but since error messages tend to be implementation-dependent, the code falls back to a null value. By doing so, the main loop goes back to its starting point, recovering a fresh list with a new resourceVersion and resuming watch operations.
提取代码(又称丑陋的黑客)使用正则表达式来实现这一意图,但由于错误信息往往与实现有关,所以代码会返回到一个null值。通过这样做,主循环回到了它的起点,用一个新的resourceVersion恢复了一个新的列表,并恢复了观察操作。
Anyhow, even with this caveat, the key point is that now the event list will not start from scratch on every watch.
无论如何,即使有这个注意事项,关键的一点是,现在的事件列表不会在每个手表上从头开始。
5.2. Bookmarks
5.2. 书签
Bookmarks are an optional feature that enables a special BOOKMARK event on events streams returned from a Watch call. This event contains in its metadata a resourceVersion value that we can use in subsequent Watch calls as a new starting point.
书签是一个可选的功能,它可以在从Watch调用返回的事件流中启用一个特殊的BOOKMARK事件。该事件在其元数据中包含一个resourceVersion值,我们可以在随后的Watch调用中作为一个新的起点使用。
As this is an opt-in feature, we must explicitly enable it by passing true to allowWatchBookmarks on API calls. This option is valid only when creating a Watch and ignored otherwise. Also, a server may ignore it completely, so clients should not rely on receiving those events at all.
由于这是一个选入功能,我们必须通过在API调用中传递true到allowWatchBookmarks来明确启用它。该选项仅在创建Watch时有效,否则将被忽略。另外,服务器可能会完全忽略它,所以客户端根本不应该依赖接收这些事件。
When comparing with the previous approach using resourceVersion alone, bookmarks allow us to mostly get away with a costly synchronization call:
与之前单独使用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;
}
}
}
Here, we only need to get the full list on the first pass and whenever we get an ApiException in the inner loop. Notice that BOOKMARK events have the same object type as other events, so we don’t need any special casting here. However, the only field we care about is the resourceVersion, which we save for the next Watch call.
在这里,我们只需要在第一遍和在内循环中得到ApiException时获得完整的列表。请注意,BOOKMARK事件与其他事件具有相同的对象类型,所以我们在这里不需要任何特殊的转换。然而,我们关心的唯一字段是resourceVersion,我们将其保存到下一次Watch调用。
6. Conclusion
6.结论
In this article, we’ve covered different ways to create Kubernetes Watches using the Java API client. As usual, the full source code of the examples can be found over on GitHub.
在本文中,我们介绍了使用Java API客户端创建Kubernetes Watch的不同方法。像往常一样,可以在GitHub上找到这些示例的完整源代码。