Skip to content

Commit

Permalink
perf: add caching for extension getter to enhance performance (#7102)
Browse files Browse the repository at this point in the history
#### 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% 以上
```
  • Loading branch information
guqing authored Dec 4, 2024
1 parent 2b4d1ab commit eb96912
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -86,9 +80,7 @@ private <T extends ExtensionPoint> Flux<T> 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(
Expand All @@ -101,15 +93,7 @@ private <T extends ExtensionPoint> Flux<T> getEnabledExtensions(String epdName,

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());
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -34,7 +31,10 @@
class DefaultExtensionGetterTest {

@Mock
ReactiveExtensionClient client;
ExtensionPointDefinitionGetter extensionPointDefinitionGetter;

@Mock
ExtensionDefinitionGetter extensionDefinitionGetter;

@Mock
PluginManager pluginManager;
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit eb96912

Please sign in to comment.