构建Uber端到端技术栈的十条经验

标签: uber 技术 十条 | 发表时间:2018-02-01 09:45 | 作者:jayeye
出处:http://www.iteye.com
[size=large]我在Uber这几年,做了很多系统稳定性及可扩展性的工作, 也包括很多快速迭代试错的产品,另外还做了一些移动开发的工作,因此我对Uber的端到端的技术栈还比较熟悉。在这里以我的经历为例跟大家分享一下如何以Uber的方式快速稳定的做一个端到端的大型应用。

我刚加入Uber时,Uber正处于飞速成长期。这样的情况对之前工程师设计的简单系统造成了极大的压力。下面我谈谈实战中的系统设计的经验。

一、选择微服务

系统设计包括若干个层面。先说顶层的系统设计原则, 如REST,SOA。由于Uber之前一直是算一个创业公司,所以开发速度至关重要,由于微服务 能够极大的促进不同组件的平行开发,SOA成为了Uber的选择。

在这种选择下,我们需要先按功能设计出不同责任的Service,每一个Service作为这个责任的唯一真实信息源。在开发新的功能时,只需要先设计好不同Service之间的合约, 就可以按照合约平行开发了。在实际工作中,这点被证明非常有效。

二、服务要设计为幂等(idempotent)

第二点是不同Service之间的合约和依赖。一个Service的合约决定了它跟上游Service之间的关系,如果这个合约设计的不好,那就会给上游Service上的开发带来各种不方便和重复工作。

比如说如果一个节点可以被设计成幂等(多次操作均返回相同结果)但却没有这么做,那就会导致上游Service在使用这个节点时,失败处理逻辑会复杂很多--如果是幂等, 上游只需要重新调用就可以了;但是如果不是幂等, 上游就需要跟据出错信息来判断依赖系统的状态 (有时甚至很难判断,比如在下游系统状态更新后网络出错) ,然后再根据状态来选择不同的处理方式。

在有些情况下(比如下游系统挂掉了),上游系统甚至需要记录下游系统的状态,这样在backfill的时候才可以直接做正确的处理;而在幂等的情况下,我们只需要无脑调用下游的Service就好。举个例子,很久以前Uber有次分单系统坏了,导致之后要重新backfill,由于依赖 Service设计的是幂等, 该次backfill就一个简单script跑完即可。当然,现在Uber的分单系统还是非常稳定的。

三、考虑RPC消息的语义(semantics)

同时,我们也要考虑RPC semantics是at least once, 还是at most once。具体的应用情境下有不同的适用。比如说如果是要做一个付钱的有状态更新的api, 那我们就应该保持at most once的使用,当调用 api 出错时,我们不能贸然再次调用该api。At least once和at most once在大部分情况下对应于幂等 和非幂等的操作。另外,我们在实现系统时也要考虑已有系统提供的接口,比如说一个已有的接单系统已经提供了一个at least once的消息队列,而我们需要做的是跟据累积的交易数来做一些行为,在这样的情况下,我们就需要我们的系统能够消重,或者保证我们要做的行为是幂等的。

四、Design for failure

第二个层面是Service之间交互可能发生的问题,在设计一定要考虑周全,比如通信可能发生的failure case。我们要假定在线上各种奇怪的情况都会发生。比如我们曾经有上下游Service之间通信时使用的kakfa ingester一直不是非常稳定,导致不时发生下游Service 无法拿到数据来计算,最后我们干脆把kafka换成了http polling, 再也没有问题了。

第三个层面是Service内部的故障, 比如缓存, 数据库断了,或者依赖的第三方Service挂掉了,我们需要根据情况进行处理,做好日志和监控。

五、合理选择存储系统

如果一个Service是无状态的,那往往它做的事情是根据请求把下游各个Service的返回结果加工一下然后返回。我们可以见到很多这样的Service, 比如各种gateway,各种只读的Service。

服务无状态的情况下往往只需要缓存(如Redis),而不需要持久化存储。对于持久化存储, 我们需要考虑它的数据模型、对ACID的支持、稳定程度、可维护性、内部员工对它的熟练程度、跨数据中心复本的支持程度,等等。到底选择哪一种取决于实际应用情景,我们对各个指标要不同的需要,比如说Uber对于跨数据中心复本的要求就很高,因为Uber每一个请求的用户的期待值都很高,如果因为存储系统坏了,或存储系统阻挡failover,那用户体验会非常差。

另外关于可维护性和内部员工的熟练程度,我们也有血淋淋的例子,比如说一个非常重要的系统在订单最多的一天挂掉了,原因是当时使用的PostgreSQL数据库不知为什么原因而锁死了,不能读也不能写,而公司又没有专业到能够深入解析PostgreSQL的人,这样的情况就很糟糕,最好是换成一个更易维护的数据库。

六、重视系统的QPS和可响应性

这两点是系统在扩张过程中需要保证的,为了保证系统的QPS和可响应性,有时甚至会牺牲一些其它的指标,如数据一致性。

支持这两点,我们需要考虑几件事情。

第一是后端框架的选择,通常实时响应系统都是IO密集型的,所以选择能够non-blocking的处理请求的框架就很大好处,既可以降低延迟,因为可以并行调用下游多个系统;又可以增加QPS,因为以前阻塞在IO上的时间可以被用来处理其它的请求。

比较流行的Go,是用后台线程池来支持异步处理,由于是Google支持的,所以比较稳定,当然由于是新语言,设计上也有一些新的略奇怪的地方,如”Why is my nil error value not equal to nil?”;以前的Node.js和Tornado都是用主线程的io-loop来处理。

关于Node.js, 我自己也做过一些benchmarking, 在仅仅链接缓存的情况下,在同样的延迟下,可以达到Python Flask 3倍的QPS。关于Tornado, 由于是使用exception来实现coroutine, 所以略为别扭,也容易出问题,比如Uber在使用过程中发现了一些内存泄露的bug,所以不是特别推荐。

第二是加缓存, 当流量大了以后,可以加缓存的地方,尽量加缓存。当然,缓存本身也会引入一个可能导致故障的点,所以如果不是很稳定,不加为好。因为通常cache connection的timeout都不会设的非常小,所以如果缓存挂掉了,那请求可能要在缓存上阻塞一阵子,导致高延迟。很久以前Uber的溢价系统就曾经因为这个出过一次问题,不过好在通常Redis都比较稳定,且修复很快。

第三点是做负载测试, 这个是个必要步骤。

七、Failure处理和预防

这点跟前面几点都有重叠的地方,而且对系统至关重要。failure处理有几个层面需要考虑,首先是Service之间的隔离保护,不是一定要放在一起的功能,尽量不要放在一个Service里。比如把运算量很大的溢价计算和serving放在一个Service中,那当流量突然增大时,serving和溢价计算都会受影响,而如果他们是两个Service,那如果serving受到压力,我们只需要解决serving的问题就好,不用担心溢价计算的问题。

又比如我们很久之前的一个事故是当运营分析系统大量读取溢价时,给serving造成了很大压力。这个事故的出错原因固然很低级(数据库读取不合理),但是从大的角度出发,这也引出了第二个要点,Service之间的SLA中应包含该Service的优先级,当出现问题要牺牲Service时,应该先牺牲优先级低的Service,把注意力放在保证优先级高的Service不挂掉。假设我们有一个专门针对内部服务的Service,那我们就可以牺牲该Service,从而有效避免该事故的发生。

由于优先级高的Service通常极其重要,因而往往具有不可替代性,获得的维护资源也多,所以在依赖该Service时往往可以认为它是不会挂掉的,因为它挂掉了调用者Service也没什么用了。而对于优先级低的Service, 我们通常要做好准备它是有可能挂掉的,所以我们要避免这样的Service成为单点故障中的那个点,并且积极寻找当它不可用时的备用方案。

Service之间保护的第三个要点是除了两个Service之间本身的保护,我们还需要关注它们的依赖之间的保护。如果他们的依赖没有很好的隔离, 那么它们的保护并没有到位。比如让不同的Service共享同一个MySQL集群, 于是当一个Service里有不恰当的代码,使劲写入该集群时,其他一些共享该集群的Service也会受到影响。通常会共享这种集群的Service的优先级都不会太高,在资源有限的情况下共享是无奈的选择,但是我们要知道危险性。

八、产品工程和快速迭代

我在用户增长组主要聚焦在产品工程,即如何用最少的资源,最快的速度,来实现非常具有可扩展性的解决方案,因为迭代速度越快,代价越小,对竞争对手的优势就越大。同时要和产品经理保持默契,适应不断变化的需求。另外还要和其它组的产品经理和工程师保持沟通,尽量减少和消除产品远景规划上的冲突。

具体的说,为了实现最具可扩展性的方案,我们需要了解我们所能覆盖的使用情景,然后抽象出我们系统的行为。有了行为以后,我们可以在看看还有没有其它的使用情境,也可以用这样的行为所支持,如果可以,我们就达到了用最少的工作来达到最大产出的的结果。

当我们抽象出来这个系统的行为后,我们发现我们要处理的是由注册开始的一系列事件,并且根据这些事件和运营人员设置的规则来做各种处理。在这样的情况下,不仅司机推荐司机奖励,其它的各种司机奖励(比如老司机奖励),和其它的各种推荐活动,也可以用这个系统来处理。

所以我们只需要把这系统的主线架构(事件激发机制)写好,当有需要要加新的奖励规则时,我们只需要让工程师写针对该规则的模块插入即可。同时,我们会对主线架构上的代码进行严格审查,并对插入模块进行出错隔离,这样如果插入的模块有问题,只会影响该模块本身,而不会搞挂掉整个系统。

做产品工程,顾名思义,产品是自变量,工程是因变量。跟产品经理保持好的默契,跟别的组的产品经理和工程师保持好的沟通,至关重要。关于这点要展开说就是另一篇文章了。

九、Uber Android App框架Presidio

我在不同时期也做了很多移动开发的工作。这里我简单谈谈Uber的移动技术栈和App框架Presidio。我将以Android为例。

Presidio是一个组织UI组件和非UI task的框架。先来看看Uber以前的App架构,一般来说,每个UI界面都是按MVVM来写的,在Android的情况下,往往每个界面会对应于一个Activity, View, Controller, Data Manager, 同时该Activity会包括这些View, Controller等等。这种结构往往会导致一个非常大的Controller, 里面有很多不同组的人的代码相互作用,这非常容易给App带来bug,也会延缓试验新功能的速度。

Presidio吸取了这个教训,在组织代码时粒度更小,比如把Controller的功能切分成了Builder,Router,Interactor等等,有点类似VIPER。在这个体系中,一个组件,官方名称为Riblet,包括Router, Interactor, Builder, Component, View, Presenter。而在实现中,我们只有一个Activity, 而在Activity上插了一个以Riblet为节点的树,每个Riblet在被插拔时管理自己的lifecycle。这样也避免了在Activity中使用易出bug的Fragment的lifecycle。

在Presidio中,Builder的主要任务是根据父级传入的参数创建整个Riblet和下层Riblet的Builder。Router是根据lifecycle和Interactor的指令对下层Riblet进行插拔。Interactor是真正的业务逻辑,会根据用户事件或其它事件来做各种决定,并通过Presenter来控制View显示各种信息。

Presidio的另一个优点是不同优先级的模块间的保护(这点是四海皆准)。Presidio主干结构和关键功能上的代码会被严格审查而保证不会有错,而产品工程师为了做实验而开发的Riblet会有默认的flag来关闭,如果实验feature里有bug,最坏的情况是关掉这些非主干功能,从而保证主要功能仍然可以工作。

十、App网络与推送的处理

除了UI,移动端上还有很多其它功能,如各种组件之间通信的和网络通讯。我先说说组件间的通信,一般来说EventBus是一个常用的方式,但是它的不好的一点是所有的组件间的通信都通过一个渠道,这样就缺乏组件间的保护,也不好debug,因为每个激发事件的点都可能是出错点。而RxJava这点就好很多,因为不同的通信是用不同的Observable,所以无关组件间不会相互影响。另外,在代码的组织上,我们可以很干净很容易的把一些列的事件激发和处理串起来,而Event Bus就要繁琐很多。

再说网络通信,通常都是使用Retrofit, 由于它的执行是异步的,所以配合上RxJava就可以把要对返回结果要做的操作串起来。

通常如果客户端的信息有时效性的话,我们需要及时的把信息发给后台,那么我们就需要隔一段时间发些信息回后台,具体的间隔和payload,取决于具体的应用情景。

另外如果我们有后台的消息要发给移动端,我们就需要Push功能。具体的Push其实还分两种,一种就是大家所熟悉的Google Firebase和Apple Push Notification Service,这种Push是不分Mobile App状态而推送过去的,所以即便在App被杀死的情况下,我们可以用它来唤醒App。另一种是App本身可以实现的,只在App在前台的情况下获得推送的功能,这个功能相对第一种来说更轻便,也不需要过Google或Apple。比如说,我们可以试图跟后端保持一个HTTP长链接,然后不时的让后端喂些数据保持这个长链接即可。如果要实现提示消息数,在线提示等功能,这个方案就足够了。

关于网络,我们还需要关注客户端的故障恢复机制。比如在和App通信的数据中心断电了,我们需要让客户端自动跳转到其它备用的数据中心。这就需要我们在移动端事先写好所有的备用选择,并配置各种的降级机制,比如在主数据中心 3次没有响应后跳转到其它数据中心。或者是接到后端的指令后跳转到其它数据中心。

最后关于网络,我们还需要让网络调用的Data Model非常严格,比如把网络调用的interface定义成严格的Protocol Buffer,然后编译成移动端和后端使用的代码,这样就可以防止比较随意的后端payload改动搞坏App。

最后一点是关于monorepo,Uber的移动端代码有很多library,散落在不同的代码仓库之中,这对于并行开发有些好处,但是对于维护就不太方便,比如要改一个annotation可能要改很多代码库并且升级版本等等,最后还是决定合成一个repo, 然后工程师build代码时只需要build相关的代码,这点使用buck可以实现。[/size]

已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [uber 技术 十条] 推荐:

构建Uber端到端技术栈的十条经验

- - 企业架构 - ITeye博客
[size=large]我在Uber这几年,做了很多系统稳定性及可扩展性的工作, 也包括很多快速迭代试错的产品,另外还做了一些移动开发的工作,因此我对Uber的端到端的技术栈还比较熟悉. 在这里以我的经历为例跟大家分享一下如何以Uber的方式快速稳定的做一个端到端的大型应用. 我刚加入Uber时,Uber正处于飞速成长期.

Uber先进技术集团首席科学家:自动驾驶汽车大规模普及还需要很长时间

- - TechWeb 新酷网站 RSS阅读
【TechWeb】4月9日消息,据国外媒体报道,自动驾驶是近几年汽车行业的一大热点,谷歌、苹果、特斯拉、通用等众多企业都在这方面有布局,其中也包括了以打车服务出名的Uber,但Uber自动驾驶方面的高管近日表示,自动驾驶汽车大规模普及还需要很长的时间. 认为自动驾驶汽车还需要很长的时间才能普及的,是Uber先进技术集团的首席科学家拉克尔·乌尔塔森(Raquel Urtasun),先进技术集团是Uber进行自动驾驶等先进技术研发的部门,拉克尔·乌尔塔森负责领导这一集团在加拿大多伦多的部门.

[翻译]十条有用的 Go 技术

- - Gopher beyond Eliphants
这里是我过去几年中编写的大量 Go 代码的经验总结而来的自己的最佳实践. 某个应用需要适配一个灵活的环境. 你不希望每过 3 到 4 个月就不得不将它们全部重构一遍. 许多人参与开发该应用,它应当可以被理解,且维护简单. 许多人使用该应用,bug 应该容易被发现并且可以快速的修复. 我用了很长的时间学到了这些事情.

Uber 是如何利用大数据的

- - 博客 - 伯乐在线
这篇文章概述了 Uber 是如何利用大数据分析实现商业上的成功. 文章首次发表于作者在 Data Science Central 的专栏中. Uber 是一款基于智能手机应用的出租车预定服务,将需要出行的用户和愿意提供驾驶服务的司机联结起来. 由于传统出租车的司机认为这破坏了他们的生计,而且大众对 Uber 对司机在管理上的不足也有所顾虑,这项服务已经引起了巨大的争议.

关于Uber机制的思考

- - KantHouse 追从本心,笑斩荆棘
昨天写了一篇关于滴滴打车改版的文章(文章链接),引发了一些关于Uber和滴滴的对比讨论. 质疑明显歪了楼,大家主要讨论的是,Uber忽略目的地的问题和它的派单机制,而我在昨天文章中说的是Uber的首页设计的一些问题,具体来说,是它的出发地和开始用车的按钮不在一起的问题,以及出发地带搜索icon带来误解的问题.

成为成功技术领导的十条经验

- - 研发管理 - ITeye博客
        如果你带领的团队必须完成某个任务,而任务现在面临困境,你愿意:.         a.优先考虑任务.         b.优先考虑人.         c.平衡人和任务.         d.逃避.         e.上述无一可选择.         上面的问题出自一本管理教科书.

Uber 在运营策略上到底厉害在哪?

- - 知乎每日精选
感谢各位知友提醒,博客昨两天访问量太大,一下子冲挂了. 已经升级服务器配置,现在可以正常访问了:). 看不下去了,一些事实+各种吹捧+美化缺点+你学会了吗. 明显的朋友圈文章居然有700多人点赞,特别是还有一个敬佩的前辈点赞,心碎……我觉得真的要学习的话,应该在好的地方辩证思考,在不好的地方承认并且想解决办法,而不是一通乱夸.

Uber火了!它改变了哪些营销游戏规则?

- - 互联网的那点事
一面是专车司机揽客被抓罚款弄得人尽皆知,一面又被媒体视为宠儿上着各大媒体、自媒体的头条要闻. 作为与Airbnb、facebook等同样令人瞩目的创新先锋,为了拉动车源和客源,Uber表现出了许多灵光乍现的创意,如“一键呼叫英雄”、“一键叫高管”、“一键叫人力三轮”、“打船”等,那么除了被媒体曝光的看的见的那些创意噱头,还有哪些Uber修炼的真功夫值得市场营销者学习借鉴的呢.

想要复制 Uber 的成功,你得先知道这些

- - TECH2IPO创见
本文来源: Medium , 译文创见首发 由 TECH2IPO / 创见 阿沫 编译 转载请注明出处. Uber 的成功无疑让创业圈中不少人看到了新商机,一时间「共享经济」的热潮席卷全球,催生了各种各样「XX 领域的 Uber」——只要核心业务带着点分享性质,创业者们都乐意将自己的产品冠上和 Uber 相关的称号,仿佛这层关联性,能让自家产品离成功更近一些.

Uber容错设计与多机房容灾方案

- - 互联网 - ITeye博客
此文是根据赵磊在【QCON高可用架构群】中的分享内容整理而成. 赵磊,Uber高级工程师,08年上海交通大学毕业,曾就职于微软,后加入Facebook主要负责Messenger的后端消息服务. 这个系统在当时支持Facebook全球5亿人同时在线. 目前在Uber负责消息系统的构建并推进核心服务在高可用性方向的发展.