查看原文
其他

面试必考:如何设计承载千万用户的Uber实时架构

BitTiger 云时代架构 2019-05-09




欢迎回看本文原始视频:

http://t.cn/RYn1mMm


今天我们解读的是Uber的首席架构师Matt Ranney所分享的Uber实时架构的从1到万。也欢迎大家阅读上一期文章:Uber实时架构的从0到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》



推荐一起学习《分布式服务架构:原理、设计与实战》一书,它是一本不可多得的理论与实践相结合的架构秘籍,是作者多年工作经验积累的结晶。京东购买请扫描下方二维码。


如果你想成为优秀的架构师

在【云时代架构】精品群免费进!

我在【云时代架构】技术社区,你在哪里?

还等什么,赶快加入【云时代架构】技术社区!

请猛扫下面二维码。

云时代架构

做互联网时代最适合的架构

开放、分享、协作

快速关注,请猛扫下面二维码!


  

简书博客                      云时代架构

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存