基本的业务需求可分为两大部分,第一部分是手机端间隔必定时间上报一次位置信息,第二部分是后台系统能够实时看看手机设备当前所在的位置,并绘制轨迹。node
总之就是用户安装了此应用,就至关于给本身装上了一个跟踪器,所到之处,都将有所记录。nginx
咱们先来保守计算一组数据,假定用户基数为10万,每隔5秒上报一次位置信息,而这5秒期间,地图SDK大概会给出2次定位数据,由此得出,一次上报的瞬时峰值大概是20万条数据,一天将会达到300多万的数据。sql
这样的QPS已经算是很高了,固然,绝大多数状况下是不会触顶的。数据库
另一方面,后台系统在查询的时候,也面临着一个巨大的挑战:如何从海量的数据中找寻符合条件的那一部分?服务器
查询的条件无非是围绕三个维度展开的:时间、对象和区域。网络
相似“查询某块地理位置栅栏内的全部用户在一段时间内的位置信息”,这就是一个典型的查询业务需求,涵盖了三个维度的条件。架构
固然,还有一些距离的测算和排序的需求也是在所不免的。并发
综上所述,咱们要解决两个难点:负载均衡
一、上报用户的实时位置信息; 二、处理海量数据的读写请求。框架
以上两个难点是任何LBS应用不得不攻克的,这是支撑业务发展的关键点。
咱们能够将整个请求连接进行拆解,分别用对应的策略解决这部分的难题:
一、客户端收集数据。这一部分有四个基本要求:准实时,不丢数据,低功耗,高效率。
二、网关限流。主要为了不涌入大量无效请求,消耗系统资源,拖跨服务器。
三、负载均衡。为了应对不断增加的访问量,服务实例必须支持横向扩展,多个实例之间按照必定的策略进行负载均衡。
四、异步化读写数据。须要借助中间件,起到削峰的做用,这一部分的设计会重点阐述。
五、数据存储层高可用。采集的数据不要求强一致性,依据CAP理论,知足AP两个条件便可。解决办法一般是集群化,避免单点故障,进而实现程序上的读写分离策略。
接下来是针对上述五大部分在技术实现上的考量。
目前客户端是苹果机和安卓机,将来极可能会接入友商的车载OS。
在网络情况良好的时候,能够实时采集,实时上报,假若网络情况很差,也不会丢数据,采用MMAP实现本地缓冲区。固然,为了上报效率,综合考虑仍是按批次上传,过于频繁的上报也会导致耗电量攀升。
网关限流和负载均衡交给了nginx,学习和开发成本低,重要的是效果很好,而且可以达到服务横向扩展的目的。
咱们使用了nginx基于ip地址的限流策略,同一个ip在1秒钟内最多容许3个请求,burst=5,为了应对忽然的流量的爆发,还有一个nodelay参数,咱们选择不加了,意味着请求队列和缓冲区都被塞满以后,直接返回503错误码,再也不等待了。
目前,是1个master进程,5个worker子进程,此外,超时时间、最大链接数以及缓冲区的设置值得斟酌,服务器性能好的话,能够设置得高一些。负载均衡的策略不是基于权重的设置,而是使用的最少链接 (least_conn),由于在我看来,每一个服务实例都是能够被同等对待的,哪台空闲,就优先请求哪台。若是非要在此基础上设置一个weight,那就把性能高的服务器设置成较高的权重。
为了应对大量的位置上报请求,必然须要引入消息队列。由于技术团队一直在使用RabbitMQ,而且一番压测后,表现依然坚挺,因此成为了咱们的不二选择。
此外,为了减轻存储层的压力,在数据落盘前有一个缓冲机制,是典型的主从双Buffer缓冲池,避免与存储层频繁交互,占用过多connection,提升了存储层的工做效率,可是缓冲池Flush的时候,会带来一次“IO尖刺”,能够调整缓冲池的Threshold,把“IO尖刺”控制在一个合理的范围内。
最后,也是最引人关注的是存储层的选型。咱们面临的选择比较多,咱们预研了4种有可能的方案:MySQL,Redis,PostGIS,MongoDB。
MySQL的方案有两条路可走,第一是使用纯SQL进行计算,很明显这条路子受限于数据量,随着数据量的增大,计算量剧增,系统性能急剧降低。第二是使用MySQL的Spatial Indexes,可是这类空间索引是MySQL5.7版本引入的,不巧的是,咱们用的是阿里云RDS,MySQL版本是5.6.16,不得不放弃MySQL的方案。
基于Redis GeoHash的方案,在性能方面是没任何问题的,可是有一个重大的缺憾,就是很是受限于距离查询,而没法方便的匹配另外的附加属性,也就是上述三维需求(时间、对象、区域)中,只能知足区域查询的需求,若想进一步过滤,还须要借助其余的存储技术。
留下来的PostGIS和MongoDB是如今比较通用的解决方案。
PostGIS的专业性很高,是地图服务商的首选,对OGC的标准支持得很是全面,提供的函数库也十分丰富。固然,MongoDB也不赖,3.0版本引入的WiredTiger存储引擎,给MongoDB增色了很多,性能和并发控制都有了较大幅度的提高。
单就LBS应用来讲,二者均可以很好的知足各方面的业务需求,性能也不是咱们首要考虑的因素,由于二者的可扩展性都很强,不容易触及瓶颈。最后咱们选择了MongoDB,是由于技术团队对此接受程度更高,MongoDB原本就是公司技术栈中的一员。PostGIS相对来讲学习成本较高,由于Postgresql是一种关系型数据库,若是只接入普通的数据类型,其实至关因而维护了另外一种MySQL,彻底不必,可是若是想用上GIS相关数据类型,现有的ORM框架支持得并不友好,开发效率低,维护成本高。
出于对用户体验的考虑,不能由于读写速度慢让用户傻傻的等待,这类问题有一个通用的解决方案:异步化。
异步化的写请求很好实现,如前述所说,经过消息中间件来解决此问题,对于客户端的位置上报请求,不须要强一致性,能肯定数据被塞入消息队列中便可。
除此以外,还在消息队列和存储层之间加入了双Buffer缓冲池,用以增长flush的效率。
上图中的Buffer-1和Buffer-2共同组成一个循环队列,每次只有一个Buffer用了缓冲数据。图中的Flush Point表示每一个Buffer达到必定的阈值(Threshold)后,将会触发数据落盘,而且会在此刻切换Buffer。图中所示的阈值是50%,能够根据实际状况上下调整。
值得注意的是,在Flush Point,应该是先切换Buffer,再flush数据,也就是在flush的同时,还能接收上报的位置信息。若是要等待flush完毕再切换,将会致使流量过渡不平滑,出现一段时间的队列拥塞。
接下来要解决大量读请求,应对这种问题,有一个不二法门就是:拆。
很是相似“分表”的思想,可是在这个场景下,没有传统意义上“分表”所带来的诸多反作用。
“分表”有一个规则,好比按Hash(user_id)进行“横向分表”,按照经常使用字段进行“纵向分表”。受到Hadoop关于资源管理的启发,在此位置采集系统中,使用了“元数据”来代替分表规则,这里的“元数据”至关于NameNode,是管理数据的数据,将散落在不一样位置的数据集中化管理。
如上图所示,按城市级别进行分库,每一个城市的位置数据相对独立,体如今客户端的功能就是“切换城市”。“分表”意即分红多份Collection,以一天为最小粒度,由于一个城市的当天数据一般是“最热”的,众多查询请求都须要这份数据,分页,聚合,排序等需求都不是问题了,所以一天一个Collection是比较合理的切分。固然,若是一天的数据量过少,能够适当延长数据切分的周期。
那么,若是查询请求的时间段跨天了,该怎么办呢?
这时候,“元数据”就发挥威力了。
查询请求分为两部分,第一个是定位元数据,就是在meta data中找寻符合条件的全部Collection(s),为了尽量缩小查询范围,须要指定必定的时间区段,这个也是有现实意义的。全部的位置数据被分为了三种等级:Hot,Cold,Freeze,Hot等级一般是指当天的数据,读写最频繁的部分;Cold等级被界定为过去一个月内的数据,应对的各种查询和分析的需求;Freeze等级是已归档的数据,没有特殊状况不会被解冻。
经过时间区段拿到N个Collection(s)后,附加上其余的查询条件,循环N次,成功循环一次,就渲染一次获取到的数据,直到循环结束。
上述N>1的业务场景只可能出如今后台管理系统,也就是移动端不会出现跨天的业务需求,提供的API都是针对Hot级别的数据。若是一旦出现了这种状况,能够限制时间区段,好比只能获取两天内的数据。
本文详细阐述了手机定位采集系统的设计,整个系统的复杂性集中在了数据的存储和呈现,我在此文中都给出了相应的解决方案。除此以外,整个采集系统还会有其余的功能模块,诸如日志采集与分析,实时监控等,限于篇幅,再也不逐一叙述了。
如下是扩展阅读,对于理解整个系统会有所帮助,建议阅读之。
扫描下方二维码,进入原创干货,搞“技”圣地。