[Javascript]揭秘後台技術架構!看 Uber 怎麼練成 4 年業務增長近 40 倍類型:
Javascript
本文由伯樂線上 – 至秦翻譯,黃利民校稿。未經許可,禁止轉載!
英文出處:Todd Hoff。
據報導,Uber 只有在過去 4 年的時間裏,業務就激增了 38 倍。Uber 首席系統架構師
Matt Ranney ?在一個非常有趣和詳細的訪談《可延伸的 Uber 實時市場平台》中告訴我們 Uber 軟體是如何工作的。
本次訪談中沒有涉及你可能感興趣的峰時定價(Surge pricing,譯註:當 Uber 平台上的車輛無法滿足大量需求時,將提升費率來確保乘客的用車需求)。但我們了解到 Uber 的排定系統,他們如何實作地理空間索引、如何擴充系統、如何提高可用性和如何處理故障,例如在處理資料中心故障時,他們甚至會把司機電話作為一個外部分散式存儲系統用於恢復系統。
訪談的總體印像是 Uber 成長得非常快速。很多他們選擇的的架構是快速成長的結果,同時也想讓建置不久的團隊可以盡可能快地行動。因為他們的主要目標是讓團隊的工程速度盡可能得快,所以在後台使用了大量的技術。
在經歷一個稍顯混亂但非常成功的開端後,Uber 似乎學習到很多:他們的業務和他們需要做什麼才能成功。他們早期的排定系統只是為了送人。由於 Uber 的使命成長為除了送人以外,還要處理箱子和雜物(編註:Uber 已涉及快遞業務。),他們的排定系統已經被抽象並組建在可靠的和智慧的架構基礎上。
雖然 Matt 認為他們的架構可能有點瘋狂,使用
一致性哈希環(Consistent Hashing)和 gossip 協定的想法非常適合他們的使用場景。
很難不被 Matt 幹事業的熱情所迷住。當談到他們的排定系統——DISCO,他興奮地說就像學校裡的旅行推銷員問題(traveling salesman problem)。這是一個很酷的電腦科學問題。雖然解決方案不是最優的,但這是現實世界中一個規模很大,要求實時性,由容錯和可延伸的部件建立起來的問題。這是不是很酷?
讓我們看看 Uber 內部是如何工作的。下面是我對 Matt’s 談話的註解:
統計·Uber 地理空間索引的目標是每秒一百萬次寫入,讀取速度比寫入速度快很多倍
·排定系統有數以千計的節點
平台·Node.js (譯者註:Node.js 是一個開源的、跨平台的、用於伺服器和網路應用的執行環境。Node.js 應用用 JavaScript 編寫)
·Python 語言
·Java 語言
·Go 語言
·iOS 和 Android 上的本機應用程式
·微服務
·Redis(譯者註:Redis 是一個開源、支援網路、基於記憶體、鍵值對存儲的資料庫,使用 ANSI C 編寫。)
·Postgres(譯者註:PostgreSQL 標榜自己是世界上最先進的開源資料庫。)
·MySQL 資料庫
·Riak (譯者註:Riak是由技術公司basho開發的一個類 Dynamo 的分散式 Key-Value 系統。以分散式、水平延伸性、高容錯性等特點著稱。)
·Twitter 公司提供基於 Redis 的 Twemproxy (譯者註:一個快速和輕量的代理)
·谷歌的 S2 地理函式庫
·ringpop —— 一致哈西環
·TChannel ——網路多路復用和 RPC 幀協定(譯者註:RPC,Remote Procedure Call,遠端程序調用)
·Thrift (譯者註:Thrift 是一個跨語言的服務部署訊框架)
概述Uber 是一個用來連線乘客和司機的運輸平台。
他們的挑戰是:
實時符合動態的需求和供給。在供給方面,司機可以自由地做他們想做到的任何事情。在需求方面,乘客可以隨時要求運輸服務。
而 Uber 的排定系統是一個實時的市場平台,通過行動電話來符合司機和乘客。根據統計,新年前夕是 Uber 一年中最忙碌的時候。
架構概述驅動了所有這些的原因是乘客和司機在他們的手機上執行他們的 App。後台主要是服務行動電話的流量。客戶端通過行動資料和盡力而為的網路和後台溝通。10 年前你可以想像有個基於行動資料的業務嗎?而我們現在可以做這樣的事情,太棒了。沒有使用私有網路,沒有花哨的 Q0S (服務品質),只有只有是開放的網路。
客戶端連線排定系統,它協調司機和乘客,
供給和需求。排定系統幾乎都是用 node.js 編寫的,原來排程把它移植到 io.js 上,不過後來 io.js 和 node.js 合併了。
你可以用 javascript 做一些有趣的分散式系統的工作。不過記得決不要低估
熱情帶來的生產力,而且節點開發者都相當有熱情。他們可以非常快速地完成很多事情。
整個 Uber 系統可能看上去相當簡單。為什麼你還需要這些子系統和這些人呢?只要它看上去是那樣,那就是成功的標記。只要看上去他們很簡單地完成了他們的工作,就有很多事情需要去做。
[b][b]地圖或 ETA(預期到達時間):[/b]為了讓排定做出更加智慧的選擇,必須要取得地圖和路線訊息。街道地圖和曾經的行駛時間可以用來預測當前的行駛時間。至於語言很大程度上取決於系統整合,所以這裏有 Python、C++ 和 Java。
服務:這裏有大量的業務邏輯服務。使用了一種微服務的方法;大部分用 Python 編寫。
[b]資料庫:[/b]使用了很多不同的資料庫,最老的系統是用 Postgres 編寫的;Redis 也使用了很多,而有些是基於 Twemproxy;有些是基於一個客制化的集群系統。
此外也使用了 MySQL 資料庫;Uber 正在建立自己的分散式列存儲,那是一堆精心策劃的 MySQL 案例。最後有些排定服務還停留在 Riak 上。
旅行後期的流水處理:一個旅行結束後要處理很多事情,包括收集評分、發 email、更新資料庫、安排支付;用 Python 編寫。
金流:Uber 整合了很多支付系統。
[/b]
舊的排定系統原有排定系統的局限性
開始限制了公司的成長,因此 Uber 不得不改變它。
儘管?
Joel Spolsky?聲稱幾乎整個系統都被重寫了。但大部分其它系統沒有被觸及,甚至有些排定系統的服務也被保留下來。
舊系統是為專用客車運輸所設計的,做了很多假設:
·每個車輛一個乘客,不適用? Uber Pool (拼車服務)。
·運送人的想法深深內嵌到資料模型和介面裡。這樣限制了延伸到新的市場和產品上,比如運送食物和箱子。
·最初的版本是按城市劃分的。這對於可延伸性而言是好的,因為每個城市可以獨自運營。但當越來越多的城市加入,這變得越來越難以管理。城市有大有小,負載也不一樣。
由於建造得很快,他們沒有單點故障,都是多點故障。
新的排定系統為了解決城市分片和支援更多產品,供給和需求的概念應該是廣義的,所以
供給服務和需求服務被創建出來。
》供給服務追蹤所有供給的效能和狀態機:
有很多屬性模型可以追蹤車輛:座位數目、車輛類型、是否有兒童座椅、可以放進輪椅嗎,諸如此類。
規格需要被追蹤。例如,一輛車可能有三個座位但是有兩個都被佔用了。
》需求服務追蹤需求、訂單和需求的方方面面:
如果一名乘客要求一個小車座位,庫存必須滿足需求。
如果一名乘客為了更便宜的價錢,不介意和別人分享一輛車,這也是要塑模的。
如果需要行動一個箱子,或是遞送食物呢?
》符合所有供給和需求的邏輯是一個被稱為 DISCO(排定最佳化)的服務:
舊系統只符合當前可用的供給,這意味?當前路上等著工作的車輛。
DISCO 支援未來規劃和使用可用的訊息。例如,在旅行程序中修改路線。
[b]geo 供給:基於供給來自哪裡和哪裡需要它,DISCO 需要一個地理空間索引做決策。
geo 需求:需求也需要一個 geo 索引。
要使用所有這些訊息需要有一個更好的路由引擎。
[/b]
排定當車輛行動的位置更新被傳送到 geo 提供者。為了符合乘客和司機,或是只有是在地圖上顯示車輛,DISCO 傳送一個請求給 geo 提供者。
接? geo 提供者會先粗略過濾一遍,得到附近滿足需求的候選人。然後清單和需求傳送給路線或 ETA(預計到達時間);用以計算它們距離遠近的 ETA,是基於道路系統而不是地理上的。
接?根據 ETA 排序然後把它傳回給提供者,再派給司機。至於在機場,Uber 不得不類比一個虛擬的計程車佇列。因為考慮到他們到達的順序,提供者必須排隊。
地理空間索引必須有相當的可延伸性。設計目標是
每秒處理一百萬次寫入。寫入的速度源自司機每 4 秒傳送的行動更新。至於讀取速度的目標是要比寫入速度快很多,因為每個開啟應用的人都在進行讀取作業。
通過一個簡化的假設——只有追蹤可排定的供給,舊地理空間索引可以很好地工作。大部分供給正在忙?做其它事情,所以支援可用供給的子集就很容易。在為數不多的行程中,有一個全域索引存儲在記憶體裡。很容易做簡單的符合。
在新世界裏
必須追蹤所有狀態下的供給。Uber?必須追蹤它們涉及的路線;這是相當多的資料。此外,新的服務
執行在好幾百個行程上。
而因為地球是一個球體,Uber 很難只有依靠經度和緯度做出總結和近似。所以 Uber 通過 Google S2 函式庫將地球分割成微小的單元,每個單元有一個唯一的 ID。
可以通過一個 64 位整數(int64)代表地球上的??每一平方厘米。Uber 使用一個等級為 12 的單元,根據你所在的位置,面積從 3.31 到 6.38 平方公里。盒子根據它們在球體中的位置,改變它們的形狀和大小。
S2 可以給出一個形狀的覆蓋面積是多大。如果你想以倫敦為中心畫一個半徑 1 公里的圓,S2 可以告訴你填充這塊欄位需要多少單元。由於每個單元都有一個 ID,這個 ID 可以作為一個分區鍵。當供給到達一個位置,這個位置的單元 ID 就知道了。可以用一個做為分區鍵的單元 ID 來更新供給位置。然後傳送多個副本。
當 DISCO 需要找到附近位置的供給,會以乘客所在位置為中心計算一個圓的面積。藉助單元 ID,讓所有在這個範圍內的分區都回饋供給資料。
所有這些都是可延伸的。儘管它不像你想像得那樣高效,但因為扇出相對便宜,寫入負載總是可以通過加入更多的節點來加以擴充。讀取負載可以通過使用複制來擴充。如果需要更大的讀取能力,可以加入複制因子。(譯者註:fanout,扇出,IC 概念,一個邏輯門在正常工作下,其輸出端可接的同族系 IC 門的數目,成為此門的扇出數。簡單的說,其所能推動同種類的次級門的數目就稱為扇出。)
一個限制條件是單元尺寸固定在等級 12 的大小。未來可能會支援動態的單元尺寸。但這需要權衡利弊,儲存格越小,查詢的扇出就越多。
路線討論完地理空間,我們來討論路線的選擇必須分級。
有一些主要目的:
[b]減少空載([b]extra driving):[/b]開車是人們的工作,他們希望可以更有效率。空載不會給他們帶來收入(譯者註:感覺此處有筆誤)。理想情況下,司機一直在行駛中。一堆賺錢的工作排隊等?他們。
減少等待:乘客等待要盡可能的短。
整體 ETA 最少(整體預計到達時間)
[/b]
舊系統讓需求查詢當前可用的供給,加以符合並最終完成。這很容易實作和讓人理解。這在專車運輸下工作得相當好。
但只有看當前可用的,並不能做出好的選擇:其想法是一個正在運送乘客的司機可能更適合這位叫車的客戶,因為目前空閒的司機距離比較遠。挑選正在途中的司機減少了客戶的等待時間,也讓遠端司機的空載時間降到最小。
在可預見的未來,這個模型可以更好地處理動態條件:
例如,一名客戶附近剛好有一名司機上線,但是這個客戶之前已經分派給另一位距離位置遠一點的司機,這種情況下就不應該改變排定決策。
另一個例子是客戶希望可以分享一輛車。通過在非常複雜的情況下嘗試預測未來,可以進行更多的最佳化。
當考慮到運送箱子或是食物,所有這些決策會更加有趣。在這些情況下,人們通常會做其它事情,就需要有其它不同的考量。
可延伸的排定排定使用 node.js 組建;他們組建了一個有狀態的服務,所以無狀態的延伸方法不能工作。
Node 執行在一個單獨行程上,所以必須想一些辦法讓 Node 可以執行在同一台機器的多個 CPU 上和多台機器上。而用 Javascript 重新實作所有 Erlang 的實作是個笑話。
延伸 Node 的一個解決方案是 ringpop,它是一個基於 gossip 協定的一致哈希環,實作了一種可延伸的和容錯的應用層分區。在 CAP 術語中,ringpop 是一個 AP 系統,權衡一致性和可用性。一些不一致性要比無法服務更好解釋。最好是可以一直可用只是偶爾出錯。
ringpop 是一個可以包含在每一 Node 行程的內嵌式模組。
Node 基於一個成員集合實作 gossip 。一旦所有節點相互認可,它們可以獨立和高效地進行查詢和轉寄的決策。這是真正得可延伸:加入更多的行程可以完成更多的工作。這可以被用來切分資料,或作為一個分佈的閉鎖系統、或協調一個發表或是訂閱的會合點、或是一個長時間輪詢的 socket。
Gossip 協定一種基於可擴充可傳導的弱一致性行程群組成員協定(
SWIM,Scalable Weakly-consistent Infection-style Process Group Membership Protocol);為了提升收斂時間已經做了一些改善。
一系列線上的成員都在「傳播流言」?(gossip around 譯註:雙關用語)。當更多的節點加入,它就是可擴充的。SWIM 中的「 S 」代表可延伸的,並且的確可以工作;這可以
延伸到數千個節點的程度。(SWIM 結合了健康檢查和成員變更,並把它們作為協定的一部分。)
在一個 ringpop 系統中,所有 Node 行程都包含 ringpop 模組。它們在當前成員中「傳播流言」。
從外面看,如果 DISCO 想要使用地理空間,每個節點都是相等的。可以選擇任意一個健康的節點。通過檢查哈希環,接受請求的節點會負責把這個請求轉寄給正確的節點。如下圖所示:
讓這些躍點和對端可以相互溝通聽上去很瘋狂,但可以得到一些很好的特性,比如在任意機器上加入案例就可以擴充服務。
ringpop 的組建基於 Uber 自己的遠端程序調用(RPC,Remote Procedure Call)機制,被稱為 TChannel。它是什麼?
這是一個雙向的請求和響應協定,它的靈感來自 Twitter 的 Finale。
一個重要的目標是控制跨不同語言的效能;特別是在 Node 和 Python 中,很多現有的 RPC 機制不能很好地工作,因此需要 redis 層級的效能。而?TChannel 已經比 HTTP 快 20 倍。
需要一個高效能的轉寄路徑,這樣中間層不需要知道整個負載,就可以很容易做出轉寄的決策。
需要適合的流水線,這樣就不會有排頭擁塞的問題,任何時候任何方向都可以傳送請求和響應,每個客戶端也是一個伺服器。
需要內嵌負載檢驗、追蹤和一流的功能。在系統內處理中,每個請求都應該是可被追蹤的。
需要一個乾淨的脫離 HTTP 的方法。HTTP 可以非常自然地被封裝到 TChannel 裡。
[b]Uber 正在遠離 HTTP 和 Json 業務。都在遷往基於 TChannel 的 Thrift。
[/b]
ringpop 基於持久連線的 TChannel 實作 gossip 協定。同樣這些持久連線被用來延伸或是轉寄應用流量。TChannel 也被用來進行服務間的通信。
排定可用性可用性很重要:Uber 有競爭對手而且切換成本非常低。如果 Uber 只是短暫掛掉,這些錢就會被其它人賺走。其它產品的粘性更強,客戶也願意再次嘗試它們。Uber 不一定如此。
讓每件事情都可以重試:如果有些事情不能工作,那它就要可以重試。這就是如何繞過錯誤。這要求所有的請求是冪等的。例如一次排定的重試,不能排定兩次或是刷兩次某人的信用卡。(譯者註:一個冪等作業的特點是其任意多次執行所產生的影響均與一次執行的影響相同)
讓每件事情都可以終止:失敗是一個常見的情況。任意終止行程不應該造成損害。
只有崩潰:沒有優雅的關閉;優雅的關閉不需要練習。需要練習的是當不遇期的事情發生了(要怎麼辦)。
小塊:要把事情失敗的成本降到最低就是把它們分成小塊。可以在一個案例中處理全部流量,但如果它掛掉了怎麼辦?如果有兩個,就算一個掛了,只是效能減半。所以服務要可以被分割。這聽上去像一個技術問題,但更像一個文化問題。很容易就擁有一對資料庫。這是一件很自然的事情,但配對就不好。如果你能夠自動發起一個和重新啟動新的備用,隨機終止它們是相當危險的。
終止一切:就算終止所有資料庫來確保可以從失敗中恢復過來。這需要改變資料庫的使用規則。他們選擇 Riak 而不是 MySQL。這也意味?使用 ringpop 而不是 redis。因為 redis 案例通常相當大和昂貴,終止一個 redis 案例是一個很昂貴的作業。
把它分成小塊:談到文化轉變。通常服務 A 通過一個負載等化器和服務 B 溝通。如果等化器掛掉會怎樣?你要如何處理這種情況?如果你沒有練習過你永遠都不知道。你應該終止負載等化器。你如何繞過負載等化器?負載均衡的邏輯已經在服務裏面。客戶端需要有一些訊息知道如何繞過問題。這和 Finagle 的工作方式類似。
一個集群的 ringpop 節點創建了服務發現和路由系統,讓整個系統有可延伸性和應對後台的壓力。
整個資料中心的故障雖然不會經常發生,但還是會出現一個意想不到的串聯故障或是一個上游網路提供商的故障。Uber 維護了一個備份的資料中心,通過適當的開關可以把所有事情都切換到備份的資料中心。
問題是在途的旅行資料可能不在備份的資料中心。他們會把司機手機當作旅行資料的源頭而不是資料的副本。
結果排定系統
會周期傳送一個加密的狀態摘要給司機的手機。現在假設有一個資料中心發生故障轉移。司機手機下一次傳送位置更新給排定系統,排定系統將會偵測到它不知道這個旅行,它會問(手機)要狀態摘要。然後排定系統根據狀態摘要進行更新,這個旅行會繼續就像什麼事情都沒有發生過。
不足之處Uber 解決可延伸性和可用性問題的不足之處,可能在於 Node 處理轉寄請求和傳送訊息給大量扇出所帶來的高延遲。在一個扇出系統中,微小的波動和故障都會有驚人的影響,系統的扇出越高出現高延遲請求的機會就越大。
一個好的解決方案是可以跨伺服器取消備份的請求。這個一流的功能已經內嵌到 TChannel 中。一個請求的訊息同時傳送給服務 B1 和 B2;傳送給服務 B2 的請求會有些延遲,當 B1 完成這個請求,它會在 B2 上取消這個請求。由於這個延遲通常情況下 B2 不會工作,但如果 B1 出了問題,B2 就可以處理這個請求,這樣會比 B1 先嘗試逾時後 B2 再嘗試情況下的回饋要快一些。
(本文轉載自合作夥伴《
伯樂線上》;未經授權,不得轉載;圖片來源:
bfishadow,CC Licensed)
穩賺直達→FB, Google Adwords 密技
想在Android 手機欣賞更多有趣圖集?
免費下載 GigCasa App
原文站台:
TechOrange
分享到Facebook