From eb969122ff19fa6deb31a9120ca070dfa3c09596 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:41:09 +0800 Subject: [PATCH] perf: add caching for extension getter to enhance performance (#7102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: 为扩展获取增加缓存以提高网站整体性能 在此之前,每个请求都要经过很多过滤器,而一些过滤器会获取扩展因此导致频繁查询扩展和扩展点定义拖慢了速度 **测试情况** 初始化一个全新环境,安装并启用以下插件和主题 - 已激活主题: [Earth 1.11.0](https://github.com/halo-dev/theme-earth) - 已启动插件: - [SEO 工具集 1.0.1](https://github.com/f2ccloud/plugin-seo-tools) - [OAuth2 认证 1.5.0](https://github.com/halo-sigs/plugin-oauth2) - [Trailing Slash 1.0.0](https://github.com/halo-sigs/plugin-trailing-slash) - [评论组件 2.5.1](https://github.com/halo-dev/plugin-comment-widget) - [KaTeX 2.1.0](https://github.com/halo-sigs/plugin-katex) - [应用市场 1.9.0](https://www.halo.run/store/apps/app-VYJbF) 通过 Apache Benchmark (ab) 进行 1w 次请求并发 100 个,测试访问首页,得到以下测试结果: 核心指标对比 |指标|改进前|改进后|提升情况| |---|---|---|---| |**总耗时 (Time taken)**|27.030 秒|25.718 秒|减少约 **4.9%**| |**每秒请求数 (RPS)**|369.96 req/sec|388.83 req/sec|提升约 **5.1%**| |**单请求平均耗时**|270.298 ms|257.181 ms|减少约 **4.9%**| |**传输速率 (Transfer Rate)**|6346.44 KB/s|6670.12 KB/s|提升约 **5.1%**| 综合分析 - 性能提升主要体现在:请求处理时间(Processing)、等待时间(Waiting)以及每秒请求数(RPS)均有 约5% 左右的提升。 - 传输效率更高:通过更快的处理时间,传输速率提高了 5.1%。 - 长尾请求优化显著:最大响应时间减少了约 14.9%,意味着极端情况下的性能更优。 #### Does this PR introduce a user-facing change? ```release-note 为扩展获取增加缓存使网站整体性能提升 5% 以上 ``` --- .../AbstractDefinitionGetter.java | 44 ++++++++++++++ .../DefaultExtensionGetter.java | 26 ++------ .../ExtensionDefinitionGetter.java | 13 ++++ .../ExtensionDefinitionGetterImpl.java | 25 ++++++++ .../ExtensionPointDefinitionGetter.java | 14 +++++ .../ExtensionPointDefinitionGetterImpl.java | 26 ++++++++ .../DefaultExtensionGetterTest.java | 60 +++++++++---------- 7 files changed, 155 insertions(+), 53 deletions(-) create mode 100644 application/src/main/java/run/halo/app/plugin/extensionpoint/AbstractDefinitionGetter.java create mode 100644 application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetter.java create mode 100644 application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetterImpl.java create mode 100644 application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetter.java create mode 100644 application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetterImpl.java diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/AbstractDefinitionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/AbstractDefinitionGetter.java new file mode 100644 index 0000000000..96545966dd --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/AbstractDefinitionGetter.java @@ -0,0 +1,44 @@ +package run.halo.app.plugin.extensionpoint; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.DisposableBean; +import run.halo.app.extension.Extension; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.controller.Controller; +import run.halo.app.extension.controller.ControllerBuilder; +import run.halo.app.extension.controller.Reconciler; + +@RequiredArgsConstructor +abstract class AbstractDefinitionGetter + implements Reconciler, DisposableBean { + + protected final ConcurrentMap cache = new ConcurrentHashMap<>(); + + private final ExtensionClient client; + + private final E watchType; + + abstract void putCache(E definition); + + @Override + @SuppressWarnings("unchecked") + public Result reconcile(Request request) { + client.fetch((Class) watchType.getClass(), request.name()) + .ifPresent(this::putCache); + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder.extension(watchType) + .syncAllOnStart(true) + .build(); + } + + @Override + public void destroy() throws Exception { + cache.clear(); + } +} diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java index 42b833e16d..52163e7770 100644 --- a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java @@ -1,7 +1,5 @@ package run.halo.app.plugin.extensionpoint; -import static run.halo.app.extension.index.query.QueryFactory.equal; - import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -11,15 +9,9 @@ import org.pf4j.PluginManager; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import run.halo.app.extension.ListOptions; -import run.halo.app.extension.ListResult; -import run.halo.app.extension.PageRequestImpl; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; @@ -33,7 +25,9 @@ public class DefaultExtensionGetter implements ExtensionGetter { private final BeanFactory beanFactory; - private final ReactiveExtensionClient client; + private final ExtensionDefinitionGetter extensionDefinitionGetter; + + private final ExtensionPointDefinitionGetter extensionPointDefinitionGetter; @Override public Flux getExtensions(Class extensionPoint) { @@ -86,9 +80,7 @@ private Flux getEnabledExtensions(String epdName, } var extensions = getExtensions(extensionPoint).cache(); return Flux.fromIterable(extensionDefNames) - .flatMapSequential(extensionDefName -> - client.fetch(ExtensionDefinition.class, extensionDefName) - ) + .flatMapSequential(extensionDefinitionGetter::get) .flatMapSequential(extensionDef -> { var className = extensionDef.getSpec().getClassName(); return extensions.filter( @@ -101,15 +93,7 @@ private Flux getEnabledExtensions(String epdName, private Mono fetchExtensionPointDefinition( Class extensionPoint) { - var listOptions = new ListOptions(); - listOptions.setFieldSelector(FieldSelector.of( - equal("spec.className", extensionPoint.getName()) - )); - var sort = Sort.by("metadata.creationTimestamp", "metadata.name").ascending(); - return client.listBy(ExtensionPointDefinition.class, listOptions, - PageRequestImpl.ofSize(1).withSort(sort) - ) - .flatMap(list -> Mono.justOrEmpty(ListResult.first(list))); + return extensionPointDefinitionGetter.getByClassName(extensionPoint.getName()); } } diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetter.java new file mode 100644 index 0000000000..fb534cbb1b --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetter.java @@ -0,0 +1,13 @@ +package run.halo.app.plugin.extensionpoint; + +import reactor.core.publisher.Mono; + +public interface ExtensionDefinitionGetter { + + /** + * Gets extension definition by extension definition name. + * + * @param name extension definition name + */ + Mono get(String name); +} diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetterImpl.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetterImpl.java new file mode 100644 index 0000000000..83e4e0b004 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetterImpl.java @@ -0,0 +1,25 @@ +package run.halo.app.plugin.extensionpoint; + +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.extension.ExtensionClient; + +@Component +public class ExtensionDefinitionGetterImpl + extends AbstractDefinitionGetter + implements ExtensionDefinitionGetter { + + public ExtensionDefinitionGetterImpl(ExtensionClient client) { + super(client, new ExtensionDefinition()); + } + + @Override + public Mono get(String name) { + return Mono.fromSupplier(() -> cache.get(name)); + } + + @Override + void putCache(ExtensionDefinition definition) { + cache.put(definition.getMetadata().getName(), definition); + } +} diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetter.java new file mode 100644 index 0000000000..d2fb248e1d --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetter.java @@ -0,0 +1,14 @@ +package run.halo.app.plugin.extensionpoint; + +import reactor.core.publisher.Mono; + +public interface ExtensionPointDefinitionGetter { + + /** + * Gets extension point definition by extension point class. + *

Retrieve by filedSelector: spec.className

+ * + * @param className extension point class name + */ + Mono getByClassName(String className); +} diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetterImpl.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetterImpl.java new file mode 100644 index 0000000000..075aadf64a --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetterImpl.java @@ -0,0 +1,26 @@ +package run.halo.app.plugin.extensionpoint; + +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.extension.ExtensionClient; + +@Component +public class ExtensionPointDefinitionGetterImpl + extends AbstractDefinitionGetter + implements ExtensionPointDefinitionGetter { + + public ExtensionPointDefinitionGetterImpl(ExtensionClient client) { + super(client, new ExtensionPointDefinition()); + } + + @Override + public Mono getByClassName(String className) { + return Mono.fromSupplier(() -> cache.get(className)); + } + + @Override + void putCache(ExtensionPointDefinition definition) { + var className = definition.getSpec().getClassName(); + cache.put(className, definition); + } +} diff --git a/application/src/test/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetterTest.java b/application/src/test/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetterTest.java index b876aa10c7..e78dc787b5 100644 --- a/application/src/test/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetterTest.java +++ b/application/src/test/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetterTest.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.same; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static run.halo.app.infra.SystemSetting.ExtensionPointEnabled.GROUP; @@ -22,10 +22,7 @@ import org.springframework.core.annotation.Order; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import run.halo.app.extension.ListOptions; -import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; -import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; import run.halo.app.plugin.extensionpoint.ExtensionPointDefinition.ExtensionPointType; @@ -34,7 +31,10 @@ class DefaultExtensionGetterTest { @Mock - ReactiveExtensionClient client; + ExtensionPointDefinitionGetter extensionPointDefinitionGetter; + + @Mock + ExtensionDefinitionGetter extensionDefinitionGetter; @Mock PluginManager pluginManager; @@ -54,15 +54,14 @@ class DefaultExtensionGetterTest { @Test void shouldGetExtensionBySingletonDefinitionWhenExtensionPointEnabledSet() { // prepare extension point definition - when(client.listBy(same(ExtensionPointDefinition.class), any(ListOptions.class), any())) - .thenReturn(Mono.fromSupplier(() -> { - var epd = createExtensionPointDefinition("fake-extension-point", + when(extensionPointDefinitionGetter.getByClassName(any())) + .thenReturn(Mono.fromSupplier( + () -> createExtensionPointDefinition("fake-extension-point", FakeExtensionPoint.class, - ExtensionPointType.SINGLETON); - return new ListResult<>(List.of(epd)); - })); + ExtensionPointType.SINGLETON)) + ); - when(client.fetch(ExtensionDefinition.class, "fake-extension")) + when(extensionDefinitionGetter.get(eq("fake-extension"))) .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition( "fake-extension", FakeExtensionPointImpl.class, @@ -94,13 +93,12 @@ void shouldGetExtensionBySingletonDefinitionWhenExtensionPointEnabledSet() { @Test void shouldGetDefaultSingletonDefinitionWhileExtensionPointEnabledNotSet() { - when(client.listBy(same(ExtensionPointDefinition.class), any(ListOptions.class), any())) - .thenReturn(Mono.fromSupplier(() -> { - var epd = createExtensionPointDefinition("fake-extension-point", + when(extensionPointDefinitionGetter.getByClassName(any())) + .thenReturn(Mono.fromSupplier( + () -> createExtensionPointDefinition("fake-extension-point", FakeExtensionPoint.class, - ExtensionPointType.SINGLETON); - return new ListResult<>(List.of(epd)); - })); + ExtensionPointType.SINGLETON)) + ); when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class)) .thenReturn(Mono.empty()); @@ -124,21 +122,20 @@ void shouldGetDefaultSingletonDefinitionWhileExtensionPointEnabledNotSet() { @Test void shouldGetMultiInstanceExtensionWhileExtensionPointEnabledSet() { // prepare extension point definition - when(client.listBy(same(ExtensionPointDefinition.class), any(ListOptions.class), any())) - .thenReturn(Mono.fromSupplier(() -> { - var epd = createExtensionPointDefinition("fake-extension-point", + when(extensionPointDefinitionGetter.getByClassName(any())) + .thenReturn(Mono.fromSupplier( + () -> createExtensionPointDefinition("fake-extension-point", FakeExtensionPoint.class, - ExtensionPointType.MULTI_INSTANCE); - return new ListResult<>(List.of(epd)); - })); + ExtensionPointType.MULTI_INSTANCE)) + ); - when(client.fetch(ExtensionDefinition.class, "fake-extension")) + when(extensionDefinitionGetter.get(eq("fake-extension"))) .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition( "fake-extension", FakeExtensionPointImpl.class, "fake-extension-point"))); - when(client.fetch(ExtensionDefinition.class, "default-fake-extension")) + when(extensionDefinitionGetter.get(eq("default-fake-extension"))) .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition( "default-fake-extension", FakeExtensionPointDefaultImpl.class, @@ -177,13 +174,12 @@ void shouldGetMultiInstanceExtensionWhileExtensionPointEnabledSet() { @Test void shouldGetMultiInstanceExtensionWhileExtensionPointEnabledNotSet() { // prepare extension point definition - when(client.listBy(same(ExtensionPointDefinition.class), any(ListOptions.class), any())) - .thenReturn(Mono.fromSupplier(() -> { - var epd = createExtensionPointDefinition("fake-extension-point", + when(extensionPointDefinitionGetter.getByClassName(any())) + .thenReturn(Mono.fromSupplier( + () -> createExtensionPointDefinition("fake-extension-point", FakeExtensionPoint.class, - ExtensionPointType.MULTI_INSTANCE); - return new ListResult<>(List.of(epd)); - })); + ExtensionPointType.MULTI_INSTANCE)) + ); when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class)) .thenReturn(Mono.empty());