CloudMongoDB优化提升LBS服务性能十倍后端服务的挑战。MongoDB对LBS查询有着友好的支持,也是各大LBS服务商的首选数据库。腾讯云MongoDB团队在运行过程中发现,原生MongoDB在LBS业务场景下存在较大的性能瓶颈。经过腾讯云团队的专业定位分析和优化,云MongoDB在LBS服务中的综合性能提升了10倍以上。腾讯云MongoDB出色的综合性能为摩拜单车等国内主要LBS服务商提供了有力保障。LBS业务特点以共享单车业务为例,LBS业务有两个特点,即时间周期性和坐标分布不均。1.周期性高峰期和低谷期QPS量有显着差异,高峰期和低谷期的时间点相对固定。2、坐标分布不均。乘坐地铁的通勤者如果注意的话,可能会发现在早高峰时段,地铁周边的共享单车很多,而在下班时间,地铁周边的共享单车数量就很少了。如下图,99%以上的坐标都集中在经纬度(121、31.44)附近。此外,一些特殊赛事也会造成积分分配不均。例如,特殊节假日大量顾客涌入深圳湾公园,大量自行车也将放置在该区域。一些地区的自行车交通非常集中,而另一些地区则非常稀疏。MongoDB的LBS服务原理MongoDB使用2d_index或2d_sphere_index来创建地理位置索引(geoIndex)。两者之间几乎没有区别。下面以2d_index为例进行介绍。1.二维索引的创建和使用db.coll.createIndex({"lag":"2d"},{"bits":int}))通过上面的命令创建一个二维索引,索引的精度由bits指定,位数越大,索引的精度越高。大bit造成的插入开销可以忽略不计index,其中spherical:true|false表示如何理解创建的二维索引,false表示将索引理解为平面二维索引,true表示将索引理解为球面经纬度索引。这更有趣。二维索引可以表达两种含义,不同的含义是在查询时理解的,而不是在创建索引时理解的。2、二维索引原理MongoDB使用GeoHash技术构建二维索引(参见wikigeohash文本链接https://en.wikipedia.org/wiki/Geohash)。MongoDB使用平面四叉树划分生成GeoHashId。每条记录都有一个GeoHashId,通过GeoHashId->RecordId索引映射存储在Btree中。显然,2bits的精度可以把平面分成4个格子,4bits的精度可以把平面分成16个格子。2d索引的默认精度在长度和宽度上都是26。该指数将地球分成(2^26)(2^26)个区块,每个区块的边长估计为2PI6371000/(1<<26)=0.57米。mongodb官网上面提到的60cm的精度是这样估算的。默认情况下,传统坐标对上的2d索引使用26位精度,这大致相当于2英尺或60厘米的精度,使用默认范围-180到1803.2DMongodb中的索引存储我们上面提到Mongodb使用平面四叉树来计算Geohash。事实上,扁平四叉树只存在于运算过程中,实际存储中不会用到。Insert对于一个经纬度坐标[x,y],MongoDb计算该坐标在2d平面中的网格数。number是一个52bit的int64类型,作为btree的key,所以实际的数据是按照{GeoHashId->RecordValue}插入到btree中的。Query对于geo2D索引的查询,常用的有geoNear和geoWithin两种。geoNear找到离某个点最近的N个点的坐标并返回。这个需求可以说构成了LBS服务(陌陌、滴滴、摩拜)的基础。geoWithin查询多边形内的所有点并返回它们。我们重点介绍使用最广泛的geoNear查询。geoNear的查询过程,查询语句如下}//filtersmaxDistance:0.0001531//distanceinaboutonekilometer}geoNear可以理解为一个从起点开始不断向外扩散的循环搜索过程。如下图所示:由于圆本身的性质,距离上任意一点的距离外环到圆心的距离必须大于内环上任意一点到圆心的距离,所以使用环展开迭代的好处是:1)减少了需要排序比较的点2)可以尽快找到满足条件的点返回,避免不必要的搜索MongoDB的实现细节这种情况下,如果搜索到的内环点数太多小的,环每次扩展的步长都会加倍。MongoDBLBS服务遇到的问题一些大客户在使用MongoDB的geoNear功能查找附近对象时,经常会遇到查询慢的情况。有很多问题。早高峰的压力是低谷的10-20倍。不均匀坐标慢,查询严重,濒临雪崩。初步分析发现这些查询扫描了太多的点集。如下图所示,查找500米内最近的10条记录耗时500ms,扫描了24000+条记录。类似的慢查询占到高峰期查询量的5%左右。测试环境复现并定位了排查数据库的性能问题,主要从锁等待、IO等待、CPU消耗等方面进行分析。上面截图扫描的记录太多,直观上是CPU或IO消耗瓶颈。为了严谨起见,在复现测试环境后,发现slowlog中并没有明显的timeAcquiringMicroseconds项,排除了MongoDB执行层面的锁竞争问题,选择内存较大的机器进行制作驻留在内存中的数据。我们发现上面的用例仍然需要200ms以上的执行时间。10核CPU资源对于截图中的情况只能支持50QPS。为什么扫描集这么大我们上面说了,MongoDB寻找最近点的过程是一个循环扩容的过程。如果内环中满足条件的点数不够多,则每次展开半径都会增加一倍。因此,当遇到内环点稀少而外环点密集的场景时,很容易陷入BadCase。如下图所示,我们希望找到距离中心点最近的三个点。因为环膨胀太快,外环做了很多无用的扫描和排序。这样的用例符合实际场景。早高峰时段,车辆聚集在地铁周围。你从家里出发,一路走到地铁,边走边找。在共享单车软件上动态搜索离你最近的10辆车。附近只有三两辆,所以扩大搜索半径以地铁为中心,将地铁周围的几千辆汽车全部扫描计算出来,将剩下的离自己最近的七八辆汽车返回。我们已经知道了问题的解决方案,我们对此的优化方法是控制每个圆。为此,我们在geoNear命令中添加两个参数,并将它们传递给NearStage。hintCorrectNum可以控制结果质量的下限,返回的前N个点必须是距离中心点最近的N个点。hintScan用于控制扫描集大小的上限。hintScan:扫描的点集大小大于hintScan后,会进行模糊处理。hintCorrectNum:返回结果个数大于hintCorrectNum后,进行模糊处理。这种优化本质上是通过牺牲质量来尽快返回结果。对于中国大部分的LBS服务来说,完全没有必要严格。并且可以通过控制参数来获得最严格的近期效果。在搜索过程中,密集的点落在一个环中,它们之间的距离不会小。优化上线后,部分大客户MongoDB性能上限提升10倍,从单机1000QPS提升至10000QPS以上。与Redis3.2的对比Redis3.2也增加了地理位置查询的功能,我们也会将开源的Redis和云数据库MongoDB进行对比。如何使用Redis:GEORADIUSappname120.99396531.449034500mcount30asc.在密集数据集场景下,使用腾讯云MongoDB和开源Redis进行性能对比。下图是MongoDB单实例和Redis单实例在密集数据集上24核CPU机器上的测试对比。需要注意的是Redis本身是一个单线程的内存缓存数据库。MongoDB是一个多线程的高可用持久化数据库,两者的使用场景有很大的不同。总结MongoDB原生的geoNear接口是国内各大LBS应用的主流选择。当原生MongoDB出现密集点集时,geoNear接口的效率会急剧下降,单机甚至可能不到1000QPS。腾讯云MongoDB团队对此不断优化。在不影响效果的情况下,geoNear的效率提升了10倍以上,为摩拜单车等我们的客户提供了有力的支持。同时,与Redis3.2相比,它也有很大的性能优势。原文链接:https://cloud.tencent.com/community/article/723875【本文为专栏作者《腾讯云技术社区》原创稿件,转载请联系原作者获得授权】点此查看更多关于作者的好文
