怎樣才能提高研發(fā)效率?是依賴于各自獨(dú)立的本地開發(fā)測試環(huán)境,還是依賴完整的端到端測試?Lyft的這一系列文章介紹了其開發(fā)環(huán)境的歷史和發(fā)展,幫助我們思考如何打造一套適合大規(guī)模微服務(wù)的高效研發(fā)環(huán)境。本系列共4篇文章,這是第1篇。原文:Scaling productivity on microservices at Lyft (Part 1)[1]

2018年底,Lyft工程團(tuán)隊(duì)完成了將最初的PHP單體拆分為Python和Go微服務(wù)的工作,在接下來的幾年里,微服務(wù)在很大程度上成功的幫助團(tuán)隊(duì)獨(dú)立運(yùn)行和發(fā)布服務(wù)。微服務(wù)所帶來的關(guān)注點(diǎn)分離使我們能夠更快試驗(yàn)和交付特性(可以每天部署數(shù)百次),并且提供了足夠的靈活性,可以在合適的地方采用不同的編程語言,還可以根據(jù)服務(wù)的關(guān)鍵程度采用更嚴(yán)格或更寬松的需求,等等。然而,隨著工程師、服務(wù)、測試數(shù)量的增加,開發(fā)工具很難跟上微服務(wù)的爆炸式增長,拖累了生產(chǎn)率的增長。
本系列分為四部分,將介紹Lyft工程團(tuán)隊(duì)從100名工程師和少量服務(wù)發(fā)展到1000多名工程師以及數(shù)百項(xiàng)服務(wù)的過程中所使用的開發(fā)環(huán)境。我們將討論導(dǎo)致我們放棄這些環(huán)境的規(guī)?;魬?zhàn),以及從主要基于大量集成測試(通常接近端到端)的測試方法,轉(zhuǎn)變?yōu)橐元?dú)立測試組件為中心的本地優(yōu)先方法。
- 第一部分:開發(fā)和測試環(huán)境的歷史(本文)
- 第二部分:優(yōu)化快速本地開發(fā)
- 第三部分:利用覆蓋機(jī)制在預(yù)發(fā)環(huán)境中擴(kuò)展服務(wù)網(wǎng)格
- 第四部分:基于自動(dòng)驗(yàn)收測試的部署門禁
開發(fā)和測試環(huán)境的歷史
我們在綜合開發(fā)環(huán)境的第一個(gè)重大投資始于2015年,當(dāng)時(shí)我們有100名工程師,幾乎所有的開發(fā)工作都集中在一個(gè)單體PHP系統(tǒng)上,在某些用例中開始出現(xiàn)一些微服務(wù)(比如司機(jī)登錄)。
由于預(yù)計(jì)到需要服務(wù)的工程師和服務(wù)的數(shù)量將會(huì)持續(xù)增長,因此有必要采用容器化方案。我們計(jì)劃構(gòu)建一個(gè)基于docker的容器編排環(huán)境(當(dāng)時(shí)docker還處于起步階段),首先服務(wù)于開發(fā)人員的測試工作,然后再擴(kuò)展到生產(chǎn)環(huán)境,在生產(chǎn)環(huán)境中,多租戶工作負(fù)載的成本更低、擴(kuò)展速度更快,我們將因此受益。
利用Devbox進(jìn)行本地開發(fā)
Devbox是Lyft的即開即用開發(fā)環(huán)境,于2016年初發(fā)布,很快就被大多數(shù)工程師所采用。Devbox的工作方式是代表用戶管理一個(gè)本地虛擬機(jī),這樣工程師就不必安裝或更新依賴包、配置runit[2]啟動(dòng)服務(wù)、添加共享文件夾,等等。VM運(yùn)行后,只需一個(gè)命令和幾分鐘就可以獲取最新版本的鏡像、創(chuàng)建/初始化數(shù)據(jù)庫、啟動(dòng)envoy proxy sidecar[3],以及在開始發(fā)送請求前所需的一切依賴。
與之前相比,這次升級(jí)非常棒,我們手動(dòng)為每個(gè)開發(fā)人員及其負(fù)責(zé)的服務(wù)提供了一個(gè)EC2實(shí)例,這使得設(shè)置和保持更新非常繁瑣。我們第一次有了一種一致的、可重復(fù)的、簡單的方法來完成跨多個(gè)服務(wù)的開發(fā)。
利用Onebox進(jìn)行遠(yuǎn)程開發(fā)
很快就出現(xiàn)了新的需求,需要能夠與其他工程師或團(tuán)隊(duì)(如設(shè)計(jì)團(tuán)隊(duì))共享的、可長期維持的環(huán)境,因此我們打造了Onebox。Onebox本質(zhì)上是一個(gè)EC2實(shí)例上的Devbox,它有許多吸引用戶放棄Devbox的優(yōu)點(diǎn)。我們將其部署在r3.4xlarge實(shí)例上,擁有16個(gè)vCPU和122G內(nèi)存,比工程師隨身攜帶的MacBook Pro要強(qiáng)大得多。Onebox可以運(yùn)行更多的服務(wù),下載容器鏡像更快(因?yàn)榛贏WS),更不用說還可以避免VirtualBox讓筆記本電腦的風(fēng)扇聲音大的就像噴氣發(fā)動(dòng)機(jī)。

集成測試
除了單元測試以外,Onebox的云基礎(chǔ)設(shè)施也很適合在CI上運(yùn)行集成測試。服務(wù)可以簡單的在manifest.yaml文件中定義需要的依賴項(xiàng),一個(gè)臨時(shí)的Onebox會(huì)啟動(dòng)這些服務(wù),并對每一個(gè)pull request執(zhí)行測試。許多服務(wù),特別是靠近移動(dòng)客戶端的服務(wù)組合,會(huì)需要構(gòu)建大型集成測試套件來應(yīng)對異常的服務(wù)失效,并且每次事故分析通常都會(huì)以添加新的集成測試結(jié)束。有了如此靈活和強(qiáng)大的測試功能,單元測試逐漸退居次要地位。
定義要在CI中運(yùn)行的集成測試的服務(wù)示例:
name: api
type: service
groups:
- name: integration
members:
- driver_onboarding
- users
tests:
- name: integration
group: integration
預(yù)發(fā)環(huán)境(Staging environment)
Lyft的預(yù)發(fā)環(huán)境與生產(chǎn)環(huán)境幾乎相同(除了使用更少的資源,也沒有生產(chǎn)數(shù)據(jù)),所有服務(wù)都是和生產(chǎn)環(huán)境交付一致的過程部署的。盡管不是開發(fā)環(huán)境,但因?yàn)轭A(yù)發(fā)環(huán)境在端到端測試中扮演著越來越重要的角色,因此同樣值得討論。
在2017年初Devbox和Onebox發(fā)布后不久,我們還解決了另一類不斷增長的問題:負(fù)載測試。那些會(huì)造成拼車需求流量激增的事件(比如新年和萬圣節(jié)),會(huì)暴露我們系統(tǒng)的瓶頸,并且往往會(huì)導(dǎo)致宕機(jī)。為了解決這些問題,我們構(gòu)建了一個(gè)框架來模擬大規(guī)模流量。該框架針對我們的生產(chǎn)環(huán)境,協(xié)調(diào)數(shù)以萬計(jì)具有不同配置的模擬用戶(例如,模擬洛杉磯的一名經(jīng)常取消訂單的司機(jī)),并將Lyft視為黑匣子。
作為階段性測試仿真框架本身的副產(chǎn)品,我們意識(shí)到生成的流量對于一般的端到端測試也是有價(jià)值的。在預(yù)發(fā)環(huán)境中不斷測試公共接口可以為真正的部署提供很好的信號(hào)。例如,如果部署破壞了讓乘客下車的接口,部署的發(fā)起者幾乎立即就能看到錯(cuò)誤日志和警報(bào)。模擬還會(huì)持續(xù)生成用戶、車輛、支付等最新數(shù)據(jù),減少了開發(fā)過程中必須進(jìn)行的手動(dòng)測試的設(shè)置時(shí)間。隨著負(fù)載測試的努力,預(yù)發(fā)環(huán)境變得比以往任何時(shí)候都更加現(xiàn)實(shí)和有用,團(tuán)隊(duì)將PR分支部署在那里,從而可以獲得真實(shí)數(shù)據(jù)的一致的反饋,這已經(jīng)成為一種普遍現(xiàn)象。
新的問題
快進(jìn)到2020年(在將Devbox和Onebox作為容器化開發(fā)環(huán)境引入4年后),盡管我們盡了最大的努力,但“Lyft-in-a-box”風(fēng)格的環(huán)境仍然難以跟上。使用這些環(huán)境的工程師增加了十倍,現(xiàn)在有數(shù)百個(gè)微服務(wù)為更復(fù)雜的業(yè)務(wù)提供支撐。雖然在依賴關(guān)系較小的服務(wù)上開發(fā)仍然相當(dāng)高效,但大多數(shù)開發(fā)都是在已經(jīng)構(gòu)建了巨大依賴關(guān)系樹的服務(wù)上進(jìn)行的,這使得在CI上啟動(dòng)環(huán)境或運(yùn)行測試非常緩慢。
雖然這些環(huán)境和測試功能非常強(qiáng)大和方便,但卻達(dá)到了弊大于利的程度。我們構(gòu)建了一個(gè)為測試少量服務(wù)而優(yōu)化的系統(tǒng),當(dāng)服務(wù)的數(shù)量從5個(gè)增加到50個(gè),從50個(gè)增加到100個(gè),甚至更多的時(shí)候,我們沒有重新評(píng)估我們的策略。這不僅需要大量的服務(wù)來進(jìn)行維護(hù)和擴(kuò)展,而且還會(huì)因?yàn)槠仁归_發(fā)人員不斷的從整個(gè)系統(tǒng)的角度而不是從一個(gè)組件的角度來考慮而降低開發(fā)人員的生產(chǎn)力。
讓我們更詳細(xì)的看看這個(gè)問題的一些細(xì)節(jié):
擴(kuò)展性問題
由于涉及的資源數(shù)量龐大,且與類似于生產(chǎn)環(huán)境的環(huán)境存在分歧,Onebox環(huán)境的擴(kuò)展變得很不現(xiàn)實(shí)。例如,在數(shù)百個(gè)環(huán)境中運(yùn)行相同的可觀察性工具是不可行的。當(dāng)出現(xiàn)問題時(shí),很難找出確切的原因(運(yùn)行的70個(gè)服務(wù)中哪個(gè)可能有問題?),人們傾向于在放棄并在預(yù)發(fā)測試之前按幾次“reset”按鈕。
另一方面,預(yù)發(fā)環(huán)境既容易縮放,又能更忠實(shí)的反映生產(chǎn)環(huán)境。它提供了同樣的日志記錄、跟蹤和度量功能來幫助調(diào)試。部署到共享的預(yù)發(fā)環(huán)境的主要缺點(diǎn)是:(1)實(shí)驗(yàn)更改可能會(huì)破壞他人的使用環(huán)境,(2)每次只能有一個(gè)服務(wù)做出一次變更才能夠有效進(jìn)行測試,(3)由于需要同步代碼和熱加載,需要花費(fèi)更多的時(shí)間(分鐘)來構(gòu)建和部署。
維護(hù)困難
由于上述伸縮性的挑戰(zhàn),維護(hù)和優(yōu)化這些環(huán)境花費(fèi)了大量時(shí)間,導(dǎo)致技術(shù)落后。生產(chǎn)環(huán)境和預(yù)發(fā)環(huán)境已經(jīng)用Kubernetes進(jìn)行容器編排,同時(shí)切換到更小的單進(jìn)程容器鏡像。開發(fā)使用了捆綁了sidecars和其他基礎(chǔ)設(shè)施組件(指標(biāo)、日志等)的更重的多進(jìn)程鏡像,使得構(gòu)建和下載鏡像的速度更慢。
每周都有一些變更會(huì)造成問題,這些變更不會(huì)影響預(yù)發(fā)或生產(chǎn)環(huán)境,但會(huì)影響開發(fā)環(huán)境。由于大多數(shù)開發(fā)者需要運(yùn)行大多數(shù)服務(wù),一個(gè)服務(wù)的問題會(huì)造成很大的影響。一些團(tuán)隊(duì)已經(jīng)將他們所有的端到端測試轉(zhuǎn)移到預(yù)發(fā)階段,使得他們的服務(wù)在開發(fā)過程中變得越來越弱,進(jìn)一步加劇了這種問題。
問題所有權(quán)不清
在開發(fā)環(huán)境中,問題的所有權(quán)是不清楚的。誰應(yīng)該負(fù)責(zé)修復(fù)引起問題的特定服務(wù)?是啟動(dòng)這個(gè)Onebox的人、服務(wù)的負(fù)責(zé)人還是開發(fā)者基礎(chǔ)設(shè)施團(tuán)隊(duì)?在實(shí)踐中,這常常落在開發(fā)者基礎(chǔ)設(shè)施團(tuán)隊(duì)的頭上,但他們無法診斷和解決與應(yīng)用程序相關(guān)的問題(例如,配置變更導(dǎo)致應(yīng)用程序在啟動(dòng)時(shí)崩潰)。
臃腫的測試
笨重的集成測試套件已經(jīng)成為生產(chǎn)力的一大消耗。長達(dá)一小時(shí)的測試套件隨處可見,運(yùn)行在復(fù)雜的分片基礎(chǔ)設(shè)施上,通過自動(dòng)重試來彌補(bǔ)不穩(wěn)定的環(huán)境造成的問題。造成這一問題有兩個(gè)主要的驅(qū)動(dòng)因素,依賴關(guān)系的膨脹和測試本身。由于依賴的傳遞性,依賴的服務(wù)會(huì)在服務(wù)所有者沒有注意到的情況下逐漸增加,從而消耗掉測試時(shí)間。測試套件本身也在穩(wěn)步增長,盡管我們會(huì)在出現(xiàn)問題時(shí)添加測試,但很少會(huì)因?yàn)榧僭O(shè)現(xiàn)有的測試有作用而被刪除。
那么,為什么我們要為合并一個(gè)PR而花費(fèi)幾小時(shí)的等待時(shí)間呢?當(dāng)然是因?yàn)榭梢栽赽ug進(jìn)入生產(chǎn)環(huán)境之前捕獲它們!但通過在實(shí)踐中進(jìn)一步檢驗(yàn),這一理論并不成立。對我們開發(fā)得最活躍的一些服務(wù)的集成測試進(jìn)行分析發(fā)現(xiàn),80%或更多的測試要么是不必要的(例如,過時(shí)的或現(xiàn)有單元測試的副本),要么可以重寫,從而可以在不依賴外部的情況下以較短的時(shí)間運(yùn)行。當(dāng)測試失敗時(shí),大多數(shù)都是誤報(bào),而這將耗費(fèi)數(shù)小時(shí)的調(diào)試時(shí)間,其余測試通常會(huì)在通過預(yù)發(fā)或金絲雀環(huán)境并造成生產(chǎn)問題之前被捕獲。
# 2013 (monolith), duration: 1 minute
def test_driver_approval():
"""
Requires:
- api
"""
user = get_user()
approve_driver(user)
assert user.is_approved
# ------------------------------------------------------------ #
# 2015 (mostly monolithic, a few services), duration: 3 minutes
def test_driver_approval():
"""
Requires:
- api (monolith)
- users
- mongodb
- driver_onboarding
- mongodb
- redis
"""
user = user_service.create_user()
user = driver_onboarding_service.approve_driver(user)
assert user.is_approved
# ------------------------------------------------------------ #
# 2018 (post-decomp, microservices), duration: 20 minutes
def test_driver_approval__california():
"""
Requires:
- users
- redis
- experimentation
- fraud
- dynamodb
- messaging
- mongodb
- driver_onboarding
- messaging
- email
- experimentation
- dmv_checks
- vehicles
- payments
"""
user = user_service.create_user()
user = driver_onboarding_service.approve_driver(user)
assert user.is_approved
def test_driver_approval__newyork():
# ...
def test_driver_approval__montreal():
# ...
隨著我們繼續(xù)分離出新的微服務(wù),集成測試變得越來越笨拙。
改變過程
在大約一年前開始將我們的開發(fā)環(huán)境遷移到Kubernetes之后,工程資源的變化成為了我們縮小并重新審視發(fā)展方向的催化劑。維護(hù)基礎(chǔ)設(shè)施以支持隨需應(yīng)變的環(huán)境變得過于昂貴,而且隨著時(shí)間的推移會(huì)變得越來越糟。要解決這種情況,我們需要對開發(fā)和測試微服務(wù)的方式進(jìn)行更徹底的改變,是時(shí)候用對由數(shù)百個(gè)微服務(wù)組成的系統(tǒng)具有可持續(xù)性的替代方案來取代在CI上的Devbox、Onebox和集成測試了。
仔細(xì)觀察開發(fā)人員是如何使用現(xiàn)有環(huán)境的,我們確定了三個(gè)關(guān)鍵的工作流(在下圖中用紫色表示),這三個(gè)工作流對維護(hù)非常重要,并且需要進(jìn)行投資:

- 本地開發(fā): 對于任一給定服務(wù),運(yùn)行單元測試或啟動(dòng)web服務(wù)器并發(fā)送請求都應(yīng)該非常簡單快捷。
- 手動(dòng)端到端測試: 測試特定變更在更大的系統(tǒng)中如何執(zhí)行是許多工程師依賴的關(guān)鍵工作流程。我們希望擴(kuò)展預(yù)發(fā)測試,使開發(fā)人員可以更容易、更安全的獨(dú)立進(jìn)行測試。
- 自動(dòng)端到端測試: 盡管我們過度依賴于這種測試,但如果沒有自動(dòng)化的端到端測試提供的信心,我們無法繼續(xù)每天交付數(shù)百次變更。我們將保留一小部分有價(jià)值的測試作為驗(yàn)收測試,在部署到生產(chǎn)環(huán)境時(shí)運(yùn)行。
本系列后續(xù)文章將深入研究這三個(gè)領(lǐng)域,我們將討論相關(guān)問題、如何處理以及學(xué)到了什么。
References:
[1] Scaling productivity on microservices at Lyft (Part 1): https://eng.lyft.com/scaling-productivity-on-microservices-at-lyft-part-1-a2f5d9a77813
[2] runit - a UNIX init scheme with service supervision: http://smarden.org/runit/
[3] Envoy Proxy: https://www.envoyproxy.io/
你好,我是俞凡,在Motorola做過研發(fā),現(xiàn)在在Mavenir做技術(shù)工作,對通信、網(wǎng)絡(luò)、后端架構(gòu)、云原生、DevOps、CICD、區(qū)塊鏈、AI等技術(shù)始終保持著濃厚的興趣,平時(shí)喜歡閱讀、思考,相信持續(xù)學(xué)習(xí)、終身成長,歡迎一起交流學(xué)習(xí)。
微信公眾號(hào):DeepNoMind