今天我们解读的是Uber的首席架构师Matt Ranney所分享的Uber实时架构的从1到万。

首先,Uber是什么?Uber是连接乘客和司机的交通平台,它专注的是运输业。当我们输入自己的位置,然后请求一辆车,Uber可以马上满足我们的需求。

Uber面临的挑战是什么?

最核心的有两点:动态的供给,和动态的需求。

顾客不断地从各个地方出现,所以需求的位置都是不一样的,并且需求也会随着时间而变化。此外,供给也是各不相同的,因为司机来自各个不同的位置,并且每个司机的车也是不一样的。

Uber的架构

那么Uber的架构是怎样的呢?首先必然有司机和乘客,他们分别代表着供给和需求。对他们提供服务的过程,我们称为派遣服务。派遣服务会用到一些模块,最基本的是地图和时间预估,因为有了这两者后,我们就能知道对于一个用户的请求,一辆车大概什么时间能到达用户身边,以及距用户不同距离的车辆分别都需要多长时间。这是基于地图和交通的历史信息进行评估的。而在Uber内部,它的派遣服务是用Node.js实现的。

除此之外,还有很多复杂的逻辑,我们称之为业务服务,其采用的是微服务的方式(微服务的架构参见:技术丨解读Microservices)

再往后就是数据库,微服务里会有各种各样的数据库,因为历史原因Uber有大量各种各样的数据。

在传统的服务之后,还有服务后流程,比如用户给出点评、付费、收到一些邮件通知等。

支付的过程也很复杂,因为需要和各个银行合作,而银行之间的各种延迟、不同的协议等也会造成很多困难。

因此在这个架构中存在很多挑战:

  1. 能否支持顺风车。因为在Uber的传统架构设计里,他们假设的是一名司机载一名乘客,这是一个简化的模型。但如果出现多个乘客乘一辆车,或者搭顺风车的情况,传统架构就无法满足了。
  2. Uber想做任何东西的运输者,所以送餐就出现了,随之而来也有一些问题。Uber之前假设传输的都是人,所以当司机送的不是人而是货物、食物或其他东西时,该怎么办?这个架构该怎么改?
  3. 跨城市运输。以前Uber一直是按照城市来切分数据。但有的城市大,有的城市小,如果这样切分数据,结果就会很不均匀,也会造成流量不均衡,那怎么处理这个问题呢?
  4. 最后是多点失败,即系统中有多个单点失败。

应该如何重构架构呢?

我们要重新理解司机和乘客,司机是供给方,乘客是需求方。所谓供给,不仅是指提供车,还包括车上是不是有儿童座椅、剩余座位的数量、车型是什么,这些都属于供给的一部分,所以要建立更强大的Profile。

那需求方的具体需求有哪些呢?乘客是否带着孩子,同行人数或是否愿意和别人共享一辆车,这些都是需求。供给与需求凑到一起成为一个舞会,在舞会上男女会搭配着跳舞。同理,Uber也想把一个供给和一个需求搭配到一起,在Uber里这被称为舞会服务。

之前舞会服务往往是基于当前服务状态来匹配,只用考虑当前情况。但在面向未来时,会出现很多情况,怎么考虑到未来的需求呢?这就是面向未来的匹配。我们之后会讲到这是如何实现的。

当然还有一些特殊的场景,比如在机场要模拟出一个队列的方式来提供服务,这都是由舞会服务来提供的。舞会服务也会调用底层的供给位置信息、地图和时间估计,以及需求方的位置信息,并把它们结合到一起,构成了整个派遣服务。所以派遣服务拆解开来,完成更细粒度的操作。

Uber当前的目标是什么?

2015年8月,他们的目标是写操作1M/s,即每秒100万。如果面向未来设计,实际当前的写操作是每秒10万左右,再结合每4秒一次的写GPS位置信息,那么同时运营的车辆大概在3万左右,这只是一个估算。

如何唯一标识一块空间?

为了实现这些服务,要解决一些基本的问题,其中一个是:如何能够唯一地标志一块空间?

一个地图,如果不按城市切分,那按什么方式切分呢?答案是用Google S2。它是一个基于地理的图数据库。它将地理上的每个空间用一个四边形切分出来,切分时按照从大到小的规则,0级表示全部的空间,而切到最小是1平方厘米的空间。所以它可以标识出任何一个位置,并且形成一个唯一的二进制串,用一个id表示出来。通过这种方式,它能标识出任何一个位置。

标识出地图上的每一块空间后,要选择粒度,在Uber里选择了12这个公里级别的粒度。

如何表示一个区间?

有了这些以后,我们如何表示这个圆形的空间呢?比如用户现在在这个圆形空间中发送需求,我们怎样找到他附近的所有可以满足请求的司机呢?

答案就是切分开来。按照上文我们讲到的地理空间方块的覆盖,把它切分开,只要能覆盖到这个蓝色的空间就算一个,所以它会在这五个红色区域里寻找满足的汽车。

如何匹配供需?

我们再来看第二个基本问题:如何匹配供需?首先我们要想我们的目标是什么。

我们的目标是:

  1. 减少乘客的等待时间。
  2. 减少司机的空驾,这样司机才能赚更多的钱。
  3. 减少乘客在路上的通勤时间。

当满足了这三个目标时,我们就会发现一个场景:尽量将司机连成串,尽量走最近的路径就行了。这其中还有很多细节值得大家去思考。

什么是最优策略?

在最优策略里,除了有面向当前的设计,还有面向未来的设计,二者有什么区别呢?

举一个例子,乘客1发送了请求,我们发现离他最近的司机是1号,距离八分钟,我们也许就会让司机1去接他。但是,也许还有一个司机2,他的当前任务还剩两分钟就完成了,他与顾客的距离是一分钟,如果让司机2先完成当前的派送,然后再接这一单,耗时会比司机1更少,这就是面向未来的策略。如果把这种情况考虑进去,也许就能设计出一些更好的策略,这是我们经常碰到的NP问题。

如何保存供给?

接下来我们具体来看系统上的实现。如何才能把供给保存起来呢?在Uber里,它的难题是,全球有几万或几十万辆汽车,这是一个很大的数据。我们刚才讲过,我们已经通过Google的地图实现了任何区间的切割,不需要单独按照城市保存,那我们怎么进一步来计算呢?

在存储上,他们提出了一个概念叫Ringpop,它的本质类似于Cassandra的分布式平台,里面所有的节点都分布在这里,这些节点之间是完全等价的,每个节点负责某一个区域范围内的位置信息。

当一个供给司机将他的供给位置告诉舞会服务之后,舞会服务会算出他具体的位置区间,然后通知环上的任何一个点,这样就能把位置存放起来了,这就是保存。

另外一个问题是,如何匹配需求?

匹配就是搜索,比如乘客有一个需求,想查他周围五公里之内的汽车。把这个请求发给舞会服务后,舞会服务发现影响到了三个位置。所以它会把这三个位置信息发给环上任何一个节点。节点会把这些信息路由到具体的位置2、5、7,然后这些节点会返回匹配结果,最终返回给这个用户。

所以我们可以看出,任何节点都是等价的,它们能接收任何服务,并且路由到相应的位置上,得到具体的信息,这是一个非常好的Ringpop架构。

如何远程通讯?

有了存储,我们还要解决通讯问题。通讯需要有哪些特点呢?

  1. 首先,它需要性能优秀。之所以要重新做一个通讯,就是因为当前的HTTP太慢了,他们希望能有20倍以上的优化。
  2. 要能提供消息转发。我们可以看到,Ringpop里每一个节点都能转发消息。
  3. 跨语言支持,因为底层用了很多种不同的语言。
  4. 希望能有一些消息调度优化,不要因为某些消息就卡死在那里。
  5. 校验和追踪,发现问题并且改正问题。
  6. 消息封装。比如我的上层跑的是某一个协议,能不能把HTTP也封装到里面,兼容HTTP协议呢?这就是一个封装的问题。

解决方案就是TChannel,大家可以Google一下,了解其具体的设计。

服务的设计原则是什么?

我们现在开始考虑服务,服务即微服务,对于大规模系统,它的错误是常态,所以我们要考虑很多问题。服务的设计原则可以归结为以下三点:

  1. 服务可以重试。如果服务经常挂掉而不能重试,就很容易出现错误。比如转账的时候,第一次转错了,再转第二次的时候发现出问题了,这就是不能重试,或重试出错。又或者是执行两次转账,而两次执行的结果不一样,这样也是不行的。所以要保证服务是只执行一次的。
  2. 服务可以被杀。因为这个系统的节点特别多,随时可能挂掉,所以需要可以被杀,甚至有时能故意搞坏一些东西来杀掉服务,以测试服务是否鲁棒。
  3. 服务要尽量切分。因为细分到原子服务上,服务的耦合性就解开了,因而不会相互影响。

如何负载均衡?

服务设计出来以后,如何解决负载均衡的问题呢?

传统理解中,我们可能认为两边是服务,负载均衡在中间,把他们搭到一块儿。但是在我们这种情况里,如果负载均衡在中间,负载均衡挂掉了以后,是不是也不能服务了?能不能有一个负载均衡的负载均衡呢?答案就是刚才提到的Ringpop。

如何改进负载均衡?

也就是说,Ringpop不仅能存储数据,还能实现路由功能。比如服务A想访问服务B,它就可以找到服务B的某个位置对接,并且找到它。同理,后面有很多服务B,它们可能连到不同的位置,这样就能分布地实现所有的需求,解决所有问题,并且把流量全部分解开来。有兴趣的朋友可以多看看Ringpop相关的论文。

木桶延迟问题

还有一个问题叫木桶延迟问题,很有意思。如果一个大消息只有一个小消息,假设平均延迟1毫秒,从统计数据来看,可能有1%的消息变成了1秒,于是就有1%的时候是失败的,因为超过一秒了。

但是,如果大消息是100个消息的集合,你会发现失败率是63%,因为在这100个消息里,只要有一个消息超过1秒,整个消息最终的延迟肯定会超过1秒,它取决于最慢的那一个,这就是木桶原理。

如何解决木桶延迟问题?

答案就是在服务A和服务B之间加一个服务2,也就是让几个服务器进行重算,不仅是服务1算,服务2也算。

那是同时发吗?不是的,其实是有延迟的。比如,我和服务B1说:完成消息1。同时我也告诉他我会让服务B2也算。同理,过一段时间后我也会和服务B2说:你完成消息1。同时我也会说我告诉过服务B1。正常情况下,B1先收到请求,所以他会先完成,然后他就会告诉服务A他搞定了。但是完成以后,B2算的不就浪费了吗?那B1就会告诉B2:取消消息1的计算。这样就可以节省B2的时间开销。同理,如果B2完成了,他也会告诉服务A他完成了,同时也告诉B1让他取消计算。这种计算两遍的方式,能够极大地加快性能。

数据中心挂掉怎么办?

最后一个问题是,数据中心也会挂,当数据中心挂掉时该怎么办呢?

大家想一想,数据中心挂了的核心是什么?比如司机的App在运行时会不断地给数据中心提供他的位置信息。如果数据中心A挂了,那么数据立即就没有了。为了解决这个问题,我要不停地把数据中心A的数据同步到另外一个数据中心吗?当然可以,但是很麻烦。

Uber用了一个很巧的方法,在运行过程中数据中心A会将司机本身的数据进行摘要,并且加密,返回到司机的App。这样司机的手机本地是存有自己完整的各种重要信息的。

当数据中心A挂了的时候,数据中心B出来了,他是没有任何数据的,但没关系,他可以直接对司机App说:你把信息摘要给我吧。然后司机App就会把信息摘要给数据中心B,数据中心B就拥有所有数据了,就能完成所有的操作。同时,由于现在有了数据中心B,接下来更新地理位置的信息就会发给数据中心B。这就是Uber解决数据中心挂掉问题的方法,非常巧妙。

总结

希望大家能用到以下三个好帮手:

  • Google S2是你地图的好帮手,在地图问题上大家可以用它。
  • Ringpop是分布式存储和负载均衡的好帮手。
  • TChannel是远程调用的好帮手。

参考文献:

《Scaling Uber’s Real-time Market Platform》