Travis CI:最小的分布式系統(下)
大約1年之前,我們發現當時的架構有些不合理了。尤其是Hub,它上面承擔了太多的任務。Hub要接收新的處理請求,處理并推動構建日志,它要同步用戶信息到Github,它要通知用戶構建是否成功。它跟一大群外部API打交道,全部都是在一個進程中處理。
Hub需要繼續演化,但它卻不太可能自由擴展。Hub只能以單進程的方式運行,也因此成為我們最有可能發生的單點錯誤。
Github API是一個有趣的例子。我們是Github API的重度用戶,依靠這些API我們的構建任務才能執行。無論是獲取構建配置信息,更新構建狀態,還是同步用戶數據,都離不開這些API。
回顧歷史,當這些API中的某一個不可用,hub就會停止當天正在處理的任務,而轉移到下一個任務上。所以,當Github API不可用時,我們的很多構建都會失敗。
我們對這些API賦予了很多信任,當然現在也一樣,但是說到底,這些是我們不能掌控的資源。這些資源不是我們自己來維護,而是由另外的一個團隊,在另外的網絡系統中,有他們自身的弱點。
我們過去沒有這樣想。過去我們總是把這些資源當做我們可以信賴的朋友,以為他們隨時都會響應我們的請求。
我們錯了。
一年之前,這些API無聲無息的對某個功能做了修改。這個一個雖然沒有文檔說明,但是我們非常依賴的功能。這個功能就是這么消無聲息的被修改了,于是導致了我們這邊的問題。
結果,我們的系統完全亂套了。原因很簡單,我們把Github API當做了自己的朋友,我們耐心的等待這些API回應我們的請求。每一次新的提交,我們都等了很長的時間,每次都有幾分鐘。
我們的超時設置太寬松了。因為這個原因,當對Github API的請求最終超時時,我們的系統也已經發生錯誤。那天晚上我們花了很長的時間處理這個問題。
即便是小問題,當某個時刻湊到一塊了,也能夠破壞一個系統。
我們開始隔離這些API請求,設置更短的超時時間。為了保證我們不會因為Github方面的中斷而導致構建失敗,我們同樣加了重試機制。為了保證我們能夠更好的處理外部的異常,我們的每一次重試都會依次延長過期時間。
你應該接受那些在你控制之外的外部API隨時可能失敗的現實。如果你不能將這些失敗完全隔離,你就有必要考慮如果去處理他們。
如何去處理每一個單點錯誤場景是基于商業上的考慮。我們可以承受一個構建出異常嗎?當然,這不是世界末日。如果因為外部系統的問題,我們能夠讓數百個構建出現異常嘛?我們不能,因為無論什么原因,這些構建異常夠影響到了我們的客戶。
Travis CI最初是一個好心的家伙。它總是很樂觀地認為每一件事情總會正確的工作。
很不幸,那不是事實。每一件事在任何時刻都可能導致混亂,但是我們的代碼卻從來沒考慮過這一點。我們做過很多努力,現在我們仍然在努力,去改變這種情況,去提高我們的代碼處理外部API或者系統內部異常的能力。
回到我們的系統,hub承擔的任務很容易導致異常,所以我們將其分割成很多的小應用。每個小應用都有其自身的目的和承擔的任務。
做好任務隔離,這樣我們就能更輕松的擴展系統。大部分任務都是直接的從上到下運行的。
現在我們有了三個進程;處理新的提交,處理構建通知,和處理構建日志。
突然之間,我們有了新的問題。
#p#
雖然我們的應用已經分割開了,但是他們都依賴一個叫做travis-core的核心。核心包括數量很多的Travis CI所有部分的商業邏輯。這可真是一個big ball of mud。
對核心的依賴意味著核心代碼的改動可能影響到所有應用。我們的應用是按照各自的任務進行劃分,但是我們的代碼不是。
我們現在還在為最早的架構設計而支付學費。如果你增加功能,或是修改代碼,對公用部分的一點點改變都可能帶來問題。
為了保證所有應用的代碼都可以正常工作,當travis-core做了修改,我們需要部署所有應用去驗證。
任務并不僅僅意味著你必須從代碼的角度將其分隔。任務本身也同樣需要物理分隔。
復雜的依賴影響了部署,同樣,它也影響了你交付新代碼、新功能的能力。
我們慢慢的將代碼的依賴變小,真正的從代碼隔離開每個應用間的任務。幸運的是,代碼本身已經有很好的隔離程度了,所以這個過程顯得容易多了。
有一個應用需要特別關注,因為它是我們做擴展最大的挑戰。
日志的作用有兩個:當構建日志的數據塊通過消息隊列進來時,更新數據庫對應行,然后推送它到Pusher用于實時的用戶界面更新。
日志塊以流的形式在同一個時間從不同的進程中進來,然后被一個進程處理。這個進程每秒最高可處理100個消息。
一般情況下這樣處理日志流的方式也相當OK,但是這也意味著我們很難處理某些時刻突然增長的日志消息,因此這個唯一的進程對于我們系統的擴展會成為一個很大的障礙。
問題在于,進程是按照這些消息到達消息隊列的先后順序來進行處理的,而Travis CI中的所有事情都依賴于這些消息。
更新數據庫里的一條日志流意味更新包含所有日志的一行數據。更新用戶界面的日志當然意味著在DOM樹上附加一個新的結點。
為了解決這個棘手的問題,我們需要改很多代碼。
但是首先,我們需要理清楚什么才是一個更好的解決方案,好的解決方案應該是能夠讓我們很方便的擴展日志處理的部分。
我們決定讓處理的順序作為消息本身的一個屬性,而不是隱含的依賴于它們被放進消息隊列的順序。
這個想法是受到Leslie Lamport于1978年發表的一篇論文《Time, Clocks, and the Ordering of Events in a Distributed System》的啟發。
在這篇論文中,Lamport描述了在分布式系統中,使用遞增計數器來保留事件發生的順序的方法。當一個消息被發送,發送者會在消息被接收者接收到之前增加計數器的值。
我們可以簡化那個想法,因為在我們的場景中一個日志塊只能來自一個發送者。進程只要不斷增加計數器的值,就可以讓之后的日志收集工作變得簡單。
剩下的工作就是根據計數器的值來對日志塊進行排列了。
困難之處在于,這樣設計之后等同于允許向數據庫寫入小的日志塊,這些小日志塊只有在對應任務結束后才會寫入到完整的日志中。
但是這會直接影響到用戶界面。我們不得不面對消息以無序的方式到來。這個變化的確影響的范圍大了些,但它反過來簡化了很多部分的代碼。
從表面看,這個改動似乎無關緊要。但是依賴于你本不需要依賴的順序會帶來更多潛在的復雜性。
我們現在不用依賴于信息是如何傳送的,因為現在我們可以在任何時間得到他們的順序。
我們修改了不少代碼,因為那些代碼做了一個假設,任何信息都是順序過來的,而這個假設是完全錯誤的。在分布式系統中,事件可以以任何順序在任何時間到達。我們只需要確保之后我們可以將這些片段重新組裝回去。
你可以從我們的博客獲取這個問題更詳細的說明。
到了2013年,我們每天已經在運行45000次構建。我們還是在為早先的設計付出著代價,但是我們也在慢慢的改進設計。
我們現在還有一個麻煩。系統所有的組件還是在共享同一個數據庫。如果數據庫出現問題,自然的所有組件都會出現問題。這個故障上周我們剛剛遇見一次。
這同樣意味著日志寫入的數量(目前可以達到每秒300次)影響到了我們API的性能,當用戶瀏覽我們的用戶界面時可能會慢一點。
另外,當我們從構建任務的數量上考慮時,我們的下一個挑戰就是如何去擴展我們的數據容量。
Travis CI在500臺構建服務器上運行,這已經不能再算是一個小的分布式系統了。我們現在正著手解決的問題還是從一個相當小的維度來考慮的,但即便在那個維度上,你也能夠遇到很多有趣的挑戰。根據我們的經驗,簡單直接的解決方案總是比那些更復雜的要好。
原文鏈接:http://www.paperplanes.de/2013/10/18/the-smallest-distributed-system.html