摘要:因爲 SBT 可以配置多個遠程倉庫源(通過 Resolver),默認情況下 SBT 會從所有的遠程倉庫去拉取指定版本的  SNAPSHOT 依賴, 然後比對它們的發佈時間,取最新的那一個。雖然走了私有庫,但是我每次刷新都會請求倉庫,這就不符合道理了,難道 SBT 連基本的依賴緩存都沒有。

作者 | 田偉然

回首向來蕭瑟處,歸去,也無風雨也無晴。

杏仁工程師,關注編碼和詩詞。

前言

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

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

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

最關鍵的是由於依賴的很多服務升級很快 (幾乎每天都有升級),所以這個操作每天也會持續很多次,難以想象耗費在這方面的時間是何其之多。

人生苦短,在刷新了幾次之後,我再也受不了這漫長的等待時間,於是開始了這漫漫的優化之路。

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

Round 1: 十八般武藝齊上陣

不知道大家碰見這種問題會怎麼做,我反正是二話不說打開 Google 直接搜:  SBT 依賴下載慢

還別說,有共鳴的人還不少, 總結了下幾乎都是以下的解決方案

  1. 添加代理

  2. 添加國內鏡像源

我這肯定不是源的問題啊,我司用的私有倉庫,既然私有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   加了兩行配置

offline := true

,

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]

相關文章