在2015年初我们创建了一个微服务,它只做一件事(也确实做得很好)就是地理围栏查询。一年后它成了Uber高频查询(QPS)服务,本次要讲的故事就是我们为什么创建这个服务,以及编程语言新秀Go如何帮我们快速创建和扩展该服务。
背景
在Uber,一个地理围栏就是在地表人为定义的地理区域(或多边形几何区域)。地理围栏在Uber被广泛用于基于地理位置的设置。向用户展示给定区域有哪些产品可以使用,根据特殊需要(如机场)定义区域,并在乘车高峰时在相邻区域实施动态定价是我们产品的重要应用场景。
一个科罗拉多地理围栏示例。
第一步是通过用户手机获取地理位置信息如经纬度,进而确定用户所在地理围栏。这个功能分散在多个服务或模块中。因为我们从整体架构向微服务架构迁移,我们选择将这个功能做成一个新的微服务。
使用Go语言
Node.js曾经是我们实时市场团队主力开发语言,所以我们在Node.js上有较多的知识储备和经验。但是Go在以下几个方面更符合我们的需求:
1、高吞吐低延迟的需要。Uber手机应用中的每个请求都需要地理围栏查询,而且响应快速(99% < 100毫秒)频繁(每秒成千上万),
2、适用于CPU密集型。地理围栏查询是点聚计算的CPU密集型服务。Node.js非常适合我们其他I/O密集型应用,但由于Node天生就是解释型动态语言,所以它不适合此类应用。
3、非中断后台加载。为了给查询服务提供最新的地理围栏数据,服务需要在后台不断的从多个数据源加载内存数据。因为Node.js是单线程的,所以后台更新会对CPU造成较长时候的堵塞(例如,CPU密集的JSON解析),从而影响到查询响应时长。但Go不存在这些问题,因为goroutines 可以使用多核,后台任务和前台查询可以并行。
是否使用地理信息索引:这是一个问题
通过经纬度指定一个地理位置后,如果从我们成千上万的地理围栏中确定它属于哪一个?简单粗暴的做法是:使用点聚检查方式,如光线投射算法,从所有地理围栏数据中查找。但这种式太慢。所以,我们如何缩小查询范围以提高效率?
我们没有使用R-tree做地理围栏索引和比较复杂的S2,通过观察我们发现Uber的业务模式是以城市为中心的;业务规则和地理围栏通常用一个城市来定义,所以我们选择了一个简单的路由方式。我们把地理围栏整理为两层结构,第一层是城市地理围栏(定义城市边界),第二层是每个城市内的地理围栏。
对于每个查询,我们首先对所有城市地理围栏做线性扫描查找所在城市,然后对该城市的地理围栏数据做线性扫描。这个解析方案的运算复杂度是O(N), 通过这个简单的技术我们将N从10,000s减少到100s。
架构
我们希望这个服务是无状态的,这样每个请求可以发送到任意实例,而且得到结果是一致的。这意味着每个实例都拥有全量数据,而不是只存储部分数据。我们生成了一个统一的拉取计划,这样不同服务实际的地理围栏数据可以保持同步。因面这个服务的架构也就变得简单。后台任务定时从不同的数据存储拉取地理围栏数据。这些数据是在内存中存储,以提高查询速度,当服务需要重启时会序列化到本地文件。
我们的地理围栏数据查询架构
处理Go内存模型
在我们的架构中需要对内存中的地理索引数据并发读写。当后台拉取任务写索引时,可能前台查询引擎同步读取索引。有Node.js经验的人熟悉了单线程模式,Go的内存模型对他们是一个挑战。这对我们曾产生对负面影响。我们试图使用sync/atomic 包的原语StorePointer/LoadPointer 来管理内存边界问题,但这导致代码很脆弱且难以维护。
最后,我们采取了折中的方式,使用读写锁来异步处理对地理索引的访问。为了减少锁的争夺,新的索引在以原子的方式合并到主索引之前先建立索引片段。与 StorePointer/LoadPointer的方式相比,这些稍微增加一些延迟,但我们有理由相信代码的简洁和可维护性比这一点小小的延迟更有价值。
我们的经验
回顾以往,我们很庆幸当初使用Go语言,并使用这种新的语言开发我们的服务。亮点如下:
1、开发效率高。C++,Java和Node.js的开发者只需要很短的时间就可以掌握Go,代码易于维护。(静态语言更加清晰,没有莫名其妙的意外)。
2、在吞吐量和延迟方面性能很好。我们主数据中心,有针对非中国区的独立服务,在2015年度高峰期间40台服务器在170k QPS的负载情况下CPU只使用了35%。95%的响应时间小于5毫秒,99%的响应时间小于50毫秒。
3、超级稳定,这个服务自上线以来,99.99%的时间正常运转。当机时间主要是由初学者的编程错误和第三方库的文件描述符泄露导致。我们至今尚未遇到Go的运行时错误。
接下来?
过去Uber主要使用Node.js和Python,很多Uber新的服务开始选择使用Go来创建。Go是Uber未来的趋势,所以如果你对Go很有激情,无论是专家还是初学者,都欢迎你来应聘,我们正在招聘Go开发者,噢对了,传送门请点这里!
图片来源:“金门地鼠”,作者:Conor Myhrvold,摄于三藩市的金门公园。标题解释:地鼠(Go gopher)是Go项目的吉祥物,是Go的标识。