最近在整理之前记录的工作笔记时,看到之前给团队内一组服务优化启动耗时记录的笔记,简单整理了一下分享出来。问题原因并不复杂,主要是如何精准测量和分析,优化后如何定量测量优化效果,说人话就是用实际数据证明优化效果。

背景

团队内有一组服务启动明显较其它服务要慢(线上启动超过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中学习一下如何使用。

总结

  1. 通过对启动过程进行profiling定位到消耗大的方法,结合源代码理解相关逻辑,找到可行的优化点,代码优化后再对比前后测量结果验证。
  2. 通过JMH等微基准测试工具对单个方法进行基准测试,形成容易被复现的测试结果的数据。
  3. 找到可以帮助review方案和提供建议的人,文章的例子是找到了Apollo社区成员同时也是Apollo作者之一,通过讨论最终形成的方案相较最初的“补丁式”优化更加优雅和合理。

本文最先发表于: https://aops.io/article/improve-bootstrap.html

为了解决服务启动慢的问题,我为什么要给Apollo和Spring提交PR?的更多相关文章

  1. (转)解决:本地计算机 上的 OracleOraDb10g_home1TNSListener服务启动后停止

    原文地址:http://justsee.iteye.com/blog/1320059 手动启动一个问题:本地计算机 上的 OracleOraDb10g_home1TNSListener服务启动后停止. ...

  2. 替换系统数据库解决SQLSERVER服务启动不了的问题

    替换系统数据库解决SQLSERVER服务启动不了的问题 当遇到SQLSERVER服务启动不起来的时候,我们试过把系统的四个数据库master ,model ,tempdb,msdb 替换掉,Windo ...

  3. paip.花生壳 服务启动失败 以及不能安装服务,权限失败的解决

    paip.花生壳 服务启动失败 以及不能安装服务,权限失败的解决 系统win7 NewPhDDNS_1.0.0.30166.exe  作者Attilax  艾龙,  EMAIL:1466519819@ ...

  4. 安装ArcGIS License 10.1 许可管理器 破解版 服务启动又失败的解决办法

    安装破解文件的提示执行 替换许可管理器Bin下面的service.txt  文件,之后会发现,许可管理器启动不了(有时候又可以,挺郁闷), 经过多次的试验,我找到了一种折中解决的方法,供大家参考 解决 ...

  5. Win2008服务启动不能调用Office Word的解决方法

    本文为大家分享一下如何解决Windows Server 2008 服务启动不能调用Office Word的问题,分享这个教程的原因是,今天在Windows server2008上部署一个应用时发现了一 ...

  6. win2003 HookPort 服务启动失败的解决办法!

    Win2003系统每次开机启动时都弹出个对话框报HookPort 服务启动失败,很多网友都遇到同类问题,问题根源是360安全卫士引起的,官方一直没有给出解决方案 问题描述:Win2003系统每次开机启 ...

  7. 破解windows下MySQL服务启动不了的情况下不能对其进行全然卸载的解决方式

    下面的文章主要介绍的是在MySQL服务启动不了的情况下,不能对其进行全然卸载的实际解决的方法的描写叙述,下面就是对解决MySQL服务启动不了的情况下详细方案的描写叙述,希望在你今后的学习中会对你有所帮 ...

  8. Windows 共享无线上网 无法启动ICS服务解决方法(WIN7 ICS服务启动后停止)

    Windows 共享无线上网 无法启动ICS服务解决方法(WIN7 ICS服务启动后停止) ICS 即Internet Connection Sharing,internet连接共享,可以使局域网上其 ...

  9. mysql57重新安装后无法再次启动mysql57服务“本地计算机上的MySQL服务启动后停止。某些服务在未由其他服务或程序使用时将自动。”--解决方法

    本地计算机上的MySQL服务启动后停止.某些服务在未由其他服务或程序使用时将自动. (win10,mysql5.7+) 解决方法: 第一步:查看MySQL57安装路径 只要在programData路径 ...

  10. Mysql安装后在服务里找不到和服务启动不起来的解决方法

    一,在安装完Mysql数据库后,发现在控制面板->管理->服务中找不到Mysql的服务启动 解决方法如下:开启命令行,按照如下步骤即可: 1.进入到mysql的安装包,在bin里执行:my ...

随机推荐

  1. 基于 Github 平台的 .NET 开源项目模板 - ISSUE 相关

    上一篇已经介绍了工程和脚本.下面我们将以具体的功能作为着手点, 展开对 project.yml 工程配置文件的讲解. 赘述 project.yml 使用周期 文件来源: 运行跟目录下的 scanner ...

  2. 推荐手绘工具神器Excalidraw素描草图风格白板,支持AI-开源免费

    推荐手绘工具神器Excalidraw素描草图风格白板,支持AI-开源免费 原创 IT软件部落 IT软件部落 Excalidraw 一个开源的虚拟手绘风格的白板,是一个很好的素描工具.它真的很容易使用, ...

  3. 多校A层冲刺NOIP2024模拟赛08 排列

    多校A层冲刺NOIP2024模拟赛08 排列 一种连续段 dp 的解法. 题面 小 Y 最近在研究组合数学,他学会了如何枚举排列. 小 Z 最近在研究数论,他学会了求最大公约数. 于是小 Y 和小 Z ...

  4. 一键生成美观的彩页演示+AI的训练过程科普

    一键生成美观彩页 + AI训练揭秘:让你的内容瞬间高大上! 阅读时间: 8分钟 | 字数: 1300+ 你是否曾为制作精美的演示文稿而烦恼?是否对AI的训练过程充满好奇?今天,让我们一起探索如何用AI ...

  5. mysql与mongodb、redis的性能对比,包含源代码

    最近因为工作需要,所以研究了一下mongodb,但是不知道它的真实性能怎么样,所以对比mysql做了一些测试,现在只是插入操作,更新还没来得及弄,贴出来和大家分享一下(已经补齐redis部分) 1.数 ...

  6. Swing 线程之SwingUtilities.invokeLater()

    现在我们要做一个简单的界面. 包括一个进度条.一个输入框.开始和停止按钮. 需要实现的功能是: 当点击开始按钮,则更新进度条,并且在输入框内把完成的百分比输出(这里只做例子,没有真正去做某个工作). ...

  7. python的orjson

    简介 首先我们先来了解下orjson的优缺点: 可以将datetime.date和time实例序列化为RFC 3339格式,例如:"2022-06-12T00:00:00+00:00&quo ...

  8. golang不同版本特性

    Go 1.0[1]  - 2012 年 3 月: 随着 Go 第一个版本发布的还有一份兼容性说明文档[2] .该文档承诺,Go 的未来版本会尽可能确保向后兼容性,不会破坏现有程序. For insta ...

  9. 简单端口映射、转发、重定向工具之Rinetd

    ◆一.概述 Rinetd是为在一个Unix和Linux操作系统中为重定向传输控制协议(TCP)连接的一个工具.将 TCP 连接从一个 IP 地址和端口重定向到另一个.它处理文件中/etc/rinetd ...

  10. docker-compose之配置docker-compose.yml

    当前有三种版本的Compose配置文件格式: Version 1 旧版格式,通过省略YAML的根配置项version来指定. 未声明版本的Compose配置文件都被视为V1版,所有的服务都作为根选项在 ...