從 30 分鐘到 1 分鐘 - 一個 Scala 項目的編譯速度優化
摘要:因爲 SBT 可以配置多個遠程倉庫源(通過 Resolver),默認情況下 SBT 會從所有的遠程倉庫去拉取指定版本的 SNAPSHOT 依賴, 然後比對它們的發佈時間,取最新的那一個。雖然走了私有庫,但是我每次刷新都會請求倉庫,這就不符合道理了,難道 SBT 連基本的依賴緩存都沒有。
作者 | 田偉然
回首向來蕭瑟處,歸去,也無風雨也無晴。
杏仁工程師,關注編碼和詩詞。
前言
公司有項目是基於 Scala 編寫的,與之配套的構建工具是 SBT , 它是 Simple Build Tool 的縮寫,雖然我覺得它一點也不簡單。
這個項目有一個很大的痛點就是刷新依賴 (對應 SBT 的 update)非常之耗時,可以參見下圖:
注意圖中紅框部分,耗時1266秒,近半個小時。在刷新期間資源佔用也很高,導致電腦很卡 (風扇還呼呼呼的轉,溫度蹭蹭蹭的長)。
最關鍵的是由於依賴的很多服務升級很快 (幾乎每天都有升級),所以這個操作每天也會持續很多次,難以想象耗費在這方面的時間是何其之多。
人生苦短,在刷新了幾次之後,我再也受不了這漫長的等待時間,於是開始了這漫漫的優化之路。
正所謂工欲善其事必先利其器
Round 1: 十八般武藝齊上陣
不知道大家碰見這種問題會怎麼做,我反正是二話不說打開 Google 直接搜: SBT 依賴下載慢 。
還別說,有共鳴的人還不少, 總結了下幾乎都是以下的解決方案
-
添加代理
-
添加國內鏡像源
我這肯定不是源的問題啊,我司用的私有倉庫,既然私有jar都下載下來了,肯定是走的私有倉庫啊。
翻了幾頁,沒有滿意的答案,也試了幾個方案,也沒啥用。
看來還是得自己從問題的根源開始找起啊......
爲了保險起見, 我還是先排查一下是不是鏡像問題, 項目的 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 連基本的依賴緩存都沒有 ?
Round 2: 從半小時到五分鐘
對抓到的數據包進行再次過濾,只看 Http Request
,發現請求的都是 SNAPSHOT
版本的依賴庫, 參見下圖
這說明 SBT 是有緩存的,因爲正式版都沒有請求倉庫,可是爲什麼 SNAPSHOT
每次都去請求遠程倉庫呢?難道是 SNAPSHOT
被區別對待,不會被緩存?
既然 SBT 是基於 Ivy 的,那就從 Ivy下手。
我在Ivy 的官網
(http://ant.apache.org/ivy/history/2.0.0/settings/caches.html)
找到了下面的一個關於 緩存 的表格:
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
(https://www.scala-sbt.org/1.x/docs/Dependency-Management-Index.html ),
看名字似乎和依賴管理有關。
其中的 Cached-Resolution (https://www.scala-sbt.org/1.x/docs/Cached-Resolution.html)似乎和緩存相關, 而且開頭就是下面這段話
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。
真是無心插柳柳成蔭啊......
Round 3:從五分鐘到一分鐘
雖然現在時間只要以前的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 (https://www.scala-sbt.org/1.x/docs/Dependency-Management-Flow.html#Caching+and+Configuration)
一節找到了相關內容
When offline := true
, remote SNAPSHOTs will not be updated by a resolution, even an explicitly requested update. This should efectively 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
加了兩行配置
,
updateOptions
:= updateOptions.value.withCachedResolution( true ).withLatestSnapshots( false )整個分析問題的思路也很簡單,就是先找到問題根源,再去找解決方案。
在尋找解決方案的時候一般都是搜索引擎,文檔或者源碼,正常情況下文檔應該都能解決問題了,這期間我就繞了不少彎路,我甚至曾去看了 SBT 的 Resolver 的源碼, 現在看來,絕對是跑偏了。
整個解決過程並沒有多麼高深莫測甚至可以說是無聊至極,因爲大部分時間都是看文檔並驗證其配置。
不過還是那句話: 工欲善其事必先利其器
參考
1. sbt Reference Manua
https://www.scala-sbt.org/1.x/docs/
2. sbt 源碼
https://www.scala-sbt.org/0.13/sxr/
3. Wireshark User’s Guide
https://www.wireshark.org/docs/wsug_html_chunked/
4. Apache Ivy Documentation (2.0.0)
http://ant.apache.org/ivy/history/2.0.0/index.html
5. Offline mode and Dependency Locking
https://github.com/sbt/sbt/wiki/User-Stories:--Offline-mode-and-Dependency-Locking
全文完
以下文章您可能也會感興趣:
我們正在招聘 Java 工程師,歡迎有興趣的同學投遞簡歷到 [email protected] 。