Skip to content

Commit 8143d56

Browse files
kick2nickVIMPELCOM_MAIN\NKuksovraman-m
authored
#2168 A brand-new WatchKube provider for Kubernetes service discovery (#2174)
* Added 'WatchKube' discovery provider * Error handling, first results awaiting, unit tests * acceptance test, description id docs * increase first results timeout * remove BDDfy * fixes after rebase * minor fixes * added tests * adjust tests * add ObservableExtensions tests * Code review by @raman-m * Is unit coverage 100% ? * Converting the internal constants into public static properties * Update docs --------- Co-authored-by: VIMPELCOM_MAIN\NKuksov <nkuksov@beeline.ru> Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
1 parent ed8ba5b commit 8143d56

18 files changed

+829
-79
lines changed

.config/dotnet-tools.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
"commands": [
1414
"csmacnz.Coveralls"
1515
]
16+
},
17+
"dotnet-reportgenerator-globaltool": {
18+
"version": "5.4.7",
19+
"commands": [
20+
"reportgenerator"
21+
],
22+
"rollForward": false
1623
}
1724
}
1825
}

docs/features/kubernetes.rst

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,21 @@ The following examples show how to set up a route that will work in Kubernetes.
179179
The most important thing is the ``ServiceName`` which is made up of the Kubernetes service name.
180180
We also need to set up the ``ServiceDiscoveryProvider`` in ``GlobalConfiguration``.
181181

182+
Regarding global and route configurations, if your downstream service resides in a different namespace, you can override the global setting at the route level by specifying a ``ServiceNamespace``.
183+
184+
.. code-block:: json
185+
186+
"Routes": [
187+
{
188+
"ServiceName": "my-service",
189+
"ServiceNamespace": "my-namespace"
190+
}
191+
]
192+
193+
.. _k8s-kube-provider:
194+
182195
``Kube`` provider
183-
^^^^^^^^^^^^^^^^^
196+
-----------------
184197

185198
The example here shows a typical configuration:
186199

@@ -203,7 +216,7 @@ The example here shows a typical configuration:
203216
}
204217
}
205218
206-
Service deployment in ``Dev`` namespace, and discovery provider type is ``Kube``, you also can set :ref:`k8s-pollkube-provider` type.
219+
Service deployment in ``Dev`` namespace, and discovery provider type is ``Kube``, you also can set :ref:`k8s-pollkube-provider` or :ref:`k8s-watchkube-provider` type.
207220

208221
**Note 1**: ``Scheme``, ``Host``, ``Port``, and ``Token`` are not used if ``usePodServiceAccount`` is true when `KubeClient`_ is created from a pod service account.
209222
Please refer to the :ref:`k8s-install` section for technical details.
@@ -215,7 +228,7 @@ Service deployment in ``Dev`` namespace, and discovery provider type is ``Kube``
215228
.. _k8s-pollkube-provider:
216229

217230
``PollKube`` provider
218-
^^^^^^^^^^^^^^^^^^^^^
231+
---------------------
219232

220233
You use Ocelot to poll Kubernetes for latest service information rather than per request.
221234
If you want to poll Kubernetes for the latest services rather than per request (default behaviour) then you need to set the following configuration:
@@ -236,23 +249,81 @@ The polling interval is in milliseconds and tells Ocelot how often to call Kuber
236249
We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request.
237250
There is no way for Ocelot to work these out for you, except perhaps through a `discussion <https://github.com/ThreeMammals/Ocelot/discussions>`_.
238251

239-
Global vs Route levels
240-
^^^^^^^^^^^^^^^^^^^^^^
252+
.. _k8s-watchkube-provider:
241253

242-
If your downstream service resides in a different namespace, you can override the global setting at the route-level by specifying a ``ServiceNamespace``:
254+
``WatchKube`` provider [#f3]_
255+
-----------------------------
256+
.. _Kubernetes API: https://kubernetes.io/docs/reference/using-api/
257+
.. _watch requests: https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes
258+
259+
With this configuration, `Kubernetes API`_ "`watch requests`_" are used to fetch service configuration.
260+
Essentially, it establishes one streamed HTTP connection with the `Kubernetes API`_ per downstream service.
261+
Changes streamed through this connection will be used to update the list of available endpoints.
243262

244263
.. code-block:: json
245264
246-
"Routes": [
247-
{
248-
"ServiceName": "my-service",
249-
"ServiceNamespace": "my-namespace"
250-
}
251-
]
265+
"ServiceDiscoveryProvider": {
266+
"Namespace": "dev",
267+
"Type": "WatchKube"
268+
}
269+
270+
The provider has an implicit configuration for fine-tuned watching, which are available and can only be initialized in C# code.
271+
272+
* ``WatchKube.FirstResultsFetchingTimeoutSeconds``: `This <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FirstResultsFetchingTimeoutSeconds&type=code>`_ is the default number of seconds to wait after Ocelot starts, following the provider's creation, to fetch the first result from the Kubernetes endpoint. :sup:`1`
273+
* ``WatchKube.FailedSubscriptionRetrySeconds``: `This <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20FailedSubscriptionRetrySeconds&type=code>`__ is the default number of seconds to wait before scheduling the next retry for the subscription operation. :sup:`1`
274+
275+
.. _break3: http://break.do
276+
277+
**Note 1**: For both ``static int`` properties, the default value is 1 (one) second. The constraint ensures that the assigned value is greater than or equal to 1 (one). Therefore, the minimum value is 1 (one) second.
278+
279+
**Note 2**: The ``WatchKube`` provider is specifically designed for high-load Ocelot vs. Kubernetes environments with high RPS ratios.
280+
To better understand which type is suitable for your needs, we have added a table :ref:`k8s-comparing-providers`.
281+
282+
.. _k8s-comparing-providers:
283+
284+
Comparing providers
285+
-------------------
286+
This table explains the most important indicators that may influence Ocelot vs. Kubernetes deployment or DevOps strategy.
287+
The evolution path of all providers follows: ``Kube`` -> ``PollKube`` -> ``WatchKube``, with ``WatchKube`` being the most advanced provider.
288+
289+
.. list-table::
290+
:widths: 34 22 22 22
291+
:header-rows: 1
292+
293+
* - *Indicators \\ Providers*
294+
- :ref:`Kube <k8s-kube-provider>`
295+
- :ref:`PollKube <k8s-pollkube-provider>`
296+
- :ref:`WatchKube <k8s-watchkube-provider>`
297+
* - Extra latency
298+
- One hop per route
299+
- \-
300+
- \-
301+
* - Speed of response to endpoints changes
302+
- High
303+
- Low :sup:`1`
304+
- High
305+
* - Pressure on `Kubernetes API`_
306+
- High
307+
- Low :sup:`1`
308+
- Low
309+
* - Ocelot load (estimated) :sup:`2`
310+
- < 1000 RPS
311+
- > 1000 RPS
312+
- > 5000 RPS
313+
* - Ocelot deployment :sup:`3`
314+
- Single instance
315+
- Multiple instances
316+
- Cluster of instances
317+
318+
.. _break4: http://break.do
319+
320+
| :sup:`1` Depends on the ``PollingInterval`` option.
321+
| :sup:`2` Please consider this a rough load estimation, as our team has not provided any tests or benchmarks.
322+
| :sup:`3` The term "instance" refers to an Ocelot instance, not a Kubernetes one.
252323
253324
.. _k8s-downstream-scheme-vs-port-names:
254325

255-
Downstream Scheme vs Port Names [#f3]_
326+
Downstream Scheme vs Port Names [#f4]_
256327
--------------------------------------
257328

258329
Kubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset.
@@ -286,7 +357,7 @@ you must define ``DownstreamScheme`` to enable the provider to recognize the des
286357
}
287358
]
288359
289-
.. _break3: http://break.do
360+
.. _break5: http://break.do
290361

291362
**Note**: In the absence of a specified ``DownstreamScheme`` (which is the default behavior), the ``Kube`` provider will select **the first available port** from the ``EndpointSubsetV1.Ports`` collection.
292363
Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``.
@@ -295,13 +366,16 @@ you must define ``DownstreamScheme`` to enable the provider to recognize the des
295366

296367
.. [#f1] The :doc:`../features/kubernetes` feature was requested as part of issue `345`_ to add support for `Kubernetes <https://kubernetes.io/>`_ :doc:`../features/servicediscovery` provider, and released in version `13.4.1`_
297368
.. [#f2] The :ref:`k8s-addkubernetes-action-method` was requested as part of issue `2255`_ (PR `2257`_), and released in version `24.0`_
298-
.. [#f3] The :ref:`k8s-downstream-scheme-vs-port-names` feature was requested as part of issue `1967`_ and released in version `23.3`_
369+
.. [#f3] The :ref:`k8s-watchkube-provider` was discussed in thread `2168`_ and released in version `24.1`_
370+
.. [#f4] The :ref:`k8s-downstream-scheme-vs-port-names` feature was requested as part of issue `1967`_ and released in version `23.3`_
299371
300372
.. _345: https://github.com/ThreeMammals/Ocelot/issues/345
301373
.. _1134: https://github.com/ThreeMammals/Ocelot/pull/1134
302374
.. _1967: https://github.com/ThreeMammals/Ocelot/issues/1967
375+
.. _2168: https://github.com/ThreeMammals/Ocelot/discussions/2168
303376
.. _2255: https://github.com/ThreeMammals/Ocelot/issues/2255
304377
.. _2257: https://github.com/ThreeMammals/Ocelot/pull/2257
305378
.. _13.4.1: https://github.com/ThreeMammals/Ocelot/releases/tag/13.4.1
306379
.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0
307380
.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0
381+
.. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0

src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,41 @@ namespace Ocelot.Provider.Kubernetes;
77

88
public class EndPointClientV1 : KubeResourceClient, IEndPointClient
99
{
10-
private static readonly HttpRequest Collection = KubeRequest.Create("api/v1/namespaces/{Namespace}/endpoints/{ServiceName}");
10+
private static readonly HttpRequest EndpointsRequest =
11+
KubeRequest.Create("api/v1/namespaces/{Namespace}/endpoints/{ServiceName}");
12+
13+
private static readonly HttpRequest EndpointsWatchRequest =
14+
KubeRequest.Create("api/v1/watch/namespaces/{Namespace}/endpoints/{ServiceName}");
1115

1216
public EndPointClientV1(IKubeApiClient client) : base(client)
1317
{
1418
}
1519

1620
public Task<EndpointsV1> GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default)
1721
{
18-
if (string.IsNullOrEmpty(serviceName))
19-
{
20-
throw new ArgumentNullException(nameof(serviceName));
21-
}
22+
ArgumentException.ThrowIfNullOrEmpty(serviceName);
2223

23-
var request = Collection
24-
.WithTemplateParameters(new
25-
{
26-
Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,
27-
ServiceName = serviceName,
28-
});
24+
var request = EndpointsRequest.WithTemplateParameters(new
25+
{
26+
Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,
27+
ServiceName = serviceName,
28+
});
2929

3030
return Http.GetAsync(request, cancellationToken)
31-
.ReadContentAsObjectV1Async<EndpointsV1>(operationDescription: $"get {nameof(EndpointsV1)}");
31+
.ReadContentAsObjectV1Async<EndpointsV1>(operationDescription: $"{nameof(GetAsync)} {nameof(EndpointsV1)}");
32+
}
33+
34+
public IObservable<IResourceEventV1<EndpointsV1>> Watch(string serviceName, string kubeNamespace, CancellationToken cancellationToken = default)
35+
{
36+
ArgumentException.ThrowIfNullOrEmpty(serviceName);
37+
38+
var request = EndpointsWatchRequest.WithTemplateParameters(new
39+
{
40+
ServiceName = serviceName,
41+
Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,
42+
});
43+
44+
return ObserveEvents<EndpointsV1>(request,
45+
$"{nameof(Watch)} {nameof(EndpointsV1)} for '{serviceName}' in the namespace '{kubeNamespace ?? KubeClient.DefaultNamespace}'");
3246
}
3347
}

src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ namespace Ocelot.Provider.Kubernetes.Interfaces;
66
public interface IEndPointClient : IKubeResourceClient
77
{
88
Task<EndpointsV1> GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default);
9+
10+
IObservable<IResourceEventV1<EndpointsV1>> Watch(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default);
911
}

src/Ocelot.Provider.Kubernetes/Kube.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private async Task<EndpointsV1> GetEndpoint()
5656
try
5757
{
5858
return await _kubeApi
59-
.ResourceClient<IEndPointClient>(client => new EndPointClientV1(client))
59+
.EndpointsV1()
6060
.GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace);
6161
}
6262
catch (KubeApiException ex)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Ocelot.Provider.Kubernetes.Interfaces;
2+
3+
namespace Ocelot.Provider.Kubernetes;
4+
5+
public static class KubeApiClientExtensions
6+
{
7+
public static IEndPointClient EndpointsV1(this IKubeApiClient client)
8+
=> client.ResourceClient<IEndPointClient>(x => new EndPointClientV1(x));
9+
}

src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
using Ocelot.Configuration;
33
using Ocelot.Logging;
44
using Ocelot.Provider.Kubernetes.Interfaces;
5+
using System.Reactive.Concurrency;
56

67
namespace Ocelot.Provider.Kubernetes;
78

89
public static class KubernetesProviderFactory // TODO : IServiceDiscoveryProviderFactory
910
{
1011
/// <summary>String constant used for provider type definition.</summary>
1112
public const string PollKube = nameof(Kubernetes.PollKube);
13+
public const string WatchKube = nameof(Kubernetes.WatchKube);
1214

1315
public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider;
1416

@@ -17,14 +19,18 @@ private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provide
1719
var factory = provider.GetService<IOcelotLoggerFactory>();
1820
var kubeClient = provider.GetService<IKubeApiClient>();
1921
var serviceBuilder = provider.GetService<IKubeServiceBuilder>();
20-
2122
var configuration = new KubeRegistryConfiguration
2223
{
2324
KeyOfServiceInK8s = route.ServiceName,
2425
KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace,
2526
Scheme = route.DownstreamScheme,
2627
};
2728

29+
if (WatchKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase))
30+
{
31+
return new WatchKube(configuration, factory, kubeClient, serviceBuilder, Scheduler.Default);
32+
}
33+
2834
var defaultK8sProvider = new Kube(configuration, factory, kubeClient, serviceBuilder);
2935

3036
return PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Reactive.Concurrency;
2+
using System.Reactive.Linq;
3+
4+
namespace Ocelot.Provider.Kubernetes;
5+
6+
public static class ObservableExtensions
7+
{
8+
public static IObservable<TSource> RetryAfter<TSource>(this IObservable<TSource> source, TimeSpan dueTime, IScheduler scheduler)
9+
=> RepeatInfinite(source, dueTime, scheduler).Catch();
10+
11+
private static IEnumerable<IObservable<TSource>> RepeatInfinite<TSource>(IObservable<TSource> source, TimeSpan dueTime, IScheduler scheduler)
12+
{
13+
yield return source;
14+
while (true)
15+
{
16+
yield return source.DelaySubscription(dueTime, scheduler);
17+
}
18+
}
19+
}

src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@
4848
</ItemGroup>
4949
<ItemGroup>
5050
<ProjectReference Include="..\Ocelot\Ocelot.csproj" />
51+
<InternalsVisibleTo Include="Ocelot.UnitTests" />
5152
</ItemGroup>
5253
</Project>

0 commit comments

Comments
 (0)