在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在2015年初,我们构建了一个只做一件事(也的确做的非常好)的微服务——查找地理围栏(geofence lookup)。一年后,这项服务已经成为Uber数百个正在运行的服务中每秒查询次数(QPS)最高的服务。接下来,本文将谈论我们构建这项服务的原因以及我们是如何使用Go语言快速构建和扩展这项服务的。\ 背景\在Uber,一个地理围栏就表示地球表面上人为划分的一个地理区域。此外,我们进一步在基于地理的配置中使用地理围栏的概念。地理围栏的概念在很多地方发挥了很重要的作用——向用户展示在某个位置可使用的产品时,定义机场等特殊用途区域时以及在很多人同时呼叫实现动态定价时。\ \Colorado的一个地理围栏样例\ 在提取用户手机的经纬度坐标等基于地理位置的配置信息时,需要做的第一件事情就是确定该位置落在哪个地理围栏中。而该功能已经在多个服务或模块中被实现。但是,当我们从一体化架构转移到基于微服务的架构时,我们选择了将这项功能集中在一个新的单独的微服务中进行实现。\ 选择Go的原因\当我们评估所要使用语言的时候,Node.js正是广大的服务设计团队普遍采用的编程语言。而我们也在Node.js的使用方面有着丰富的经验。然而,Go语言却由于以下原因满足了我们的需求:\
是否建立地理信息索引——这是个问题\当给定了一个经纬度坐标的位置信息时,如何从上万个地理围栏中找到该位置所在的那一个呢?最直接而暴力的解决方法为:浏览所有的地理围栏,然后采用光线投射(Ray Casting)等算法进行PIP检查。但是,这种方法实在是太慢了。那么,我们怎么才能有效的缩小搜索空间呢?\ 由于Uber的商业模型是以城市为中心的,我们并没有采用R-tree或S2等结构来索引地理围栏,而是采用了一个相对要简单很多的算法;商业规则以及相关的地理围栏都和城市相关。这使得我们可以采用层次式的方式来组织地理围栏——第一层是定义城市边界的围栏,而第二层是城市内的城市围栏。\ 对于每一次查询,我们首先线性扫描所有的边界城市围栏,找到目的城市。然后,我们采用另外一种线性扫描的方法来找到城市内的目标城市围栏。尽管新算法的复杂度仍然是O(N),它却把N从万降到了百,大大减少了算法执行的复杂性。\ 架构\根据设计需求,我们希望这项服务是无状态的。因此,每一次请求都可以发送给任意的服务实例,并获得相同的结果。这意味着每一个服务实例都必须掌握全局信息,而非局部信息。我们采用了一种确定性的轮询调度策略,从而保证了不同服务实例的地理围栏数据都是同步的。这样,该服务的架构也非常简单。后台任务周期性地轮询来自不同数据源的数据。而这些数据就保存在主存中以服务不同的查询。同时,数据被串行保存在本地文件系统中,以实现系统重启时的快速引导。\ \查询地理围栏服务的架构\ 处理Go存储模型\我们的服务架构需要对内存中的地理索引信息进行并行读写访问。在特殊情况下,后台轮询任务修改索引,而前台查询引擎同时从索引中读取信息。相比于利用单线程的Node.js进行服务编写的情况而言,Go存储模型必然会遇到挑战。尽管Go语言可以利用goroutine和channel自然的实现并行读写,其带来的性能影响不可忽视。我们试图利用sync/atomic包中的StorePointerLoadPointer原语来自己管理内存屏障。但是,这导致了代码难以理解和维护。\ 最终,我们选择了一种折衷的方式——利用读写锁来保证对地理索引的同步访问。为了减少锁的竞争,新的索引片段在被自动交换到主索引之前都是处于隐藏状态的。相比于StorePointerLoadPointer方法,这种锁会轻微增加查询延迟。但是,我们维护代码库的工作却变得简单很多,非常值得。\ 我们的经验\回首过往,我们非常开心选择了Go语言来编写我们的服务。其带来的好处包括:\
未来的展望\尽管Uber曾经主要采用Node.js和Python,Go语言正在成为Uber设计师构建新服务的选择。\\\ 感谢郭蕾对本文的审校。 \给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected]。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们。 |
请发表评论