02.10 Uber:利用Golang構建高性能查詢服務實踐

Uber作為世界上最大的的互聯網在線約車服務商,有著世界上上最大的地理信息查詢和服務,如何提高這些在線服務的響應時間,提高其查詢性能(QPS,每秒查詢數)是一個極大的極大的挑戰,本文就給大家分享一個Uber利用golang語言提高服務性能的案例。

Uber:利用Golang構建高性能查詢服務實踐

背景

在2015年初,Uber構建了一個微服務,用來進行地理圍欄的查詢服務。隨著業務的擴展,在一年後,這個服務成了Uber在線服務中查詢量最大的服務,成了業務瓶頸。提高其查詢性能迫在眉睫!一般來說,為了提高服務的性能有兩個方法:一是通過橫向擴展,增加服務的硬件資源;還有一個就是通過優化或者重構提高服務的軟件性能。硬件的擴展還取決於服務的架構支持,不是所有架構都是可以通過橫向增加硬件來提高性能。而Uber採取的方法是使用Golang語言進行重構服務。

地理圍欄是指地球表面上人類定義的地理區域(或幾何學上的多邊形)。Uber服務中廣泛使用了地理圍欄為基礎的CIS服務。具體表現為向用戶顯示在給定位置提供哪些服務,定義具有特定要求的區域(例如機場)以及對許多人同時打車的地點實施動態定價等。比如下面是一個地理圍欄的示意圖:

Uber:利用Golang構建高性能查詢服務實踐

從用戶的手機中檢索緯度/經度之類的東西的基於地理位置的服務基礎是找到該位置所屬的地理圍欄。在Uber初始版本的服務中該功能簡單的在多個服務/模塊中複用。在進行微服化後該服務功能被集中到一個新的微服務。

起步

在評估語言時,Uber考慮了Node.js和Golang。前者是實時產品團隊的主要編程語言,對該語言的成員比較熟悉,而且有大量的項目經驗積累。但是最終還是選擇了Golang,主要考慮了一下的因素:

首先,性能上要求高吞吐量和低延遲。 基本上Uber移動應用程序的每個請求都需要地理圍欄查詢,並且必須能快速響應結果(99%響應不得大於100毫秒),查詢的QPS要達到100,000次。

其次,計算密集型服務。地理圍欄查找需要佔用大量CPU的多邊形點算法。儘管Node.js可以很好地用於他I/O密集型服務,但由於Node具有解釋型和動態類型化的性質,因此並不是很適合計算密集型的服務。

最後,需要無中斷後臺加載。為確保擁有最新的地理圍欄數據來執行查找,服務必須不斷在後臺刷新來自多個數據源內存中的地理圍欄數據。由於Node.js是單線程的,因此後臺刷新會佔用較長時間的CPU(例如, CPU密集型JSON解析工作),從而導致查詢響應時間突增。而Golang的協程可以支持多CPU運行,配合前臺查詢並行運行後臺作業。

查詢索引問題

給定一個指定為經緯度對的位置,如何找到該位置屬於數萬個地理圍欄中的哪個?暴力遍歷的方法很簡單:遍歷所有地理圍欄,並用諸如光線投射算法之類的算法進行多邊形點檢查。問題,這種方法太慢,不能滿足服務性能要求。

Uber拋棄了業界常用的用R-tree或S2為地理圍欄建立索引的方法,使用基Uber商業模式以城市為中心,選擇了一條更簡單的路線;商業規則和其定義的基礎地理圍欄通常與城市相關聯。所以架構上將地理圍欄組織成兩級層次結構,其中第一級是城市地理圍欄(定義城市邊界的地理圍欄),第二級是每個城市內的地理圍欄。

對於每次查找,首先通過對所有城市地理圍欄進行線性掃描找到所需的城市,然後通過第二次線性掃描在該城市內查找包含的地理圍欄。雖然解決方案的運行時複雜度保持為O(N),但這種簡單的技術將所需的N從10,000s減少到了100s。

架構

服務的架構總體設計是無狀態的,因此每個請求都可以調度到該服務的任何一個實例,並能期望得到相同的結果。這樣每個服務實例都可以服務整個領域,無需使用分區。架構上還是使用了確定性的輪詢計劃,因此來自不同服務實例的地理圍欄數據保持同步。因此,該服務具有最簡單的體系結構。後臺作業定期輪詢來自各種數據存儲的地理圍欄數據。這些數據保存在主存儲器中以服務查詢,並序列化到實例本地文件系統,可以在服務重啟時快速啟動,總體機構圖如下:


Uber:利用Golang構建高性能查詢服務實踐

處理Go Memory模型

服務的體系結構要求對內存中的地理索引同時進行讀/寫訪問。特別是後臺輪詢作業要寫到索引,而前臺查詢引擎則從索引中讀取。Golang的內存模型可能會有一些問題。Golang中常用的方法是對併發讀寫用協程和通道同步,但是又會影響性能。團隊嘗試使用sync/atomic包中的StorePointer和LoadPointer方法自己編寫了管理內存代理,這樣導致了代碼脆弱且難以維護。

最終,Uber使用了折衷的辦法:使用讀寫鎖來同步對地理位置索引的訪問。為了最大程度地減少鎖爭用,在將新的索引段交換到主索引中以用於服務查詢之前,會建立新的索引段。和StorePointer/LoadPointer方法相比,鎖的使用導致查詢等待時間略有增加,但是代碼庫的簡單性和可維護性方面有了非常大的提高。

經驗總結

回顧整個案例,整個服務重構取得了很好的效果,並給予了很多啟示:

提高了團隊生產力和快速迭代。對於C++,Java或Node.js開發人員來說,Golang通常只需要幾天的時間就可以上手,Golang項目代碼簡便易於維護和實施部署。

高吞吐量,低延遲。在主要數據中心,該服務用了40臺運行35% CPU的計算機,峰值負載為170k QPS。響應時間極大縮短:95%服務請求響應時間不大於5毫秒。99%服務請求時間不大於50毫秒。

超級可靠。自啟動以來,該服務的正常運行時間為99.99%。唯一的故障是由初學者編程錯誤和第三方庫中的文件描述符洩漏錯誤引起。Golang的運行時沒有發現任何問題。


分享到:


相關文章: