This is an automated email from the ASF dual-hosted git repository. dmsolr pushed a commit to branch feat/7586-k8s-controller-tuning in repository https://gitbox.apache.org/repos/asf/shenyu.git
commit d7a1c9dbc312e97cf25c2db770fa0e77e2208d06 Author: Luke Haochao Zhuang <[email protected]> AuthorDate: Tue Oct 13 22:25:12 2026 +0800 feat(k8s): configure ingress controller tuning (#7586) --- .../k8s/IngressControllerConfiguration.java | 56 +++++++-- .../k8s/ShenyuKubernetesControllerProperties.java | 76 ++++++++++++ .../k8s/IngressControllerConfigurationTest.java | 135 +++++++++++++++++++++ 3 files changed, 255 insertions(+), 12 deletions(-) diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java index 7548b9d524..43d4a4a97a 100644 --- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java @@ -50,25 +50,29 @@ import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.netty.tcp.TcpSslContextSpec; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; /** * The type shenyu ingress controller configuration. */ @Configuration @ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "ingress", matchIfMissing = true) +@EnableConfigurationProperties(ShenyuKubernetesControllerProperties.class) public class IngressControllerConfiguration { /** @@ -83,9 +87,34 @@ public class IngressControllerConfiguration { public ControllerManager controllerManager(final SharedInformerFactory sharedInformerFactory, @Qualifier("ingress-controller") final Controller ingressController, @Qualifier("endpoints-controller") final Controller endpointsController) { - ControllerManager controllerManager = new ControllerManager(sharedInformerFactory, ingressController, endpointsController); - Executors.newSingleThreadExecutor().submit(controllerManager); - return controllerManager; + return new ControllerManager(sharedInformerFactory, ingressController, endpointsController); + } + + /** + * Controller executor service. + * + * @return controller executor service + */ + @Bean(destroyMethod = "shutdown") + public ExecutorService ingressControllerExecutorService() { + return Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "shenyu-k8s-ingress-controller"); + thread.setDaemon(true); + return thread; + }); + } + + /** + * Controller lifecycle. + * + * @param controllerManager controller manager + * @param executorService controller executor service + * @return controller lifecycle + */ + @Bean + public SmartLifecycle ingressControllerLifecycle(@Qualifier("controller-manager") final ControllerManager controllerManager, + @Qualifier("ingressControllerExecutorService") final ExecutorService executorService) { + return new ControllerManagerLifecycle(Collections.singletonList(controllerManager), executorService); } /** @@ -93,16 +122,17 @@ public class IngressControllerConfiguration { * * @param sharedInformerFactory sharedInformerFactory * @param ingressReconciler ingressReconciler + * @param properties controller properties * @return Ingress Controller */ @Bean("ingress-controller") - public Controller ingressController(final SharedInformerFactory sharedInformerFactory, final IngressReconciler ingressReconciler) { + public Controller ingressController(final SharedInformerFactory sharedInformerFactory, final IngressReconciler ingressReconciler, + final ShenyuKubernetesControllerProperties properties) { DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(sharedInformerFactory); builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(V1Ingress.class, q) - .withResyncPeriod(Duration.ofMinutes(1)) + .withResyncPeriod(properties.getResyncPeriod()) .build()); - // TODO support config in application.yaml - builder.withWorkerCount(2); + builder.withWorkerCount(properties.getWorkerCount()); return builder.withReconciler(ingressReconciler).withName("ingressController").build(); } @@ -133,15 +163,17 @@ public class IngressControllerConfiguration { * * @param sharedInformerFactory sharedInformerFactory * @param endpointsReconciler endpointsReconciler + * @param properties controller properties * @return Endpoints Controller */ @Bean("endpoints-controller") - public Controller endpointsController(final SharedInformerFactory sharedInformerFactory, final EndpointsReconciler endpointsReconciler) { + public Controller endpointsController(final SharedInformerFactory sharedInformerFactory, final EndpointsReconciler endpointsReconciler, + final ShenyuKubernetesControllerProperties properties) { DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(sharedInformerFactory); builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(V1Endpoints.class, q) - .withResyncPeriod(Duration.ofMinutes(1)) + .withResyncPeriod(properties.getResyncPeriod()) .build()); - builder.withWorkerCount(2); + builder.withWorkerCount(properties.getWorkerCount()); return builder.withReconciler(endpointsReconciler).withName("ingressController").build(); } diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java new file mode 100644 index 0000000000..b3033e0d01 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/ShenyuKubernetesControllerProperties.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.k8s; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * Configuration properties for Kubernetes ingress and endpoints controllers. + */ +@ConfigurationProperties(prefix = "shenyu.k8s.controller") +public class ShenyuKubernetesControllerProperties { + + /** + * Number of workers used by each controller. Values greater than one rely on the + * ingress, selector, secret and service relation caches remaining thread-safe. + */ + private int workerCount = 2; + + /** + * Period between full informer resynchronizations. + */ + private Duration resyncPeriod = Duration.ofMinutes(1); + + /** + * Get worker count. + * + * @return worker count + */ + public int getWorkerCount() { + return workerCount; + } + + /** + * Set worker count. + * + * @param workerCount worker count + */ + public void setWorkerCount(final int workerCount) { + this.workerCount = workerCount; + } + + /** + * Get resync period. + * + * @return resync period + */ + public Duration getResyncPeriod() { + return resyncPeriod; + } + + /** + * Set resync period. + * + * @param resyncPeriod resync period + */ + public void setResyncPeriod(final Duration resyncPeriod) { + this.resyncPeriod = resyncPeriod; + } +} diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java new file mode 100644 index 0000000000..99a0ea8859 --- /dev/null +++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/test/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfigurationTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.k8s; + +import io.kubernetes.client.extended.controller.Controller; +import io.kubernetes.client.extended.controller.ControllerManager; +import io.kubernetes.client.extended.controller.DefaultController; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1Ingress; +import org.apache.shenyu.k8s.reconciler.EndpointsReconciler; +import org.apache.shenyu.k8s.reconciler.IngressReconciler; +import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link IngressControllerConfiguration}. + */ +public final class IngressControllerConfigurationTest { + + private final IngressControllerConfiguration configuration = new IngressControllerConfiguration(); + + @Test + public void testDefaultProperties() { + ShenyuKubernetesControllerProperties properties = new ShenyuKubernetesControllerProperties(); + + assertThat(properties.getWorkerCount(), is(2)); + assertThat(properties.getResyncPeriod(), is(Duration.ofMinutes(1))); + } + + @Test + public void testPropertiesBinding() { + new ApplicationContextRunner() + .withUserConfiguration(PropertiesConfiguration.class) + .withPropertyValues( + "shenyu.k8s.controller.worker-count=4", + "shenyu.k8s.controller.resync-period=30s") + .run(context -> { + ShenyuKubernetesControllerProperties properties = context.getBean(ShenyuKubernetesControllerProperties.class); + assertThat(properties.getWorkerCount(), is(4)); + assertThat(properties.getResyncPeriod(), is(Duration.ofSeconds(30))); + }); + } + + @Test + @SuppressWarnings("unchecked") + public void testControllersUseConfiguredTuning() { + SharedInformerFactory sharedInformerFactory = mock(SharedInformerFactory.class); + SharedIndexInformer<V1Ingress> ingressInformer = mock(SharedIndexInformer.class); + SharedIndexInformer<V1Endpoints> endpointsInformer = mock(SharedIndexInformer.class); + when(sharedInformerFactory.getExistingSharedIndexInformer(V1Ingress.class)).thenReturn(ingressInformer); + when(sharedInformerFactory.getExistingSharedIndexInformer(V1Endpoints.class)).thenReturn(endpointsInformer); + ShenyuKubernetesControllerProperties properties = new ShenyuKubernetesControllerProperties(); + properties.setWorkerCount(4); + properties.setResyncPeriod(Duration.ofSeconds(30)); + + Controller ingressController = configuration.ingressController(sharedInformerFactory, mock(IngressReconciler.class), properties); + Controller endpointsController = configuration.endpointsController(sharedInformerFactory, mock(EndpointsReconciler.class), properties); + + assertThat(ingressController, instanceOf(DefaultController.class)); + assertThat(endpointsController, instanceOf(DefaultController.class)); + assertThat(((DefaultController) ingressController).getWorkerCount(), is(4)); + assertThat(((DefaultController) endpointsController).getWorkerCount(), is(4)); + verify(ingressInformer).addEventHandlerWithResyncPeriod(any(), eq(30_000L)); + verify(endpointsInformer).addEventHandlerWithResyncPeriod(any(), eq(30_000L)); + } + + @Test + public void testControllerLifecycleAndExecutor() throws Exception { + ControllerManager controllerManager = mock(ControllerManager.class); + ExecutorService executorService = configuration.ingressControllerExecutorService(); + try { + SmartLifecycle lifecycle = configuration.ingressControllerLifecycle(controllerManager, executorService); + assertThat(lifecycle, instanceOf(ControllerManagerLifecycle.class)); + assertThat(executorService.submit(() -> Thread.currentThread().getName()).get(5, TimeUnit.SECONDS), + is("shenyu-k8s-ingress-controller")); + + lifecycle.start(); + lifecycle.stop(); + + verify(controllerManager).run(); + verify(controllerManager).shutdown(); + } finally { + executorService.shutdownNow(); + } + } + + @Test + public void testLifecycleDoesNotStartDuringCreation() { + ControllerManager controllerManager = mock(ControllerManager.class); + ExecutorService executorService = mock(ExecutorService.class); + + configuration.ingressControllerLifecycle(controllerManager, executorService); + + verifyNoInteractions(controllerManager, executorService); + } + + @Configuration + @EnableConfigurationProperties(ShenyuKubernetesControllerProperties.class) + static class PropertiesConfiguration { + } +}
