如何加快CI(maven編譯和單元測試)速度

如何加快CI(maven編譯和單元測試)速度

CI速度慢到什麼程度會無法接受

CI速度或者單元測試速度一般不會在某一次特定的提交後突然變慢,如果真的是突然變慢,一般來說可以立刻定位到這次提交,問題一般也能解決了,但是就怕沒有發現變慢,或者問題不是某一次提交暴露出來的,而是慢慢堆積起來的。

根據Jenkins構建歷史,可以看到CI時間趨勢,如果CI時間是遞增的,並且單元測試、模塊數量、代碼行數一直在增加,那麼總有一天會慢到無法忍受,一旦發現CI速度已經佔據每次提交流程的大部分時間,特別是在每次緊急問題提交的時候,很多人都在等待CI結束,以完成代碼審覈、代碼合併、提交驗證,如果這種情況下的CI速度已經超出了大部分人可以容忍的限度,那麼加快maven編譯或單元測試速度就顯得非常有必要了。

當然,可以容忍的程度因人而異,對於我個人來說,如果我這次提交的CI時間超過了歷史平均值,我覺得就無法忍受,但是每次CI的速度取決於CI環境的併發數、機器的狀態等等不可控因素,所以每一次CI都比較又顯得不是特別理性,所以這是個很微妙的過程,只要突然某天覺得變慢了,哪怕是因爲緊急BUG導致的精神緊張,都可以開始着手考慮能否加快CI速度,即使是最後沒辦法優化,也會讓自己每次等待CI變得心甘情願,而不是無法忍受。

加快編譯速度

增量編譯

Maven是默認是基於增量編譯的,只要在任何一個包含complie的過程中(比如 mvn install),不使用clean,都是增量編譯的,但是maven的增量編譯不支持“減量編譯”,即如果某次提交刪掉了一個類,這個類的class文件並不會在maven編譯的結果中刪掉,或者某個靜態變量的引用在編譯時被優化成了直接使用變量值,如果這個靜態變量的值在某次提交被修改,那麼引用處的值可能不會發生變化,因爲因用處的類並沒有變化,不會在增量編譯過程中重新編譯,這類問題參考:

maven增量編譯

maven增量編譯的思考

增量編譯是個好東西,但是要慎用,所以爲了避免這樣的問題產生,我們一般會在mvn命令後緊跟clean(如 mvn clean install),以清除上次編譯的結果,這樣增量編譯就沒有比較的基礎,規避了增量編譯帶來的風險,所以需要謹慎使用增量編譯。

模塊並行編譯

如果一個項目P,下面有多個子項目,比如M A B C D,A B C D共同依賴M,但是A B C D之間並無依賴關係,那麼他們完全可以併發構建,這樣理論上整個項目的構建時間只需要M的構建時間加上A B C D四個子項目中構建時間最長的那個,而不是之前M A B C D五個子項目構建時間的總和。

模塊並行也很簡單,只需要給mvn命令加上幾個必要的參數即可:

mvn -T 2 clean test: 指定2個線程並行

mvn -T 1C clean test: 指定每個CPU核分配一個線程構建

其中-T可以指定同時多少個線程併發執行對應的階段,後面的數值要麼指定具體線程數,要麼指定每個CPU核分配的線程數。

這樣的參數可以讓mvn各個階段併發執行,以縮短CI時間。

加快單元測試速度

單元測試並行

單元測試之間應該做到沒有任何關聯,包括類與類之間、同一個類中的用例之間。那麼這些單元測試類或者同一個測試類中的多個單元測試理論上是可以併發執行的,maven的sunfire插件可以做到Junit單元測試並行,具體參考:Running JUnit Tests in Parallel with Maven

對於只包含簡單Junit單元測試的應用來說,sunfire插件可以保證所有單元測試的並行執行,並且能夠有效減少單元測試執行時間,但是對於spring boot單元測試或者其他需要加載上下文的單元測試,可能並不是很友好。因爲正常來說spring boot單元測試都是通過SpringRunner.class或者SpringJUnit4ClassRunner.class等runner來運行的,這些Runner保證同一個模塊下的所有單元測試只加載一次spring上下文,但如果使用並行的單元測試,可能需要多次加載spring上下文,那麼就會導致單元測試執行時間下降不明顯,甚至不降反增。

優化單元測試

剛剛我們提到了可能出現的spring上下文多次加載,即使我們沒有使用並行執行單元測試,依然還是會有這樣的情況出現。

我們在使用spring boot test時,一般在測試類上加@SpringBootTest註解即可,註解中可以指定一些選項值,比如啓動類、特殊配置等。也正因爲有這些註解選項值,會導致存在多個選項值不同的SpringBootTest註解,SpringRunner.class或者SpringJUnit4ClassRunner.class認爲每個唯一的SpringBootTest註解是一個上下文,那麼就會存在不同選項的SpringBootTest註解會被多次加載,從而加長單元測試時間。

PowerMock也會導致單元測試變慢,特別是一些應該寫在@BeforeClass而非@Before中的初始化過程,如果寫在@BeforeClass中,會顯著減少單元測試初始化時間。

Thread.sleep()也可能會導致單元測試變慢,我們應該避免顯式地調用Thread.sleep(),而是應該使用一個包裝的服務或靜態方法,以達到可以被正常mock的效果,那麼不管sleep多少時間,都可以mock爲doNothing,從而減少等待時間。

如何找到執行時間較長的單元測試

執行mvn test後,控制檯會輸出各個單元測試類的執行時間,可以將全部輸出保存到文本文件中,然後再使用一些命令或者文本處理工具篩選出相關行並排序,控制檯輸出如下:

[INFO] Running github.clyoudu.match.PowerMockitoTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.655 s - in github.clyoudu.match.PowerMockitoTest
[INFO] Running github.clyoudu.match.SpyTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 s - in github.clyoudu.match.SpyTest

我們可以篩選出形如[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.655 s - in github.clyoudu.match.PowerMockitoTest這樣的行,然後再以空格分割,根據Time elapsed: 0.655 s中的0.655降序排列即可,具體命令和示例輸出如下:

cat log.txt | grep "Time elapsed:" | sort -t ' ' -k 13rn,13 | head -15
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 90.209 s - in xxx
[INFO] Tests run: 22, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 18.839 s - in xxx
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.079 s - in xxx
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.066 s - in xxx
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.32 s - in xxx
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.289 s - in xxx
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.405 s - in xxx
[INFO] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.22 s - in xxx
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.737 s - in xxx
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.627 s - in xxx
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.594 s - in xxx
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.496 s - in xxx
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.443 s - in xxx
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.307 s - in xxx
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.287 s - in xxx

上面的命令輸出整個項目所有單元測類試執行時間最長的Top15,然後可以通過行末的輸出找到對應的單元測試類,最後做出可能的優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章