第一是后端框架的选择,通常实时响应系统都是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的优先级都不会太高,在资源有限的情况下共享是无奈的选择,但是我们要知道危险性。
八、产品工程和快速迭代
我在用户增长组主要聚焦在产品工程,即如何用最少的资源,最快的速度,来实现非常具有可扩展性的解决方案,因为迭代速度越快,代价越小,对竞争对手的优势就越大。同时要和产品经理保持默契,适应不断变化的需求。另外还要和其它组的产品经理和工程师保持沟通,尽量减少和消除产品远景规划上的冲突。
具体的说,为了实现最具可扩展性的方案,我们需要了解我们所能覆盖的使用情景,然后抽象出我们系统的行为。有了行为以后,我们可以在看看还有没有其它的使用情境,也可以用这样的行为所支持,如果可以,我们就达到了用最少的工作来达到最大产出的的结果。
当我们抽象出来这个系统的行为后,我们发现我们要处理的是由注册开始的一系列事件,并且根据这些事件和运营人员设置的规则来做各种处理。在这样的情况下,不仅司机推荐司机奖励,其它的各种司机奖励(比如老司机奖励),和其它的各种推荐活动,也可以用这个系统来处理。