我在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和可响应性,有时甚至会牺牲一些其它的指标,如数据一致性。
支持这两点,我们需要考虑几件事情。