无论是本地调试,还是线上发布,应用启动所需的时间变得越来越久,逐渐变成了一种“煎熬”,而且每天都需要默默的忍受。不愿做”奴隶“的人们,又打响了反抗的第二枪(第一枪见《如何优化Spring应用镜像》)。如何减少应用的构建、启动时间已经成为了提高生产效率的”制胜法宝“。结合前期的镜像构建优化经验和方法,今天,我们从应用本身查找下优化方法。
子曰:“工欲善其事,必先利其器。“
一款好的Profile工具是发现问题、解决问题的”金刚钻“。今天我选选择的是一款基于JavaAgent技术的国产的开源工具-”Spring Startup Ananlyzer“。虽然是一款个人开源的工具,star目前也不多(不足1K),但其使用体验、设计思想、加速效果,以及出于支持国产开源产品Buff加持,它值得我们试用。
Spring Startup Analyzer 是一款基于Java-Agent技术的数据采集工具,采集Spring应用启动过程数据,生成交互式分析报告(HTML),用于分析Spring应用启动卡点,同时支持Spring Bean异步初始化,以优化Spring应用启动时间。
arduino
复制代码
`public interface EventListener extends Startable {
/**
*/
void start();
/**
*/
void stop();
/**
*/
boolean filter(String className);
/**
*/
default boolean filter(String methodName, String[] methodTypes) {
return true;
}
/**
*/
void onEvent(Event event);
/**
*/
List<Event.Type> listen();
}`
typescript
复制代码
`@MetaInfServices
public class FindResourceCounter implements EventListener {
private final AtomicLong COUNT = new AtomicLong(0);
@Override
public boolean filter(String className) {
return "java.net.URLClassLoader".equals(className);
}
@Override
public boolean filter(String methodName, String[] methodTypes) {
if (!"findResource".equals(methodName)) {
return false;
}
return methodTypes != null && methodTypes.length == 1 && "java.lang.String".equals(methodTypes[0]);
}
@Override
public void onEvent(Event event) {
if (event instanceof AtEnterEvent) {
// 开始进入findResource方法
} else if (event instanceof AtExitEvent) {
// findResource方法返回
}
// 统计调用次数
COUNT.incrementAndGet();
}
@Override
public List<Event.Type> listen() {
return Arrays.asList(Event.Type.AT_ENTER, Event.Type.AT_EXIT);
}
@Override
public void start() {
System.out.println("============== my extension start =============");
}
@Override
public void stop() {
System.out.println("============== my extension end =============");
System.out.println("findResource count: " + COUNT.get());
}
}`
less
复制代码
`public class Interceptor {
// BEFORE
@AtEnter
public static void atEnter(@Binding.Class Class<?> clazz,
@Binding.This Object target,
@Binding.MethodName String methodName,
@Binding.MethodDesc String methodDesc,
@Binding.Args Object[] args) {
Bridge.atEnter(clazz, target, methodName, methodDesc, args);
}
// RETURN
@AtExit
public static void atExit(@Binding.Class Class<?> clazz,
@Binding.This Object target,
@Binding.MethodName String methodName,
@Binding.MethodDesc String methodDesc,
@Binding.Args Object[] args,
@Binding.Return Object returnObj) {
Bridge.atExit(clazz, target, methodName, methodDesc, args, returnObj);
}
// THROWS
@AtExceptionExit
public static void atExceptionExit(@Binding.Class Class<?> clazz,
@Binding.This Object target,
@Binding.MethodName String methodName,
@Binding.MethodDesc String methodDesc,
@Binding.Args Object[] args,
@Binding.Throwable Throwable throwable) {
Bridge.atExceptionExit(clazz, target, methodName, methodDesc, args, throwable);
}
}`
ini
复制代码
`agentLoader = createAgentClassLoader();
Class<?> transFormer = agentLoader.loadClass("io.github.linyimin0812.profiler.core.enhance.ProfilerClassFileTransformer");
Constructor<?> constructor = transFormer.getConstructor(Instrumentation.class, List.class);
Object instance = constructor.newInstance(instrumentation, getManifests());`
java
复制代码
`public class AsyncProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!beanFactory.containsBeanDefinition(beanName)) {
return bean;
}
String methodName = AsyncInitBeanFinder.getAsyncInitMethodName(beanName, beanFactory.getBeanDefinition(beanName));
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetClass(bean.getClass());
proxyFactory.setProxyTargetClass(true);
AsyncInitializeBeanMethodInvoker invoker = new AsyncInitializeBeanMethodInvoker(bean, beanName, methodName);
proxyFactory.addAdvice(invoker);
return proxyFactory.getProxy();
}
class AsyncInitializeBeanMethodInvoker implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (AsyncTaskExecutor.isFinished()) {
return invocation.getMethod().invoke(targetObject, invocation.getArguments());
}
Method method = invocation.getMethod();
String methodName = method.getName();
if (this.asyncMethodName.equals(methodName)) {
logger.info("async-init-bean, beanName: {}, async init method: {}", beanName, asyncMethodName);
AsyncTaskExecutor.submitTask(() -> {
invocation.getMethod().invoke(targetObject, invocation.getArguments());
});
return null;
}
return invocation.getMethod().invoke(targetObject, invocation.getArguments());
}
}
}`
监听ContextRefreshedEvent事件,等待所有异步执行的init-method完成;
scala
复制代码
`public class AsyncBeanPriorityLoadPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
List<String> asyncBeans = AsyncConfig.getInstance().getAsyncBeanProperties().getBeanNames();
for (String beanName : asyncBeans) {
if (beanFactory instanceof DefaultListableBeanFactory && !((DefaultListableBeanFactory) beanFactory).containsBeanDefinition(beanName)) {
continue;
}
beanFactory.getBean(beanName);
}
}
}`
该工具的数据采集展示使用了Spring Boot Startup Report,一个开源工具,生成一个启动报告,展示了Spring Boot应用程序的各个组件和依赖项以及它们对应用程序启动时间的组成。
提供了手动安装和一键脚本安装两种安装方式
bash
复制代码
`mkdir -p ${HOME}/spring-startup-analyzer
cd 下载路径
tar -zxvf spring-startup-analyzer.tar.gz -C ${HOME}/spring-startup-analyzer`
bash
复制代码
curl -sS https://raw.githubusercontent.com/linyimin0812/spring-startup-analyzer/main/bin/install.sh | sh
ini
复制代码
`# app health check timeout, unit is minute
spring-startup-analyzer.app.health.check.timeout=20
spring-startup-analyzer.app.health.check.endpoints=http://localhost:8080/actuator/health
spring-startup-analyzer.admin.http.server.port=8065
spring-startup-analyzer.invoke.count.methods=org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(java.lang.String,org.springframework.beans.factory.support.RootBeanDefinition,java.lang.Object[])\
|java.net.URLClassLoader.findResource(java.lang.String)
spring-startup-analyzer.async.profiler.sample.thread.names=main
spring-startup-analyzer.async.profiler.interval.millis=5`
配置项
说明
默认值
spring-startup-analyzer.app.health.check.timeout
应用启动健康检查超时时间,单位为分钟
20
spring-startup-analyzer.app.health.check.endpoints
应用启动成功检查url,可配置多个,以","分隔
http://127.0.0.1:7002/actuator/health
spring-startup-analyzer.admin.http.server.port
管理端口
8065
spring-startup-analyzer.async.profiler.sample.thread.names
async profiler采集的线程名称,支持配置多个,以","进行分隔
main
spring-startup-analyzer.async.profiler.interval.millis
async profiler采集间隔时间(ms)
5
spring-startup-analyzer.linux.and.mac.profiler
指定linux/mac下火焰图采样器:async_profiler/jvm_profiler
async_profiler
此项目是以agent的方式启动的,所以在启动命令中添加参数-javaagent:$HOME/spring-startup-analyzer/lib/spring-profiler-agent.jar即可。
如果是以java命令行的方式启动应用,则在命令行中添加,如果是在IDEA中启动,则需要在VM options选项中添加。
日志文件路径:$HOME/spring-startup-analyzer/logs
应用启动完成后会在console和startup.log文件中输出
arduino
复制代码
======= spring-startup-analyzer finished, click http://localhost:8065 to visit details. ======
可以通过此输出来判断采集是否完成。
less
复制代码
`@Bean(destroyMethod = "destroy")
@ConditionalOnClass(BusinessHttpClient.class)
@ConditionalOnProperty(prefix = "business.client.options.remote", name = "address")
public BusinessClient businessHttpClientWrapper() throws Exception {
log.info("start build business http client, options is {}", businessClientProperties.getOptions());
BusinessHttpClient businessHttpClient = BusinessHttpClient.builder()
.options(businessClientProperties.getOptions()).build();
GlobalOptions.setENV(businessClientProperties.getEnv());
businessHttpClient.init();
if (Optional.ofNullable(businessClientProperties.getEnableCustomEventListener()).orElse(true)
&& CollectionUtils.isNotEmpty(listeners)) {
listeners.forEach(listener -> businessHttpClient.getBusinessEventBus().register(listener));
}
if (Optional.ofNullable(businessClientProperties.getAutoStart()).orElse(true)) {
long now = System.currentTimeMillis();
log.info("business http client starting");
businessHttpClient.start();
log.info("business http client started, cost: {}", System.currentTimeMillis() - now);
}
return businessHttpClient;
}`
ini
复制代码
`# Asynchronous beans may be at the end of the Spring bean initialization order, which may result in suboptimal effects of asynchronous optimization. Open the configuration to prioritize loading asynchronous beans.
spring-startup-analyzer.boost.spring.async.bean-priority-load-enable=true
spring-startup-analyzer.boost.spring.async.bean-names=businessHttpClientWrapper,subLabelService,cdcEsService,distributeIdService
spring-startup-analyzer.boost.spring.async.init-bean-thread-pool-core-size=4
spring-startup-analyzer.boost.spring.async.init-bean-thread-pool-max-size=8`
[TextStorage -master]
[TextStorage-86609]
csharp
复制代码
`2023-09-08 14:24:37.406 INFO 10284 --- [ main] c.n.i.a.b.c.h.BusinessHttpClient : load CUSTOM_FUNCTION init data from remote success, total count is 4076, cost is 1440ms.
2023-09-08 14:24:35.808 INFO 10284 --- [ main] c.n.i.a.b.c.h.BusinessHttpClient : load TARGET_CHECKER init data from remote success, total count is 84087, cost is 3851ms.
2023-09-08 14:24:31.957 INFO 10284 --- [ main] c.n.i.a.b.c.h.BusinessHttpClient : load TARGET_CONFIG init data from remote success, total count is 10885, cost is 10225ms.`
ini
复制代码
`this.options.getOperateTargetTypes().forEach((operateTargetType) -> {
long lastOperateTargetId = 0L;
BusinessDataInitRequest businessDataInitRequest = BusinessDataInitRequest.builder().batchSize(this.options.getLoadBatchSize()).operateTargetType(operateTargetType.getCode()).build();
long totalCount = 0L;
long startTime = System.currentTimeMillis();
boolean hasMoreData = true;
while(hasMoreData) {`
ini
复制代码
`private void loadFromRemote() {
AtomicLong lastRefreshTime = new AtomicLong(System.currentTimeMillis());
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
for (OperateTargetType operateTargetType : options.getOperateTargetTypes()) {
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> consumer(operateTargetType,lastRefreshTime));
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
this.lastRefreshTime = lastRefreshTime.get();
}`
当我们直观的去面对应用启动过程中的瓶颈时,我们会发现,“罪魁祸首”还是我们自己业务层面的问题。为了追求“产出”、“效率”,我们时常会忽略一些重要的“底线”和“操守”。最终,整个团队会为此背锅。当我们为此反抗时,我们又会有收益;当我们选择沉默时,等待我们的是无止境的煎熬。下面总结下启动优化建议:
原网址: 访问
创建于: 2024-02-23 11:47:58
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论