【感謝@NULL_文龍 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。】
背景補充:日本網民一直都有在電視節目播出的同時,在網路平臺上吐槽或跟隨片中角色喊出臺詞的習慣,被稱作“實況”行為。宮崎駿監督的名作動畫《天空之城》於2013年8月2日晚在NTV電視臺迎來14次電視重播。當劇情發展到男女主角巴魯和希達共同念出毀滅之咒“Blase”時,眾多網友也在推特上同時發出這條推特,創造了每秒推特傳送數量的新紀錄。
根據推特日本官方帳號,當地時間8月2日晚11時21分50秒,因為“Blase祭”的影響,推特傳送峰值達到了143,199次/秒。這一數字高於此前推特傳送峰值的最高紀錄,2013年日本時區新年時的33,388次/秒。更高於拉登之死(5106次/秒)、東日本大地震(5530次/秒)、美國流行天后碧昂斯宣佈懷孕(8868次/秒)。(前兩段摘編自微漫畫)
下圖是峰值發生的鄰近時間段的訪問頻率圖,我們通常每天的推文數是 5 億條,平均下來每秒大概產生5700條。這個峰值大概是穩定狀態下訪問量的25倍!
在這個峰值期間,使用者並沒有感覺到暫時性的功能異常。無論世界上發生了什麼,Twitter始終在你身邊,這是我們的目標之一。
“新的Tweets峰值誕生:143,199次Tweets每秒。通常情況:5億次每天;平均值5700Tweets每秒”
這個目標在3年前還是遙不可及的,2010年世界盃直接把Twitter變成了全球即時溝通的中心。每一次射門、罰球、黃牌或者紅牌,使用者都在發推文,這反覆地消耗著系統頻寬,從而使其在短時間內無法訪問。工程師們在這期間徹夜工作,拼命想找到並實現一種方法可以把整個系統的負載提升一個量級。不幸的是,這些效能的提升很快被Twitter使用者的快速增長所淹沒,工程師們已經開始感到黔驢技窮了。
經歷了那次慘痛的經歷,我們決定要回首反思。那時我們決定了要重新設計Twitter,讓它能搞定持續增長的訪問負載,並保證平穩執行。從那開始我們做了很大努力,來保證面臨世界各地發生的熱點事件時,Twitter仍能提供穩定的服務。我們現在已經能扛住諸如播放“天空之城”,舉辦超級碗,慶祝新年夜等重大事件帶來的訪問壓力。重新設計/架構,不但使系統在突發訪問峰值期間的穩定性得到了保證,還提供了一個可伸縮的平臺,從而使新特性更容易構建,其中包括不同裝置間同步訊息,使Tweets包含更豐富內容的Twitter卡,包含使用者和故事的富搜尋體驗等等特性。其他更多的特性也即將呈現。
下文將詳述我們做了的事情。我們學到了很多。我們改變了我們的工程師組織架構。並且,在將來的數個星期,我們將釋出更多的文章對本文提到的一些主題進行深入講解。
開始重新架構
2010年世界盃塵埃落定,我們總覽了整個專案,並有如下的發現:
- 我們正執行著世界上最大的Ruby on Rails叢集,我們非常快速的推進系統的演進–在那時,大概200個工程師為此工作,無論是新使用者數還是絕對負載都在爆炸式的增長,這個系統沒有倒下。它還是一個統一的整體,我們的所有工作都在其上執行,從管理純粹的資料庫,memcache連線,站點的渲染,暴露共有API這些都集中在一個程式碼庫上。這不但增加了程式設計師搞清整個系統的難度,也使管理和同步各個專案組變得更加困難。
- 我們的儲存系統已經達到閾值–我們依賴的MySQL儲存系統是臨時切分的,它只有一個單主節點。這個系統在消化/處理快速湧現的tweets時會陷入麻煩,我們在運營時不得不不斷的增加新的資料庫。我們的所有資料庫都處於讀寫的熱點中。
- 我們面臨問題時,只是一味的靠扔進更多的機器來扛住,並沒有用工程的方式來解決它–根據機器的配置,前端Ruby機器的每秒事務處理數遠沒有達到我們預定的能力。從以往的經驗,我們知道它應該能處理更多的事務。
- 最後,從軟體的角度看,我們發現自己被推到了一個”優化的角落“,在那我們以程式碼的可讀性和可擴充套件性為代價來換取效能和效率的提升。
結論是我們應該開啟一個新工程來重新審視我們的系統。我們設立了三個目標來激勵自己。
- Twitter一直都需要一個高屋建瓴的建構來確保效能/效率/可靠性,我們想要保證在正常情況下有較好的平均系統響應時間,同時也要考慮到異常峰值的情況,這樣才能保證在任何時間都能提供一致的服務和使用者體驗。我們要把機器的需求量降低10倍,還要提高容錯性,把失敗進行隔離以避免更大範圍的服務中斷–這在機器數量快速增長的背景下尤為重要,因為機器數的快速增長也意味著單體機器故障的可能性在增加。系統中出現失敗是不可避免的,我們要做的是使整個系統處於可控的狀態。
- 我們要劃清相關邏輯間的界限,整個公司工作在一個的程式碼庫上的方式把我們搞的很慘,所以我們開始嘗試以基於服務的鬆耦合的模式進行劃分模組。我們曾經的目標是鼓勵封裝和模組化的最佳實踐,但這次我們把這個觀點深入到了系統層次,而不是類/模組或者包層。
- 最重要的是要更快的啟動新特性。以小並自主放權的團隊模式展開工作,他們可以內部決策併發布改變給使用者,這是獨立於其他團隊的。
針對上面的要求,我們構建了原型來證明重新架構的思路。我們並沒有嘗試所有的方面,並且即使我們嘗試的方面在最後也可能並像計劃中那樣管用。但是,我們已經能夠設定一些準則/工具/架構,這些使我們到達了一個憧憬中的更靠譜的狀態。
The JVM VS the Ruby VM
首先,我們在三個維度上評估了前端服務節點:CPU,記憶體和網路。基於Ruby的機器在CPU和記憶體方面遭遇瓶頸–但是我們並未處理預計中那麼多的負載,並且網路頻寬也沒有接近飽和。我們的Rails伺服器在那時還不得不設計成單執行緒並且一次處理一個請求。每一個Rails主機跑在一定數量的Unicorn處理器上來提供主機層的併發,但此處的複製被轉變成了資源的浪費(這裡譯者沒太理清,請高手矯正,我的理解是Rails服務在一臺機器上只能單執行緒跑,這浪費了機器上多核的資源)。歸結到最後,Rails伺服器就只能提供200~300次請求每秒的服務。
Twitter的負載總是增長的很快,做個數學計算就會發現搞定不斷增長的需求將需要大量的機器。
在那時,Twitter有著部署大規模JVM服務的經驗,我們的搜尋引擎是用Java寫的,我們的流式API的基礎架構還有我們的社交圖譜系統Flock都是用Scala實現的。我們著迷於JVM提供的效能。在Ruby虛擬機器上達到我們要求的效能/可靠性/效率的目標不是很容易,所以我們著手開始寫執行在JVM上的程式碼。我們評估了這帶來的好處,在同樣的硬體上,重寫我們的程式碼能給我們帶來10倍的效能改進–現今,我們單臺伺服器達到了每秒10000-20000次請求的處理能力。
Twitter對JVM存在相當程度的信任,這是因為很多人都來自那些運營/調配著大規模JVM叢集的公司。我們有信心使Twitter在JVM的世界實現鉅變。現在我們不得不解耦我們的架構從而找出這些不同的服務如何協作/通訊。
程式設計模型
在Twitter的Ruby系統中,並行是在程式的層面上管理的:一個單個請求被放進某一程式的佇列中等待處理。這個程式在請求的處理期間將完全被佔用。這增加了複雜性,這樣做實際上使Twitter變成一個單個服務依賴於其他服務的回覆的架構。基於Ruby的程式是單執行緒的,Twitter的響應時間對後臺系統的響應非常敏感,二者緊密關聯。Ruby提供了一些併發的選項,但是那並沒有一個標準的方法去協調所有的選項。JVM則在概念和實現中都灌輸了併發的支援,這使我們可以真正的構建一個併發的程式設計平臺。
針對併發提供單個/統一的方式已經被證明是有必要的,這個需求在處理網路請求是尤為突出。我們都知道,實現併發的程式碼(包括併發的網路處理程式碼)是個艱鉅的任務,它可以有多種實現方式。事實上,我們已經開始碰到這些問題了。當我們開始把系統解耦成服務時,每一個團隊都或多或少的採用了不盡相同的方式。例如,客戶端到服務的失效並沒有很好的互動:這是由於我們沒有一致的後臺抗壓機制使伺服器返回某值給客戶端,這導致了我們經歷了野牛群狂奔式的瘋狂請求,客戶端猛戳延遲的服務。這些失效的區域警醒我們–擁有一個統一完備的客戶/伺服器間的庫來包含連線池/失效策略/負載均衡是非常重要的。為了把這個理念深入人心,我們引入了”Futures and Finagle”協議。
現在,我們不僅有了一致的做事手段,我們還把系統需要的所有東西都包含進核心的庫裡,這樣我們開新專案時就會進展飛速。同時,我們現在不需要過多的擔心每個系統是如何執行,從而可以把更多的經歷放到應用和服務的介面上。
獨立的系統
我們實施了架構上的重大改變,把整合化的Ruby應用變成一個基於服務的架構。我們集中力量建立了Tweet時間線和針對使用者的服務–這是我們的核心所在。這個改變帶給組織更加清晰的邊界和團隊級別的責任制與獨立性。在我們古老的整體/整合化的世界,我們要麼需要一個瞭解整個工程的大牛,要麼是對某一個模組或類清楚的程式碼所有者。
悲劇的是,程式碼膨脹的太快了,找到了解所有模組的大牛越來越難,然而實踐中,僅僅依靠幾個對某一模組/類清楚的程式碼作者又不能搞定問題。我們的程式碼庫變得越來越難以維護,各個團隊常常要像考古一樣把老程式碼翻出來研究才能搞清楚某一功能。不然,我們就組織類似“捕鯨征程”的活動,耗費大量的人力來搞出大規模服務失效的原因。往往一天結束,我們花費了大量的時間在這上面,而沒有精力來開發/釋出新特性,這讓我們感覺很糟。
我們的理念曾經並一直都是–一個基於服務的架構可以讓我們並行的開發系統–我們就網路RPC介面達成一致,然後各自獨立的開發系統的內部實現–但,這也意味著系統的內部邏輯是自耦合的。如果我們需要針對Tweets進行改變,我們可以在某一個服務例如Tweets服務進行更改,然後這個更改會在整個架構中得到體現。然而在實踐中,我們發現不是所有的組都在以同樣的方式規劃變更:例如一個在Tweet服務的變更要使Tweet的展現改變,那麼它可能需要其他的服務先進行更新以適應這個變化。權衡利弊,這種理念還是為我們贏得了更多的時間。
這個系統架構也反映了我們一直想要的方式,並且使Twitter的工程組織有效的運轉。工程團隊建立了高度自耦合的小組並能夠獨立/快速的展開工作。這意味著我們傾向於讓專案組啟動執行自己的服務並呼叫後臺系統來完成任務。這實際也暗含了大量運營的工作。
儲存
即使我們把我們板結成一坨的系統拆開成服務,儲存仍然是一個巨大的瓶頸。Twitter在那時還把tweets儲存在一個單主的MySQL資料庫中。我們採用了臨時資料儲存的策略,資料庫中的每一行是一個tweet,我們把tweet有序的儲存在資料庫中,當一個庫滿了我們就新開一個庫然後重配軟體開始往新庫中新增資料。這個策略為我們節省了一定的時間,但是面對突發的高訪問量,我們仍然一籌莫展,因為大量的資料需要被序列化到一個單個的主資料庫中以至於我們幾臺區域性的資料庫會發生高強度的讀請求。我們得為Tweet儲存設計一個不同的分割槽策略。
我們引入了Gizzard並把它應用到了tweets,它可以建立分片並容錯的分散式資料庫。我們創造了T-Bird(沒懂啥意思,意思是我們的速度快起來了?)。這樣,Gizzard充當了MySQL叢集的前端,每當一個tweet抵達系統,Gizzard對其進行雜湊計算,然後選擇一個適當的資料庫進行儲存。當然,這意味著我們失去了依靠MySQL產生唯一ID的功能。Snowflake很好的解決了上述問題。Snowflake使我們能夠建立一個幾乎可以保證全域性唯一的ID。我們依靠它產生新的tweet ID,作為代價,我們將沒有“把某數加1產生新ID”的功能。一旦我們得到一個ID我們靠Gizzard來儲存它。假設我們的雜湊演算法足夠好,從而我們的tweets是接近於均勻的分佈於各個儲存的,我們就能夠實現用同樣數量的資料庫承載更多的資料。我們的讀請求同樣也接近平均的分佈於整個分散式叢集中,這也增加了我們的吞度量。
可觀察性和可統計性
把那坨脆弱的板結到一起的系統變成一個更健壯的/良好封裝的/但也蠻複雜的/基於服務的應用。我們不得不搞出一些工具來使管理這頭野獸變得可能。基於大家都在快速的構建各種服務,我們需要一種可靠並簡單的方式來得到這些服務的執行情況的資料。資料為王是預設準則,我們需要是使獲取上述的資料變得非常容易。
當我們將要在一個快速增長的巨大系統上啟動越來越多的服務,我們必須使這種工作變得輕鬆。執行時系統組開發為大家開發了兩個工具:Viz和Zipkin。二者都暴露並整合到了Finagle,所以所有基於Finagle的服務都可以自動的獲取到它們。
1 2 3 |
stats.timeFuture("request_latency_ms") { // dispatch to do work } |
上面的程式碼就是一個服務生成統計報告給Via所需做的唯一事情。從那裡,任何Viz使用者都可以寫一個查詢來生成針對一些有趣的資料的時間/圖表,例如第50%和第99%的request_latency_ms。
執行時配置和測試
最後,當我們把所有的好東西放一起時,兩個看似無關的問題擺在面前:第一,整個系統的啟動需要協調多個系列的不同的服務,我們沒有一個地方可以把Twitter這個量級的應用所需要的服務弄到一起。我們已經不能依靠通過部署來把新特性展現給客戶,應用中的各個服務需要協調。第二,Twitter已經變得太龐大,在一個完全封閉的環境下測試整個系統變得越來越困難。相對而言,我們測試自己孤立的系統是沒有問題的–所以我們需要一個辦法來測試大規模的迭代。我們接納了執行時配置。
我們通過一個稱作Decider的系統整合所有的服務。當有一個變更要上線,它允許我們只需簡單開啟一個開關就可以讓架構上的多個子系統都和這個改變進行幾乎即時的互動。這意味著軟體和多個系統可以在團隊認為成熟的情況下產品化,但其中的某一個特性不需要已經被啟用。Decider還允許我們進行二進位制或百分比的切換,例如讓一個特性只針對x%的使用者開放。我們還可以先把完全未啟用並完全安全的特性部署上線,然後梯度的開啟/關閉,知道我們有足夠的自信保證特性可以正確的執行並且系統可以負擔這個新的負荷。所有這些努力都可以減輕我們進行團隊之間溝通協調的活動,取而代之我們可以在系統執行時做我們想要的定製/配置。
(譯註:Twitter 官博文章後面還有一段內容,不過有點“吹牛”嫌疑,就不翻譯了。有興趣的朋友,請自己去看。)