如何提高一個(gè)研發(fā)團(tuán)隊(duì)的“代碼速度”?

什么是代碼速度(Code Velocity)?

Code Velocity的定義是:一段代碼變更,從git里的commit time,到在生產(chǎn)環(huán)境里運(yùn)行,中間經(jīng)過了多少時(shí)間。換句話說,代碼從寫完開始,多快能到達(dá)生產(chǎn)環(huán)境。

舉個(gè)例子,C公司的一個(gè)團(tuán)隊(duì),他們今天的code velocity一般在是2-4周左右:

他們的一個(gè)典型的迭代周期是4周?1?:第一周系分測(cè)分,第二、三周coding、testing、修bug,第三周末或第四周初合并回master、部署集成測(cè)試環(huán)境、跑回歸、上預(yù)發(fā)、上生產(chǎn)環(huán)境。在這樣的迭代節(jié)奏和“分支開發(fā)、主干發(fā)布” ?2? 的模式里,從commit time到進(jìn)生產(chǎn)環(huán)境,平均是2周左右。

他們還有一些比較長周期的項(xiàng)目。例如,有幾個(gè)項(xiàng)目是四月中上旬拉的分支,一直到五月下旬才合回master,六月初發(fā)布上線。從四月上旬到五月下旬,這幾個(gè)項(xiàng)目分支里的代碼沒有合回master過。這幾個(gè)項(xiàng)目的code velocity就比較長,平均是4周左右。

為什么要度量和提高Code Velocity?

Code velocity體現(xiàn)的是一個(gè)研發(fā)團(tuán)隊(duì)快速響應(yīng)業(yè)務(wù)需求的能力。

以上文C公司這個(gè)團(tuán)隊(duì)今天的快速響應(yīng)、交付的能力水平,在兩周一次發(fā)布窗口的節(jié)奏里,大部分時(shí)候可能已經(jīng)夠了,但一旦遇到各種意外,就捉襟見肘了,例如:臨時(shí)封網(wǎng),需求變更,項(xiàng)目因故延期等。

快速響應(yīng)、快速交付的能力要有一定的“儲(chǔ)備”,這就好像足球運(yùn)動(dòng)員要有體能儲(chǔ)備:要想贏下加時(shí)賽,就要有踢兩個(gè)加時(shí)賽的體能。研發(fā)團(tuán)隊(duì)要能在兩周一次發(fā)布窗口的節(jié)奏里游刃有余,就要有一周一發(fā)甚至一周兩發(fā)的能力。況且,可以預(yù)見在不遠(yuǎn)的將來,兩周一次的發(fā)布窗口也嫌太久了,業(yè)務(wù)壓力會(huì)倒逼一周一發(fā)成為常態(tài)。那時(shí)候,這個(gè)團(tuán)隊(duì)就要有“天天發(fā)”的能力,才能游刃有余。

研發(fā)團(tuán)隊(duì)的code velocity和他們拿到的業(yè)務(wù)結(jié)果之間的關(guān)系,就像飯店上菜時(shí)間長短和生意火不火之間的關(guān)系一樣,兩者是相關(guān)的,但不是強(qiáng)因果關(guān)系:

  • 有些飯店上菜挺快的,但生意不火。不能就因此說“上菜時(shí)間長短”不重要。
  • 有些飯店,上菜很慢,但生意也還是很火。也不能因此就說“上菜時(shí)間長短”不重要。

一家飯店要火,還要看地段、裝潢、菜單、原料、廚子、服務(wù)員、宣傳等。

除了快速響應(yīng)業(yè)務(wù)需求以外,提高code velocity還能幫助開發(fā)和測(cè)試同學(xué)降低項(xiàng)目并發(fā)、減少上下文切換、提高幸福感。在兩周一次發(fā)布窗口的節(jié)奏下,很多時(shí)候研發(fā)同學(xué)把一個(gè)需求寫完、測(cè)完,要等其他需求,等集成環(huán)境測(cè)試,再回來搞一波,然后到了生產(chǎn)環(huán)境發(fā)布再回來搞一波。事情是不連續(xù)的,開發(fā)測(cè)試其實(shí)是被打斷的。Code velocity提高了以后,開發(fā)測(cè)試有連續(xù)性,寫完了測(cè)完了的代碼就發(fā)走了,研發(fā)同學(xué)也不用身上同時(shí)背著一串項(xiàng)目了。

如何提高一個(gè)研發(fā)團(tuán)隊(duì)的“代碼速度”?

為什么Code Velocity快不起來?

仔細(xì)想想,一段代碼從git commit到生產(chǎn)環(huán)境,這個(gè)過程中時(shí)間大部分是花在等待上的:等著和其他代碼一起發(fā)布上線。之所以會(huì)要把很多代碼合到一起,每兩周發(fā)一次,是出于cost vs. benefit的權(quán)衡:

  • 每次常規(guī)發(fā)布,不管payload(即發(fā)布的代碼量)有多大,有些固定工作是逃不掉的:

首先,由于采取了“分支開發(fā)、主干發(fā)布”的模式,代碼要從各個(gè)項(xiàng)目分支和迭代分支合并回master,要解決沖突,確保合并時(shí)沒有漏代碼。

然后,要對(duì)master里的代碼跑一次全量的回歸:準(zhǔn)備環(huán)境、部署代碼和配置、執(zhí)行回歸測(cè)試用例、分析結(jié)果。這個(gè)過程做一遍,短則半天一天,長則兩三天甚至更長。如果發(fā)現(xiàn)問題,需要修bug,這個(gè)過程還要再重復(fù)。

與此同時(shí),有些團(tuán)隊(duì)還要寫發(fā)布計(jì)劃,詳細(xì)列出發(fā)布的步驟:要改哪些配置,各個(gè)系統(tǒng)的發(fā)布順序是什么,回滾的步驟是什么,等等。發(fā)布計(jì);劃寫好了還要評(píng)審。

最后,要走一遍發(fā)布流程:先上預(yù)發(fā),上去以后QA要做預(yù)發(fā)驗(yàn)證;上生產(chǎn)環(huán)境,按照發(fā)布計(jì)劃一步步做,藍(lán)綠切流的過程中要讓各個(gè)系統(tǒng)的owner確認(rèn)OK,再繼續(xù)藍(lán)綠切流。整個(gè)發(fā)布過程需要很多人的協(xié)同。

  • 在某些項(xiàng)目中,把代碼拆成小塊分多次發(fā)布會(huì)增加開發(fā)的難度和工作量。

例如,X系統(tǒng)的API增加了一個(gè)新參數(shù),要求Y系統(tǒng)在調(diào)用這個(gè)API的時(shí)候必須要傳這個(gè)參數(shù)。如果兩個(gè)系統(tǒng)上的代碼變更一起發(fā)(而且是藍(lán)綠發(fā)布),就比較簡單。但如果把這個(gè)工作拆解成小塊,開發(fā)工作就變復(fù)雜了:X的API新增的這個(gè)參數(shù)必須先做成optional的,等Y那邊的代碼改好發(fā)上線了以后,再把X的這個(gè)新參數(shù)改成required。

另外,在有些實(shí)際項(xiàng)目中,實(shí)際情況比上面舉的這個(gè)例子更復(fù)雜,并不是那么容易一眼就能看出來怎么拆解的。

如何提高Code Velocity?

要提高code velocity,就要對(duì)上面提到的這些原因?qū)ΠY下藥,提升四個(gè)關(guān)鍵能力:

  1. 能頻繁地把代碼合回master
  2. 非常強(qiáng)大的跑回歸的能力
  3. 一鍵部署乃至無人值守發(fā)布的能力
  4. 把大項(xiàng)目拆成小項(xiàng)目做的能力

提高code velocity,要實(shí)現(xiàn)質(zhì)的飛躍,第一個(gè)能力“能頻繁的把代碼合回master”是關(guān)鍵抓手。把這個(gè)能力建設(shè)好了,提升code velocity的四個(gè)關(guān)鍵能力中的三個(gè)就具備了,因?yàn)椤澳茴l繁地把代碼合回master”有三個(gè)前置條件:

  1. 實(shí)行了代碼門禁
  2. 有非常強(qiáng)大的跑回歸的能力(即上面四個(gè)關(guān)鍵能力的第二個(gè))
  3. 把大項(xiàng)目拆成小項(xiàng)目做的能力(即上面四個(gè)關(guān)鍵能力的第四個(gè))

代碼門禁(Gated Checkin)

代碼門禁能夠確保每一個(gè)進(jìn)入主分支?3?的commit都達(dá)到了一定的質(zhì)量標(biāo)準(zhǔn),例如:編譯必須通過,單元測(cè)試和接口測(cè)試必須通過,新代碼的覆蓋率不能低于某個(gè)水平,靜態(tài)代碼掃描必須通過,等等。其實(shí)今天很多公司已經(jīng)有post-checkin的CI在跑這些檢查項(xiàng)了。代碼門禁看似平淡無奇,無非就是把這些檢查項(xiàng)從post-checkin挪到了pre-checkin。但別小看這一挪,它的效果,不亞于把“當(dāng)月業(yè)績決定本月提成”改成“當(dāng)月業(yè)績決定下月提成”的效果。

代碼門禁是很典型的“測(cè)試左移”的做法,和我們對(duì)質(zhì)量的基本規(guī)律的認(rèn)知也是一致的:問題發(fā)現(xiàn)得越早,修復(fù)起來代價(jià)越小。實(shí)施了代碼門禁后,能確保主分支常年處于良好狀態(tài)。代碼門禁實(shí)施起來也很容易,很多開源和商用的CI/CD平臺(tái)都支持,例如GitLab+Jenkins。

只要做得好,代碼門禁是不會(huì)降低工程師的日常效率的?!白龅煤谩钡臉?biāo)準(zhǔn)是:

  • 執(zhí)行時(shí)間:一般能接受的是10-20分鐘,95%的情況下不應(yīng)超過30分鐘,否則體感就不好了。
  • False negative率:也就是說,代碼門禁如果失敗,有多少比例是因?yàn)榇a(包括測(cè)試用例代碼)本身的確有問題,有多少是因?yàn)榇a門禁的infrastructure的問題(比如,底層機(jī)器的資源和穩(wěn)定性)。一般來說,要把false negative率控制在5%以下。False negative率如果達(dá)到20%-30%(也就是說,五次失敗里面就有一次失敗是跟提交的代碼變更無關(guān)的),團(tuán)隊(duì)里面就會(huì)開始怨聲載道了。

非常強(qiáng)大的跑回歸的能力

有了強(qiáng)大的回歸能力,就能在代碼頻繁的合并回master的情況下,仍然保持master分支處于可發(fā)布狀態(tài)或者接近可發(fā)布的狀態(tài),有了強(qiáng)大的回歸能力,我們甚至可以把一小部分的回歸放到代碼門禁里面去跑,那將會(huì)進(jìn)一步有助于保持master分支處于可發(fā)布狀態(tài)。

回歸能力的強(qiáng)大體現(xiàn)在以下幾方面:

  • 無人值守:準(zhǔn)備環(huán)境、部署代碼和配置、執(zhí)行測(cè)試、拿回結(jié)果,整個(gè)過程都必須沒有任何人的參與。
  • 頻次:跑回歸不嫌多,最理想的是每次CI都跑回歸,那樣發(fā)現(xiàn)問題更早、定位問題更精確。
  • 覆蓋率:主要是業(yè)務(wù)覆蓋率???。
  • 穩(wěn)定性:很高的通過率,很低的噪音率,結(jié)果非常repeatable。
  • 執(zhí)行時(shí)間:也許6小時(shí)和4小時(shí)看上去沒有什么大差別,其實(shí)是有本質(zhì)區(qū)別的。如果回歸跑一遍要6小時(shí),那么“改代碼-跑回歸-看結(jié)果”這個(gè)過程一天只能干兩輪;但如果回歸一遍只要4小時(shí),那么這個(gè)過程一天就能干三輪。如果能再縮短到2小時(shí),一天就能干六七輪。

這幾方面的回歸能力相互之間是相輔相成的,能夠形成正循環(huán),產(chǎn)生“飛輪效應(yīng)”:

  • 回歸的運(yùn)行,只有真正做到了無人值守,才有可能長期高頻次運(yùn)行。
  • 高頻次的運(yùn)行,可以充分暴露各種穩(wěn)定性問題,提高回歸的穩(wěn)定性。
  • 縮短執(zhí)行時(shí)間,一方面可以縮短“反饋弧”,加速各種穩(wěn)定性問題的修復(fù),另一方面可以提高測(cè)試環(huán)境的“周轉(zhuǎn)率”,在不增加硬件成本的前提下實(shí)現(xiàn)更高頻次的回歸。
  • 提高了穩(wěn)定性,可以縮短用于分析回歸結(jié)果的時(shí)間。如果一個(gè)有5,000個(gè)用例的回歸用例集只有90%的通過率,那每次跑完回歸有500個(gè)失敗的用例需要分析 ? ??。但如果通過率有99%,那就只有50個(gè)用例需要分析了。

強(qiáng)大的回歸能力的背后需要的支撐能力是:

  • 優(yōu)質(zhì)的測(cè)試環(huán)境:要在預(yù)算允許的范圍內(nèi),確保測(cè)試環(huán)境的穩(wěn)定和資源充沛,這樣才能支撐起回歸的穩(wěn)定性和高頻次執(zhí)行。
  • 配置代碼化(configuration-as-code)的能力。今天常見的web-based centralized配置變更管理模式不足以支持高頻詞、高并發(fā)的回歸運(yùn)行模式。實(shí)現(xiàn)了配置代碼化,才能實(shí)現(xiàn)快速的環(huán)境部署,以及在不同的環(huán)境之間用不同的配置跑回歸。配置代碼化并不是簡單地把配置寫在config文件里面,和代碼一起打包發(fā)布。配置代碼化是對(duì)這種config文件做法的否定之否定:配置可以在git里面修改;配置也可以在配置管理系統(tǒng)里面直接修改,變更會(huì)回沉到git里面。部署的時(shí)候,部署工具會(huì)把git里面的配置值以增量的方式推到配置管理系統(tǒng)里面。

把大項(xiàng)目拆成小項(xiàng)目做的能力

如前所述,把代碼拆成小塊分多次發(fā)布,的確是會(huì)增加開發(fā)的工作量的。有不少開發(fā)同學(xué)不理解為什么要這樣做。增加了這些工作量,能讓我們的研發(fā)模式更加敏捷。這個(gè)代價(jià)是值得付出的,這些額外的時(shí)間是值得花的。

大項(xiàng)目拆成小項(xiàng)目做的一些常見套路包括:

  • 分兩部走:先向下兼容,再去掉兼容性。這就是前文舉的那個(gè)例子:X系統(tǒng)的API增加了一個(gè)新參數(shù),要求Y系統(tǒng)在調(diào)用這個(gè)API的時(shí)候必須要傳這個(gè)參數(shù)。拆成小項(xiàng)目的拆解方法是:首先,X的API新增的這個(gè)參數(shù)做成optional的,把X發(fā)布上線。然后等Y那邊的代碼改好發(fā)上線了以后,再把X的這個(gè)新參數(shù)改成required,再發(fā)布一次X。或者,也可以用一個(gè)feature flag來控制這個(gè)新參數(shù)是否required。
  • Feature flag:有了feature flag,新功能的代碼寫了一半也沒關(guān)系,可以把feature flag關(guān)掉,就算代碼發(fā)上線了也不會(huì)被執(zhí)行到。有時(shí)候,有些新功能所需要的代碼變更是改動(dòng)在老代碼里面的。這樣的代碼變更無法用feature flag來屏蔽。但這也沒關(guān)系,因?yàn)槲覀冇袕?qiáng)大的回歸能力,能盡我們所能確信這些的代碼變更至少不會(huì)break老功能、不會(huì)在發(fā)上線后造成故障。Anyway, 哪怕不是為了把大項(xiàng)目拆成小項(xiàng)目,feature flag也是需要的。Feature flag、白名單等都是很常見的continuous delivery手段。
  • Capability probing:很多新功能涉及整條鏈路上各個(gè)系統(tǒng)的改造?,F(xiàn)在往往上游系統(tǒng)的發(fā)布依賴于下游系統(tǒng)的發(fā)布。解耦這種依賴關(guān)系的一種方法是讓每個(gè)系統(tǒng)都通過一個(gè)統(tǒng)一的API接口來暴露自己當(dāng)前的能力。這樣,上游系統(tǒng)可以判斷下游系統(tǒng)當(dāng)前是否支持某個(gè)新功能所需要的能力Foo(例如,某種支付渠道),根據(jù)結(jié)果走不同的code path。
  • 按域獨(dú)立發(fā)布也是一種很成熟的拆分的方法。按域獨(dú)立發(fā)布,實(shí)現(xiàn)域和域之間的解耦,能減少每次發(fā)布的系統(tǒng)的數(shù)量,降低發(fā)布風(fēng)險(xiǎn),增加發(fā)布的靈活度。

大項(xiàng)目拆成小項(xiàng)目,還需要有比較強(qiáng)的需求拆分的能力:能夠把一個(gè)全鏈路級(jí)別的需求文檔拆分成域級(jí)別、系統(tǒng)級(jí)別的需求,這樣每個(gè)域、每個(gè)系統(tǒng)可以“分而治之”。

如何提高一個(gè)研發(fā)團(tuán)隊(duì)的“代碼速度”?

Code Velocity和質(zhì)量、線上穩(wěn)定性的關(guān)系

從上面的分析可以看出來,提高code velocity并不是以犧牲質(zhì)量為代價(jià)的。上面這些提高code velocity的手段,并沒有cut corner,并沒有降低質(zhì)量標(biāo)準(zhǔn),并沒有比今天少執(zhí)行任何測(cè)試。即便是頻繁的把代碼合回master,即便是把大項(xiàng)目拆成小項(xiàng)目做,該運(yùn)行的各種驗(yàn)證和測(cè)試還是繼續(xù)運(yùn)行。而且,為了要提高code velocity,實(shí)行了代碼門禁,建設(shè)了強(qiáng)大的跑回歸的能力,反而是對(duì)質(zhì)量有提高作用的。

提高code velocity也并不會(huì)降低線上穩(wěn)定性。把大項(xiàng)目拆成小項(xiàng)目做、更加頻繁的發(fā)布小塊代碼,能夠降低單次發(fā)布的風(fēng)險(xiǎn);發(fā)布中如果出了問題,因?yàn)閜ayload小,排查和回滾也更方便。另外,在投入資源提高code velocity的同時(shí),我們不會(huì)降低對(duì)故障發(fā)現(xiàn)能力、止血能力、應(yīng)急能力、監(jiān)控核對(duì)等能力的投入。提高code velocity不會(huì)導(dǎo)致線上技術(shù)風(fēng)險(xiǎn)防控體系變?nèi)酢?/p>

將來

如果一個(gè)團(tuán)隊(duì)的“能頻繁的把代碼合回master”的能力做得足夠好了,就可以完全拋棄項(xiàng)目分支和迭代分支,每一個(gè)commit都直接checkin進(jìn)master,而且master分支每天都有若干個(gè)可以發(fā)布的版本???,每個(gè)版本都可以用一個(gè)不同的release分支來保存。這就是所謂的“主干開發(fā)、分支發(fā)布”(Trunk-based Development)模式了。

到那時(shí)候,就有做到“天天發(fā)”的能力了。那時(shí)候,代碼從commit到上線可能平均只需要兩三天時(shí)間。那時(shí)候,因?yàn)橛辛恕疤焯彀l(fā)”的能力,甚至連緊急發(fā)布都不怎么需要了。

如果你希望加入百度天津服務(wù)中心,可以隨時(shí)與我們直接聯(lián)系。爬蟲、產(chǎn)品、Java開發(fā)、測(cè)試開發(fā)和工具開發(fā)等崗位虛位以待,有興趣的童鞋可發(fā)簡歷至:

iamjasonchoi@qq.com

【注】

1.一般會(huì)有兩個(gè)為期四周的迭代并行,每個(gè)迭代有自己的目標(biāo)發(fā)布窗口。發(fā)布窗口一般是每兩周一次。

2.“分支開發(fā)、主干發(fā)布”的開發(fā)模式來自于A successful Git branching model。但這種模式在實(shí)踐中是有不少問題的(參見A succesful Git branching model considered harmful)。更好的模式是“主干開發(fā)、分支發(fā)布”(aka. Trunk-based Development)

3.主分支可以是master,也可以是項(xiàng)目分支或者迭代分支。

4.單元測(cè)試和接口測(cè)試看代碼覆蓋率,回歸測(cè)試看業(yè)務(wù)覆蓋率。這在行業(yè)內(nèi)的一部分開發(fā)和測(cè)試之間已經(jīng)形成共識(shí)了。

5.當(dāng)然,我們可以用技術(shù)的手段使得分析500個(gè)失敗的用例變得更容易。但這并不應(yīng)該成為我們不去提高通過率的理由。

6.版本:對(duì)于“大庫模式”(monolithic repo)來說就是一個(gè)commit,對(duì)于“小庫模式”來說就是每個(gè)repo的一個(gè)commit構(gòu)成的一個(gè)“截面”。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容