天天看點

高效用戶端持續內建實踐之路

作者:閑魚技術景松

—— 安心送出代碼,讓需求釋出不再加班

背景

靈活開發以使用者的需求進化為核心,采用疊代、循序漸進的方法進行軟體開發。閑魚目前采用泳道任務模式進行疊代開發,開發周期是兩周一個版本,發版頻率比較高,并行開發的業務需求又很多,怎麼才能高效的疊代開發?測試資源相對緊缺,如何保證用戶端的研發品質?于此同時,疊代過程中,建構、內建以及測試都需要人工幹預,溝通成本和出錯機率都比較高。

如何有效的解決上面這些問題?首先想到的是持續內建,能夠做到自動化、內建測試和及時回報問題,才能減少開發和測試的成本,提高團隊的工程能效。閑魚在用戶端持續內建方案上面做了些探索和實踐,本文主要以iOS多bundle的工程為例,講解下如何用

SpringBoot

Vue

實作持續內建方案,将

需求

-

代碼

測試

關聯,做到代碼結構化并持續內建。

1. 資料模型

1.1 泳道模型

首先,讓我們來看下泳道模型,讓我們對他有個大體了解。先來看一張圖:

高效用戶端持續內建實踐之路

這是大家常用的git flow模型,內建分支就是

develop

,如果需要開發需求,就從內建分支拉出對應的特性分支

Feature

,等開發結束,再将

Feature

合并回

develop

分支,內建分支測試通過的話,再拉出釋出分支

Release

,由于

master

分支不是很常用,是以在閑魚這邊暫時沒用。

這是單個庫的情況,都比較好了解,前面背景也介紹過,iOS的工程在拆庫,拆庫的情況大緻如下:

高效用戶端持續內建實踐之路

iOS會有一個主工程來管理這些子庫,這是8個子庫,外加一個主工程,也就意味着會有9個git位址,在開發需求的時候,主要改動的bundle集中在:

IFMatrix

IFContainer

;如果改動的庫一多,那就意味着每個庫都需要拉出一個

Feature

分支。等操作完了,再進行合并到

Develop

分支,又是一個不小的工作量。

上面隻是介紹了一個需求的情況,如果有n個需求,對于內建人員來說,就是

9*n

的工作量。這個隻是iOS工程,還有一個android工程,以後可能還有weex的、flutter的,最壞的情況,工作量就是

4*9*n

,相信對于任何一個開發/測試/PM,都是一個不小的挑戰。

是以,自動內建對于閑魚來說,迫在眉睫,要想做用戶端自動內建,擺在我們面前有這麼幾個問題:

  1. 多個需求,怎麼才能保證有條不紊的內建?
  2. 如何持續內建,方案應該如何設計?
  3. 內建結束,怎樣觸發測試?

讓我們先來看下開發過程标準化,将需求、代碼、內建關聯起來,做到從源頭到結尾的自動化。

1.2 關聯需求和代碼

需求都是在

Aone

平台上面來管理的,每個需求都對應有一個

id

,怎麼将需求跟代碼關聯?

注:Aone是一個需求管理平台

閑魚的解決方案是:在git送出commit中,添加上需求的資訊,比如需求的

id

。實作的原理就是攔截git commit事件,然後将相關的需求添加到commet中,接下來的問題就是怎麼取到相關需求的資訊?

有2個方法:

  1. 統一分支命名規範,例如

    task/task_<AoneId>_<desc>

  2. 送出的時候主動輸入需求資訊,例如

    fix ##<AoneId>

這樣在送出的時候,就可以擷取到

<AondId>

,最終将代碼和需求關聯起來,結果如下圖所示:

高效用戶端持續內建實踐之路

第2行就是關聯需求的連結,每個commit上面就攜帶了需求的資訊,主要是為了後面定位測試範圍。這個需求在測試通過後,可以監聽需求狀态變更的metaq消息,先合并分支代碼,再自動删除分支。

關聯需求和代碼,詳情可參考這篇文章:

Hook Git實作代碼與需求的一緻性

1.3 關聯需求和內建項

前面也交代過背景,閑魚測試組期望是能做到開發階段和內建階段都能觸發相關的內建和測試件,這就要求我們,要做到需求與內建項關聯起來,一個需求對應一個摩天輪的項目。

注:摩天輪是一個建構平台,可以配置相關的子產品依賴
高效用戶端持續內建實踐之路

在資料庫中,就可以将

産品

需求

摩天輪項目

關聯起來,每個

projectId

就對應一個摩天輪的項目,每個

摩天輪項目

會對應很多個配置項,資料庫中就有了內建項的資料關系,包括工程之間的依賴關系。

接下來第2個問題:如何持續化內建,方案應該如何設計?

2. 自動內建架構

資料中已經存儲了

需求

摩天輪項目

的關系,怎麼将關聯的代碼應用起來的,做到可持續內建?閑魚目前采用webservice承載服務之間的串聯,形成了一個

pipline

模式。

2.1 平台架構

此平台使用

springboot

搭建,采用前後端分離的設計,服務端對外暴露的接口都是restful,前端用

Vue

編寫,通過

axios

發送AJAX請求與服務端通訊。資訊來源除了gitlab、摩天輪和Aone平台之外,還會在本地資料庫存儲一份關系映射表。

高效用戶端持續內建實踐之路

這個圖中,可以看到主要分成幾個大塊:

資料層

業務層

接口層

前端UI+用戶端

。整個平台算是一個大的用戶端,是以針對

gitlab基礎服務

MTL基礎服務

Aone基礎服務

Jenkins基礎服務

都作為一個資料層。本地的資料庫,主要儲存

需求

代碼

打包

的映射關系,比如子庫的代碼變更,需要觸發摩天輪工程打包,需要反向查找。

此服務在日常環境的一台伺服器,但是會另外一個問題:摩天輪是在預發環境的,日常環境和預發環境本身是網絡隔離的,也就無法直接調用摩天路提供的

hsf

服務。為了解決這個問題,我們在預發環境,搭建了一套橋接服務,通過vipserver來中轉服務。

2.2 事件驅動

整個平台是由事件驅動,主要分3部分:Merge Request、Gitlab Push和機械觸發(包括手工、定時)。

Gitlab提供了很人性化的接口,可以監聽到代碼的變更,配置方法也很簡單,見下圖:

高效用戶端持續內建實踐之路

主要處理的是

push event

merge request

,平台提供一個post的restfull的接口,然後配置在gitlab項目裡面,就可以監聽到代碼變更。

/**
 * 監控gitlab webhook的主入口
 * @param payload
 */
@RequestMapping(value = "webhook", method = RequestMethod.POST)
public void webhooks(@RequestBody String payload) {

    logger.info(payload);

    GitlabHookEvent event = JSON.parseObject(payload, GitlabHookEvent.class);
    eventService.dispatchGitlabEvent(event);
}
           
注:gitlab的push和merge事件會有重複發送的情況,是以需要做一下去重的處理

在這邊會解析出GitlabHookEvent,然後再由

GitlabEventService

去分發,再由內建子產品去觸發打包,那就讓我們來看下持續打包的解決方案。

2.3 持續打包

持續建構,優先要解決的是bundle之間的依賴,現在隻支援單項依賴的處理,映射關系會在資料庫中會儲存一份,當需要觸發一個摩天輪項目建構,就可以解析出它對應的依賴庫。主體流程如下圖所示:

高效用戶端持續內建實踐之路

通常情況下,子bundle會改動多個,拿到需要建構的子bundle清單之後,先檢測子bundle是否需要重新打包,檢測規則:可以根據最後一次commit資訊和上一次內建成功的時間差,如果內插補點大于一個門檻值,表明不需要重新打包;否則加入到打包隊列裡面。

主工程+子bundle的一個集合作為一個整體內建任務,添加到打包任務隊列裡面,由于沒辦法擷取到摩天輪打包成功的metaq消息的回調,隻能去輪詢結果。先檢測子bundle是否已經結束,如果已經結束,則觸發主工程的打包;如果沒有子bundle在打包,就檢查主工程是否結束。

private void triggerMTLBuildInterval(FMPackageTask task, MTLProduct product, int mtlProjectId){
        // 分析子子產品
        ArrayList<MTLBuildConfig> modulesConfigs = gitlabMTLBridge.getModuleBuildConfigList(mtlProjectId);

        if (modulesConfigs != null) {
            for (MTLBuildConfig moduleConfig : modulesConfigs) {
                boolean rebuild = isNeedRebuildForConfig(moduleConfig);

                if (!rebuild) {
                    continue;
                }

                // 檢測目前是否在打包,如果再打包,需要取消目前的編譯
                MTLBuildResult latestBuildResult = mtlService.getLatestBuildResult(moduleConfig.id, null);
                if (latestBuildResult != null){
                    String status = latestBuildResult.buildStatus;
                    if (status.equals(MTLBuildStatus.RUNNING.getValue()) ||
                            status.equals(MTLBuildStatus.WAITING.getValue())){
                        mtlService.cancelBuildTask(product.rpc_key, latestBuildResult.id);
                        logger.info("【取消打包】:" + moduleConfig.toString());
                    }
                }

                // 執行打包操作
                int resultId = triggerBuildWithConfig(moduleConfig);
                if (resultId != 0) {
                    task.moduleConfigs.add(moduleConfig);
                }
            }
        }

        // 如果有子工程,需要先打自工程,然後再打主工程
        if (task.moduleConfigs.isEmpty()){
            triggerBuildWithConfig(task.mainConfig);
        }
    }           

異常情況的處理,比如任何一個子bundle失敗,則需要取消整個建構任務。等建構結束,會通過

ApplicationEvent

廣播事件,需要的service監聽到結果,再做相關的處理。

/**
 * 廣播建構事件
 * @param task
 */
private void sendApplicationEvent(FMPackageTask task){
    ApplicationEventMTLPackage event = new ApplicationEventMTLPackage(context);
    event.task = task;
    context.publishEvent(event);
}           

接下來要看下第3個問題:自動內建結束,怎樣觸發CI測試?

3. 內建測試

現在我們已經得到了建構結果,不管成功還是失敗,都會觸發相關的CI測試,怎麼确定測試校驗件的測試範圍來提高測試效率?

首先要解決2個問題:

  1. 怎麼定義測試範圍?

對于用戶端來說,基于頁面來回歸是比較合适,是以跟測試系統定的協定,按照頁面的scheme來回歸,這樣做還有個好處,就是可以定制化相關的參數,而且還支援weex和flutter頁面。

  1. 怎麼确定測試範圍?

在前面的文章中,我們也提到了,現在

需求

代碼

建構

現在是關聯的,針對每次內建,都會有相關的驅動事件。

  1. Merge Request:有相關的mr,就可以拿到commits清單
  2. Push:針對每次push,也可以拿到相關的commits清單
  3. 機械觸發:可以拿到一定時間間隔的commits清單

針對上面3個事件源,都可以拿到commits清單,接着就可以拿到檔案修改清單、修改的人員、以及關聯的需求;拿到上面這些資訊,就可以框定出代碼變動範圍。

高效用戶端持續內建實踐之路

每次在跑CI測試的時候,就能知道這是改的哪個需求。

示例代碼如下:

/**
 * 擷取修改範圍
 * @param projectId
 * @param commits
 * @return
 */
public FMCITriggerParam getChangeScope(int projectId, String branch, ArrayList<GitlabCommit> commits){

    // 擷取平台資訊
    Repo projectRepo = repoMapper.getRepoByProjectId(projectId);

    String platform = "ios";

    if (projectRepo != null){
        platform = projectRepo.platform;
    }

    // 送出人員
    ArrayList<String> authors = getCommitsAuthors(commits);

    // 修改的檔案
    ArrayList<String> changeFiles = commitService.getCommitsChangeFiles(projectId, commits);

    // 修改範圍
    ArrayList<String> pages = getPagesByFiles(projectId, changeFiles);

    // 觸發方式
    ArrayList<String> triggerTypes = new ArrayList<>();
    triggerTypes.add("uiauto");
    triggerTypes.add("monkey");

    FMCITriggerParam change = new FMCITriggerParam();
    change.projectid = projectId;
    change.platform = platform;
    change.mergerequestid = 0;
    change.branchName = branch;
    change.userlist.addAll(authors);
    change.pages = String.join(";", pages);
    change.triggertype.addAll(triggerTypes);

    return change;
}           

擷取到送出人員、修改範圍等資訊後,測試件可以提示相關的錯誤。

高效用戶端持續內建實踐之路

4. 結果統計

下面是在一周之内內建的數量,以7.8 - 7.15号的周期為例(iOS工程):

高效用戶端持續內建實踐之路

從上面是已分支次元統計,

  1. 內建分支

    develop

    ,每天都會定時觸發相關內建
  2. 需求分支,在開發周期内,會有較高的觸發量
高效用戶端持續內建實踐之路

從事件觸發的次元上:

  1. 主要還是以定時觸發為主
  2. 在開發周期内,push觸發的數量會有所增長

目前,內建的需求比較少,是以數量也相對較少,但總體來說整個方案是穩定的,後續會完善相關的資料統計,比如建構時長、需求開發時長等。

5. 踩坑紀要

在開發過程中,踩過一些坑也做一些記錄。

5.1 Axios網絡請求

由于采用前後端分離的設計,是以在調試的時候會有跨域的問題,解決方案就是在vue的config中做相關的代理設定。

proxyTable: {
  '/fishci': {
    target: 'http://127.0.0.1:8090', //api端口
    // changeOrigin: true,   //允許跨域
    pathRewrite: {
      '^/fishci': '/'
    }
  }
}           

跨域的問題解決了,但是在對接buc認證的時候,需要重定向請求,前端用的axios,無法攔截到302的傳回。為了解決這個問題,服務端也做了相關處理,服務端将302的傳回轉化成200的傳回,并将重定向的内容放在response裡面,然後再有前端axios攔截到進行處理。

首先在

configuration

裡面添加相關的

Filter

,添加相關配置

registration.addInitParameter("HTTP_302_JSON_RESPONSE", "json");

,前端在請求的時候,以json結尾的請求,如果需要重定向,就能擷取到200的傳回,隻不過重定向的内容會在response裡面以文本形式呈現,然後再去做攔截。

axios.interceptors.response.use((response) => {
    if (response.status === 200 && response.data.hasError) {
        return window.location = "<重定向的連結>";
    }

    return response;
}, function (error) {
    return Promise.reject(error);
});           

5.2 內建限流

單個需求的打包業務邏輯相對簡單,但是由于push或merge都會觸發全量打包,頻率會比較高,就需要做相關的限流邏輯,如下圖所示:

高效用戶端持續內建實踐之路

會有2個隊列儲存目前的打包任務:

執行隊列

等待隊列

  1. 新來Feature2,目前已經有重複任務已經在內建,會放在等待隊列,如果有重複的任務,則删除
  2. 新來Feature5,目前沒有相同任務正在執行,直接添加到執行隊列
  3. 執行隊列已經達到最大容量5,新來的Feature6添加到等待隊列

當執行隊列裡面任務結束,會從等待隊列裡面選取一個沒有在打包的任務,放到執行隊列裡面。

6. 結語

本篇文章主要是整理,在泳道開發模式下,如何有效的提高用戶端工程能效所做的實踐。現在主體流程已經串通,接下來就可以有針對性的統計相關資料,比如建構的時間長短、測試有效性的度量,有了這些資料就可以對用戶端的內建效率有整體的度量,再反向的優化用戶端的內建方案。

在整套方案中,測試校驗很重要,如何做到高效的測試?原則上成本比較低的,跑的頻次可以高點,例如:代碼檢測、單元測試;成本比較高的,頻次可以低點,例如:UI Automation。總體來說,現在從

需求

代碼

建構

都關聯起來,就可以統計出一個需求的代碼送出量、建構數量和bug數量,反向的對需求就有了一個度量,比如需求拆分的好壞、開發周期的長短等。最終目的,作為一個用戶端團隊,能夠做快速的疊代業務,提高各團隊之間的協同效率,進而在整體上提高能效。在閑魚,我們推崇無人化的方式解決問題,如果你也是一個對技術有追求的同學,歡迎加入我們。

履歷投遞: [email protected]

參考文檔

  1. Git工作流指南
  2. iView - A high quality UI Toolkit based on Vue.js
  3. Spring Boot
  4. GitHub - axios/axios: Promise based HTTP client for the browser and node.js
  5. https://cn.vuejs.org/

繼續閱讀