從30分鐘到1分鐘 - SBT的update耗時優化記錄

前言

公司有項目是基於 Scala 編寫的,與之配套的構建工具是 SBT , 它是 Simple Build Tool 的縮寫,雖然我以爲它一點也不簡單。html

這個項目有一個很大的痛點就是刷新依賴 (對應 SBT 的 update)很是之耗時,能夠參見下圖:java

注意圖中紅框部分,耗時1266秒,近半個小時。在刷新期間資源佔用也很高,致使電腦很卡 (風扇還呼呼呼的轉,溫度蹭蹭蹭的長)。git

最關鍵的是因爲依賴的不少服務升級很快 (幾乎天天都有升級),因此這個操做天天也會持續不少次,不可思議耗費在這方面的時間是何其之多。github

人生苦短,在刷新了幾回以後,我再也受不了這漫長的等待時間,因而開始了這漫漫的優化之路。shell

正所謂工欲善其事必先利其器apache

Round 1: 十八般武藝齊上陣

不知道你們遇見這種問題會怎麼作,我反正是二話不說打開 Google 直接搜: SBT 依賴下載慢緩存

還別說,有共鳴的人還很多, 總結了下幾乎都是如下的解決方案網絡

  1. 添加代理
  2. 添加國內鏡像源

我這確定不是源的問題啊,我司用的私有倉庫,既然私有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 連基本的依賴緩存都沒有 ?

Round 2: 從半小時到五分鐘

對抓到的數據包進行再次過濾,只看 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。

真實無意插柳柳成蔭啊......

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 一節找到了相關內容

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 的源碼, 如今看來,絕對是跑偏了。

整個解決過程並無多麼高深莫測甚至能夠說是無聊至極,由於大部分時間都是看文檔並驗證其配置。

不過仍是那句話:工欲善其事必先利其器

參考

  1. sbt Reference Manua
  2. sbt 源碼
  3. Wireshark User’s Guide
  4. Apache Ivy Documentation (2.0.0)
  5. Offline mode and Dependency Locking
相關文章
相關標籤/搜索