您的位置 首页 golang

Uber:利用Golang构建高性能查询服务实践

Uber作为世界上最大的的互联网在线约车服务商,有着世界上上最大的地理信息查询和服务,如何提高这些在线服务的响应时间,提高其查询性能(QPS,每秒查询数)是一个极大的极大的挑战,本文就给大家分享一个Uber利用golang语言提高服务性能的案例。

背景

在2015年初,Uber构建了一个 微服务 ,用来进行地理围栏的查询服务。随着业务的扩展,在一年后,这个服务成了Uber在线服务中查询量最大的服务,成了业务瓶颈。提高其查询性能迫在眉睫!一般来说,为了提高服务的性能有两个方法:一是通过横向扩展,增加服务的硬件资源;还有一个就是通过优化或者重构提高服务的软件性能。硬件的扩展还取决于服务的架构支持,不是所有架构都是可以通过横向增加硬件来提高性能。而Uber采取的方法是使用Golang语言进行重构服务。

地理围栏 是指地球表面上人类定义的地理区域(或几何学上的多边形)。Uber服务中广泛使用了地理围栏为基础的CIS服务。具体表现为向用户显示在给定位置提供哪些服务,定义具有特定要求的区域(例如机场)以及对许多人同时打车的地点实施动态定价等。比如下面是一个地理围栏的示意图:

从用户的手机中检索纬度/经度之类的东西的基于地理位置的服务基础是找到该位置所属的地理围栏。在Uber初始版本的服务中该功能简单的在多个服务/模块中复用。在进行微服化后该服务功能被集中到一个新的微服务。

起步

在评估语言时,Uber考虑了Node.js和Golang。前者是实时产品团队的主要编程语言,对该语言的成员比较熟悉,而且有大量的项目经验积累。但是最终还是选择了Golang,主要考虑了一下的因素:

首先,性能上要求高吞吐量和低延迟。 基本上Uber移动应用程序的每个请求都需要地理围栏查询,并且必须能快速响应结果(99%响应不得大于100毫秒),查询的QPS要达到100,000次。

其次,计算密集型服务。地理围栏查找需要占用大量CPU的多边形点算法。尽管Node.js可以很好地用于他I/O密集型服务,但由于Node具有解释型和动态类型化的性质,因此并不是很适合计算密集型的服务。

最后,需要无中断后台加载。为确保拥有最新的地理围栏数据来执行查找,服务必须不断在后台刷新来自多个数据源内存中的地理围栏数据。由于Node.js是单线程的,因此后台刷新会占用较长时间的CPU(例如, CPU密集型JSON解析工作),从而导致查询响应时间突增。而Golang的协程可以支持多CPU运行,配合前台查询并行运行后台作业。

查询索引问题

给定一个指定为经纬度对的位置,如何找到该位置属于数万个地理围栏中的哪个?暴力遍历的方法很简单:遍历所有地理围栏,并用诸如光线投射算法之类的算法进行多边形点检查。问题,这种方法太慢,不能满足服务性能要求。

Uber抛弃了业界常用的用R-tree或S2为地理围栏建立索引的方法,使用基Uber商业模式以城市为中心,选择了一条更简单的路线;商业规则和其定义的基础地理围栏通常与城市相关联。所以架构上将地理围栏组织成两级层次结构,其中第一级是城市地理围栏(定义城市边界的地理围栏),第二级是每个城市内的地理围栏。

对于每次查找,首先通过对所有城市地理围栏进行线性扫描找到所需的城市,然后通过第二次线性扫描在该城市内查找包含的地理围栏。虽然解决方案的运行时复杂度保持为O(N),但这种简单的技术将所需的N从10,000s减少到了100s。

架构

服务的架构总体设计是无状态的,因此每个请求都可以调度到该服务的任何一个实例,并能期望得到相同的结果。这样每个服务实例都可以服务整个领域,无需使用分区。架构上还是使用了确定性的轮询计划,因此来自不同服务实例的地理围栏数据保持同步。因此,该服务具有最简单的体系结构。后台作业定期轮询来自各种数据存储的地理围栏数据。这些数据保存在主存储器中以服务查询,并序列化到实例本地文件系统,可以在服务重启时快速启动,总体机构图如下:

处理Go Memory模型

服务的体系结构要求对内存中的地理索引同时进行读/写访问。特别是后台轮询作业要写到索引,而前台查询引擎则从索引中读取。Golang的内存模型可能会有一些问题。Golang中常用的方法是对并发读写用协程和通道同步,但是又会影响性能。团队尝试使用sync/atomic包中的StorePointer和LoadPointer方法自己编写了管理内存代理,这样导致了代码脆弱且难以维护。

最终,Uber使用了折衷的办法:使用 读写锁 来同步对地理位置索引的访问。为了最大程度地减少锁争用,在将新的索引段交换到主索引中以用于服务查询之前,会建立新的索引段。和StorePointer/LoadPointer方法相比,锁的使用导致查询等待时间略有增加,但是代码库的简单性和可维护性方面有了非常大的提高。

经验总结

回顾整个案例,整个服务重构取得了很好的效果,并给予了很多启示:

提高了团队生产力和快速迭代。对于C++, Java 或Node.js开发人员来说,Golang通常只需要几天的时间就可以上手,Golang项目代码简便易于维护和实施部署。

高吞吐量,低延迟。在主要数据中心,该服务用了40台运行35% CPU的计算机,峰值负载为170k QPS。响应时间极大缩短:95%服务请求响应时间不大于5毫秒。99%服务请求时间不大于50毫秒。

超级可靠。自启动以来,该服务的正常运行时间为99.99%。唯一的故障是由初学者编程错误和第三方库中的文件描述符泄漏错误引起。Golang的运行时没有发现任何问题。

文章来源:智云一二三科技

文章标题:Uber:利用Golang构建高性能查询服务实践

文章地址:https://www.zhihuclub.com/98239.shtml

关于作者: 智云科技

热门文章

网站地图