[译] 解密 Uber 数据团队的车辆定位查询算法

clipboard.png

英文原文做者: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

clipboard.png

咱们首先须要解决的是点在多边形内判断的成本(一般我称之为 多边形包含查询)。维基百科的文章很好地分别介绍了两种经常使用算法用于多边形包含查询: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-Tree

r树数据结构是一种为多维对象带有特殊的序列定义的基于b树的算法。序列的定义方法是比较一个最小边界矩形(MBR)到另外一个对象的MBR之间是否知足彻底包含关系。在二维状况下的地理围栏,MBR是一个经过一组最大最小的坐标系来框定的边界框(简称bbox)。检查一个对象的bbox与另外一个对象的bbox的包含关系的复杂度是常数时间O(n)。这篇在维基百科上的文章解释得很是详细,下面是一张可视化的图来解释对象的边界框。

clipboard.png

若是本身实现不靠谱,也能够直接用Go里面的 rtreego 这个包来实现,不过对于二维的状况下还须要额外消耗一些空间。

总结下r-tree搜索算法,其实就是在多个bbox同时进行多边形包含查询,r-tree搜索平均时间复杂度是 O(logMn) ,假设 M 表示用户定义的每一个节点下同时检索的最多子节点数量。

M := 最多子节点数量
Rtree() -> O(logM(Cn)) + O(v)

QuadTree

四叉树是一种特殊的kd树二维索引。首先,须要将搜索空间的平面投影分红几个分区,咱们称之为格网。而后将这些格网分为几个分区递归直到遇到一个定义的最大深度将树的叶子才终止。

若是咱们把地球的墨卡托投影和标签的每个格网、一个标识符添加到父标签,咱们能够利用四叉树结构构建快速地理空间搜索,像必应地图那样建立一个瓦片系统。必应地图将每一个格网标签称为“QuadKeys”,他们直接可转化这些索引到地图瓦片上。

clipboard.png

S2

为何我还在谈论四叉树? 由于在文章中提到的“复杂”的S2算法只是一个四叉树的实现。必应地图瓦片系统和S2的主要区别是 S2 投影是经过立方体投影贴图,所以每一个格网都有近视的表面积。这个格网也用在空间扩散曲线来存储格网中的空间位置标签。这是一篇有更深层次的解释文章:S2 最初是用C++完成的,同时绑定到了许多其余语言上,Go也是其中之一(在geo库中)。诚然,在s2.Polygon中缺少核心的s2.Region的实现,那么s2.RegionCoverer 方法也不能在边界框以外使用。这里有两个例子,能够看到扁平覆盖各级和RegionCoverer生成多层次覆盖。

clipboard.png

回到咱们的分析上来看,若是咱们为每一个功能和格网的查询获得了一组格网标签,那么咱们能够在两个多边形上在常数时间内缩小搜索空间,而后作一个多边形包含检查。咱们将为带有同一个网格标签的多边形数量添加一个新的参数 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个节点。

clipboard.png

每一个城市有多少又包含多少栅栏多边形呢?从相同的逻辑做为城市的数量和咱们知道纽约有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算法使用了一个标准的空间索引。

clipboard.png

参考资料

英文原文地址: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商业使用请联系做者。

相关文章
相关标签/搜索