perf: add caching for extension getter to enhance performance (#7102)

#### 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% 以上
```
pull/7094/head^2
guqing 2024-12-04 10:41:09 +08:00 committed by GitHub
parent 2b4d1ab8d8
commit eb969122ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 155 additions and 53 deletions

View File

@ -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<E extends Extension>
implements Reconciler<Reconciler.Request>, DisposableBean {
protected final ConcurrentMap<String, E> 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<E>) 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();
}
}

View File

@ -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.ExtensionPoint;
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 <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPoint) {
@ -86,9 +80,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
}
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 @@ public class DefaultExtensionGetter implements ExtensionGetter {
private Mono<ExtensionPointDefinition> fetchExtensionPointDefinition(
Class<? extends ExtensionPoint> 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());
}
}

View File

@ -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<ExtensionDefinition> get(String name);
}

View File

@ -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<ExtensionDefinition>
implements ExtensionDefinitionGetter {
public ExtensionDefinitionGetterImpl(ExtensionClient client) {
super(client, new ExtensionDefinition());
}
@Override
public Mono<ExtensionDefinition> get(String name) {
return Mono.fromSupplier(() -> cache.get(name));
}
@Override
void putCache(ExtensionDefinition definition) {
cache.put(definition.getMetadata().getName(), definition);
}
}

View File

@ -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.
* <p>Retrieve by filedSelector: <code>spec.className</code></p>
*
* @param className extension point class name
*/
Mono<ExtensionPointDefinition> getByClassName(String className);
}

View File

@ -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<ExtensionPointDefinition>
implements ExtensionPointDefinitionGetter {
public ExtensionPointDefinitionGetterImpl(ExtensionClient client) {
super(client, new ExtensionPointDefinition());
}
@Override
public Mono<ExtensionPointDefinition> getByClassName(String className) {
return Mono.fromSupplier(() -> cache.get(className));
}
@Override
void putCache(ExtensionPointDefinition definition) {
var className = definition.getSpec().getClassName();
cache.put(className, definition);
}
}

View File

@ -2,7 +2,7 @@ package run.halo.app.plugin.extensionpoint;
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.beans.factory.ObjectProvider;
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 @@ import run.halo.app.plugin.extensionpoint.ExtensionPointDefinition.ExtensionPoin
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 @@ class DefaultExtensionGetterTest {
@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 @@ class DefaultExtensionGetterTest {
@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 @@ class DefaultExtensionGetterTest {
@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());