据报导,在短短四年间,Uber已经惊人地增长了38倍。现在,Uber的首席系统架构师Matt Ranney 在他的报告“可伸缩Uber实时市场平台”中,对Uber软件系统的工作原理进行了一个有趣而又详细的介绍。
如果你对Uber迅猛增长的单价感兴趣,这个并没有在报告中涉及。但是我们可以了解Uber的调度系统,怎样实行地理空间索引,怎样规划他们的系统,怎样实行高利用率和怎样处理失败,包括令人惊讶的方式处理数据中心故障,使用驱动的手机作为恢复外部分布式存储系统。
在Matt的报告中,给人印象最深刻的是Uber的快速增长。他们对于系统架构所做的很多选择都是基于公司规模的快速增长。很多技术都运行在后台,因为尽可能地让团队快速运转一直是他们的主要目标。
经过开始时期一个短暂的混乱阶段(也很成功的阶段)之后,Uber已经从自身的业务中学习到了很多,包括成功所真正需要的东西。他们早期的调度系统主要是面向移动的人。而现在,除了人之外,Uber的任务已经发展到像对人一样处理箱子和杂货,他们的调度系统已经被抽象化,并且构建了非常坚实和智能化的基础架构。
虽然Matt认为,用附带gossip 协议的一致性哈希算法的架构可能有一些疯狂,但是正好符合他们的实际情况。
不被Matt的工作热情所吸引是很困难的。在谈到他们的调度系统DISCO的时候,他的语调兴奋异常,好像一个旅行推销员在学校里面遇到的问题一样,因为这是一个很酷的计算机科学问题。尽管这个解决方案不是最佳的,对于一个在自己感兴趣的领域,实时实地创造出容错可扩展组件。难道不酷么?
统计
- Uber地理空间索引的目标是以每秒百万次的速度写入,以及以写入速度数倍的速度读出。
- 该调度系统具有数千个节点。
平台
- Node.js
- Python
- Java
- GO
- iOS和Android系统上的本地应用程序
- 微服务
- 重划学区系统
- Postgres
- MySQL
- Riak
- Twitter Twemproxy
- Google的S2 几何库
- ringpop—一致性哈希环
- TChannel—RPC网络复用和成帧协议
- Thrift
概述
- Uber 是一个连接类似乘客和司机关系的应用程序。
- 挑战 :实时搭建一个动态的需求和供应系统。在供应方,司机可以自由的做他们想做的事。在需求方,乘客可以去任何他们想去的地方。
- Uber的调度系统是一个利用手机连接司机和乘客的实时市场平台。
- 跨年夜是Uber 一年中最忙的一天。
- 跟不上工业所取得的巨大的快速的进步是显然的。技术的高速发展使所有的事物都快速地融入这个大背景中。23年前,电话、网络、GPS定位系统都仅仅是科幻的,而现在我们却视若平常。
架构概述
- 驱动这一切的是使用移动设备运行原生应用程序的乘客和司机。
- 后端主要为移动设备之间的信息处理服务。客户端与后端之间的通信是通过移动数据和开放的网络。
- 客户端连接到调度系统,以匹配乘客和司机之间的供应和需求。
- 调度系统几乎完全用Node.js编写。
*曾经计划将其移动到io.js,但之后io.js和Node.js合并所以放弃了。
*你可以在JavaScript上做一些有趣的分布式系统工作。
*不能低估热情的生产大国和节点开发人员的热情。他们可以很快地完成很多任务。
- 整个Uber系统可能看起来很简单。但这种简单的方式就是成功的标志。只要它看起来足够简单,他们的工作就完成了。
- 地图/ ETA(预计到达时间)。在调度过程中,获取地图和路由信息对于最终做出明智的选择是非常必要的。
*街道地图和历史出行时间被用来估计当前的出行时间。
*使用的语言很大程度上取决于系统所要集成的内容。因此,语言包括Python,C ++和Java。
- 服务。存在大量的业务逻辑服务。
*微服务。
*大多用Python编写。
数据库
*最早的系统是用Postgres编写。
*使用Redis。一些是在Twemproxy中,一些是在自定义集群系统中。
*MySQL
*Uber 正在构建自己分布式列存储,以存储MySQL实例。
*一些调度服务保存状态在Riak中。
评论和反馈。一次出行完成之后还需要大量的处理。
*收集评分。
*发送电子邮件。
*更新数据库。
*计划付款。
*用Python编写。
- 费用。Uber集成了多种支付系统。
旧的调度系统
- 旧的调度系统中的不足已经开始限制公司的增长,所以它不得不改变。
- 系统的大部分都需要重写。
- 旧的系统是专为个人出行而设计,它做了很多假设:
*每辆车只有一个乘客,这种假设不适合Uber Pool。
*只有移动的人被考虑到数据模型和接口中。这限制了公司进军新市场和新产品,如需要运输的食品和箱子。
*最初的版本是按城市进行分片。这具有很好的可扩展性,因为每个城市可以独立运行。但随着越来越多城市的加入,它变得越来越难以管理。城市有大有小,不同城市的交通负荷也不同。
- 因为很多东西都是被快速构建起来,因此一旦出现故障,都会相互影响。
新的调度系统
- 为了解决城市分片问题以及支持更多类型的产品,供应和需求的概念必须被扩展,所以一个供应服务和一个需求服务应该被创建。
- 供应服务跟踪所有供应的数量,以及它们的状态。
*车辆需要建模很多属性:座位数,车辆的类型,车辆是否有儿童专座,是否能容纳一个轮椅,等等。
*车辆的容量需要被跟踪。例如一辆车辆,可能有三个席位,但其中两个已经被占用了。
- 需求服务跟踪所有请求和订单,以及方方面面的要求。
*如果一个乘客需要一个汽车座位,那么请求必须与库存相匹配。
*如果乘客不介意以一个更便宜的价格分享车辆座位,这种情况也需要被建模。
*如果有箱子或食物需要运送怎么办?
- 匹配所有需求与供应的方法是一种被称为DISCO的服务(调度优化)
*旧的系统仅仅是匹配现有的供应量,这意味着仅仅针对在路上等待工作的车辆。
*DISCO支持对未来的预测,一旦车辆变成可用,系统就马上利用这些信息。
*汽车地理位置索引(geo by supply)。DISCO需要一个地理空间索引,基于所有供应的位置以及它们预计所在的地点来进行决策。
*需求地理位置索引(geo by demand)。需求也需要地理空间索引
*一个更好的路由引擎需要利用所有这些信息。
调度
- 当车辆在周围移动的时候,位置更新将发送给geo by supply。为了将乘客与司机进行匹配,或将汽车显示在地图上,DISCO发送一个请求给geo by supply。
- Geo by supply进行一个简单的初步过滤,以获得附近的符合要求的候选车辆。
- 然后列表和要求被发送到路由/ETA,以计算它们目前的距离有多近。距离并不是地理上的,而是通过道路系统计算得到。
- ETA的排序结果被发送回供应系统,然后将结果提供给司机。
- 在机场他们不得不模拟一个虚拟的出租车队列。考虑到他们要求到达的地点的不同,出租车必须排列的井然有序。
地理空间索引
- 必须有很高的可扩展性。设计目标是每秒处理百万次写入。当司机在移动的时候每4秒发送一次位置更新,写入速度由此计算出来。
- 对于读出来说,每秒读出的次数应该远多于每秒写入的次数,因为每个开放的app用户都在进行读出操作。大部分供应都处于繁忙状态,所以有效供应中只有一部分能够利用。
- 通过一个简单的假设,旧的地理空间索引运行良好,即它只追踪可调度的供应。大部分供应都处于繁忙状态,所以有效供应中只有一部分能够利用。在几个进程中存在一个全局索引存储在内存中。因此做一些简单的匹配是比较容易的。旧的地理空间索引只追踪可调度的供应。大部分供应都处于繁忙状态,所以有效供应中只有一部分能够利用。
- 在新的系统中,不同状态的所有供应都必须被跟踪。此外,它们的规划路由也必须被跟踪。
- 新的服务运行了数百个进程。
- 地球是一个球体。很难纯粹基于经度和纬度做计算和近似。所以Uber通过使用Google S2 library把地球分成小的单元。每个单元都有一个唯一的ID号。
- 使用一个64位数,地球上的每一平方厘米都可以被表示。对于每个单元的大小,Uber分成了12个层次,从3.31平方公里到6.38平方公里,每个单元的形状和大小也不同,这些都取决于你在地球上的位置。
- S2可以为一个具体的形状给出覆盖单元。如果你想在伦敦绘制一个半径为1公里的圆圈,S2可以告诉你需要哪些单元来完全覆盖这个形状。
- 由于每个单元都有一个ID号,而ID号被用作一个分片密钥。当一个位置加入到供给中时,这个位置的ID就确定了。
- 当DISCO需要在位置附近找到供应的时候,以司机所在位置为中心进行画圈,计算不同位置的价值。使用圆圈区域内的单元ID,集合所有相关的分片,然后返回供应数据。
- 所有都是可扩展的。通过增加更多的节点写入负载总是能被扩展。通过使用副本读出负载也能被扩展。如果需要更高的读出能力,可以增加更多的副本。
- 单元大小被固定在12个层次也存在不足。未来可能支持动态单元大小。
路由
- 当地理空间索引有了回复之后,选择必须立马进行归类。
- 存在几个高层次的目标:
*减少额外的驾驶。理想的情况下,司机应该一直载着乘客,但现实中总是存在排队等事情,司机应该为所有事情获得报酬。
*减少等待。司机应当等待的尽可能少。
*ETA总量应该最小。
- 旧的系统按要求搜索当前可用的供应,然后找到最匹配的 。这个操作简单容易理解。并且非常利于私人出行。
- 仅仅查看当前可用的供应还不能做出好的选择。
- 我们的想法是,对于一个客户来说,问一个正载着乘客的司机比问一个闲置的但距离很远的司机要更好。 拦截一个路过的司机可以缩小客户等待的时间同时减少从远处开过来的出租车额外的开车时间。
- 通过这个预测模型,动态条件能够被更好地处理。
*例如,如果一个司机正好在一个顾客附近,但是另一个司机已经从远处被调度过来,没有办法改变这种调度决策。
*举另一个例子,对于那些想分享车辆的顾客。在很多复杂的场景中,通过尽力地对未来进行预测,更多的优化是可能的。
- 当考虑箱子或食品的运输时,所有这些决策将变得更加有趣。 在这些案例中,人们一般还会有其他的事要做,因此会涉及到其他的交易事项。
扩展调度
- 使用Node.js构建。
- 他们正在构建一个有状态的服务,因此无状态的扩展方法将无法正常工作。
- Node是单进程运行的,因此需要设计方法让Node运行在一台机器的多个cpu上,以及运行在多台机器上。
- 使用JavaScript重新实现所有的Erlang是一个笑话。
- 扩展Node的解决办法是ringpop,其是一个附带gossip协议的一致哈希ring,实 现一个可扩展的,容错的应用层分片。
- 在CAP术语中,ringpop是一个AP系统,牺牲一致性来换取可用性。这就容易解释偶尔出现些小的不一致比一个越变越差的服务要好。虽然偶尔犯错,但如果总体上越变越好,这是没关系的。
- ringpop是一个可嵌入的模块,包含在每个Node进程中。
- Node实例闲置在一个隶属集附近。一旦所有的nodes 相互同意,他们便可以独立有效地做出决定。
- 这是可伸缩的。通过添加更多的进程,可以完成更多的工作。添加的进程可以用来对数据进行分片,或者作为一个分布式锁定系统,或者为发布/订阅协调一个集合点。
- gossip协议是基于SWIM。为减少收敛时间,有几个方面做了改进。
- 很多成员在周围闲置。通过加入越来越多的节点,它就实现了扩展的目标。SWIM中的“S”代表可扩展。目前,它已经可以扩展到数千个节点。
- SWIM结合了健康检查与成员变更作为同一协议的一部分
- 在ringpop系统中,存在包含ringpop模块的所有Node进程。他们闲置在当前的成员周围。
- 从外部看,如果DISCO要消耗地理空间,每个Node是等价的。一个健康节点是随机选择的。无论该请求出现在哪,都通过使用hash ring查询负责将请求转发到正确的节点。
- 让所有这些hop和peer互相对话,可能听起来很疯狂。但它达到了非常不错的性能,例如,通过在任何机器上添加实例,服务可以被扩展。
- ringpop是构建在Uber自己的RPC机制,称为TChannel。
*这是一个双向的请求/响应协议,它的灵感来自于Twitter的Finagle。
*一个重要的目标是跨很多不同的语言控制性能。特别是在Node和Python中,,很多现有的RPC机制工作得并不是很好。想要获取Redis级别的性能.TChannel已经比HTTP快20倍。
*希望获取一个高性能的转发路径,因此中间层可以让决策转发变得容易一些, 而不必了解全部有效载荷。
*希望获取合适的流水线,因此没有队头阻塞,请求和响应可以在任何时间往任何一个方向发送。
*希望获取有效的载荷校验与跟踪,以及一流的功能。每个请求都应该是可追溯的。
*希望获取一条迁移HTTP的清晰路径。HTTP可以在TChannel中被自然封装。
*Uber正在摆脱HTTP和Json业务。TChannel上的所有技术正往Thrift上迁移。
- ringpop基于持久连接处理所有TChannel中的gossip。这些相同的持久连接用来扇出或转发应用数据。TChannel也用于服务之间的对话。
调度可用性
- 可用性是相当重要的。Uber有竞争对手,用户变更产品的成本是非常低的。如果Uber不行,利益就会流向其他竞争对手。其他的产品更粘着性并且客户只会在之后才会使用。那对于Uber 来说并不是对的事。
- 让一切可重试。如果有什么不能工作了,它必须是可重试的。这要求所有请求幂等。例如,重试一个调度,不能调度他们两次或刷取别人的信用卡两次。
- 使所有可关闭。故障是一种常见的情况。随机杀死进程不应该造成破坏。
- 崩溃。不存在正常关闭。正常关闭没有什么需要练习。需要练习的是当意外情况发生时。
- 小块。为了尽量减少故障的代价,将它们切为更小的块。在一个实例中处理全局业务是可能的,但是实例死亡的时候会发生什么呢?如果两个里面有一个失败,则能力会减少一半。因此,服务需要被切分。 这听起来像是一个技术问题,但它更多的是一个文化方面的问题。拥有一对数据库会更容易。这是一件自然的事情,但是,拥有一对并不意味着就是好的。如果你能够自动的生成一个并且重启一个新的适中的系统那么随意的清楚他们就会是危险的。
- kill一切。即使kill所有的数据库,也要确保出现故障时系统可以幸免。这需要对使用什么数据库做决策改变。他们选择Riak 代替MySQL。这也意味着使用ringpop代替Redis。 清除一个redis 实例是一个昂贵的操作,它们经常回事相当大且耗费多的。
- 将其切分成更小的块。通常,通过一个负载均衡器实现服务之间的对话。如果负载平衡器死去会怎么样?如果你没有实际处理过这种情况,你可能永远不知道。所以,你不得不kill负载平衡器。这时你怎么解决围绕负载均衡器关闭而出现的问题?负载均衡逻辑已经在服务中被采用以解决这个问题。客户端都被要求有一定的智能,以了解如何找到解决问题的途径。这在很大程度上类似于Finagle的工作方式。
- 为了扩展整个系统,并处理后端压力,基于一个ringpop节点集群,创建了一个服务发现和路由系统。
整个数据中心失效
- 这种事情并不会经常发生,但一些意想不到的级联故障是可能出现的,或者上游网络提供商也可能会不能工作。
- Uber维护了一个备份数据中心,通过将所有工作转移到备份数据中心,可以实现及时切换。
- 问题是在进程中的出行数据可能还不在备份数据中心。代替数据副本,他们使用司机手机作为出行数据的来源。
- 当调度系统定期发送一个加密的状态摘要到司机的手机时,会发生什么。现在,让我们假设有一个数据中心失效。下一次,司机的手机发送一个位置更新到调度系统,调度系统会检测到它不知道这次出行的任何信息,这次就可以问状态摘要。 调度系统从状态摘要中更新,这个过程就像什么都没有发生过一样。
不足
- Uber解决可扩展性和可用性问题的方法也存在不足,主要表现在Node进程在向彼此转发请求以及用大的扇出发送消息的过程中,存在潜在的高延迟。
- 在扇出系统中,很小的错误都有一个非常大的影响。一个系统的扇出越高,出现高时延请求的机会就越大。
- 一个好的解决办法是利用交叉服务器对请求进行备份。这作为第一等级的功能被融入到TChannel中。一个请求被发送到服务B(1),同时也附带该请求被发送到服务B(2)的信息。等待一些时间之后,请求被发送到服务B(2)。当B(1)完成请求时,它在B(2)上取消这个请求。使用一些延迟意味着通常情况下B(2)没有进行任何工作。但是,如果B(1)失败了,则B(2)将处理该请求,并以一个较低的 延迟返回一个回应,如果B(1)第一次尝试的过程中,发生超时,然后再让B(2)尝试。
- 想了解更多,可以参考 Google On Latency Tolerant Systems: Making A Predictable Whole Out Of Unpredictable Parts。http://highscalability.com/blog/2012/6/18/google-on-latency-tolerant-systems-making-a-predictable-whol.html
原文发布时间为:2017-11-20
本文作者:readygo@
本文来自合作伙伴“中生代技术”,了解相关信息可以关注“中生代技术”微信公众号