banner
田野放空

田野放空

认真扮演一个擅长白日做梦的普通人

如何设计一个精壮的服务

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

image-20230401221427201

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

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

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

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

image-20230401222233706

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

  • 一个服务的每个节点都需要有能够实时表征自己服务负载的指标,当指标超出服务能力上限的时候,拒绝超出服务能力的请求。
  • 服务端要和客户端(这里的客户端不只是指 APP,而是这个服务自己的客户端,一般来说是集成够知晓后端的负载情况,调整自己的策略。在另一个服务里的 SDK,当然,也可能是 APP 里的 SDK,下同)之间要有反馈机制,客户端要能知晓后端的负载情况,调整自己的策略。

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

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

表征服务负载的指标#

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

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

  1. 只要这个服务节点是可以水平扩展的,就不需要太精确,不需要过度追求让服务压着 CPU 或其他资源极限跑,根据压测结果,设置一个不那么激进的值即可,可以通过混布甚至超售进一步压榨服务器资源利用率。
  2. 要考虑超时时间,否则就算处理完了请求也没用,客户端可能已经超时了,请求返回也只会被丢弃。
  3. 要考虑一些机器是否会运行其他功能的服务,比如说 机器监控工具、主备节点同步功能、心跳功能。因此,在阈值的设置上,要留有余地。更精细的服务甚至可以考虑请求分级,针对每个优先级设置不同的计数器。
  4. 有人可能会想这个计数器的阈值是不是搞成自适应的,即根据服务节点内部资源情况动态调整阈值大小,是不是更牛逼。说实话,绝大多数情况下,不需要搞成这样子,越简单越不容易出错,很少的情况下需要死扣单节点的性能,在如今机器爆炸的年代,服务稳定比机器更贵。

客户端与服务端的协同#

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

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

但仔细去想,重试实际上是为了解决两类问题。一个是对网络丢包或瞬断的 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, 需要等待刷盘等等),因此,在线存储系统的健壮性,需要考虑的因素通常是要更多的。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。