公司有项目是基于 Scala 编写的,与之配套的构建工具是 SBT , 它是 Simple Build Tool 的缩写,虽然我以为它一点也不简单。html
这个项目有一个很大的痛点就是刷新依赖 (对应 SBT 的 update)很是之耗时,能够参见下图:java
注意图中红框部分,耗时1266秒,近半个小时。在刷新期间资源占用也很高,致使电脑很卡 (风扇还呼呼呼的转,温度蹭蹭蹭的长)。git
最关键的是因为依赖的不少服务升级很快 (几乎天天都有升级),因此这个操做天天也会持续不少次,不可思议耗费在这方面的时间是何其之多。github
人生苦短,在刷新了几回以后,我再也受不了这漫长的等待时间,因而开始了这漫漫的优化之路。shell
正所谓工欲善其事必先利其器apache
不知道你们遇见这种问题会怎么作,我反正是二话不说打开 Google 直接搜: SBT 依赖下载慢。缓存
还别说,有共鸣的人还很多, 总结了下几乎都是如下的解决方案网络
- 添加代理
- 添加国内镜像源
我这确定不是源的问题啊,我司用的私有仓库,既然私有jar都下载下来了,确定是走的私有仓库啊。app
翻了几页,没有满意的答案,也试了几个方案,也没啥用。maven
看来仍是得本身从问题的根源开始找起啊......
为了保险起见, 我仍是先排查一下是否是镜像问题, 项目的 build.sbt
配置文件中是有私有仓库的相关配置项的:
lazy val commonSettings = Seq(
//....
// ... 私有仓库
resolvers := {Resolver.url("xr-ivy-releasez", new URL("http://nexus.xxxx.com/repository/ivy-releases/"))(Resolver.ivyStylePatterns) +: resolvers.value},
resolvers := { {"xr-maven-public" at "http://nexus.xxxx.com/repository/public/"} +: resolvers.value},
// ....
)
复制代码
此时我突然想到一种状况:难道是默认走的公共库,在公共库找不到依赖才会走私有库 ?
为了验证猜测,我使用 wireshark 抓包进行分析,过滤器指定协议 http (由于仓库是走的http)
还能够指定 ip.src 和 ip.dst 从而使得数据包更加符合咱们的要求
而后打开 sbt shell 进行 update 操做
观察抓包结果
发现访问的都是 /repository/public/***
的请求,对应的 Host 也是我司的私有库,这说明配置是生效了的,并且都是从私有仓库进行下载。
可是我也发现了一些404的请求
好吧,是我司的私有库没有 /repository/ivy-release
,果断将对应的仓库配置去掉,省去不必的请求。
虽然走了私有库,可是我每次刷新都会请求仓库,这就不符合道理了,难道 SBT 连基本的依赖缓存都没有 ?
对抓到的数据包进行再次过滤,只看 Http Request
,发现请求的都是 SNAPSHOT
版本的依赖库, 参见下图
这说明 SBT 是有缓存的,由于正式版都没有请求仓库,但是为何 SNAPSHOT
每次都去请求远程仓库呢? 难道是 SNAPSHOT
被区别对待,不会被缓存?
既然 SBT 是基于 Ivy 的,那就从 Ivy下手。
我在Ivy 的官网(ant.apache.org/ivy/history…缓存的表格:
Attribute | Description | Required |
---|---|---|
default | the name of the default cache to use on all resolvers not defining the cache instance to use | No, defaults to a default cache manager instance named 'default-cache' |
defaultCacheDir | a path to a directory to use as default basedir for both resolution and repository cache(s) | No, defaults to .ivy2/cache in the user's home directory |
resolutionCacheDir | the path of the directory to use for all resolution cache data | No, defaults to defaultCacheDir |
repositoryCacheDir | the path of the default directory to use for repository cache data. This should not point to a directory used as a repository! | No, defaults to defaultCacheDir |
注意关键字 defaultCacheDir
, 这个就是 Ivy 的缓存目录,对应路径为用户目录下的 .ivy2/cache
。
个人是 mac, 对应目录就是 ~/.ivy/cache
, 果不其然,进入该目录查看一下:
在 ~/.ivy/cache
下发现了不少依赖库的目录, 下面就须要验证一下有没有缓存 SNAPSHOT
的版本了, 以我司的 user-client 4.1.2-SNAPSHOT
为目标进行查找:
从图中显示,目录中明明有缓存 SNAPSHOT
的啊,可为何不走本地缓存呢 ?
这没办法了,只能去 SBT 官网找答案了,在官网文档找到了 Dependency Management ,看名字彷佛和依赖管理有关,
其中的 Cached-Resolution 彷佛和缓存相关, 并且开头就是下面这段话
To set up Cached Resolution include the following setting in your project’s build:
updateOptions := updateOptions.value.withCachedResolution(true)
说的是要配置缓存解析,那就得加上 updateOptions := updateOptions.value.withCachedResolution(true)
的配置, 这也太简单了吧?
无论啦,先加上试试。
加配置,刷新,抓包一鼓作气, 然而结果惨不忍睹
看着一页页的请求发出去,此刻我是奔溃的!贼子安敢欺我!
正在我想静静之际,SBT 刷新完成,我一不当心瞄了一眼,耗时竟然只有之前的1/4 了?
我靠,怎么肥四(回事)?不是没生效吗,怎么时间缩短了这么多?
为了确保不是眼花,我又重启刷新了几回,发现耗时相差无几,并且我发现若是不重启直接update,通常耗时都只有几秒,个人天啦。
不死心的我又去看了下文档,原来是我对这个配置理解错了,这个配置的意思并非说 SNAPSHOT
就不请求远程仓库了。
这里的缓存指的是sbt启动后第一次执行update后,会缓存全部的依赖解析信息, 也就是说缓存是和进程相关的。
而个人项目是有4个子项目,每一个子项目都共同依赖了 service
模块, 该模块维护着几乎全部的依赖。
当第一个项目 update 后,其余三个项目 update 时都会直接走缓存了,这也是为何耗时只有最开始1/4。
真实无意插柳柳成荫啊......
虽然如今时间只要之前的1/4了,可仍是要5分钟啊,这绝对不是一个能够将就的数字!
并且还有另一个很是重要的缘由,由于穷!
此话怎讲?由于 SBT 一直启动着太耗内存了,我这可怜的 8G 可得省着点儿。但是停掉 SBT,缓存就得从新构建了,因此是穷激发了个人进一步探索......
再次思考一下:为何 SNAPSHOT
依赖每次启动都要去远程仓库拉取呢 ? 能不能只在依赖的版本有更新的时候再去拉取呢 ?
在文档 Cached-Resolution中, 发现了关键词 SNAPSHOT and dynamic dependencies,其中对 SNAPSHOTR
和缓存作了一些描述:
When a minigraph contains either a SNAPSHOT or dynamic dependency, the graph is considered dynamic, and it will be invalidated after a single task execution. Therefore, if you have any SNAPSHOT in your graph, your experience may degrade.
说的是依赖关系中若是有 SNAPSHOT
版本,会致使某个子依赖关系缓存失效, 而这个子依赖就是动态的,反正就是不会走缓存的意思。
既然得知问题的根源是由于使用了 SNAPSHOT
, 若是不使用 SNAPSHO
不就没这个问题了嘛。
然而现实是骨感的,公司内部几十个服务大多数都用的 SNAPSHOT
做为版本号,并且各类互相依赖,短期内是不可能直接过渡的了,因此直接PASS该方案了。
只能继续在文档中摸索,发现一个相关配置
updateOptions := updateOptions.value.withLatestSnapshots(false)
这个配置的做用是什么呢?
由于 SBT 能够配置多个远程仓库源(经过 Resolver),默认状况下 SBT 会从全部的远程仓库去拉取指定版本的 SNAPSHOT
依赖, 而后比对它们的发布时间,取最新的那一个。
经过配置 withLatestSnapshots(false)
能够禁用该策略, 这样 SBT 就直接使用从远程仓库拉取到的第一个 SNAPSHOT
依赖。
加上配置而后测试,发现网络请求数确实少了,总体update耗时减小了一分钟左右,可是这个会致使没法拉取到同版本的最新SNAPSHOT
由于快照在不改变版本的状况下是能够重复发布的,区分同版本不一样快照就只能按照时间戳来了。
SBT 没法肯定本地的快照是最新的,因此每次启动都会去仓库拉取最新快照。
使用 withLatestSnapshots(false) 后就不会取最新的,而是直接取第一个。
不取最新的 SNAPSHOT
对咱们影响不大, 由于咱们内部的服务若是有改动,基本就会升级版本号(就算是 SNAPSHOT
), 不多有一直重复发同版本的SNAPSHOT
的状况。
这么一说,彷佛咱们连用
SNAPSHOT
的意义都不大了,然而历史缘由......
虽然有所提高,可是最关键的问题,SNAPSHOT
每次 update 都会走网络请求的问题仍是没解决。
只能继续在文档中挣扎,还好黄天不负有心人啊, 在官方文档 Cache And Configuration 一节找到了相关内容
When
offline := true
, remote SNAPSHOTs will not be updated by a resolution, even an explicitly requested update. This should effectively support working without a connection to remote repositories. Reproducible examples demonstrating otherwise are appreciated. Obviously, update must have successfully run before going offline.
文档说若是配置了offline := true
, 是不会从远程仓库更新 SNAPSHOT
的依赖了,这不正是咱们要的东西吗?
可是后面又说了,更新必须在进入离线模式以前就完成,这句话的意思是否是离线模式下我连版本升级也作不到呢?
只有本身动手了才知道,在不升级版本的状况下,加上配置再次进行 update 并抓包, 没有任何的请求到达仓库了
再来看看最终的更新时间
只须要一分钟不到,此刻我得先压制心里的狂喜,再验证一下在 offline := true
的状况下,升级版本是否会从远程仓库请求?
随意修改了一个库的版本,而后重启 sbt 执行 update, 发现是成功从远程仓库拉取到了的,哈哈,一切都不是问题!
意外老是伴随着惊喜同时到来,在我随后的使用中却又发现了另外的问题:若是 SBT 的第一次update完成之后, 我随后修改依赖的版本,在不重启SBT的状况下再次执行update,是读不到最新的依赖版本的。
初步猜想是和缓存有关系的,可是问题也不大了,就算更新依赖版本而后重启 SBT 进行 update, 耗时也不过1分钟左右 ,比最开始的半小时已经好多了。
要不,我把这个问题留给大家了?
最后从30分钟到1分钟实际上就是在 build.sbt
加了两行配置
offline := true,
updateOptions := updateOptions.value.withCachedResolution(true).withLatestSnapshots(false)
复制代码
整个分析问题的思路也很简单,就是先找到问题根源,再去找解决方案。
在寻找解决方案的时候通常都是搜索引擎,文档或者源码,正常状况下文档应该都能解决问题了,这期间我就绕了很多弯路,我甚至曾去看了 SBT 的 Resolver 的源码, 如今看来,绝对是跑偏了。
整个解决过程并无多么高深莫测甚至能够说是无聊至极,由于大部分时间都是看文档并验证其配置。
不过仍是那句话:工欲善其事必先利其器