在如何构建高可用的系统(一):Overview中, 我提到了以下几点:
如果把这几个问题,映射到现在非常流行的“微服务架构”下,就可以统一用“服务治理”来概括。 下面我们具体探讨下,如何解决这几个问题。
系统所依赖的各个下游系统是不可靠的, 它们随时可能会出问题。 比如我们开发了一个用户服务, 会访问数据库、缓存等, 那这些下游的数据库、缓存出问题的时候, 我们该怎么办呢?
当下游出现问题时, 我们的服务或多或少都会受影响。 为了保证核心场景仍然高可用, 我们需要把相关功能降级,以减少损失。 比如以下场景:
当数据库挂掉的时候,所有对数据库的读写都会失败。 为了使创建用户的功能仍然对外可用, 我们可以选择打开降级开关, 把创建用户的请求发到消息队列中, 在数据库恢复之后, 从队列中拉取消息, 再把数据写入数据库中。
对上游来说,写入成功之后, 当然得能通过读接口获取到数据。 因此, 我们还得把数据写入到缓存中, 这样即使数据库挂了, 上游也能够尽量不受影响。
这里只是随便举了个降级的例子, 在实际场景中, 降级处理方案还是得仔细思考,具体问题具体分析。
在很多场景下, 服务接收到一个请求后, 会调用非常多的下游, 当调用某个下游的请求迟迟没有得到响应, 会造成两个结果:
因此我们需要控制对下游依赖的超时情况, 一般来说, 对核心依赖的超时可以适当大一些,对非核心依赖的超时可以适当小一些。
当某个下游出问题后, 对它的调用失败率就会上升。 当上升到某个程度时, 往往没必要再每次都去调用了, 而是默认失败, 只发出一小部分的请求。 当这小部分的请求成功率达到某个阈值时, 才恢复到正常状态。这种策略就叫“熔断”。
现在已经有一些成熟的组件来帮助我们做了熔断这件事, 比如Hystrix等。
为了防止某个上游的抽风行为把系统打挂, 或者为了防止离线系统的异常流量影响在线系统的正常运行, 我们往往会采用资源隔离的方式。
假设我们在维护一个用户服务, 服务的上游有很多, 包括在线系统和离线系统:
对于这种情况, 我们可以部署多套环境, 来为不同的上游提供服务。 “一套环境”,在这个例子中, 包括用户服务的实例、 用户服务所依赖的数据库和缓存等。
这种资源隔离的思想落到具体实现,则可以通过服务发现机制来解决。 比如我们用consul来做服务注册和发现, 那可以通过给实例打不同的tag来做区分。
一个系统对外提供的服务量是有上限的, 如果我们对外提供的承诺是为1w/s的请求量提供4个9的可用性, 那在请求量超出了1w/s, 影响可用性的时候, 就应该采用限流的方式来避免系统被打挂。
限流算法有很多, 比较经典的有漏桶算法和令牌桶算法等。 两者的区别是,漏桶算法的限流是匀速的,不支持突发的流量增长, 而令牌桶算法则是在一个时间窗口内提供一定的总量, 并不限制在时间窗口内的流量分配。
为了实现限流算法, 我们往往需要记录时间戳、请求余量等数据。 如果是单机限流的话, 只需要把这些数据保存在内存中即可。
在实际场景下, 我们需要的往往是分布式限流,即多台机器对外提供的总量是有限制的。 比如用户服务所依赖的缓存和数据库加起来只能扛100w/s的读, 那么所有的服务实例能支持的qps, 就不应该超过100w/s。
对于分布式限流, 上面保存在内存中的方案就不适用了, 因此需要依赖外部存储来保存相关数据, 通常来说,redis是一个比较好的选择。
上面谈到的限流, 是比较简单粗暴的: 如果一个服务无法处理所有的请求, 那么就随机抛弃一部分请求。
但是这种限流方案并不一定能有效缓解整个系统的负载, 并且浪费了很多计算资源,比如以下场景:
假设api层处理一个客户端请求时,依赖N个下游服务, 而这N个下游服务都处于过载状态, 以p的概率随机拒绝服务, 那么这个客户端请求最后被成功处理的概率就是(1-p)^N。 如果P和N很大, 那么这个请求很可能没有被成功处理,但是却浪费了很多计算资源(比如N-1个服务都处理了这个请求,但是1个服务拒绝了)。
因此微信提出了面向业务和用户优先级的限流方案, 可以有效解决上述问题。 我在之前的文章中对微信的解决方案做了解读, 感兴趣的朋友可以去看下。