阿里巴巴代碼平臺架構的演進之路
代碼平臺的發(fā)展之路
相信很多做后端服務的同學在看到單機、讀寫分離、分片這些字眼一定不會覺得陌生。沒錯,代碼服務在發(fā)展的開始階段面臨的問題和其他web服務大體一致,所以使用的解決方案也大體一致。
單機服務
眾所周知,Git是一種分布式的版本控制軟件,每個人的本地都有一份完整的代碼版本數(shù)據(jù)。但為了解決多人協(xié)同開發(fā)和流程管控(評審、測試卡點等),需要一個集中式的遠端中央倉庫來完成這些功能,這就是單機服務的來源。
讀寫分離
隨著協(xié)同人數(shù)的增多,以及提交數(shù)量的變多,單機升配也無法解決調(diào)用量過高的問題。而通過統(tǒng)計我們發(fā)現(xiàn),對于Git服務讀寫比例大概是20:1,為了保證主鏈路提交的正常,我們做了讀寫分離,來分散壓力。通過主備同步來完成數(shù)據(jù)的同步,并使用一寫多讀的架構來擴展讀服務的能力。
分片
但無論如何做讀寫分離,所有機器的規(guī)格始終是一樣的。雖然倉庫數(shù)量的增加、平臺用戶的增加,無論是存儲還是計算都會達到單機所能承載的極限。這個時候我們采用了分片的方式,即將不同的倉庫劃分到不同的片中,而每個片都是一個完整的讀寫分離架構。當一個請求到來時,服務會根據(jù)查詢庫特征信息,決定這個倉庫所在的分片,而后又根據(jù)接口的讀寫特性,將請求轉發(fā)到具體的機器上。有點類似于數(shù)據(jù)庫中的分庫分表。這樣以來,通過分片+讀寫分離的架構,我們理論上可以支持水平上的無限擴容。
問題及思考
那么分片+讀寫分離是否是解決代碼服務大規(guī)模、高并發(fā)問題的銀彈呢?在我們看來,答案是否定的。
伴隨著代碼服務體量的發(fā)展,我們解決的核心問題一直以來主要是兩個:
集中式的Git存儲服務,既是I/O密集型也是計算密集型(Git的壓縮算法);
文件數(shù)量眾多,單個倉庫的文件數(shù)量也可能是十萬甚至百萬級,對數(shù)據(jù)一致性的保證和運維可靠性的挑戰(zhàn)極大。
實際上,代碼平臺架構的發(fā)展,就是在這兩個問題之間找平衡,以在一定規(guī)模情況下保證整個平臺的穩(wěn)定性。但一直沒有根本性地解決掉這兩個問題。然而隨著規(guī)模逐步上漲,上述的兩個核心問題引發(fā)的劣勢又逐步變得明顯起來。
代碼服務主備架構:有狀態(tài)服務帶來的問題
對高可用系統(tǒng)比較熟悉的同學,從名字上應該就看出些許端倪。主備架構的讀寫分離方案其天然引入的就是有狀態(tài)服務的問題。
整個系統(tǒng)的請求流轉,主要分兩次轉發(fā):
通過統(tǒng)一的代理層,可將用戶的不同客戶端請求轉發(fā)到對應的系統(tǒng)上,如Git命令行客戶端的SSH協(xié)議和HTTP協(xié)議、頁面的訪問及API接口請求等。
然后對接的模塊會將用戶不同協(xié)議的請求轉換為內(nèi)部的RPC調(diào)用,并通過統(tǒng)一的RPC代理模塊RPC Proxy和分片服務Shard Config將請求按分片和讀寫轉發(fā)到對應的服務上。
讀和寫操作如何處理
如果是一個寫操作(如:push,從頁面上對文件、分支等進行新增、刪除、修改等操作),請求則會落到倉庫對應分片的RW/WO服務器上,在RW/WO服務寫入完成以后,再通過Git協(xié)議的同步方式,同步到同分片的其他機器上,這樣一次寫操作就完成了。而對于讀操作,則是在倉庫對應分片中隨機找一個RO的機器進行轉發(fā)。
問題分析
當某個分片的主節(jié)點發(fā)生異常(服務crash或服務器宕機等),分片內(nèi)的機器狀態(tài)會發(fā)生變化。原本的RW/WO狀態(tài)會置為不可用,Backup的機器會取代原來的RW/WO服務來承接寫操作的請求。
從系統(tǒng)的角度上分析,主備架構存在以下四個問題:
1、可用性:
由于讀寫操作是分離的,所以在寫服務器failover期間,服務的寫功能是無法使用的;
對于單片而言,寫操作是單點的,一臺服務波動則整個分片都波動。
2、性能:
主備機器在同步上需要額外的時間開銷。對于松散文件、文件壓縮的Git倉庫,這個耗時比單文件拷貝耗時更久。
3、安全:
用戶側的短時間內(nèi)的瞬時操作,對于節(jié)點同步來說可能是并發(fā)的,無法保證同步中的事務順序。
4、成本:
同分片寫,主備機器要求規(guī)格完全一致。但由于接收的請求不同,存在嚴重的資源消耗不均;
由于同步的小文件多,對延時敏感,跨機房異步同步,機器規(guī)格一比一復制。
這四個系統(tǒng)上缺陷帶來的問題,在一定使用規(guī)模和服務穩(wěn)定性要求下是可以容忍的。但隨著商業(yè)化的深入和用戶規(guī)模的增長,這些問題的解決變得迫在眉睫。接下來我將和大家分享在過去的一年中,我們團隊對這些架構上問題的思考和解決思路。
代碼服務多副本架構:消滅有狀態(tài)的存儲服務
在上一個小節(jié)中,我們已經(jīng)比較清晰地認識到架構上面臨的四個問題主要是有狀態(tài)服務帶來的。那么在新架構的設計中,我們的目標只有一個——消滅有狀態(tài)的服務。目標有了,如何去實現(xiàn)?我們首先對業(yè)內(nèi)幾個流行的分布式系統(tǒng)做了深入的了解和學習,比如ETCD、Paxos協(xié)議的學習等。同時我們也學習了代碼服務的老大哥——Github開源的寥寥文章,但Github認為分布式架構是他們的核心競爭力,所以可參考的文章較少,但從這些文章中我們依然深受啟發(fā)。首先對于任何架構升級,要能做到“開著飛機換引擎”,讓架構軟著陸是架構升級的保底要求。因此在grpc的代理層之上沒有任何的改動。從內(nèi)部的RPC調(diào)用以下則是我們新架構實施的地方。
和前一節(jié)的不同,在新的底層設計中:
我們希望設計一個GPRC D-PROXY的模塊,能將gRPC的請求做到寫時復制,從而達到多寫的第一步;
其次在Proxy Config的模塊中來存放倉庫、副本、機器等元數(shù)據(jù);
再次通過一個分布式的鎖(D-Lock)來完成對倉庫級別的鎖控制;
最后我們希望有一個快速的算法能計算倉庫的checksum,快速識別倉庫的副本是否一致。
通過這些模塊的組合完成倉庫多副本的并發(fā)寫入、隨機讀取,我們就可以去除底層存儲節(jié)點的狀態(tài),從而能將倉庫的副本離散到不同機房中。此外得益于機器與副本的解耦,每個服務器都可以有獨立的配置,這也為后續(xù)的異構存儲打下了基礎。
代碼服務多副本架構的實現(xiàn)
有了基礎的設計,通過MVP的實現(xiàn),我們驗證了這個架構的可行性。并通過一年多的時間進行開發(fā)和壓測,最終將我們的多副本架構成功上線并逐步開始提供服務。在實現(xiàn)過程中,我們?yōu)檫@個系統(tǒng)起了一個頗有意義的名字——伽利略,因為在我們的多副本架構中,最小的副本數(shù)是3,而伽利略在發(fā)明了天文望遠鏡觀察到火星的衛(wèi)星恰好就是3個。我們希望秉承這個不停探索的精神,所以起了這個有意義的名字。在具體的設計中,我們通過對gRPC proxy模塊的改寫,讓來自用戶的一次寫操作完成寫復制,并通過對git的改寫以支持分段提交。我們基于git的數(shù)據(jù)特性,編寫了checksum的模塊,可以對git倉庫進行快速的全量和增量一致性計算。
伽利略架構在系統(tǒng)架構層面解決了之前主備架構帶來的問題:
1、可用性提升:多寫和隨機讀讓底層的git存儲服務不存在寫單點和failover的切換問題;
2、寫性能提升:副本并發(fā)多寫,讓底層的副本間不存在主備復制的時間開銷,性能比肩單盤;
3、安全:寫操作的分段提交和鎖的控制,在解決分布式系統(tǒng)寫安全的基礎上也控制了用戶寫操作的事務性;
4、成本大幅度降低:
每個副本都會承擔讀寫操作,水位平均
副本與機器解耦,釋放機器的規(guī)格限制,可以根據(jù)倉庫的訪問熱度采用不同涉及機型和存儲介質(zhì)
說了這么多,當用戶執(zhí)行push后,在伽利略中會發(fā)生什么呢?我們用一個動畫來簡單演示下:
用戶3和用戶4是兩個著急下班的同學,他們本地master分支分別是提交3和提交4;
底部灰色的圓圈代表的是服務端一個倉庫三個副本的裝填,可以看到其中兩個master分支指向的是提交2,而其中有一個可能因為網(wǎng)絡或者其他原因導致其master的指向為1;
當用戶3和用戶4前后提交了代碼,由于用戶3更快達到服務,所以會率先啟動寫的流程;
在一個寫的流程開始,首先會對當前倉庫副本的一致性進行檢查,系統(tǒng)很容易就發(fā)現(xiàn)落后的副本1與其他兩個副本不一致,因此會將其標記為unhealthy。對于unhealthy的副本,是不會參與伽利略寫或讀的任何操作;
當用戶4的請求也被受理后,也會觸發(fā)一致性檢查,但是由于副本1已經(jīng)被標記為unhealthy,所以對于用戶4的流程,這個副本就“不可見”了;
在用戶3的進程檢查后,發(fā)現(xiàn)多數(shù)副本是一致,則認為是可以寫入的,便會將非引用數(shù)據(jù)傳輸?shù)礁北局?;同理用?的進程也是一樣,且因為非引用數(shù)據(jù)不會變更分支信息,所以不需要加鎖,可以同時操作;
當用戶3的進程收到非引用的數(shù)據(jù)傳輸成功后,便要開始進行引用的更新,這時第一步先去搶占這個倉庫的鎖。很幸運鎖是空閑的,用戶3的進程加鎖成功,開始改寫副本的引用指向;
當用戶4的進程也完成了非引用數(shù)據(jù)傳輸后也開始進行引用的更新。同樣,再更新操作前要去搶鎖,但發(fā)現(xiàn)鎖已經(jīng)被占用,則用戶4的進程進入等待階段(如果等待超時,則直接告訴用戶4提交失?。?br />
當用戶3的進程改寫引用成功后,會釋放掉倉庫鎖。此時服務端的倉庫的master分支已經(jīng)被修改為指向3。用戶3可以開心下班了;
當鎖被釋放后,用戶4的進程快速搶占,并嘗試修改引用指向,但發(fā)現(xiàn)更改的目標引用master已經(jīng)和之前不同(之前是2,現(xiàn)在是被用戶3更改的3),此時用戶4的進程會失敗并釋放掉鎖。在用戶4的本地會收到類似“遠端分支已經(jīng)被更新,請拉取最新提交后再推送”的類似提示。
以上就是多個用戶同時提交時,伽利略系統(tǒng)內(nèi)部發(fā)生的事情。眼力好的同學可能會問,那在系統(tǒng)中被判定unhealthy的副本會怎么辦呢?這個就是剛才在架構實現(xiàn)中提到的運維系統(tǒng)會做的事務補償了,運維系統(tǒng)在收到unhealthy上報和定時掃描后都會觸發(fā)對unhealthy副本的修復,修復后的副本在確認一致后會置為healthy并繼續(xù)提供服務,當然這個過程在判斷修復是否一致也是加鎖操作的。
代碼托管運維管理平臺
除了上小節(jié)提到的副本修復,在系統(tǒng)運行中,還需要很多的運維操作。以往這些都是由運維同學操作的。Git服務涉及數(shù)據(jù)和同步,比單純的業(yè)務系統(tǒng)要運維復雜度要高,對應運維風險也比較高。在過去的一年中我們結合以往的運維經(jīng)驗,以及新架構的需要,以工具化、自動化、可視化為目標,為伽利略系統(tǒng)打造了對應的運維管理平臺:
通過這個平臺我們大大提升了運維的質(zhì)量、也充分釋放了運維人員的精力。