为了解决服务启动慢的问题,我为什么要给Apollo和Spring提交PR?
最近在整理之前记录的工作笔记时,看到之前给团队内一组服务优化启动耗时记录的笔记,简单整理了一下分享出来。问题原因并不复杂,主要是如何精准测量和分析,优化后如何定量测量优化效果,说人话就是用实际数据证明优化效果。
背景
团队内有一组服务启动明显较其它服务要慢(线上启动超过2分钟),偶尔还会超过容器liveness的最大超时时间导致服务启动到一半又被kill掉重新启动,由于服务没有启动完成是不会接入流量的,影响最明显的是日常变更维护时等待服务滚动部署的时间较长,另外服务在本地启动也比较慢,浪费程序员的时间。
当时我并不是该服务的开发人员,相关的业务逻辑了解不多,据相关同学反映,该服务在Apollo(一个开源的应用配置管理中心)上有大量配置,启动时需要加载这些配置,随着配置逐渐增多,启动时间也越来越慢。我去Apollo上统计了一下,大约有5000行properties配置。这5000行配置加载到Spring容器的配置bean需要分钟级?听起来就不大合理。
分析过程
配置项大致长下面这个样子,层级说多不算多,说少也不算少。
xxxx.dp.caption.converter[0].convertRules[0].castTo=BYTESTOSTRING
xxxx.dp.caption.converter[0].convertRules[0].sourceField=city_id
xxxx.dp.caption.converter[0].convertRules[0].targetField=cityId
xxxx.dp.caption.converter[0].convertRules[1].castTo=BYTESTOSTRING
xxxx.dp.caption.converter[0].convertRules[1].sourceField=city_name
xxxx.dp.caption.converter[0].convertRules[1].targetField=cityName
由于启动时间很长,有充足的时间执行jstack抓取threaddump,main线程如下:
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at java.util.HashMap.resize(HashMap.java:735)
at java.util.HashMap.putVal(HashMap.java:663)
at java.util.HashMap.put(HashMap.java:612)
at com.ctrip.framework.apollo.internals.DefaultConfig.stringPropertyNames(DefaultConfig.java:115)
at com.ctrip.framework.apollo.internals.DefaultConfig.getPropertyNames(DefaultConfig.java:105)
at com.ctrip.framework.apollo.spring.config.ConfigPropertySource.getPropertyNames(ConfigPropertySource.java:24)
at org.springframework.core.env.CompositePropertySource.getPropertyNames(CompositePropertySource.java:87)
at org.springframework.boot.context.properties.source.SpringIterableConfigurationPropertySource$CacheKey.get(SpringIterableConfigurationPropertySource.java:214)
at org.springframework.boot.context.properties.source.SpringIterableConfigurationPropertySource.getCache(SpringIterableConfigurationPropertySource.java:134)
...
由于stacktrace过长,这里就不贴完整的,感兴趣的读者可以查看我在Apollo github仓库提交的issue分析。
看上面的stacktrace,可以看到Apollo内部的一个方法调用DefaultConfig.getPropertyNames在不断向一个HashMap中填充数据致使其扩容。通过静态的stacktrace,我们能知道的信息也就仅限于此了,我们还不知道这段看起来消耗比较大的逻辑在整个启动过程的占比大概是多少,因此这里就需要用到Profiler工具了,当时我在本地用的是JProfiler,一款商业的Profiler工具。今天我们完全可以用开源免费的async-profiler替代。通过对启动过程进行profiling,得到下面结果:

有了这样自顶向下的树状分析结果,消耗最大的方法就很明显了,这里是ConfigPropertySource#getPropertyNames,到这里就需要结合Apollo的源代码进行分析了,最终定位到上面方法在启动过程中会在Binder.bind中调用很多次,这个方法的底层实现又是每次构造一个全新的数组,时间复杂度和加载的配置数量呈线性关系。在配置较多的场景下,消耗就非常明显了。由于这里的配置发生变化时,Apollo的客户端是可以感知到的,因此我们完全可以替换成使用缓存,配置变化后清理掉缓存的方式。因此基于这个思路,给Apollo提了PR:
测试结果显示在配置加载阶段的耗时,从平均2分钟优化到不到1秒。
在对大量配置场景下的启动测试profiling过程中,同时也发现了Spring framework的一处优化点,上面Apollo的热点方法优化之后,通过profile观测到的下一个热点方法就是CompositePropertySource#getPropertyNames了,CompositePropertySource顾名思义是对多个PropertySource的Composite模式。
在Spring这种超级流行的项目中,哪怕是启动过程可以优化1秒,全世界大量项目可以从中收益,总量还是相当可观的。因此抱着这样的心态,给Spring也提交了PR:
由于涉及到spring-core模块的变动,并且这个方法调用在启动过程中也属于hot code path,因此是需要编写相应的微基准测试的。Spring使用openjdk衍生出的Java Microbenchmark Harness (JMH)工具做微基准测试。
通过前后对比维基准测试的结果,形成实实在在的数据验证代码优化后的效果,上面的调整最终大约有-30%的执行时间缩减。
关于JMH,可以通过视频和它的代码仓中自带的samples中学习一下如何使用。
总结
- 通过对启动过程进行profiling定位到消耗大的方法,结合源代码理解相关逻辑,找到可行的优化点,代码优化后再对比前后测量结果验证。
- 通过JMH等微基准测试工具对单个方法进行基准测试,形成容易被复现的测试结果的数据。
- 找到可以帮助review方案和提供建议的人,文章的例子是找到了Apollo社区成员同时也是Apollo作者之一,通过讨论最终形成的方案相较最初的“补丁式”优化更加优雅和合理。
为了解决服务启动慢的问题,我为什么要给Apollo和Spring提交PR?的更多相关文章
- (转)解决:本地计算机 上的 OracleOraDb10g_home1TNSListener服务启动后停止
原文地址:http://justsee.iteye.com/blog/1320059 手动启动一个问题:本地计算机 上的 OracleOraDb10g_home1TNSListener服务启动后停止. ...
- 替换系统数据库解决SQLSERVER服务启动不了的问题
替换系统数据库解决SQLSERVER服务启动不了的问题 当遇到SQLSERVER服务启动不起来的时候,我们试过把系统的四个数据库master ,model ,tempdb,msdb 替换掉,Windo ...
- paip.花生壳 服务启动失败 以及不能安装服务,权限失败的解决
paip.花生壳 服务启动失败 以及不能安装服务,权限失败的解决 系统win7 NewPhDDNS_1.0.0.30166.exe 作者Attilax 艾龙, EMAIL:1466519819@ ...
- 安装ArcGIS License 10.1 许可管理器 破解版 服务启动又失败的解决办法
安装破解文件的提示执行 替换许可管理器Bin下面的service.txt 文件,之后会发现,许可管理器启动不了(有时候又可以,挺郁闷), 经过多次的试验,我找到了一种折中解决的方法,供大家参考 解决 ...
- Win2008服务启动不能调用Office Word的解决方法
本文为大家分享一下如何解决Windows Server 2008 服务启动不能调用Office Word的问题,分享这个教程的原因是,今天在Windows server2008上部署一个应用时发现了一 ...
- win2003 HookPort 服务启动失败的解决办法!
Win2003系统每次开机启动时都弹出个对话框报HookPort 服务启动失败,很多网友都遇到同类问题,问题根源是360安全卫士引起的,官方一直没有给出解决方案 问题描述:Win2003系统每次开机启 ...
- 破解windows下MySQL服务启动不了的情况下不能对其进行全然卸载的解决方式
下面的文章主要介绍的是在MySQL服务启动不了的情况下,不能对其进行全然卸载的实际解决的方法的描写叙述,下面就是对解决MySQL服务启动不了的情况下详细方案的描写叙述,希望在你今后的学习中会对你有所帮 ...
- Windows 共享无线上网 无法启动ICS服务解决方法(WIN7 ICS服务启动后停止)
Windows 共享无线上网 无法启动ICS服务解决方法(WIN7 ICS服务启动后停止) ICS 即Internet Connection Sharing,internet连接共享,可以使局域网上其 ...
- mysql57重新安装后无法再次启动mysql57服务“本地计算机上的MySQL服务启动后停止。某些服务在未由其他服务或程序使用时将自动。”--解决方法
本地计算机上的MySQL服务启动后停止.某些服务在未由其他服务或程序使用时将自动. (win10,mysql5.7+) 解决方法: 第一步:查看MySQL57安装路径 只要在programData路径 ...
- Mysql安装后在服务里找不到和服务启动不起来的解决方法
一,在安装完Mysql数据库后,发现在控制面板->管理->服务中找不到Mysql的服务启动 解决方法如下:开启命令行,按照如下步骤即可: 1.进入到mysql的安装包,在bin里执行:my ...
随机推荐
- Linux基础-查看和设置环境变量
一,查看环境变量 二,环境变量类型 三,设置环境变量 四,参考资料 一,查看环境变量 在 Linux中,环境变量是一个很重要的概念.环境变量可以由系统.用户.Shell 以及其他程序来设定,其是保存在 ...
- Rust 版本一直是 1.4 或者其它版本
Rust 版本一直是 1.4 或者其它版本 通过rustup update 升级或者 rustup default 设置版本也不行 解决方法 删除 rust-toolchain 这个东西,这个东西覆盖 ...
- STM32F103RCT6搭配“ST_LINK V2 √RoHS 'A 2023 04'”在CubeIDE中下载程序到单片机
一.请参考本站大佬文章进行接线: ST_LINK V2接口和连接方式 二.步骤: 到此,大功告成. 小手点赞,水逆退散!!!
- 【Flink 日常踩坑】Could not find ExecutorFactory in classpath
Description 一段简单的 FlinkSQL 程序,在 IDE 中运行没问题,但是 maven 打包后发布到终端启动却报错了. import org.apache.flink.configur ...
- JDBC中驱动加载的过程分析
江苏 无锡 缪小东 本篇从java.sql.Driver接口.java.sql.DriveManager类以及其它开源数据库的驱动类讨论JDBC中驱动加载的全过程以及JDBC的Framework如何做 ...
- pycharm之激活
激活相关文件: https://github.com/lanlangdeai/develop-kit/tree/master/software/editor/pycharm 一. 激活码激活 步骤: ...
- ORACLE本地磁盘备份恢复
1.部署新备份磁盘(源和目标) [oracle@cmxdb /ora_bak]$ mkdir -p /oracle/rmanback [oracle@cmxdb /ora_bak]$ chown -R ...
- C#中 自定义验证规则ValidationAttribute的使用
C#中 自定义验证规则ValidationAttribute的使用 迷恋自留地 进行接口请求的时候难免会对请求字段进行验证,验证对象的所有字段的值是否合乎要求,如进行非空检测,长度检测等等. Requ ...
- 以下哪一项是对CSMA/CA和CSMA/CD LAN控制通用的CSMA方法的适当描述?
A. 检测载波信号并控制数据传输. B. 获得具有传输权的消息(令牌)的终端传输数据. C. 如果在数据传输过程中发生冲突,立即重新发送. D. 即使在使用传输线时也可以传输数据. = ...
- 当年的毕设-cpf (一个简易的协议 fuzzer)
整理文件时发现了这个,看是否有人需要... https://github.com/hac425xxx/cpf/ cpf 一个简单的协议Fuzz工具. 毕设答辩胶片&演示视频 https://g ...