QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

Uber 是如何基于 Go 语言构建高 QPS 服务的?

  • 2016-04-11
  • 本文字数:2255 字

    阅读完需:约 7 分钟

在 2015 年初,我们构建了一个只做一件事(也的确做的非常好)的微服务——查找地理围栏(geofence lookup)。一年后,这项服务已经成为 Uber 数百个正在运行的服务中每秒查询次数(QPS)最高的服务。接下来,本文将谈论我们构建这项服务的原因以及我们是如何使用 Go 语言快速构建和扩展这项服务的。

背景

在 Uber,一个地理围栏就表示地球表面上人为划分的一个地理区域。此外,我们进一步在基于地理的配置中使用地理围栏的概念。地理围栏的概念在很多地方发挥了很重要的作用——向用户展示在某个位置可使用的产品时,定义机场等特殊用途区域时以及在很多人同时呼叫实现动态定价时。

Colorado 的一个地理围栏样例

在提取用户手机的经纬度坐标等基于地理位置的配置信息时,需要做的第一件事情就是确定该位置落在哪个地理围栏中。而该功能已经在多个服务或模块中被实现。但是,当我们从一体化架构转移到基于微服务的架构时,我们选择了将这项功能集中在一个新的单独的微服务中进行实现。

选择Go 的原因

当我们评估所要使用语言的时候, Node.js 正是广大的服务设计团队普遍采用的编程语言。而我们也在 Node.js 的使用方面有着丰富的经验。然而,Go 语言却由于以下原因满足了我们的需求:

  • 高吞吐量和低延迟的需求。Uber 的手机端 App 在发送请求时,必然会触发一次查找地理围栏的操作。而服务器必须要能够对每秒上万次的请求以 99% 的概率响应时间小于 100ms 的速度进行响应。
  • CPU 密集型负载。查找地理围栏需要使用计算密集型的 Point-In-Polygon(PIP)算法。尽管 Node.js 可以很好的用于 I/O 密集型的服务,解释执行以及动态类型定义等的特性使得它并不适合于我们的使用场景。
  • 非中断式的后台加载。为了保证查询操作是基于最新的地理围栏信息而进行的,服务必须要能够根据多个数据源的信息在后台实时刷新内存中的地理围栏数据。因为 Node.js 是单线程的,后台刷新很可能会占用一定的CPU 时间(如CPU 密集型的 JSON 的编译工作),最终导致部分查询的响应时间过长。但是,对于 Go 语言而言,这完全不是问题。 Goroutine 可以运行在多个 CPU 核上,并且可以在响应前端查询的同时后台并行进行刷新数据的工作。

是否建立地理信息索引——这是个问题

当给定了一个经纬度坐标的位置信息时,如何从上万个地理围栏中找到该位置所在的那一个呢?最直接而暴力的解决方法为:浏览所有的地理围栏,然后采用光线投射(Ray Casting)等算法进行PIP 检查。但是,这种方法实在是太慢了。那么,我们怎么才能有效的缩小搜索空间呢?

由于Uber 的商业模型是以城市为中心的,我们并没有采用 R-tree S2 等结构来索引地理围栏,而是采用了一个相对要简单很多的算法;商业规则以及相关的地理围栏都和城市相关。这使得我们可以采用层次式的方式来组织地理围栏——第一层是定义城市边界的围栏,而第二层是城市内的城市围栏。

对于每一次查询,我们首先线性扫描所有的边界城市围栏,找到目的城市。然后,我们采用另外一种线性扫描的方法来找到城市内的目标城市围栏。尽管新算法的复杂度仍然是 O(N),它却把 N 从万降到了百,大大减少了算法执行的复杂性。

架构

根据设计需求,我们希望这项服务是无状态的。因此,每一次请求都可以发送给任意的服务实例,并获得相同的结果。这意味着每一个服务实例都必须掌握全局信息,而非局部信息。我们采用了一种确定性的轮询调度策略,从而保证了不同服务实例的地理围栏数据都是同步的。这样,该服务的架构也非常简单。后台任务周期性地轮询来自不同数据源的数据。而这些数据就保存在主存中以服务不同的查询。同时,数据被串行保存在本地文件系统中,以实现系统重启时的快速引导。

查询地理围栏服务的架构

处理Go 存储模型

我们的服务架构需要对内存中的地理索引信息进行并行读写访问。在特殊情况下,后台轮询任务修改索引,而前台查询引擎同时从索引中读取信息。相比于利用单线程的Node.js 进行服务编写的情况而言, Go 存储模型必然会遇到挑战。尽管 Go 语言可以利用 goroutine 和 channel 自然的实现并行读写,其带来的性能影响不可忽视。我们试图利用 sync/atomic 包中的 _StorePointerLoadPointer_ 原语来自己管理内存屏障。但是,这导致了代码难以理解和维护。

最终,我们选择了一种折衷的方式——利用读写锁来保证对地理索引的同步访问。为了减少锁的竞争,新的索引片段在被自动交换到主索引之前都是处于隐藏状态的。相比于_StorePointerLoadPointer_ 方法,这种锁会轻微增加查询延迟。但是,我们维护代码库的工作却变得简单很多,非常值得。

我们的经验

回首过往,我们非常开心选择了 Go 语言来编写我们的服务。其带来的好处包括:

  • 高的设计效率。对于 C++,Java 或 Node.js 的开发者而言,学习 Go 语言只需要几天。而且代码维护工作也很简单。(这一切都是因为静态类型检查。)
  • 高吞吐量和低延迟。在我们处理非中国区流量的数据中心中,该服务在 40 台机器 35% 的 CPU 利用率的情况下的最高 QPS 为 170,000。其响应时间为 95% 的概率低于 5ms,99% 的概率低于 50ms。
  • 超级可信。从启动到现在,该服务正常运行的时间为总时间的 99.99%。唯一的一次停止服务也是由初级编程错误和第三方库中的文件描述符泄露 bug 引起的。最重要的是,我们迄今为止没发现任何有关 Go 运行时的问题。

未来的展望

尽管 Uber 曾经主要采用 Node.js 和 Python,Go 语言正在成为 Uber 设计师构建新服务的选择。


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-04-11 17:276957
用户头像

发布了 268 篇内容, 共 127.2 次阅读, 收获喜欢 24 次。

关注

评论

发布
暂无评论
发现更多内容

APIitem_get:你的淘宝商品详情搜索神器

技术冰糖葫芦

API 接口

C 语言教程:数据类型和格式说明符

小万哥

c 程序员 软件 后端 开发

开源漏洞共享平台及安全奖励计划正式发布

开放原子开源基金会

Java 开源 程序员 开发者 算法

openEuler商业化进展可观:累计装机量超610万套,市场持续扩容

彭飞

拼多多ID取商品详情API:电商行业的秘密武器与实时数据获取的智慧之路

Noah

INFINI Labs 产品更新 | Easysearch 新增快照搜索功能,Console 支持 OpenSearch 存储

极限实验室

console easysearch

为开发者服务,让梦想成为可能|孙文龙理事长发表专题演讲

开放原子开源基金会

Java 开源 程序员 开发者 算法

开放原子开源基金会与9个开源项目举行捐赠签约仪式

开放原子开源基金会

Java 开源 程序员 开发者 算法

PWM 调光的线性降压 LED 恒流驱动器

芯动大师

openEuler社区与9大海外开源基金会深入合作,构建全球开源新生态

彭飞

OpenHarmony创新赛圆满收官:引领开源趋势,开启创新技术新纪元

新消费日报

小红书商品详情API:电商助力

Noah

开放原子云社区正式成立

开放原子开源基金会

Java 开源 程序员 开发者 算法

敏捷任务拆解、工作量评估和指派

laofo

Scrum 敏捷 敏捷开发 研发效能 持续交付

微服务的学习与实践 主赛道:技术人的 2023 总结

Echo_Wish

微服务 云原生 年度总结 2023 技术总结

一文搞懂Android和嵌入式Linux开发差异点

巫山老妖

生成式AI给我们带来的影响

月下独酌

大模型

Cilium CNI深度指南

俞凡

Kubernetes 云原生 网络 通信

图像处理AI软件推荐:Topaz Photo AI 激活直装版

胖墩儿不胖y

Mac软件 图像处理工具

欧拉与AI深度结合:操作系统升级带来全新智能体验

彭飞

Merlin Protocol,一个专业的比特币生态资产适配协议

TerpLayer

区块链

专业的磁盘管理工具:DiskCatalogMaker 中文激活版

胖墩儿不胖y

Mac软件 磁盘管理工具 磁盘清理管理

找不到想找的图片?半小时,帮你实现一个AI版“图片搜索引擎”

鹤涵

Redis 核心技术与实战 openai AIGC ChatGPT

大数据之云平台的使用与总结 主赛道:技术人的 2023 总结

Echo_Wish

大数据 云平台 年度总结 2023 开天平台

【PHP】thinkPHP6中的MVC思想的小案例

Geek_Angel

php mvc thinkphp6

Uber是如何基于Go语言构建高QPS服务的?_语言 & 开发_张天雷_InfoQ精选文章