`

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

阅读更多
[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]
分享到:
评论

相关推荐

    大规模的可观察性:构建Uber的预警生态系统.docx

    在大规模的可观察性领域,Uber构建了一个强大的预警生态系统,以应对公司中数千种微服务带来的复杂性。这个系统包括两个数据中心的警报系统:uMonitor和Neris,它们都连接到统一的通知和警报管道,提高了问题检测、...

    藏经阁-Uber SRE以及Cache服务在微服务环境下的演进.pdf

    Uber 的技术栈包括 Storage(Schemaless、MySQL、Cassandra、Redis/Twemproxy)、Data(Hadoop、Spark、Hive、Presto、Vertica)、Queue(Cherami、Kafka)、Search & Logging(Elasticsearch、Logstash、Kibana)等...

    Uber高可用消息系统构建

    本文档将详细介绍Uber是如何构建其高可用消息系统的,包括但不限于消息传递机制、不同技术的选择与实现、故障恢复策略以及系统设计的最佳实践。 #### 二、消息系统概述 Uber的消息系统旨在实现高效可靠的信息传输...

    Uber平台架构设计介绍.docx

    通过这样的优化,Uber构建了一个强大且灵活的架构,能够处理大规模的用户请求,同时保证了服务质量和用户体验。 总而言之,Uber平台架构的设计是一个不断迭代和优化的过程,它从最初的简单架构演变为分布式、高可用...

    人工智能在Uber的外卖服务_Uber_Eats_中的应用.pdf

    Michelangelo是Uber内部构建的一个强大的机器学习平台,它为Uber的各个部门,包括Uber Eats,提供了模型训练、实验管理、数据处理和模型部署的能力。通过这个平台,Uber能够构建和运行各种复杂的AI模型,如: - **...

    藏经阁-Uber,SRE,缓存以及微服务.pdf

    Uber 的技术架构与 SRE 实践 本文将详细介绍 Uber 的技术架构、SRE 实践、缓存机制以及微服务架构。 Uber 的技术架构 Uber 的技术架构是基于微服务架构的,使用多种编程语言,如 Go、Java、Node.js、Python、C/...

    uber高可用消息系统构建

    本文将深入探讨Uber是如何构建其高可用消息系统的,并详细介绍其中涉及的关键技术和设计理念。 #### 二、消息传递技术 ##### 2.1 服务器推送(Server Push) 为了提高用户体验并确保消息能够及时送达客户端,Uber...

    Uber-ATG-Self-Driving-Safety-Report2018.pdf

    《Uber的自动驾驶技术》章节则可能介绍了Uber的传感器系统、决策算法、车辆集成等方面的技术细节,以及如何利用这些技术实现安全的自动驾驶。Uber的自动驾驶安全原则是报告的重点,包括五个主要方面: 1. **...

    Uber的欢迎界面

    本文将深入探讨Uber的欢迎界面,也就是Android版的UberSplash,旨在揭示其设计背后的逻辑与技术实现。 首先,我们来理解什么是欢迎界面(Welcome Screen)。在移动应用中,欢迎界面通常作为用户首次打开应用时展示...

    Android 仿Uber引导页

    在Android开发中,Uber应用程序的引导页是一种常见的用户体验设计,用于在用户首次打开应用时提供一个引人入胜的互动教程或展示应用的核心功能。这种引导页通常包含一系列的全屏视图,用户可以通过滑动浏览,从而在...

    Uber外卖平台国际化架构演化之路.pdf

    - Uber Eats在技术基础设施上不断创新,构建了机器学习平台、实验平台、预测平台和动态配置等,这些平台提供了强大的数据支持和实验环境,助力业务决策和性能优化。 总结来说,Uber Eats的架构演进是一部关于如何...

    Uber近几年的论文

    在Uber近年来的研究中,深度学习、无人驾驶以及增强学习是三个重要的技术领域,这些技术不仅推动了公司的技术创新,也在全球范围内引领了智能交通行业的进步。以下将详细探讨这三个领域的相关知识点。 首先,深度...

    Presto在Uber的使用

    为了更好地利用这些数据并从中获取价值,Uber构建了一个强大的数据平台,该平台包括Kafka、Schemaless MySQL、Postgres等数据生产者以及Hadoop分布式文件系统(HDFS)等多个组件。然而,在此过程中,Uber面临了一些...

    hive-jdbc-uber-2.6.5.0-292.jar

    “Uber”一词在这里是指一种构建工件的方式,它将所有依赖项打包进一个单一的JAR文件中,避免了类冲突和依赖管理的问题。使用Uber JAR(也称为fat或shaded JAR)可以简化部署,因为不需要单独管理各个依赖库。在...

    Uber Go 语言编程规范 中文版.pdf

    Uber作为Go语言的早期采用者,积累了丰富的经验,并将其内部的编程规范整理成文,供广大Go开发者参考。这份规范旨在提高代码质量、可读性和团队协作效率,涵盖了许多重要的编程实践。 在接口设计上,Uber规范建议...

    hive-jdbc-uber-2.6.5.0-292.zip

    《Hive JDBC Uber Jar:连接Hive与Java的桥梁》 在大数据处理领域,Hive作为基于Hadoop的数据仓库工具,被广泛用于大规模数据的离线分析。而Hive JDBC(Java Database Connectivity)则为Hive提供了一种标准的Java...

    hive-jdbc-uber-2.6.5.jar

    hive-jdbc-uber-2.6.5.0-292.jar DbVisualizer (as of version 9.5.5) Below is an example configuration using DbVisualizer: Open the Diver Manager dialog ("Tools" > "Driver Manager...") and hit the ...

    UberDemo仿uber登录页面

    【UberDemo仿Uber登录页面】是一个项目实例,旨在模拟Uber应用程序的登录界面,提供用户一个直观、美观且功能完善的登录体验。在这个项目中,开发者可能会遇到多种IT技术的应用,包括前端设计、动态效果实现以及...

    论文研究 - 折现现金流模型的Uber未来价值预测

    在本研究中,预测Uber估值的目的是获得Uber的未来自由现金流和股票价值,以便我们可以为Uber的未来发展战略提供信息,并提出可行的业务决策,然后提高Uber的未来价值。它。 此外,我们希望利用Uber的估值信息进行...

Global site tag (gtag.js) - Google Analytics