英文原文做者:Buck Herouxgit
幾周之前,Uber 發佈了一篇關於如何構建 "如何使用GO實現極限QPS"的文章。我正好一直在用Go作關於地理空間方面的工做,期待Uber能夠用GO寫一些牛逼的算法來處理地理數據,然而我發現Uber低於了個人指望…github
這篇文章圍繞Uber如何處理地理圍欄的問題來構建一個服務。地理圍欄的核心問題是搜索一組邊界找到哪一個子集包含一個查詢點。這個問題有不少標準的方法,下面是Uber選擇的方案。面試
相比於使用r - tree或複雜的S2構建索引geofences,咱們選擇了一個基於業務觀察的更簡單方案:Uber的商業模式以城市爲中心;地理圍欄的使用一般也和城市緊密關聯。這讓咱們把geofences拆分爲兩個層次,第一級是城市geofences(geofences定義城市邊界),第二層次是在每一個城市的geofences。算法
若是你問某個歷來沒接觸過空間算法的人解決怎麼地理圍欄的問題,這多是他們會想出來的方案。很難想象到一個市值$50b且擁有衆多工程師的公司,他們的核心問題竟然是在地球上找到附近的車。使人失望的是在沒有具體理由的狀況下只是以"其餘方案太複雜"就選擇忽略了其餘標準解決方案。segmentfault
尤爲使人失望的是考慮到去年夏天Uber還收購了一部分base在科羅拉多州的必應地圖工程師團隊。我曾經效力過必應地圖街景團隊,也正是Uber收購的那支,據我所知有好多傢伙在這個團隊都對空間索引很是瞭解。我面試的一個問題就是怎麼在Instagram圖片上經過地理標籤找到埃菲爾鐵塔。微信
撇除偏見, 我依然十分承認 Uber 對系統制定的指標要求:特定延遲分佈要求的服務99%的全部請求在100毫秒可用(查詢+更新)。數據結構
從時間複雜性分析入手對比潛在解決方案是一個很好的起點。這個不須要作任何實際編碼,一點點的研究,應該能夠證實Uber的低效率方法。即便你不熟悉大O分析,但願也會明白證實的過程。post
咱們首先須要解決的是點在多邊形內判斷的成本(一般我稱之爲 多邊形包含查詢)。維基百科的文章很好地分別介紹了兩種經常使用算法用於多邊形包含查詢:ray casting 和 winding number。這兩種算法須要對比每一個多邊形的頂點和請求的點,所以他們的效率取決於頂點的數量。若是咱們用Uber的城市中心模型來拆分問題,咱們創建下面的模型參數:編碼
q := 查詢點 C := 城市數量 V := 定義一個城市邊界的頂點數 n := 在一個城市柵欄的多邊形數量 v := 柵欄多邊形的頂點
從這裏我將概述一些算法引用在文章中,看看他們如何解決地理圍欄問題。spa
遍歷全部圍欄而且用一個算法執行多邊形包含查詢,好比 ray casting 算法
在咱們的模型中,問題轉化爲在每C個城市中,每一個有n個柵欄多邊形,最後比較查詢點在多邊形各頂點v。
Brute() -> O(Cnv)
Uber 在暴力窮舉的基礎上增長城市索引,先找到某個城市,而後在這個城市中使用暴力窮舉每一個地理圍欄。這在城市數量快速擴張的時候,有效地修剪搜索空間。
可是城市邊界各點的查詢仍然會損失很大的成本,因此在這個兩部方法中,咱們獲得:
Uber() -> O(CV) + O(nv)
r樹數據結構是一種爲多維對象帶有特殊的序列定義的基於b樹的算法。序列的定義方法是比較一個最小邊界矩形(MBR)到另外一個對象的MBR之間是否知足徹底包含關係。在二維狀況下的地理圍欄,MBR是一個經過一組最大最小的座標系來框定的邊界框(簡稱bbox)。檢查一個對象的bbox與另外一個對象的bbox的包含關係的複雜度是常數時間O(n)。這篇在維基百科上的文章解釋得很是詳細,下面是一張可視化的圖來解釋對象的邊界框。
若是本身實現不靠譜,也能夠直接用Go裏面的 rtreego
這個包來實現,不過對於二維的狀況下還須要額外消耗一些空間。
總結下r-tree搜索算法,其實就是在多個bbox同時進行多邊形包含查詢,r-tree搜索平均時間複雜度是 O(logMn) ,假設 M 表示用戶定義的每一個節點下同時檢索的最多子節點數量。
M := 最多子節點數量 Rtree() -> O(logM(Cn)) + O(v)
四叉樹是一種特殊的kd樹二維索引。首先,須要將搜索空間的平面投影分紅幾個分區,咱們稱之爲格網。而後將這些格網分爲幾個分區遞歸直到遇到一個定義的最大深度將樹的葉子才終止。
若是咱們把地球的墨卡託投影和標籤的每個格網、一個標識符添加到父標籤,咱們能夠利用四叉樹結構構建快速地理空間搜索,像必應地圖那樣建立一個瓦片系統。必應地圖將每一個格網標籤稱爲「QuadKeys」,他們直接可轉化這些索引到地圖瓦片上。
爲何我還在談論四叉樹? 由於在文章中提到的「複雜」的S2算法只是一個四叉樹的實現。必應地圖瓦片系統和S2的主要區別是 S2 投影是經過立方體投影貼圖,所以每一個格網都有近視的表面積。這個格網也用在空間擴散曲線來存儲格網中的空間位置標籤。這是一篇有更深層次的解釋文章:S2 最初是用C++完成的,同時綁定到了許多其餘語言上,Go也是其中之一(在geo庫中)。誠然,在s2.Polygon中缺少核心的s2.Region的實現,那麼s2.RegionCoverer 方法也不能在邊界框以外使用。這裏有兩個例子,能夠看到扁平覆蓋各級和RegionCoverer生成多層次覆蓋。
回到咱們的分析上來看,若是咱們爲每一個功能和格網的查詢獲得了一組格網標籤,那麼咱們能夠在兩個多邊形上在常數時間內縮小搜索空間,而後作一個多邊形包含檢查。咱們將爲帶有同一個網格標籤的多邊形數量添加一個新的參數 T,它是和區域縮放級別(zoom-level)是相關聯的常數項。
T := 格網標籤 QTree() -> O(T) + O(v)
有不少方法咱們能夠利用S2網格或者QuadKeys內部覆蓋咱們的邊界而後在一個常數時間內完成多邊形包含查詢。權衡是否跳過某些多邊形包含查詢是全部衍生算法的關鍵,由於遍歷全部狀況會很快吃光內存。咱們能夠經過好比布隆過濾器或者一些前置層來減小內存的使用。或許咱們能夠稍後再深刻細節。
通過剛纔的枚舉,咱們對算法複雜度有下面的一個估計:
q := 查詢點 C := 城市個數 V := 定義一個城市邊界的頂點數 n := 在一個城市柵欄的多邊形數量 v := 柵欄多邊形的頂點 Brute() -> O(Cnv) Uber() -> O(CV) + O(nv) Rtree() -> O(logM(Cn)) + O(v) Qtree() -> O(T) + O(v)
如今參數有點過多,這樣分析看起來不是很清晰,若是咱們去掉一部分參數,咱們能夠更簡單高效地把每種算法說清楚。當咱們迷失在傳統的算法複雜度分析中,我認爲直接作一些實驗或許更加直觀。
原文說,城市降維的方法,能夠把搜索空間從幾萬減小到數百。咱們能夠推斷他們有上百個城市在搜索空間內。造成一個邊界的一個城市的點的數量會有很大的波動,我見過用曼哈頓距離的狀況下,距離會在從一千到六千的範圍內波動。假設他們選擇簡單的定義或者用普克法(Douglas-Peucker)簡化城市邊界到100個節點。
每一個城市有多少又包含多少柵欄多邊形呢?從相同的邏輯做爲城市的數量和咱們知道紐約有167個社區,100年再次看起來像是正確的數量級。看着柵欄的頂點,提出社區像布魯克林的威廉斯堡擁有幾百點,但用戶定義多邊形幾乎確定有簡單形狀的幾點。讓咱們來猜一猜,一樣沿用 100做爲 v的參數值。考慮到v是多邊形包含查詢自己和每一個算法都有,我不擔憂把它正確。我認爲咱們至少在正確的數量級。
C := 100 // 城市個數 V := 100 // 一個城市邊界的頂點數 n := 100 // 一個城市邊界的圍欄數 v := 100 // 一個圍欄的頂點數 M := 50 // rtree 參數 T := 4 // 每一個格網覆蓋多邊形數 Brute() -> 1 * 10^6 Uber() -> 2 * 10^4 Rtree() -> 2.3 * 10^2 Qtree() -> 4 * 10^2
若是咱們的邏輯是合理的,咱們應該獲得兩倍的效率得以提升,Uber算法使用了一個標準的空間索引。
英文原文地址:https://medium.com/@buckhx/un...
更優閱讀體驗可直接訪問原文地址:https://segmentfault.com/a/11...
做爲分享主義者(sharism),本人全部互聯網發佈的圖文均聽從CC版權,轉載請保留做者信息並註明做者 Harry Zhu 的 FinanceR專欄:https://segmentfault.com/blog...,若是涉及源代碼請註明GitHub地址:https://github.com/harryprince。微信號: harryzhustudio商業使用請聯繫做者。