
互联网从业者,关注分布式,微服务,云原生应用等领域,曾主导过公司单体架构到分布式系统的升级。开源爱好者,目前开源了6个项目,热门项目获码云GVP最具价值项目。长期维护独立博客-kl博客,热衷传播技术分享知识。现任凯京科技架构部负责人。
前言 wrk是一个开源的、热门的、现代的单机HTTP基准测试工具,目前在github开源平台累计了26.9k的star数目,足以可见wrk在Http基准测试领域的热门程度。它结合了多线程设计和可扩展的事件通知系统,如epoll和kqueue,可以在有限的资源下并发出极致的的负载请求。并且内置了一个可选的LuaJIT脚本执行引擎,可以处理复杂的HTTP请求生成、响应处理以及自定义压测报告。 wrk项目地址:https://github.com/wg/wrk 安装wrk mac下安装: brew install wrk 其他平台参考:https://github.com/wg/wrk/wiki 基础使用 wrk -t12 -c100 -d30s --latency http://localhost:8010/healthz 如上指令描述了采用12个线程,100个链接,针对http://localhost:8010/healthz 接口服务,持续压测30s。wrk本身不是依赖线程数来模拟并发数的所以线程数量设置在核心数左右最好,线程数多了测试系统消耗大,可能带来反效果。亲测核心数一致的线程数和两倍核心数的线程数,前者压出的QPS更高。结果如下:、、 Running 30s test @ http://localhost:8010/healthz (运行30s测试) 12 threads and 100 connections(12个线程100个连接) Thread Stats Avg(均值) Stdev(标准差值) Max(最大值) +/- Stdev(正负标准差值) Latency(延迟) 1.39ms 668.10us 23.95ms 90.34% Req/Sec(每秒请求数) 5.44k 545.23 10.27k 76.47% Latency Distribution(延迟直方图) 50% 1.32ms (50%请求延迟在1.32ms内) 75% 1.49ms (75%请求延迟在1.49ms内) 90% 1.72ms (90%请求延迟在1.72ms内) 99% 4.77ms (99%请求延迟在4.77ms内) 1952790 requests in 30.08s, 271.90MB read (共1952790次请求,用时30s,传输了271.9M数据) Requests/sec(每秒请求数): 64930.12 Transfer/sec(每秒传输数据): 9.04MB wrk的结果相比ab测试结果来说,多了一个延时直方图,有了这个直方图,我们可以更清晰的看到延迟的分布情况。这也是博主选择wrk最重要的原因 常用指令说明 -c, --connections: 要保持打开的HTTP连接的总数,每个线程处理数N =连接/线程 -d, --duration: 测试持续时间, 如 2s, 2m, 2h -t, --threads: 测试线程总数 -s, --script: 指定加载lua测试扩展脚本 -H, --header: 添加请求头信息, 如"User-Agent: wrk" --latency: 打印延迟直方图信息 --timeout: 如果在此时间内没有收到响应,则记录超时. -开头的指令为简写的,后面两个打印延迟直方图和超时设置没有简写的,只能--开头指定 高阶用法,lua测试脚本 wrk内置了全局变量,全局方法,以及五个测试请求发起流程的方法,还有一个模拟延迟发送的方法,wrk是内置对象,在lua测试脚本的每个方法内都可以直接使用 全局变量 -- 全局的变量 wrk = { scheme = "http", host = "localhost", port = nil, method = "GET", path = "/", headers = {}, body = nil, thread = userdata, } 全局方法 -- 返回请求字符串值,其中包含所传递的参数和来自wrk表的值。例如:返回 http://www.kailing.pub function wrk.format(method, path, headers, body); -- 获取域名的IP和端口,返回table,例如:返回 `{127.0.0.1:80}` function wrk.lookup(host, service) -- 判断addr是否能连接,例如:`127.0.0.1:80`,返回 true 或 false function wrk.connect(addr) 请求过程方法 -- 请求前,对每个线程调用一次,并接收表示该线程的userdata对象。 function setup(thread) thread.addr = "http://www.kailing.pub" -- 设置请求的地址 thread:get("name") -- 获取全局变量的值 thread:set("name", "kl") -- 在线程的环境中设置全局变量的值 thread:stop() -- 停止线程 end --初始化,每个线程执行一次 function init(args) --args为从命令行传过来的额外参数 print(args) end --发起请求,每次请求执行一次,返回包含HTTP请求的字符串。每次构建新请求的开销都很大,在测试高性能服务器时, --一种解决方案是在init()中预先生成所有请求,并在request()中进行快速查找。 function request() requests = requests + 1 return wrk.request() end --响应处理,每次请求执行一次 function response(status, headers, body) responses = responses + 1 end --请求完成,每次测试执行一次。done()函数接收一个包含结果数据的表和两个统计数据对象,分别表示每个请求延迟和每个线程请求速率。 --持续时间和延迟是微秒值,速率是以每秒请求数来度量的。 function done(summary, latency, requests) for index, thread in ipairs(threads) do local id = thread:get("id") local requests = thread:get("requests") local responses = thread:get("responses") local msg = "thread %d made %d requests and got %d responses" print(msg:format(id, requests, responses)) end end 整个脚本处理过程被分为准备阶段、运行阶段、完成阶段。准备阶段在目标IP地址被解析并且所有线程都已经初始化但还没有启动之后开始。运行阶段从对init()的单个调用开始,然后对每个请求周期调用request()和response()。init()函数接收脚本的任何额外命令行参数,这些参数必须用“——”与wrk参数分隔开。 lua测试脚本案例分析 案例:我们线上有一个带缓存场景的接口服务,根据appId的值的查询结果缓存,所以,如果单纯对指定的appId压测,就变成了测试缓存系统的负载了,测试不出实际的服务性能,这个场景就需要测试工具发起每次请求的测试参数都是动态的。根据这个场景我们定制了如下的lua测试脚本: -- 测试指令:wrk -t16 -c100 -d5s -sreview_digress_list.lua --latency htt://127.0.0.1:8081 wrk.method ="GET" wrk.path = "/app/{appId}/review_digress_list" function request() -- 动态生成每个请求的url local requestPath = string.gsub(wrk.path,"{appId}",math.random(1,10)) -- 返回请求的完整字符串:http://127.0.0.1//app/666/review_digress_list return wrk.format(nil, requestPath) end
前言 为了更好的说明给gRPC-spring-boot-starter项目提交bug修复的pr的原因,解答作者的问题。以博文的形式记录了整个过程的上下文,目前pr未合并还在沟通处理中,希望此博文可以更清楚描述问题 pr地址:https://github.com/yidongnan/grpc-spring-boot-starter/pull/454 gRPC-spring-boot-starter是什么? 这是一个spring-boot-starter项目,用来在spring boot框架下,快速便捷的使用grpc技术,开箱即用。它提供如下等功能特性: 在 spring boot 应用中,通过@GrpcService自动配置并运行一个嵌入式的 gRPC 服务。 使用@GrpcClient自动创建和管理您的 gRPC Channels 和 stubs 支持Spring Cloud(向Consul或Eureka或Nacos注册服务并获取 gRPC 服务端信息) 支持Spring Sleuth作为分布式链路跟踪解决方案(如果brave-instrument-grpc存在) 支持全局和自定义的 gRPC 服务端/客户端拦截器 支持Spring-Security 支持metric (基于micrometer/actuator) 也适用于 (non-shaded) grpc-netty 选型gRPC-spring-boot-starter 博主新入职公司接手的项目采用grpc做微服务通讯框架,项目底层框架采用的spring boot,然后grpc的使用是纯手工配置的,代码写起来比较繁琐, 而且这种繁琐的模板化代码充斥在每个采用了grpc的微服务项目里。所以技术选型后找到了gRPC-spring-boot-starter 这个开源项目,这个项目代码质量不错,非常规范,文档也比较齐全。但是鉴于之前工作经验遇到过开源项目的问题(博主选型的原则,如果有合适的轮子,就摸透这个轮子,然后基于这个轮子二开,没有就自己造一个轮子),而且一般解决周期比较长,所以 最后,我们没有直接采用他们的发行包,而是fork了项目后,打算自己维护。正因为如此,才为后面迅速解决问题上线成为可能。也验证了二开这个选择是正确的。 bug出现,grpc未优雅下线 风风火火重构了所有代码,全部换成gRPC-spring-boot-starter后就上线了,上线后一切都非常好,但是项目在第二次需求上线投产时发生了一些问题。 这个时候还不确定是切换grpc实现导致的问题,现象就是,线上出现了大量的请求异常。上线完成后,异常就消失了。后面每次滚动更新都会出现类似的异常。 这个时候就很容易联系到是否切换grpc实现后,grpc未优雅下线,导致滚动更新时,大量的进行中的请求未正常处理,导致这部分流量异常?因为我们线上 流量比较大,几乎每时每刻都有大量请求,所以我们要求线上服务必须支持无缝滚动更新。如果流量比较小,这个问题可能就不会暴露出来,这也解释了之前和同事讨论的点,为什么这么明显的问题没有被及早的发现。不过都目前为止,这一切都只是猜测,真相继续往下。 定位bug,寻找真实原因 有了上面的猜测,直接找到了gRPC-spring-boot-starter管理维护GrpcServer生命周期的类GrpcServerLifecycle,这个类实现了spring的SmartLifecycle接口,这个接口是用来注册SpringContextShutdownHook的钩子用的,它的实现如下: @Slf4j public class GrpcServerLifecycle implements SmartLifecycle { private static AtomicInteger serverCounter = new AtomicInteger(-1); private volatile Server server; private volatile int phase = Integer.MAX_VALUE; private final GrpcServerFactory factory; public GrpcServerLifecycle(final GrpcServerFactory factory) { this.factory = factory; } @Override public void start() { try { createAndStartGrpcServer(); } catch (final IOException e) { throw new IllegalStateException("Failed to start the grpc server", e); } } @Override public void stop() { stopAndReleaseGrpcServer(); } @Override public void stop(final Runnable callback) { stop(); callback.run(); } @Override public boolean isRunning() { return this.server != null && !this.server.isShutdown(); } @Override public int getPhase() { return this.phase; } @Override public boolean isAutoStartup() { return true; } /** * Creates and starts the grpc server. * * @throws IOException If the server is unable to bind the port. */ protected void createAndStartGrpcServer() throws IOException { final Server localServer = this.server; if (localServer == null) { this.server = this.factory.createServer(); this.server.start(); log.info("gRPC Server started, listening on address: " + this.factory.getAddress() + ", port: " + this.factory.getPort()); // Prevent the JVM from shutting down while the server is running final Thread awaitThread = new Thread(() -> { try { this.server.awaitTermination(); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } }, "grpc-server-container-" + (serverCounter.incrementAndGet())); awaitThread.setDaemon(false); awaitThread.start(); } } /** * Initiates an orderly shutdown of the grpc server and releases the references to the server. This call does not * wait for the server to be completely shut down. */ protected void stopAndReleaseGrpcServer() { final Server localServer = this.server; if (localServer != null) { localServer.shutdown(); this.server = null; log.info("gRPC server shutdown."); } } } 也就是说当spring容器关闭时,会触发ShutdownHook,进而关闭GrpcServer服务,问题就出现在这里,从stopAndReleaseGrpcServer()方法可知,Grpc进行shudown()后,没有进行任何操作,几乎瞬时就返回了,这就导致了进程在收到kill命令时,Grpc的服务会被瞬间回收掉,而不会等待执行中的处理完成,这个判断可以从shutdown()的文档描述中进一步得到确认,如: /** * Initiates an orderly shutdown in which preexisting calls continue but new calls are rejected. * After this call returns, this server has released the listening socket(s) and may be reused by * another server. * * <p>Note that this method will not wait for preexisting calls to finish before returning. * {@link #awaitTermination()} or {@link #awaitTermination(long, TimeUnit)} needs to be called to * wait for existing calls to finish. * * @return {@code this} object * @since 1.0.0 */ public abstract Server shutdown(); 文档指出,调用shutdown()后,不在接收新的请求流量,进行中的请求会继续处理完成,但是请注意,它不会等待现有的调用请求完成,必须使用awaitTermination()方法等待请求完成,也就是说,这里处理关闭的逻辑里,缺少了awaitTermination()等待处理中的请求完成的逻辑。 模拟环境,反复验证 验证方法: 这个场景的问题非常容易验证,只需要在server端模拟业务阻塞耗时长一点,然后kill掉java进程,看程序是否会立刻被kill。正常优雅下线关闭的话,会等待阻塞的时间后进程kill。否则就会出现不管业务阻塞多长时间,进程都会立马kill。 验证定位的bug 先验证下是否如上面所说,不加awaitTermination()时,进程是否立马就死了。直接使用gRPC-spring-boot-starter里自带的demo程序,在server端的方法里加上如下模拟业务执行耗时的代码: @GrpcService public class GrpcServerService extends SimpleGrpc.SimpleImplBase { @Override public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { HelloReply reply = HelloReply._newBuilder_().setMessage("Hello ==> " \+ req.getName()).build(); try { System._err_.println("收到请求,阻塞等待"); TimeUnit._MINUTES_.sleep(1); System._err_.println("阻塞完成,请求结束"); } catch (InterruptedException e) { e.printStackTrace(); } responseObserver.onNext(reply); responseObserver.onCompleted(); } } 上面代码模拟的执行一分钟的方法,然后触发grpc client调用。接着找到server端的进程号,直接kill掉。发现进程确实立马就kill了。继续加大阻塞的时间,从一分钟加大到六分钟,重复测试,还是立马就kill掉了,没有任何的等待。 验证修复后的效果 先将上面的代码修复下,正确的关闭逻辑应该如下,在Grpc发出shutdown指令后,阻塞等待所有请求正常结束,同时,这里阻塞也会夯住主进程不会里面挂掉。 protected void stopAndReleaseGrpcServer() { final Server localServer = this.server; if (localServer != null) { localServer.shutdown(); try { this.server.awaitTermination(); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } this.server = null; log.info("gRPC server shutdown."); } } 同样,如上述步骤验证,当kill掉java进程后,此时java进程并没有立马就被kill,而是被awaitTermination()阻塞住了线程,直到业务方法中模拟的业务阻塞结束后,java进程才被kill掉,这正是我们想要达到的优雅下线关闭的效果。被kill时的,线程堆栈如下: 即使被kill了,还是能打印如下的日志【阻塞完成,请求结束】,进一步验证了修复后确实解决了问题:
前言 动态调整线上日志级别是一个非常常见的场景,借助apollo这种配置中心组件非常容易实现。作为apollo的官方技术支持,博主经常在技术群看到有使用者询问apollo是否可以托管logback的配置文件,毕竟有了配置中心后,消灭所有的本地配置全部交给apollo管理是我们的最终目标。可是,apollo不具备直接托管logback-spring.xml配置文件能力,但是,我们可以基于spring和logback的装载机制,完全取缔logback-spring.xml配置,以apollo中的配置驱动。而且,改造后,大大提高了日志系统的灵活性和可扩展性。 apollo动态日志 何为apollo动态日志?直接这样说可能会有歧义,以为是apollo里的日志,其实不然。举个简单的例子,比如,我们项目很多地方使用了log.debug()打印日志,为了方便通过日志信息排查问题,但是一般情况下,生产环境的日志级别会配置成info。只有遇到需要排查线上问题的时候才会临时打开debug级别日志。这个时候只能需改配置文件,将日志级别调整成debug,然后重新打包部署验证。不仅流程繁琐耗时,还会破坏当时的"案发现场的环境",导致判断不准确。如果应用具备了apollo动态日志这种能力,就只需在apollo修改下配置然后提交,就可以热更新日志级别,马上打印debug级别日志。这就是所谓的apollo动态日志。实现这个效果,需要具备两个能力,分别由spring和apollo提供 spring日志系统热更新日志级别 spring应用中,spring适配了主流的日志框架,如logback、log4j2等,在这些日志框架之上,又抽象了自己的日志系统服务,这里我们用到了spring的LoggingSystem,用它来热更新日志级别,这个类在日志系统初始化时就添加到了spring的容器中,所以只要在spring的上下文管理范围内,就可以直接注入,以下为主要使用到的api描述: /** * 设置给定日志记录器的日志级别. * @param loggerName 要设置的日志记录器的名称({@code null}可用于根日志记录器)。 * @param level 日志级别 */ public void setLogLevel(String loggerName, LogLevel level) { throw new UnsupportedOperationException("Unable to set log level"); } apollo日志配置变更动态下发 apollo作为分布式配置中心,配置集中管理和配置热更新是其最核心的功能,此外,apollo还提供了配置变更下发监听的功能。基于这个配置监听的设计,实现动态日志就变得非常简单了。而且不仅可以实现日志动态热更,基于这个思路,连接池、数据源等都可以轻松实现。apollo实现监听配置变更有多种方式,可以通过Config实例手动添加,如: @ApolloConfig public Config config; public void addConfigChangeListener(){ config.addChangeListener(changeEvent->{ System.out.println("config change keys" + changeEvent.changedKeys()); }); } 也可以通过注解直接驱动 @ApolloConfigChangeListener public void addConfigChangeListener(ConfigChangeEvent changeEvent){ System.out.println("config change keys" + changeEvent.changedKeys()); } 实现日志调整热更新 有了上述能力,在结合spring支持的日志加载配置方式,如: logging.level.org.springframework.web=debug logging.level.org.hibernate=error 可以实现如下代码完成功能,遇到需要调整日志级别时,修改apollo里的配置,即可实时生效 @Configuration public class LogbackConfiguration { private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class); private static final String LOGGER_TAG = "logging.level."; private final LoggingSystem loggingSystem; public LogbackConfiguration(LoggingSystem loggingSystem) { this.loggingSystem = loggingSystem; } @ApolloConfigChangeListener private void onChange(ConfigChangeEvent changeEvent) { for (String key : changeEvent.changedKeys()) { if (this.containsIgnoreCase(key, LOGGER_TAG)) { String strLevel = changeEvent.getChange(key).getNewValue(); LogLevel level = LogLevel.valueOf(strLevel.toUpperCase()); loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level); logger.info("logging changed: {},oldValue:{},newValue:{}", key, changeEvent.getChange(key).getOldValue(), strLevel); } } } private boolean containsIgnoreCase(String str, String searchStr) { if (str == null || searchStr == null) { return false; } int len = searchStr.length(); int max = str.length() - len; for (int i = 0; i <= max; i++) { if (str.regionMatches(true, i, searchStr, 0, len)) { return true; } } return false; } } 消灭logback-spring.xml配置 在"消灭"logback-xml配置之前,先看下这个配置文件有哪些配置信息,起到了哪些作用,下面贴出一个典型的配置文件内容: <?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <appender name="Sentry" class="io.sentry.logback.SentryAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="Sentry"/> </root> <logger name="org.apache.ibatis.session" level="WARN"/> <springProfile name="dev"> <logger name="com.taptap.server" level="DEBUG"/> <logger name="com.taptap.commons" level="DEBUG"/> </springProfile> <springProfile name="prod"> <logger name="com.taptap.server" level="WARN"/> <logger name="com.taptap.commons" level="WARN"/> </springProfile> </configuration> 一个典型的logback配置文件里包含了Appender和日志级别设置的信息,Appender可以理解为日志的输出源。如上贴出的这个配置,添加了两个Appender信息,一个是spring中内置的,将日志输出到控制台的Appender。一个是将error日志信息发送到Sentry应用监控平台的Appender。其他的配置描述了每个包路径不同的日志级别信息。到这里,我们很容易想到,上文已经说过,spring已经支持以logging.level.包名=info这种配置来设置日志系统的日志级别。那么剩下的只要解决Appender的配置就ok了。在这里,其实只需要解决SentryAppender的加载就行,因为consoleAppender spring自己会处理。有了目标和方向,就好办了。以logback-spring.xml配置的信息,最终都会加载成class对象。就和spring.xml配置一样。所以研究的方向就变成了Logback的加载原理的问题。 Logback加载原理 在java的日志生态里,除了响当当的logback、log4j2、apache common log外,还有一个日志框架不得不提,就是sl4j。正因为java生态强大,日志框架层出不穷,所以sl4j出来了,不干实事,专门定义日志标准、规范定义接口。而且,在我们平时的编码过程中,也建议使用sl4j的api,这样,无论底层日志框架实现怎么切换,都不会影响。主流的日志框架都有实现sl4j的接口,spring中日志系统的加载也是面向的sl4j,而不是直接面向日志实现,加载过程是一个自动化的过程,系统会自动扫描实现了sl4j的接口实现,如: public interface ILoggerFactory { public Logger getLogger(String name); } 每个日志框架都会实现这个接口,如Logback中的LoggerContext。Logback所有的功能都集成在了这个Context中,logback-spring.xml的配置也是为了配置LoggerContext中的属性信息,所有我们只要拿到了LoggerContext实例,问题就解决了一大半。这涉及到sl4j的另一个接口,获取ILoggerFactory实例的接口: public interface LoggerFactoryBinder { public ILoggerFactory getLoggerFactory(); public String getLoggerFactoryClassStr(); } Logback的实现类为StaticLoggerBinder,也就是说,我们可以通过StaticLoggerBinder的getLoggerFactory方法拿到LoggerContext实例了。 javaBean加载SentryAppender 拿到Logback的LoggerContext后,就好办了,见代码: @Configuration public class LogbackConfiguration { private final LoggerContext ctx = (LoggerContext) StaticLoggerBinder.getSingleton().getLoggerFactory(); @Bean @Profile(PROD_ENV) public void initSenTry() { SentryAppender sentryAppender = new SentryAppender(); sentryAppender.setContext(ctx); ThresholdFilter filter = new ThresholdFilter(); filter.setLevel(Level.ERROR.levelStr); filter.start(); sentryAppender.addFilter(filter); sentryAppender.start(); ctx.addTurboFilter(new TurboFilter() { @Override public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format, Object[] params, Throwable t) { logger.addAppender(sentryAppender); return FilterReply.NEUTRAL; } }); } } 看到这种代码就非常有感觉了,配置文件中的xml其实就是描述了日志组成对象以及对象的属性。在使用java bean的方式配置时需要注意,Logback的设计里,每个日志系统组成实例都有一个start状态属性,上面的start()方法其实不是动作,只是标记了这个属性为true。而在xml里这个属性只要配置了就自动激活为true了,这里必须显示的start()一下。解决了日志级别配置和Appender配置后,Logback-spring.xml文件就可以彻底的删除了
前言 taptap-developer是一个spring boot框架驱动的纯Grpc服务,所以,只用了四步,移除了web和spring cloud相关的模块后,启动速度就稳稳的保持在了6s内。除了启动速度提升外,在服务待机状态下,内存锐减了50%左右,从500M左右的内存占用,缩减到了250M不到。 分析日志 日志是一个应用的门面,在未深入了解一个应用的架构前,通过启动的日志输出基本可以分析出这个应用的大概的技术构成。在分析日志之前,在强调一点,这个应用是一个纯Grpc的服务。如上图贴出的日志,是未优化前的系统日志输出,从上到下有四个红色箭头指向,是本次日志分析的关键信息,下面就这四个关键信息,分别分析下。然后总结出常见的优化方法 优化点一:关于Spring Data repository scanning Spring Data repository是一个高度抽象的数据访问层接口,常见的实现有redis、jdbc、jpa、MongoDB、elasticsearch等等。实现一个Spring-data-xxx包,需要实现org.springframework.data.repository.core.support.RepositoryFactorySupport抽象类,然后在!/META-INF/spring.factories文件中定义好实现类。spring容器启动时,会扫描加载factories的信息。如果一个项目里有被扫描到有多个spring-data-xxx的实现,启动时日志就会打印Multiple Spring Data modules found, entering strict repository configuration mode! 优化:看到这个日志,我们就需要检查下项目中是否用到了这些功能,比如引入了spring-data-redis,其实只用到了其携带的jedis,而且jedis实例可能还是自己实例化的,这个时候就可以禁用repository的功能。参考配置如下:spring.data.redis.repositories.enabled=false Spring Data repository有三种内置的初始化模式,分别对应如下: DEFAULT:和Spring其他Bean一样,在容器上下文加载时就初始化 DEFERRED:惰性加载,容器上下文启动完成后开始初始化 LAZY:惰性加载,并且延迟注入,容器上下文启动完成接收第一个请求时开始初始化如日志输出:Bootstrapping Spring Data repositories in DEFAULT mode,默认是随容器启动就开始初始化的, 优化:这里可以根据业务特点,选择延迟加载,参考配置spring.data.jpa.repositories.bootstrap-mode=lazy Spring Data repository会扫描项目中的实现了repository接口的类,默认情况下会盲扫所有的jar包,日志输出:Finished Spring Data repository scanning in 148ms. Found 0 repository interfaces.打印出了扫描repository接口的耗时情况。 优化:这里可以通过@EnableRedisRepositories(basePackages = "com.taptap")指定扫描的路径,可以显著提升扫描加载的速度 优化点二:关于WebApplicationContext 在spring中,WebApplicationContext是ApplicationContext的增强,由spring-web-mvc实现,增加了servlet、session等web相关的内容。从日志Initializing Spring embedded WebApplicationContext可以看出,我们初始化了一个web容器,而纯Grpc服务用不到Web的容器上下文,所以移除如下依赖即可 优化:移除implementation('org.springframework.boot:spring-boot-starter-web') 优化点三:关于servlet容器 spring-web-mvc是基于java web标准servlet设计架构的。而servlet是由servlet容器来驱动的,常见的servlet有tocmat、jetty、undertow等。从日志中可看出,我们启动了一个8081的servlet容器。这个不应该出现在纯Grpc的服务中,所以,直接移除即可。 优化:移除implementation 'org.springframework.boot:spring-boot-starter-undertow' 优化点四:关于archaius配置组件 从最后一个箭头指向的日志信息可以分析出,项目引入了archaius配置加载组件,所以项目在启动时,archaius会尝试去加载默认策略的配置源。而我们整体的技术栈,配置中心统一采用了apollo,所以可以直接移除,最后通过分析定位,archaius不是单独引入的,是随着spring-cloud-starter-netflix-hystrix一同引入,这个组件是spring-cloud-netflix微服务框架最常用的,但是在这边,目前所有的微服务都是直接注册到k8s容器的,所有服务的熔断、限流、负载均衡都下沉到了容器基数设施平台,所以应用层面虽然引入了这个包,其实没有实际作用,所以最后移除spring cloud相关组件 优化:移除implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'和 implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'组件、 附优化后的日志输出: 系统资源的变化 优化前的优化后的最后,基于资源监控图,从三个维度总结下,优化后的资源占用情况: 资源名称 优化前 优化后 内存 500M左右 250M左右 总线程数 107 78 装载类 12922 10041
2020年12月
2020年11月