如何设计一个精壮的服务

最近辞职以后，在准备面试和简历期间，回顾了之前在工作期间出现的服务问题。有一些事故中，是因为某个或某几个服务被高峰期的流量打趴，可能是数据库问题，亦有可能是内存等等各种问题。其深层次的背后，是服务本身不够健壮，服务的某个或某几个节点被打趴以后，导致重试风暴，继而又导致雪崩的出现。如果这些服务是主流程上的服务的话，那就有可能导致全系统的雪崩。

我们的很多服务在压力下的表现如上图所示，在到达最大负载之前还是可以正常提供服务的，但随着流量一旦突破服务所能承受的最大极限后，对外的服务能力急转直下，即使这时候流量没有过载了，甚至把流量完全降级为 0，其服务能力也久久不能恢复。

甚至有的情况下好不容易等到服务能力恢复了，上层流量切回来以后，服务瞬间又被大量人工或自动的重试请求给打趴下了。

如果我们的后台服务表现如此的 “脆”，只要过载立刻就嘎嘣倒下，久久不爬起来，那我们的稳定性保障工作是没法做的。在大型的应用中，全链路有几千个服务，每个服务又有很多的节点。这种情况下，是很难保证，以及很难监控到所有的节点的稳定，系统不被流量打趴。这种情况也是做不到的。因为想不到的局部的异常是永远都会出现的。

因此，我们要整个应用做好健壮性，应用的每个服务都应该具备一定的健壮性。对于单个服务本身 (无论是单机还是分布式)，其实这个要求并不高，也不太难做到。

我们要做到，一个 “精壮” 的服务，在压力下的表现应该如上图所示，当负载加大的时候输出线性提升，当外部负载超过服务理论能承受的最大负载后时，经过一个短暂的抖动后，不管外部负载有多高，服务都能稳定的对外输出，并且这个输出最好能比较贴近理论的最大负载。也就是说，一个 “精壮” 的服务即便在超负荷的情况下，也能够对外提供稳定输出的服务，超过处理能力的请求，应该果断的拒绝服务。

一个服务的每个节点都需要有能够实时表征自己服务负载的指标，当指标超出服务能力上限的时候，拒绝超出服务能力的请求。

服务端要和客户端（这里的客户端不只是指 APP，而是这个服务自己的客户端，一般来说是集成够知晓后端的负载情况，调整自己的策略。在另一个服务里的 SDK，当然，也可能是 APP 里的 SDK，下同）之间要有反馈机制，客户端要能知晓后端的负载情况，调整自己的策略。

通俗来讲，对于每个服务的每个节点：1. 你得清楚自己现在忙不忙，是不是快到极限了。2. 你忙你得跟你的客户端说一声，别它不知道，一不小心把你给弄趴下了。。。

接下来展开讲一下这两点，如何找到表征自己服务负载的指标，以及如何在过载的时候和客户端协商。

表征服务负载的指标

表征服务负载的指标，指的是当这一指标越过某个阈值的时候，该服务节点的服务能力会急剧恶化并久久不能恢复。

我们可以通过压测，去找到标准服务器环境下的较合理的 QPS、TPS 等值。然后通过限流、熔断等工具去进行峰值限制。但是可以根据实际情况，将限流峰值限制在一个区间内：

只要这个服务节点是可以水平扩展的，就不需要太精确，不需要过度追求让服务压着 CPU 或其他资源极限跑，根据压测结果，设置一个不那么激进的值即可，可以通过混布甚至超售进一步压榨服务器资源利用率。

要考虑超时时间，否则就算处理完了请求也没用，客户端可能已经超时了，请求返回也只会被丢弃。

要考虑一些机器是否会运行其他功能的服务，比如说 机器监控工具、主备节点同步功能、心跳功能。因此，在阈值的设置上，要留有余地。更精细的服务甚至可以考虑请求分级，针对每个优先级设置不同的计数器。

有人可能会想这个计数器的阈值是不是搞成自适应的，即根据服务节点内部资源情况动态调整阈值大小，是不是更牛逼。说实话，绝大多数情况下，不需要搞成这样子，越简单越不容易出错，很少的情况下需要死扣单节点的性能，在如今机器爆炸的年代，服务稳定比机器更贵。

客户端与服务端的协同

说实话我对客户端不是很了解，但是这个问题实际上还是后端繁忙情况下的重试逻辑的一个优化。

客户端的重试策略其实应该是一个很精细的逻辑，只不过特别容易被人忽略而已。一般的重试策略，大家很容易想到的都是简单的等间隔重试，周到一些的会考虑按照等比数列或者斐波那契数列等，逐步拉长间隔的重试。

但仔细去想，重试实际上是为了解决两类问题。一个是对网络丢包或瞬断的 fail over，一个是对服务端节点故障的 fail over。对于前者，重试间隔其实越小越好，最好是瞬时重试；但对于后者，这个间隔如果太小，很容易引起重试风暴，让后端节点死的更惨。然而，悲剧就在于，站在客户端的角度，我们其实区分不了到底发生的是哪种情况。

因此，最简单的解决办法是，当服务端的节点繁忙的时候，并不是简单的丢弃收到的请求，而是对于这些请求，都向客户端返回 RET_BUSY。客户端一旦收到 RET_BUSY，都应该跳出当前重试逻辑，直接拉长重试的间隔，避免造成重试风暴。更进一步的，服务端可以根据自己情况对返回值进行分级，例如 RET_RETRY，让客户端立刻发起重试，RET_BUSY 和 RET_VERY_BUSY 让客户端拉长到不同的重试周期。这里其实有非常多精细的玩法，能够解决很多复杂的问题，大家可以结合自己的工程场景实践一下。

到此为止，我们就讲完了如何做一个 “精壮” 的服务。本质上是用非常简单的工程方法，去实现能初步识别自我繁忙程度的服务端，和能够根据服务端繁忙程度自适应重试的客户端。从而做到不管外部请求压力有多大，都能提供可控输出。

在实际的工程实践中，要做到服务端节点，能够在被压到平均 CPU 利用率 70% 以上，瞬时 CPU 利用率超过 90％的情况下，提供持续稳定的对外服务。在测试环境里能够做到在极限情况下的持续稳定输出。

能不能做到服务无感实现的情况下上述健壮性

这套机制能不能通过外部监控，结合流量降级方案实现在外围，这样是不是就不需要动服务本身了？

首先，外部监控 + 流量降级方案，是无论如何一定要有的，它是一切的兜底方案。但是，监控是有滞后的，并且，流量降级是有损的，这导致降级需要谨慎。这些因素都决定了，外部监控 + 流量降级这套机制，是用来解决故障，而非让服务更加健壮的。服务的健壮性本身，要依赖服务自身来解决。

哪些服务需要改造

上述这套机制是可以做进 RPC 框架里的，对于绝大多数要求没那么高的服务，也是足够用了。

分布式存储系统会更高要求性能问题。存储系统在稳定性上的工程难度确实是最复杂的，最基本的，存储节点不能通过简单重启来解决问题；并且资源消耗维度复杂，除了 CPU 外，还有内存以及 page cache，网络吞吐和中断，磁盘 iops 和吞吐等诸多资源考量维度；更进一步的，流量降级以后通常只有存储系统是不能快速恢复服务的（例如，需要等待主从同步，需要等待 minor 或 major compaction, 需要等待刷盘等等），因此，在线存储系统的健壮性，需要考虑的因素通常是要更多的。