行動開發團隊中 CI 的演變

如今,大多數軟體產品都是團隊開發的。 成功的團隊發展的條件可以用簡單的圖表的形式來表示。

行動開發團隊中 CI 的演變

編寫程式碼後,您需要確保:

  1. Работает。
  2. 它不會破壞任何東西,包括您同事編寫的程式碼。

如果這兩個條件都滿足,那麼您就走上了成功之路。 為了輕鬆檢查這些條件並且不偏離獲利路徑,我們提出了持續整合。

CI 是一種工作流程,您可以在其中盡可能頻繁地將程式碼整合到整個產品程式碼中。 您不僅要集成,還要不斷檢查一切是否正常。 由於您需要經常進行大量檢查,因此值得考慮自動化。 您可以手動檢查所有內容,但您不應該這樣做,原因如下。

  • 親愛的人們。 任何程式設計師一小時的工作都比任何伺服器一小時的工作更昂貴。
  • 人們會犯錯。 因此,當測試在錯誤的分支上執行或為測試人員編譯了錯誤的提交時,可能會發生這種情況。
  • 人們很懶。 有時,當我完成一項任務時,我會想到:「有什麼需要檢查的? 我寫了兩行 - 一切正常! 我想你們有些人有時也會有這樣的想法。 但你應該經常檢查。

Nikolai Nesterov 表示,Avito 行動開發團隊如何實施和開發持續集成,他們如何從每天 0 次構建到 450 次構建,以及構建機器每天組裝 200 個小時。內斯特羅夫)是 CI/CD Android 應用程式所有演進變化的參與者。

該故事基於 Android 命令範例,但大多數方法也適用於 iOS。


曾幾何時,有個人在 Avito Android 團隊工作。 根據定義,他不需要持續整合的任何東西:沒有人可以整合。

但應用程式不斷成長,越來越多的新任務出現,團隊也相應壯大。 在某些時候,是時候更正式地建立程式碼整合流程了。 決定使用 Git 流程。

行動開發團隊中 CI 的演變

Git 流程的概念眾所周知:一個專案有一個公共的開發分支,對於每一個新功能,開發人員切出一個單獨的分支,提交到它,推送,當他們想要將程式碼合併到開發分支時,打開一個拉取請求。 為了分享知識和討論方法,我們引入了程式碼審查,即同事必須檢查並確認彼此的程式碼。

支票

用眼睛看到程式碼很酷,但還不夠。 因此,正在引入自動檢查。

  • 首先,我們檢查 方舟組裝.
  • 批量 聯合測試.
  • 我們考慮程式碼覆蓋率,因為我們正在運行測試。

要了解如何運行這些檢查,讓我們看看 Avito 中的開發過程。

它可以這樣示意性地表示:

  • 開發人員在他的筆記型電腦上編寫程式碼。 您可以在此處執行整合檢查 - 使用提交掛鉤,或只是在背景執行檢查。
  • 開發人員推送程式碼後,他會打開拉取請求。 為了使其程式碼包含在開發分支中,需要經過程式碼審查並收集所需數量的確認。 您可以在此處啟用檢查和建置:在所有建置成功之前,無法合併拉取請求。
  • 合併拉取請求並將程式碼包含到開發中後,您可以選擇一個方便的時間:例如,在晚上,當所有伺服器都空閒時,並執行任意數量的檢查。

沒有人喜歡在筆記型電腦上執行掃描。 當開發人員完成一個功能時,他希望快速推送它並打開拉取請求。 如果此時啟動一些長時間的檢查,這不僅不太令人愉快,而且還會減慢開發速度:當筆記型電腦正在檢查某些東西時,就無法正常工作。

我們真的很喜歡在晚上運行檢查,因為有很多時間和服務器,你可以四處閒逛。 但不幸的是,當功能程式碼進入開發階段時,開發人員修復 CI 發現的錯誤的動力就少了很多。 當我查看早晨報告中發現的所有錯誤時,我定期發現自己會想,有一天我會修復它們,因為現在 Jira 中有一個很酷的新任務,我只想開始做。

如果檢查阻止拉取請求,那麼就有足夠的動力,因為在建置變綠之前,程式碼不會進入開發,這意味著任務將無法完成。

因此,我們選擇了以下策略:我們在晚上執行盡可能多的檢查,並啟動其中最關鍵的檢查,最重要的是,根據拉取請求啟動最快的檢查。 但我們並沒有就此止步——同時,我們優化了檢查速度,以便將它們從夜間模式轉移到拉取請求檢查。

當時,我們所有的建置都很快完成了,因此我們只是將 ARK 建置、Junit 測試和程式碼覆蓋率計算作為拉取請求的攔截器。 我們打開它,思考它,然後放棄了程式碼覆蓋率,因為我們認為我們不需要它。

我們花了兩天的時間才完成了基本的CI(以下時間估計是近似的,需要規模化)。

之後,我們開始進一步思考──我們檢查是否正確? 我們是否正確地基於拉取請求運行建置?

我們在打開拉取請求的分支的最後一次提交上開始建置。 但此提交的測試只能顯示開發人員編寫的程式碼有效。 但他們並不能證明他沒有破壞任何東西。 事實上,當一個功能合併到develop分支後,你需要檢查它的狀態。

行動開發團隊中 CI 的演變

為此,我們編寫了一個簡單的 bash 腳本 預合併.sh:

#!/usr/bin/env bash

set -e

git fetch origin develop

git merge origin/develop

在這裡,所有來自開發的最新更改都被簡單地拉出並合併到當前分支中。 我們添加了 premerge.sh 腳本作為所有建置的第一步,並開始檢查我們想要的內容,即 一體化.

花了三天時間定位問題,找到解決方案,寫出這個腳本。

應用程式不斷開發,越來越多的任務出現,團隊不斷壯大,而 premerge.sh 有時開始讓我們失望。 開發中有衝突的變更破壞了建置。

這是如何發生的一個例子:

行動開發團隊中 CI 的演變

兩個開發人員同時開始開發功能 A 和 B。功能 A 的開發人員發現專案中有一個未使用的功能 answer() 然後,像一個優秀的童子軍一樣,將其刪除。 同時,功能 B 的開發人員在他的分支中加入了對此函數的新呼叫。

開發人員完成工作並同時開啟拉取請求。 建置已啟動,premerge.sh 檢查有關最新開發狀態的兩個拉取請求 - 所有檢查都是綠色的。 之後,功能 A 的拉取請求被合併,功能 B 的拉取請求被合併...繁榮! 開發中斷是因為開發程式碼包含對不存在函數的呼叫。

行動開發團隊中 CI 的演變

不發展的時候就發展了 局部災害。 整個團隊無法收集任何東西並提交進行測試。

剛好我最常從事基礎設施任務:分析、網路、資料庫。 也就是說,是我編寫了其他開發人員使用的那些函數和類別。 正因為如此,我發現自己常常遇到類似的情況。 我甚至把這張照片掛了一段時間。

行動開發團隊中 CI 的演變

由於這不適合我們,我們開始探索如何防止這種情況發生。

如何不破壞開發

第一個選項: 更新開發時重建所有拉取要求。 在我們的範例中,如果功能 A 的拉取請求是第一個包含在開發中的,則功能 B 的拉取請求將被重建,相應地,檢查將因編譯錯誤而失敗。

要了解這需要多長時間,請考慮一個具有兩個 PR 的範例。 我們打開兩個 PR:兩個構建,兩次運行檢查。 第一個PR合併到develop後,需要重建第二個PR。 總共,兩個 PR 需要運行三輪檢查:2 + 1 = 3。

原則上是沒問題的。 但我們看了一下統計,我們團隊中典型的情況是10個開放的PR,那麼檢查的數量就是級數的總和:10 + 9 + ... + 1 = 55。即接受10個PR,你需要重建55 次。 這是在理想的情況下,當所有檢查第一次通過時,當沒有人在處理這十幾個請求時打開額外的拉取請求。

想像一下自己是一名開發人員,需要第一個單擊“合併”按鈕,因為如果鄰居這樣做,那麼您將不得不等到所有構建再次完成...不,那行不通,這會嚴重減慢開發速度。

第二種可能的方式: 程式碼審查後收集拉取請求。 也就是說,您開啟一個拉取請求,從同事那裡收集所需數量的批准,更正所需的內容,然後啟動建置。 如果成功,拉取請求將合併到開發中。 在這種情況下,沒有額外的重啟,但回饋速度大大減慢。 作為一名開發人員,當我打開拉取請求時,我立即想看看它是否會起作用。 例如,如果測試失敗,您需要快速修復它。 在延遲建置的情況下,回饋會減慢,因此整個開發也會減慢。 這也不適合我們。

結果,只剩下第三個選擇—— 自行車。 我們所有的程式碼、所有來源都儲存在 Bitbucket 伺服器上的儲存庫中。 因此,我們必須為 Bitbucket 開發一個插件。

行動開發團隊中 CI 的演變

該插件覆蓋拉取請求合併機制。 開始是標準的:PR 打開,所有程序集啟動,程式碼審查完成。 但是,在程式碼審查完成並且開發人員決定按一下「合併」後,外掛程式會檢查執行檢查的開發狀態。 如果在建置後更新了開發,則外掛程式將不允許此類拉取請求合併到主分支中。 它只會重新啟動相對較新的開發的建置。

行動開發團隊中 CI 的演變

在我們的具有衝突變更的範例中,此類建置將由於編譯錯誤而失敗。 因此,功能 B 的開發人員必須修正程式碼,重新啟動檢查,然後外掛程式將自動套用拉取請求。

在實現此外掛程式之前,我們平均每個拉取請求運行 2,7 次審核。 該插件發布了 3,6 版。 這適合我們。

值得注意的是,這個插件有一個缺點:它只會重新啟動一次建置。 也就是說,仍然存在一個小窗口,透過這個窗口,衝突的變更可以進入開發階段。 但這種可能性很低,我們在啟動次數和失敗可能性之間進行了權衡。 兩年內只發射了一次,所以應該沒有白費。

我們花了兩週時間編寫了 Bitbucket 外掛程式的第一個版本。

新支票

同時,我們的團隊不斷壯大。 新增了新的檢查。

我們想:如果可以避免錯誤,為什麼還要犯錯呢? 這就是為什麼他們實施 靜態程式碼分析。 我們從 lint 開始,它包含在 Android SDK 中。 但當時他根本不知道如何使用 Kotlin 程式碼,而我們已經有 75% 的應用程式是用 Kotlin 寫的。 因此,內建的被加入到 lint 中 Android Studio 檢查。

為了做到這一點,我們必須做很多變態:使用 Android Studio,將其打包在 Docker 中,並使用虛擬監視器在 CI 上運行,以便它認為它是在真正的筆記型電腦上運行。 但它奏效了。

也是在這段時間,我們開始寫很多東西 儀器測試 並實施了 截圖測試。 這是為單獨的小視圖生成參考螢幕截圖時,測試包括從視圖中獲取螢幕截圖並將其與標準直接逐像素進行比較。 如果有差異,則表示佈局有問題或樣式有問題。

但儀器測試和螢幕截圖測試需要在設備上運行:在模擬器上或在真實設備上。 考慮到測試很多並且運行頻繁,所以需要整個農場。 建立自己的農場太費力了,所以我們找到了一個現成的選擇 - Firebase Test Lab。

Firebase 測試實驗室

之所以選擇它,是因為 Firebase 是 Google 產品,這意味著它應該可靠且不會消亡。 價格合理:真實設備運行每小時 5 美元,模擬器運行每小時 1 美元。

將 Firebase 測試實驗室實作到我們的 CI 中大約花了三週。

但團隊不斷成長,不幸的是,Firebase 開始讓我們失望。 那時,他沒有任何 SLA。 有時,Firebase 會讓我們等到所需數量的設備可以自由進行測試,而不是像我們想要的那樣立即開始執行它們。 排隊等候的時間長達半小時,這是一個非常漫長的時間。 每個 PR 都要進行儀器測試,延遲確實減慢了開發速度,然後每個月的帳單會帶來一筆總金額。 總的來說,由於團隊已經足夠壯大,因此決定放棄 Firebase 並在內部工作。

Docker + Python + bash

我們使用 Docker,將模擬器塞進其中,用 Python 編寫了一個簡單的程序,該程序會在適當的時候在所需版本中啟動所需數量的模擬器,並在必要時停止它們。 當然,還有幾個 bash 腳本 - 沒有它們我們會怎麼樣?

創建我們自己的測試環境花了五週。

因此,對於每個拉取請求,都有一個廣泛的合併阻止檢查清單:

  • 方舟組裝;
  • 聯合測試;
  • 皮棉;
  • Android Studio 檢查;
  • 儀器儀表測試;
  • 截圖測試。

這避免了許多可能的故障。 從技術上講,一切正常,但開發人員抱怨等待結果的時間太長。

多久才算太長呢? 我們將Bitbucket和TeamCity的資料上傳到分析系統中,發現: 平均等待時間 45 分鐘。 也就是說,開發人員在開啟拉取請求時,平均等待建置結果 45 分鐘。 在我看來,這已經很多了,你不能那樣工作。

當然,我們決定加快所有建置速度。

讓我們加快速度

看到構建經常排在隊列中,我們做的第一件事是 購買了更多硬件 ——粗放式開發是最簡單的。 建造停止排隊,但等待時間僅略有減少,因為某些檢查本身需要很長時間。

刪除耗時過長的檢查

我們的持續整合可以捕捉這些類型的錯誤和問題。

  • 不會。 當由於衝突的變更而無法建置某些內容時,CI 可以捕獲編譯錯誤。 正如我已經說過的,那麼沒有人可以組裝任何東西,開發就會停止,每個人都會感到緊張。
  • 行為錯誤。 例如,當應用程式已建置時,但按下按鈕時崩潰,或根本沒有按下按鈕。 這很糟糕,因為這樣的錯誤可能會影響到使用者。
  • 佈局錯誤。 例如,按一下一個按鈕,但向左移動了 10 個像素。
  • 技術債增加.

看完這個清單後,我們意識到只有前兩點是關鍵的。 我們想先抓住這樣的問題。 佈局中的錯誤是在設計審核階段發現的,然後可以輕鬆修正。 處理技術債需要單獨的流程和規劃,因此我們決定不對拉取請求進行測試。

根據這種分類,我們調整了整個檢查清單。 劃掉 Lint 並推遲了一夜的啟動:只是為了就該專案存在多少問題產生一份報告。 我們同意單獨處理技術債,並且 Android Studio 檢查被完全放棄。 Android Studio 在 Docker 中執行檢查聽起來很有趣,但在支援方面造成了很多麻煩。 Android Studio 版本的任何更新都意味著要與難以理解的錯誤作鬥爭。 支援截圖測試也很困難,因為函式庫不是很穩定,有誤報。 螢幕截圖測試已從檢查清單中刪除.

結果,我們留下了:

  • 方舟組裝;
  • 聯合測試;
  • 儀器測試。

Gradle 遠端快取

沒有嚴格的檢查,一切都變得更好。 但完美無極限!

我們的應用程式已經被分成大約 150 個 gradle 模組。 Gradle 遠端快取通常在這種情況下效果很好,因此我們決定嘗試一下。

Gradle 遠端快取是可以快取各個模組中各個任務的建置工件的服務。 Gradle 並沒有實際編譯程式碼,而是使用 HTTP 敲擊遠端快取並詢問是否有人已經執行了此任務。 如果是,它只是下載結果。

運行 Gradle 遠端快取很容易,因為 Gradle 提供了 Docker 映像。 我們在三個小時內成功完成了這件事。

您所要做的就是啟動 Docker 並在專案中寫入一行。 不過雖然可以很快啟動,但要讓一切順利進行還需要相當長的時間。

下面是快取未命中圖。

行動開發團隊中 CI 的演變

一開始,快取未命中的百分比約為 65。三週後,我們設法將該值提高到 20%。 事實證明,Android 應用程式收集的任務具有奇怪的傳遞依賴關係,因此 Gradle 錯過了快取。

透過連接緩存,我們大大加快了建置速度。 但除了組裝之外,還有儀器測試,而且需要很長的時間。 也許並非每個拉取請求都需要執行所有測試。 為了找到答案,我們使用影響分析。

影響分析

在拉取請求中,我們收集 git diff 並找到修改後的 Gradle 模組。

行動開發團隊中 CI 的演變

僅運行檢查更改的模組以及依賴它們的所有模組的儀器測試是有意義的。 對相鄰模組運行測試是沒有意義的:那裡的程式碼沒有改變,也不會破壞任何東西。

儀器測試沒那麼簡單,因為它們必須位於頂層應用程式模組中。 我們使用啟發式和字節碼分析來了解每個測試屬於哪個模組。

升級儀器測試的操作,使其僅測試涉及的模組,花費了大約八週的時間。

加快檢查速度的措施已成功。 我們從 45 分鐘增加到大約 15 分鐘。等待一刻鐘的建造已經很正常了。

但現在開發者開始抱怨他們不明白正在啟動哪些建置、在哪裡查看日誌、為什麼建置是紅色的、哪個測試失敗等等。

行動開發團隊中 CI 的演變

回饋問題會減慢開發速度,因此我們嘗試提供有關每個 PR 和構建的盡可能清晰詳細的資訊。 我們首先在 Bitbucket 中對 PR 進行評論,指出哪個構建失敗了以及原因,並在 Slack 中編寫了有針對性的消息。 最後,我們為該頁面建立了一個 PR 儀表板,其中包含目前正在運行的所有建置及其狀態的清單:已排隊、正在運行、崩潰或已完成。 您可以單擊建置並獲取其日誌。

行動開發團隊中 CI 的演變

我們花了六週的時間來提供詳細的回饋。

計劃

讓我們繼續看看最近的歷史。 在解決了反饋問題後,我們達到了一個新的水平 - 我們決定建立自己的模擬器農場。 當測試和模擬器很多時,它們很難管理。 結果,我們所有的模擬器都遷移到了具有靈活資源管理的 k8s 叢集。

此外,還有其他計劃。

  • 返回皮棉 (和其他靜態分析)。 我們已經朝這個方向努力。
  • 在 PR 攔截器上運行所有內容 端對端測試 在所有 SDK 版本上。

那麼,我們追溯了Avito中持續整合的發展歷史。 現在我想從經驗​​豐富的角度給一些建議。

Советы

如果我只能給一條建議,那就是:

請小心 shell 腳本!

Bash是一個非常靈活且強大的工具,編寫腳本非常方便且快速。 但你可能會因此陷入陷阱,不幸的是,我們也陷入了其中。

這一切都始於在我們的建置機器上運行的簡單腳本:

#!/usr/bin/env bash
./gradlew assembleDebug

但是,如您所知,隨著時間的推移,一切都會發展並變得更加複雜- 讓我們從另一個腳本運行一個腳本,讓我們在其中傳遞一些參數- 最後我們必須編寫一個函數來確定我們現在處於哪個層級的bash 嵌套插入必要的引號,讓一切開始。

行動開發團隊中 CI 的演變

開發這樣的腳本的人力成本可想而知。 我建議你不要陷入這個陷阱。

可以更換什麼?

  • 任何腳本語言。 寫給 Python 或 Kotlin 腳本 更方便,因為它是編程,而不是腳本。
  • 或在表單中描述所有的建構邏輯 自訂 gradle 任務 為您的項目。

我們決定選擇第二個選項,現在我們正在系統地刪除所有 bash 腳本並編寫大量自訂 gradle 任務。

技巧#2:將基礎架構儲存在程式碼中。

當持續整合設定不儲存在 Jenkins 或 TeamCity 等的 UI 介面中,而是以文字檔案的形式直接儲存在專案儲存庫中時,會很方便。 這提供了版本控制能力。 在另一個分支上回滾或建立程式碼並不困難。

腳本可以儲存在專案中。 環境怎麼辦?

提示#3:Docker 可以幫助改善環境。

它肯定會對 Android 開發者有所幫助;不幸的是,iOS 還沒有。

這是一個包含 jdk 和 android-sdk 的簡單 docker 檔案的範例:

FROM openjdk:8

ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip" 
    ANDROID_HOME="/usr/local/android-sdk" 
    ANDROID_VERSION=26 
    ANDROID_BUILD_TOOLS_VERSION=26.0.2

# Download Android SDK
RUN mkdir "$ANDROID_HOME" .android 
    && cd "$ANDROID_HOME" 
    && curl -o sdk.zip $SDK_URL 
    && unzip sdk.zip 
    && rm sdk.zip 
    && yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses

# Install Android Build Tool and Libraries
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" 
    "platforms;android-${ANDROID_VERSION}" 
    "platform-tools"

RUN mkdir /application
WORKDIR /application

寫完這個 Docker 檔案(我告訴你一個秘密,你不必寫它,只需從 GitHub 上拉取現成的檔案即可)並組裝鏡像,你將獲得一個可以在其上建立應用程式的虛擬機器並運行Junit 測試。

這樣做有意義的兩個主要原因是可擴展性和可重複性。 使用 docker,您可以快速建立十幾個建置代理,它們將具有與前一個完全相同的環境。 這使得 CI 工程師的生活變得更加輕鬆。 將 android-sdk 推送到 docker 非常容易,但是使用模擬器就有點困難了:你必須更加努力(或再次從 GitHub 下載完成的版本)。

提示四:不要忘記,檢查不是為了檢查而檢查,而是為了人。

最重要的是,快速且清晰的回饋對於開發人員來說非常重要:什麼出了問題,什麼測試失敗了,在哪裡可以看到建置日誌。

提示#5:開發持續整合時要務實。

清楚了解您想要防止哪些類型的錯誤,您願意花費多少資源、時間和電腦時間。 例如,花費太長時間的檢查可以推遲過夜。 而那些發現不太重要錯誤的錯誤應該被完全放棄。

提示#6:使用現成的工具。

現在有很多公司提供雲端CI。

行動開發團隊中 CI 的演變

對於小型團隊來說,這是一個很好的解決方案。 您不需要支援任何東西,只需支付一點錢,建立您的應用程序,甚至運行儀器測試。

提示#7:在大型團隊中,內部解決方案更有利可圖。

但隨著團隊的成長,內部解決方案遲早會變得更有利可圖。 這些決定有一個問題。 經濟學有一個收益遞減法則:任何項目,後續的每一次改進都會變得越來越困難,需要越來越多的投入。

經濟學描述了我們的整個生活,包括持續整合。 我為持續整合的每個發展階段製定了勞動成本表。

行動開發團隊中 CI 的演變

顯然,任何改進都變得越來越困難。 看看這張圖,你就可以明白,持續整合是需要隨著團隊規模的成長而發展的。 對於一個兩人團隊來說,花費 50 天開發一個內部模擬器農場是一個平庸的想法。 但同時,對於一個大團隊來說,根本不進行持續整合也是一個壞主意,因為整合問題、修復溝通等都可能是個壞主意。 這將需要更多的時間。

我們最初的想法是,需要自動化,因為人力成本昂貴,他們會犯錯並且很懶。 但人們也會實現自動化。 因此,所有相同的問題都適用於自動化。

  • 自動化成本高昂。 記住勞動時間表。
  • 當談到自動化時,人們會犯錯。
  • 有時人們很懶於自動化,因為一切都是這樣的。 為什麼要改進其他東西,為什麼要進行持續整合?

但我有統計數據:20% 的程式集中發現了錯誤。 這並不是因為我們的開發人員寫的程式碼很糟糕。 這是因為開發人員有信心,如果他們犯了一些錯誤,它不會最終出現在開發中,而是會被自動檢查發現。 因此,開發人員可以花更多的時間編寫程式碼和有趣的事情,而不是在本地運行和測試某些內容。

實踐持續集成。 但要適度。

順便說一下,尼古拉·涅斯特羅夫不僅親自做了精彩的報告,而且還是程序委員會的成員 應用大會 並幫助其他人為您準備有意義的演講。 下一次會議議程的完整性和實用性可以透過以下主題來評估 日程。 如欲了解詳情,請於 22 月 23 日至 XNUMX 日造訪 Infospace。

來源: www.habr.com

添加評論