初創(chuàng)公司技術(shù)困境:彈性部署與詳盡測(cè)試
作為一家初創(chuàng)公司,構(gòu)建軟件要堅(jiān)持創(chuàng)新,要有吸引力和競(jìng)爭(zhēng)力。因?yàn)?,市?chǎng)在不斷變化,新的需求也在不斷出現(xiàn)。
從軟件角度來(lái)說(shuō),要保持這樣的優(yōu)勢(shì)就意味著必須盡可能縮短文檔和開發(fā)階段所占的時(shí)間。當(dāng)然,保持軟件的彈性也很重要,提供優(yōu)秀的服務(wù)是 Algolia 的重要目標(biāo)之一。我們有許多高端用戶,搜索功能對(duì)業(yè)務(wù)有非常重要的影響,所以不能接受宕機(jī)時(shí)間,尤其是在黑色星期五之類的特殊時(shí)間段。
因此,開發(fā)者 必須在軟件的彈性與創(chuàng)新之間找到合適的平衡點(diǎn)。 這兩方面是相互牽制的:要讓軟件具有彈性,就要進(jìn)行詳盡的測(cè)試,這會(huì)消耗大量精力,占用我們進(jìn)行創(chuàng)新的時(shí)間。因此,一個(gè)比較好的折衷方案就是在生產(chǎn)環(huán)境進(jìn)行測(cè)試。
為什么要在生產(chǎn)環(huán)境進(jìn)行測(cè)試?
在生產(chǎn)環(huán)境進(jìn)行測(cè)試就是把新代碼發(fā)布到生產(chǎn)環(huán)境中,直接用真實(shí)的生產(chǎn)數(shù)據(jù)和流量進(jìn)行“測(cè)試”的過(guò)程。與之形成對(duì)比的就是運(yùn)行全面的測(cè)試用例集。這個(gè)風(fēng)險(xiǎn)很大,開發(fā)者的第一直覺(jué)肯定不要這么做。但隨著軟件規(guī)模的發(fā)展,你會(huì)發(fā)現(xiàn)進(jìn)行詳盡的測(cè)試越來(lái)越不可能了。
讓我們看看 Algolia 引擎。我們有二十多個(gè)查詢參數(shù)。假設(shè)它們?nèi)?Boolean 類型,那么要運(yùn)行的用例總數(shù)就會(huì)達(dá)到一百萬(wàn)個(gè),二十個(gè)參數(shù),每個(gè)有兩個(gè)可能值,那就是 2^20 種可能的場(chǎng)景。
談到與測(cè)試相關(guān)的工作所要消耗的時(shí)間,有三方面要考慮:
- 寫測(cè)試用例的時(shí)間
- 維護(hù)測(cè)試用例的時(shí)間
- 運(yùn)行測(cè)試用例的時(shí)間
寫出一百萬(wàn)個(gè)測(cè)試用例來(lái),這個(gè)工作量已經(jīng)很驚人了。一旦寫出來(lái),它們就成了項(xiàng)目?jī)?nèi)容的一部分。就像維護(hù)別的源代碼一樣,也要花精力去維護(hù)它們,所以每次軟件迭代所要做的事情就更多了。
假設(shè)你的團(tuán)隊(duì)足夠大,有充足的人力可以編寫和維護(hù)這些測(cè)試用例,但運(yùn)行它們一樣需要不少時(shí)間。假設(shè)運(yùn)行每個(gè)測(cè)試用例只需要 10 毫秒,那全運(yùn)行一遍就需要 2 小時(shí) 45 分鐘。不管代碼中有什么更新,都需要花 2 小時(shí) 45 分鐘才能驗(yàn)證完。
客戶購(gòu)買我們的產(chǎn)品不止是看中了當(dāng)前已經(jīng)具備的功能,更是看中了未來(lái)將會(huì)發(fā)布的功能。他們希望我們可以定期發(fā)布新功能,幫助他們成長(zhǎng),變得更容易創(chuàng)新和更具有彈性。因此,我們必須提升自己的效率。
在開發(fā)新功能時(shí),我們只會(huì)寫些簡(jiǎn)單的用例來(lái)驗(yàn)證功能,并對(duì)明顯的邊界條件進(jìn)行測(cè)試。要對(duì)功能進(jìn)行全面驗(yàn)證,我們會(huì)采取灰度發(fā)布的方式,直接在生產(chǎn)環(huán)境中進(jìn)行測(cè)試。這樣即使代碼中有缺陷,也可以把對(duì)客戶的影響控制得盡量小。通過(guò)這樣的方法,我們就可以按時(shí)發(fā)布新功能了。
真實(shí)案例
以我們最近重寫的一個(gè) Algolia 的核心功能做具體例子。如果要對(duì)它進(jìn)行充分測(cè)試,就要用所有可能的 Unicode 字符(超過(guò)一百萬(wàn)個(gè))對(duì)這二十幾個(gè)參數(shù)組合成的一百萬(wàn)個(gè)用例進(jìn)行測(cè)試。這樣算起來(lái)總數(shù)會(huì)超過(guò)十億次。假設(shè)運(yùn)行一個(gè)測(cè)試要用 10 毫秒,那完成全部測(cè)試內(nèi)容就要 11 天。
我們不得不尋找更好的解決方案。因此我們放棄了這十億次測(cè)試。不能因?yàn)檫@件事而顯著地拖慢我們的發(fā)布流程,我們決定在生產(chǎn)環(huán)境進(jìn)行測(cè)試。
我們最大的顧慮就是可能對(duì)用戶造成的影響,因此我們定義了要把它部署出去所必須要做的事:
- 一個(gè)漸進(jìn)式的發(fā)布流程
- 一種在好的基礎(chǔ)設(shè)施之上進(jìn)行重試的方法
- 積極主動(dòng)的問(wèn)題檢測(cè)
所有通過(guò)我們的網(wǎng)站發(fā)往 Algolia 的請(qǐng)求,最終都是由一個(gè)集群來(lái)提供服務(wù)的,集群由三個(gè)節(jié)點(diǎn)組成。每個(gè)節(jié)點(diǎn)中都包含 100% 的數(shù)據(jù),可以獨(dú)立響應(yīng)請(qǐng)求,因此可以提供健壯的部署方案。假設(shè)普通的服務(wù)器平均可用率為 95%,那這種方案可以提供 99.987% 的可用性。只有當(dāng)所有服務(wù)器全部宕機(jī)的時(shí)候,你的服務(wù)才會(huì)真的宕機(jī)。所以這種可能性是 5% x 5% x 5%,即可能會(huì)有 0.0125% 的宕機(jī)時(shí)間。
但即使是在這樣的架構(gòu)下,軟件的缺陷仍然可能造成服務(wù)中斷。因此,我們采取了漸進(jìn)式的灰度發(fā)布方案。新的軟件全部發(fā)布完成需要三天的時(shí)間。這樣的部署策略給了我們足夠的時(shí)間來(lái)發(fā)現(xiàn)問(wèn)題。
另外還有一點(diǎn)很重要的,就是我們的客戶端 API 會(huì)采用重試的策略。假如它正好把請(qǐng)求發(fā)往了一個(gè)有問(wèn)題的節(jié)點(diǎn),那么在請(qǐng)求失敗之后,它會(huì)自動(dòng)尋找另一個(gè)節(jié)點(diǎn)進(jìn)行重試,直到取得成功的響應(yīng)為止。因此最終用戶對(duì)這個(gè)問(wèn)題是沒(méi)有感知的。
在我們部署新版本的高亮功能時(shí),曾經(jīng)發(fā)生過(guò)一個(gè)標(biāo)準(zhǔn)化問(wèn)題。我們的目標(biāo)是把所有文本都轉(zhuǎn)化成標(biāo)準(zhǔn)化的格式,這樣就可以方便地對(duì)不同的輸入進(jìn)行對(duì)比。一般來(lái)說(shuō),標(biāo)準(zhǔn)化后的內(nèi)容長(zhǎng)度不會(huì)比輸入的原文更長(zhǎng),這也是高亮功能的前提。結(jié)果,我們卻發(fā)現(xiàn)有些字符在標(biāo)準(zhǔn)化之后長(zhǎng)度會(huì)增加:字母ß(德語(yǔ))就會(huì)被標(biāo)準(zhǔn)化成了“ss”。在重寫的時(shí)候,我們?cè)黾恿诉\(yùn)行時(shí)前置條件檢查,以確保標(biāo)準(zhǔn)化后的長(zhǎng)度比原來(lái)長(zhǎng)度更小,或者相等。這段代碼發(fā)揮了作用,把這個(gè)問(wèn)題暴露出來(lái)了。
當(dāng)我們把新版本代碼部署到第一個(gè)節(jié)點(diǎn)上時(shí),對(duì)于那些標(biāo)準(zhǔn)化之后長(zhǎng)度會(huì)增加的請(qǐng)求,它馬上就停止了響應(yīng)。幸虧我們的客戶端 API 有重試的功能,所以客戶沒(méi)有受到影響,沒(méi)有用戶注意到這一點(diǎn)。而在后臺(tái),我們的監(jiān)控系統(tǒng)則發(fā)出了告警,所以我們馬上對(duì)發(fā)布進(jìn)行了回滾,以保持整個(gè)集群的穩(wěn)定。通過(guò)這種辦法,我們?yōu)樽约籂?zhēng)取到了足夠的時(shí)間來(lái)理解問(wèn)題和完成代碼修復(fù),并進(jìn)行相應(yīng)的測(cè)試。
一種合適的部署方案
如果你也想在生產(chǎn)環(huán)境進(jìn)行測(cè)試的話,有三個(gè)至關(guān)重要的前提條件:
- 可復(fù)制的基礎(chǔ)設(shè)施;
- 彈性的軟件;
- 安全的部署策略;
- 可復(fù)制的基礎(chǔ)設(shè)施
在現(xiàn)在這個(gè)時(shí)代,配置一套可復(fù)制的基礎(chǔ)設(shè)施是非常容易的:所有云服務(wù)商都可以在多個(gè)虛擬機(jī)的前面提供負(fù)載均衡。就我們公司的架構(gòu)來(lái)說(shuō),我們?cè)谡麄€(gè)集群的級(jí)別復(fù)制搜索引擎的數(shù)據(jù),每個(gè)節(jié)點(diǎn)都 100% 地?fù)碛腥繑?shù)據(jù)。通過(guò)這種方式,每個(gè)節(jié)點(diǎn)都可以獨(dú)立完成對(duì)請(qǐng)求的響應(yīng)。
彈性的軟件
這一部分就要看你是怎樣構(gòu)建軟件的了。在 Algolia 引擎的代碼中,有許多關(guān)于健康狀態(tài)的檢查,會(huì)校驗(yàn)函數(shù)的前置條件是否滿足,以及是否處于期望的狀態(tài)等。當(dāng)運(yùn)行到非正常的狀態(tài)時(shí),引擎就會(huì)停止處理,以避免返回有問(wèn)題的數(shù)據(jù)。它會(huì)強(qiáng)制 API 客戶端在別的節(jié)點(diǎn)上透明地進(jìn)行重試。
安全的部署策略
這一點(diǎn)被最后提到,但它也一樣非常重要。這里提到的部署策略的主要目標(biāo),就是要在逐步發(fā)布新版本軟件的過(guò)程同時(shí),注意控制風(fēng)險(xiǎn)。
Algolia 的基礎(chǔ)設(shè)施主要包括四個(gè)環(huán)境:測(cè)試環(huán)境、準(zhǔn)生產(chǎn)環(huán)境、生產(chǎn)環(huán)境和安全環(huán)境。每個(gè)環(huán)境都有不同的 SLA:
測(cè)試環(huán)境只包含供內(nèi)部使用的集群。出故障時(shí)只影響公司內(nèi)部員工。準(zhǔn)生產(chǎn)環(huán)境包含面向外部大眾用戶項(xiàng)目的搜索集群。生產(chǎn)環(huán)境包含我們客戶的集群。安全環(huán)境包含我們最重要的 SLA 客戶的集群。根據(jù)部署策略,我們通過(guò)使用多個(gè)不同的環(huán)境來(lái)降低風(fēng)險(xiǎn)。也就是說(shuō),我們會(huì)先在對(duì)客戶影響最小的集群上進(jìn)行部署。
除了不同的環(huán)境,我們還利用了三份復(fù)制這個(gè)特性,制定了如下的 12 步部署流程:
先部署到測(cè)試環(huán)境的所有三個(gè)節(jié)點(diǎn)上;部署到準(zhǔn)生產(chǎn)環(huán)境的第一個(gè)節(jié)點(diǎn)上,再部署到生產(chǎn)環(huán)境的第一個(gè)節(jié)點(diǎn),再部署到安全環(huán)境的第一個(gè)節(jié)點(diǎn)上;觀察一天;部署到準(zhǔn)生產(chǎn)環(huán)境的第二個(gè)節(jié)點(diǎn)上,再部署到生產(chǎn)環(huán)境的第二個(gè)節(jié)點(diǎn),再部署到安全環(huán)境的第二個(gè)節(jié)點(diǎn)上;再觀察一天;部署到準(zhǔn)生產(chǎn)環(huán)境的第三個(gè)節(jié)點(diǎn)上,再部署到生產(chǎn)環(huán)境的第三個(gè)節(jié)點(diǎn),再部署到安全環(huán)境的第三個(gè)節(jié)點(diǎn)上;第一步可以幫我們發(fā)現(xiàn)分布式系統(tǒng)內(nèi)部的處理集群里,節(jié)點(diǎn)之間交互的代碼問(wèn)題。
接下來(lái)一個(gè)節(jié)點(diǎn)一個(gè)節(jié)點(diǎn)的部署,可以幫我們確認(rèn)集群內(nèi)部是否可以同時(shí)支持兩個(gè)不同的版本,以及代碼是否足夠穩(wěn)定。
為什么在部署節(jié)點(diǎn)的過(guò)程之中有兩次要觀察一天呢?因?yàn)檫@樣可以讓我們有充足的時(shí)間發(fā)現(xiàn)性能問(wèn)題、數(shù)據(jù)問(wèn)題或需要長(zhǎng)時(shí)間運(yùn)行才能發(fā)現(xiàn)的問(wèn)題。到這一步時(shí),我們就已經(jīng)解決掉大部分問(wèn)題了。接下來(lái)的部署步驟只是幫助我們發(fā)現(xiàn)一些可能的未知缺陷。
每當(dāng)發(fā)現(xiàn)新問(wèn)題時(shí),我們都會(huì)立刻將新版本代碼回滾。這樣我們提供的服務(wù)就可以恢復(fù)到一個(gè)穩(wěn)定的狀態(tài),我們也可以有充足的時(shí)間去修復(fù)問(wèn)題,并增加相應(yīng)的測(cè)試用例。
使用了這樣的方法,我們的測(cè)試用例集就是客戶的真實(shí)使用場(chǎng)景。這樣效率就非常高了,我們可以每周都發(fā)布新版本,滿足客戶的需求。盡管我們的代碼量已經(jīng)非常龐大,我們?nèi)匀蛔龅搅诉@一點(diǎn)。
好比完美更勝一籌
初創(chuàng)公司的生態(tài)環(huán)境是相當(dāng)嚴(yán)峻的。小團(tuán)隊(duì)要找到高效的方法,打造出比大公司的大團(tuán)隊(duì)更好的產(chǎn)品。
定期發(fā)布新功能的重要性,不亞于有著良好用戶體驗(yàn),可以滿足用戶需求的穩(wěn)定產(chǎn)品。選擇做足夠的測(cè)試還是選擇有足夠的測(cè)試覆蓋率并可以定期發(fā)布?對(duì)效率的需求逼著我們?cè)谶@兩者之間找到了一個(gè)中間的平衡點(diǎn)。
在增加新測(cè)試用例時(shí)你必須特別小心,因?yàn)橐牡臅r(shí)間太多了:要花時(shí)間去寫、去維護(hù)和運(yùn)行。那你知道什么時(shí)候該寫測(cè)試嗎?90-90 法則適用于這種情況:測(cè)試一個(gè)功能 90% 的內(nèi)容是非常容易而直接的,再測(cè)試剩下的 10% 會(huì)花費(fèi)相同的時(shí)間。所以根據(jù)客戶的使用情況來(lái)處理這剩下的 10% 非常重要,不應(yīng)該追求完全的覆蓋率。
為了降低風(fēng)險(xiǎn),請(qǐng)多花些時(shí)間對(duì)軟件和基礎(chǔ)設(shè)施進(jìn)行設(shè)計(jì),讓它們可以支持在生產(chǎn)環(huán)境進(jìn)行測(cè)試,并把對(duì)客戶的影響限制到最小。