Uber QPS最高的服務建立的背景及未來
大小:0.3 MB 人氣: 2017-10-12 需要積分:1
標簽:
2015年初,我們建立了一個微服務來負責這項任務:地理圍欄查找(geofence lookups),結果完成很出色。如今已過一年,這項技術在Uber數以百計的生產應用中脫穎而出,成為了每秒查詢量最高(QPS)的服務。本文講述了我們建立這個服務的原因,還有近來Go語言對構建和擴展該服務速度的貢獻。背景
在Uber,地理圍欄指的是地面上由人為定義的地理區域(或幾何術語中的多邊形),廣泛用于地理位置的配置中。向用戶展示在指定位置上有哪些產品可用,根據特定需求(比如機場)定義區域,在同時有多人請求搭車的周邊區域執行動態定價,這些都非常重要。下圖是位于科羅拉多州的一個地理圍欄樣例:
第一步是檢索地理位置的配置,根據用戶的手機定位,查找經緯度之類的信息,以確定該位置處于哪個地理圍欄中。這個功能曾經在多個服務/模塊中都有實現,不過隨著從單體架構遷移到面向(微)服務架構,我們選擇將這個功能集成在新的單體微服務中。
準備出發!
根據我們的評估,那時最適合市場團隊的語言是Node.js,因為我們在這種語言上有更多的內部知識和經驗。但是,出于下面這些原因,Go更符合我們的需求:
高吞吐量、低延遲的需求:從Uber移動應用發出的每個請求都需要查找地理圍欄,而且必須在很短時間內(第99個百分位《 100毫秒)快速對大量(每秒成千上萬個)查詢作出響應;
CPU密集型的工作負載:地理圍欄查找需要使用大量占用CPU資源的算法來查找點是否在多邊形內(point-in-polygon)。盡管Node.js在輸入/輸出密集型的服務中使用效果良好,但由于Node本質上屬于解釋型和動態類型的語言,在這種用例中并非最佳選擇;
無干擾后臺加載:為了確保我們獲取并執行查找的地理圍欄數據是最新的,該服務必須后臺讀取多個來源的數據,持續刷新內存中的地理圍欄數據。由于Node.js是單線程的,后臺刷新會在相當長的時間內占用CPU(例如CPU密集型的JSON解析工作),從而延遲對查詢的響應時間。對于Go來說這不是問題,用goroutines就可以通過多核CPU執行,后臺任務與前臺查詢并行執行。
Geo索引:用還是不用,這是個問題
我們如何根據經緯度指定的位置,在成千上萬個地理圍欄中查找它屬于其中的哪一個?使用簡單匹配算法(brute-force)非常簡單:只要一一查看所有地理圍欄,并使用算法(比如光線投射算法)進行點是否在多邊形內的比對。不過這個辦法速度太慢。那么,如何有效地縮小搜索范圍呢?
我們沒有使用R-tree或復雜的S2算法,而是選擇了更簡單的辦法來找出地理圍欄:Uber的商業模型是以城市為中心的,其商業規則還有定義商業規則的地理圍欄一般都與城市密切相關。這樣我們就可以將地理圍欄分為兩種層級,第一層是城市地理圍欄(定義城市邊界的地理圍欄),第二層是城市間的地理圍欄。
每次查找,我們首先會通過線性掃描,查找所有的城市地理圍欄,定位所在城市;然后再次通過線性掃描,找出其中包含的地理圍欄。根據該解決方案的復雜程度,運行時長為O(n),n被大幅縮減到100s到10000s的數量級。
架構
我們希望這項服務是無狀態的,以便適用于所有請求;同時在所有的服務實例中,每個請求的結果相同。這意味著每個服務實例都必須有全世界的信息,而不是某個分區的。我們使用確定性輪詢調度,確保來自不同服務實例的地理圍欄數據保持同步。這樣一來,該服務的架構就非常簡單了。后臺任務定期對不同的數據庫的地理圍欄數據進行輪詢,并將這些數據存儲在主內存中,為查詢提供服務;同時序列化到本地文件系統中,在服務重啟時快速引導載入:
上圖是我們的地理圍欄查找服務架構。
處理Go內存模型
我們的架構需要讀取/寫入并發訪問內存中的geo索引,特別是:在前臺查詢引擎從索引讀取時,后臺輪詢任務會對索引執行寫入。對于習慣Node.js單線程的用戶來說,Go的內存模型可能會構成挑戰。在Go中,常用的方式是通過goroutines與channels同步并發讀取/寫入任務,出于對性能負面影響的擔心,我們嘗試使用sync/atomic數據包的StorePointer/LoadPointer基元自行管理內存屏障,卻導致代碼脆弱且難以維護。
最后我們進行了妥協,使用讀寫鎖來同步到geo索引的訪問。為了將鎖定等待的時間減到最短,在轉到主索引之前,我們另外構建了新的索引區段為查詢提供服務。使用鎖定導致查詢的延遲相對于StorePointer/LoadPointer的辦法來說有稍許增加,不過在我們看來利大于弊:代碼簡單化和可維護性的好處值得用稍許性能來換。
我們的經驗
回顧之前的工作,我們非常慶幸選擇了Go這種新語言來編寫服務。
優勢:
開發人員工作效率很高:C++、Java或Node.js開發人員一般只需數日便可學會使用Go語言,而且這種語言的代碼易于維護。(多虧了這種語言是靜態類型的,免去了很多猜測和意外)。
吞吐量和延遲表現都很好:僅在我們服務于非中國區的主數據中心上,在2015年新年前夜,該服務所處理查詢數據的峰值負載就達到每秒查詢量(QPS)17萬,40臺機器都占用了35%的CPU。第95個百分位響應時間小于5毫秒,第99個百分位響應時間小于50毫秒。
超級可靠:從一開始該服務的正常運行時間就達到99.99%。唯一一次停機是由于初學者的編程錯誤,一個文件描述符將bug引入第三方數據庫。重要的是:在Go運行時我們還沒發現什么問題。
下一步的未來
盡管之前Uber的服務大多使用Node.js和Python,但Go語言逐漸成為許多Uber工程服務的新選擇。
非常好我支持^.^
(0) 0%
不好我反對
(0) 0%