正文
At least once 和 at most once 在大部分情况下对应于幂等和非幂等的操作。另外,我们在实现系统时也要考虑已有系统提供的接口,比如说一个已有的接单系统已经提供了一个 at least once 的消息队列,而我们需要做的是跟据累积的交易数来做一些行为,在这样的情况下,我们就需要我们的系统能够消重,或者保证我们要做的行为是幂等的。
第二个层面是 Service 之间交互可能发生的问题,在设计时一定要考虑周全,比如通信可能发生的 failure case。我们要假定在线上各种奇怪的情况都会发生。
比如我们曾经有上下游 Service 之间通信时使用的 Kafka ingester 一直不是非常稳定,导致不时发生下游 Service 无法拿到数据来计算溢价,最后我们干脆把 Kafka 换成了 http polling, 再也没有问题了。
第三个层面是 Service 内部的故障, 比如缓存, 数据库断了,或者依赖的第三方 Service 挂掉了,我们需要根据情况进行处理,做好日志和监控。
如果一个 Service 是无状态的,那往往它做的事情是根据请求把下游各个 Service 的返回结果加工一下然后返回。我们可以见到很多这样的 Service, 比如各种 gateway,各种只读的 Service。
服务无状态的情况下往往只需要缓存 (如 Redis),而不需要持久化存储。对于持久化存储, 我们需要考虑它的数据模型、对 ACID 的支持、稳定程度、可维护性、内部员工对它的熟练程度、跨数据中心复本的支持程度,等等。
到底选择哪一种取决于实际应用情景,我们对各个指标要不同的需要,比如说 Uber 对于跨数据中心副本的要求就很高,因为 Uber 每一个请求的用户的期待值都很高,如果因为存储系统坏了,或存储系统阻挡 failover,那用户体验会非常差。
另外关于可维护性和内部员工的熟练程度,我们也有血淋淋的例子,比如一个非常重要的系统在订单最多的一天挂掉了,原因是当时使用的 PostgreSQL 数据库不知为什么被锁死了,不能读也不能写,而公司又没有专业到能够深入解析 PostgreSQL 的人,这样的情况就很糟糕,最好是换成一个更易维护的数据库。
这两点是系统在扩张过程中需要保证的,为了保证系统的 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,所以不是特别推荐。