地图团队逆地理编码调用量优化实践

avatar
@哈啰

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

背景

逆地理编码(将经纬度转换为详细结构化的地址)调用目前是整个地图服务调用量最大的接口,业务主流程多个节点依赖逆地理服务,接口不可用会直接阻塞订单。目前高峰期高德逆地理接口的QPS(Queries Per Second 每秒查询率)经常会几倍的超掉,超限报错的请求会通过哈啰地图平台的LBS(Location Based Services 基于位置的服务)兜底返回数据。

目前的逆地理调用量日均在2-3亿左右远超现有业务体量,面对业务在业务冲单时预估的几倍调用量增长与供应商较高昂的提额价格,虽有LBS兜底但高峰期大量流量打到LBS也会极大增加服务压力,可能引发连锁反应,在冲单前优化调用量的问题保障接口稳定性迫在眉睫。

整体方案设计

首先是要明确每个节点的调用量是多少,针对调用量头部的节点我们想通过code review和与业务同学沟通调用节点的业务诉求,看能不能直接删除或通过其他方式进行优化,对于不好优化的,我们想通过缓存机制来减少一部分调用。

信息补充

目前报表只能从业务维度来拆分,并不能知道具体每个节点的调用量是多少,是哪个节点的调用量过大。所以第一步就是埋点补充调用节点参数。细致的埋点是我认为所有优化的第一步,它能明确线上究竟是什么样的,才能更好的针对性优化,并在后续的优化回收,监控等过程中提供数据支撑。拆分后报表示意如下:
image.png

逐个击破

基于第一步的拆分,我们已经能知道调用量较高的节点都是哪些,针对这些节点我们将结合业务使用诉求与代码细化场景,寻找问题和优化空间进行了逐个优化。这步优化总结下来有两部分:

当前节点并不需要调用

因信息不对称导致的业务场景使用不当,如:
1.业务页面有监控到位置更新就用当前位置触发逆地理的逻辑,实际高德定位组件内部会触发一次当前位置的逆地理调用并返回给业务,业务并不需要额外调用。
2.地图缩放时中心点并没发生变化,不需要请求逆地理。

优化调用节点减少调用

结合业务使用场景上下文,尽可能的减少调用,如:
1.业务有一个功能会检测用户如果距离当前地图中心点超过50m,就把用户当前位置变成地图中心位置并会触发逆地理的请求,但功能上线后由于在APP首页生效无法释放导致整个APP生命周期一直在触发位置跟随功能造成不必要的调用,类似问题的解决方法是对功能限制作用域,感知页面与APP的生命周期,如APP不在前台或不在当前页面就关闭功能。
2.POI(Point Of Interest 兴趣点)搜索后会触发的上车点检索并进行逆地理的调用,上车点吸附成功时需要的逆地理数据POI数据中均包含,上车点吸附失败时逻辑是使用POI搜索的数据也不需要逆地理调用,所以我们省去了POI搜索后触发的上车点检索后的逆地理调用,均用POI数据填充逆地理的数据。

阶段产出

这部分优化结束后我们APP调用量级从日均2-3亿降到了3-4千万左右。

使用缓存

我们希望能通过缓存来提高数据的使用率,用内存+磁盘缓存的方式持久化缓存数据以提高命中率。
使用逆地理接口请求的参数(经纬度+请求半径)生成key,将请求结果存入到内存缓存+磁盘缓存中,获取缓存时先从内存中查找,没有再从磁盘中查找。

经纬度的聚合问题

由于定位本身就会因为各种原因发生偏差(基站定位?信号遮挡?)或者用户在短距离内移动,直接以经纬度生成key会严重影响缓存命中率,我们希望能聚合一定范围内的经纬度,这样可以有效提升缓存命中率。

GeoHash算法
GeoHash是一种地址编码方法,他能够把二维的空间经纬度数据编码成一个字符串。

算法思想
GeoHash表示的并不是一个点,而是一个矩形区域,编码越长,表示的范围越小,位置也越精确,GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。

算法原理
经度范围是东经180到西经180,纬度范围是南纬90到北纬90,我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

如果纬度范围[-90°, 0°)用二进制0代表,(0°, 90°]用二进制1代表,经度范围[-180°, 0°)用二进制0代表,(0°, 180°]用二进制1代表,那么地球可以分成如下4个部分:

image.png

会生成类似于Z的曲线,如果在小块范围内递归对半划分呢?

image.png

当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为 Peano 空间填充曲线, Peano 空间填充曲线有突变性问题(有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大)和临界问题(与相同GeoHash编码的点的距离有可能大于临界不同GeoHash编码的点的距离 如上图红点蓝点的距离是远大于红点与绿点之间的距离),评估后对于我们的使用场景可接受。

编码长度就是对方块的划分次数。执行逻辑:

  • 根据设定的编码长度对当前经纬度分别进行划分,得到两组二进制串(10101、01010)后以偶数位放经度,奇数位放纬度的方式合并成一个二进制串(1001100110)
  • 将二进制串划分每5位一组,不足5位补0(10011、00110)
  • 将各组的5位二进制串转成十进制,5bits对应着10进制的数值为0-31(19、6)
  • 用0-9、b-z(去掉a、i、l、o)这32个字母进行Base32编码,即对照下标将其转换为字符串(m、6)

image.png

  • 最后拼在一起得到的字符串就是GeoHash编码(m6)

编码长度对应的偏差范围

image.png

目前缓存设置的编码长度为GeoHash9(5m左右误差)。

缓存淘汰机制

采用业界主流的LRU算法策略(Least Recently Used,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰)。

算法思想
如果数据最近被访问过,那么将来被访问的几率也更高。原理解析新数据插入到链表头部;每当缓存命中(即缓存数据被访问),则将数据移到链表头部;当链表满的时候,将链表尾部的数据丢弃。

image.png

其他淘汰机制

因为高德逆地理数据偶尔也会有badcase需要高德更新数据fix,我们希望数据除了LRU被淘汰以外还能有其他维度的机制来更新数据:

  • 时间维度:我们限制只使用2天内的数据,如超过则淘汰数据,重新请求并缓存
  • 访问次数维度:我们限制数据使用10次后,会主动淘汰数据,重新请求并缓存

阶段产出

冲单当日数据回收缓存命中占比iOS为26% 安卓为29.4%。

缓存算法优化

缓存命中分析

目前日均的缓存命中率在25%左右,跟我们的预期相比还是会低一些,原因分析如下:目前因为避免占用大量内存(根据附近POI多少不同一条数据在2-8kb之间)造成OOM问题(Out Of Mana法力耗尽 Out Of Memory内存溢出 占用内存过大会被系统强制杀死 造成闪退),规定了缓存最大数量为50个,那是否调大缓存个数就能提高命中率呢?比如提高到200-300个,我们认为也不能提升太高,而且会增加OOM的风险。从我们的实现与场景分析下:

1.LRU算法分析
优点:LRU算法实现简单,并且在大量频繁访问热点页面时十分高效。
缺点:由于LRU的机制,遇到偶发性或周期性的批量操作会导致LRU的命中率急剧下降,缓存污染情况比较严重。

2.结合场景分析
根据我们的出行场景,我们希望命中缓存的数据分为两部分:一是短时间内的重复请求,这部分目前已经可以满足;二是根据用户的使用习惯缓存下家或公司学校等常用地附近的逆地理信息,这部分权重较高比较容易命中缓存,但用户在行程过程中会有大量数据写入造成“缓存污染”。所以,我们需要一种增加权重机制的缓存淘汰算法来解决行程过程中的缓存污染。

LRU-K算法

算法思想
LRU-K中的K,其实是指最近访问页面的次数,LRU算法其实就是LRU-1,但是因为仅访问1次就能替代别人,可能会造成“缓存污染”的问题,因此提出了LRU-K的概念,其核心思想就是将访问一次就能替代的“1”提升为"K"。

原理解析
LRU-K算法需要维护两个队列:历史队列和缓存队列。

历史队列保存着缓存的对象(内存中),当对象访问次数达到了K次,该对象出栈,并保存至缓存队列;若尚未达到K次则继续保存,直至历史队列也满了,那就根据一定的缓存策略(FIFO、LRU、LFU)进行淘汰。

缓存队列则是保存已经访问K次的对象,当该队列满了之后,则淘汰最后一个对象,也就是第K次访问距离现在最久的那个对象。

image.png

对应到我们的实现里就是历史队列的数据获取超过K次后才会加入到内存缓存+磁盘缓存进行持久化保存,而历史队列本身也在充当内存缓存的角色不会有重复的存储,且由于有了历史队列进行权重过滤,会大大减少数据库写入,减少整体性能消耗。下图为选用的磁盘缓存(YYCache)的读写性能图。

image.png

这部分正在进行中,预计能提高缓存10-20%的命中率。

容灾方案

如果在上述优化后还出现高德QPS超限降级到自研实现且自研实现QPS也超限的情况,此时继续调用也没有任何意义了,我们希望能通过降低自身的请求的方式来减轻当前服务器的压力(是一种较无私的方案)。

满足上述条件后会触发端上的容灾策略,会去以GeoHash7(80m左右误差)生成key获取缓存,如获取不到则停止服务调用2s直接报错以减轻当前服务器的压力。

image.png

防裂化

接入到MapService(地图团队维护的新组建)后,会有独立的报表与LBS的流量监控,对调用量超过原QPS的接入方会钉钉发出告警通知。

image.png

并可通过LBSAdmin平台(LBS管理平台)对流量异常的节点进行动态降级,无需发版就能恢复线上异常节点对其他业务的影响。

image.png

整体流程如下:

image.png

收益

地图平台移动端通过对数据来源的细化、与业务同学深入交流分析、合理使用算法能力、与地图后端能力深度结合不断进行优化升级,完整的支撑了公司冲单的活动,日均调用从2-3亿降到了2-3千万左右。

总结

最后总结出我们认为在任何优化中都很重要的点:
1.保持敬畏:优化前要非常明确这段代码的作用与影响面,并且每个优化都要可灰度,可恢复(有些代码怎么看都是多余的,删除了看似也没啥影响,一发布就出现线上事故)稳扎稳打不要适得其反。
2.白盒优化:还原线上运行的真实样貌,要明确知道要优化哪里,优化后的真实效果,不能只靠YY。
3.做长远建设:长远建设会成为后续持续优化打下结实的基础。
4.防裂化:不要忽视防裂化建设,否则后面将变成“一年一度整活大赛”。后续我们将会继续针对调用量、稳定性、业务隔离、多数据源等方向做更多有趣的尝试。

作者简介:
陈东冉、刘大白、任赛龙等,均来自哈啰人工智能与地图团队-地图平台。

招聘信息:
哈啰地图团队诚招高级、资深工程师,Base上海、杭州。我们致力于为哈啰提供高性能、高可用、高体验的端到端出行解决方案服务,涵盖复杂架构设计、深度性能优化、算法支撑赋能等技术领域,端侧地图解决方案,欢迎有兴趣的同学投送简历至:careers.hellobike.com/