- 连锁故障是由于正反馈循环(positivefeedback)导致的不断扩大规模的故障。
连锁故障可能由于整个系统的一小部分出现故障而引发,进而导致系统其他部分也出现故障。例如,某个服务的一个实例由于过载出现故障,导致其他实例负载升高,从而导致这些实例像多米诺骨牌一样一个一个全部出现故障。
- 连锁故障产生的原因
(a)服务器过载:直接由于服务器过载导致,或间接由于服务器过载引发的其他问题导致
例如,某个集群内部的过载可能会导致该服务实例崩溃;这时负载均衡器会将请求发送给其他集群,使那些集群的实例过载,从而造成整个服务过载故障。这些事件连锁发生的速度可能非常快(可能在分钟级范围),因为负载均衡器和任务编排系统的响应速度通常非常快。
(b)资源耗尽:某一种资源的耗尽可以导致高延迟、高错误率,或者低质量回复的发生。这些的确是在资源耗尽时应该出现的情况:在负载不断上升到过载时,服务器不可能一直保持完全正常。
不同种类的资源耗尽会对软件服务器产生不同的影响。
- CPU:如果CPU资源不足以应对请求负载,一般来说所有的请求都会变慢。这个场景会造成一系列的副作用:
(1) 正在处理的(in-flight)请求数量上升:这会影响其他所有的资源,包括内存、活跃线程数(在每个请求一个线程的编程模型下)、文件描述符,和后端服务器的资源(该资源的耗尽可能会带来其他连锁问题)。
(2) 队列过长:意味着延迟上升(因为所有请求都要排队一段时间),同时队列会使用更多的内存。
(3) 线程卡住
(4) CPU死锁或者请求卡住
(5) RPC超时:服务器过载时,对客户端RPC的回复会变慢,最终会超过客户端所设置的超时时间。这会导致服务器对请求实际进行的处理都被浪费了,而客户端可能会重试RPC,造成更严重的过载。
(6) CPU缓存效率下降- 内存: 就算没有其他副作用,同时处理的请求数量升高也会消耗更多的内存用于存放请求、回复以及RPC对象。内存耗尽可能导致如下情况的发生:
(1) 任务崩溃:任务可能会因为超过资源限制而被容器管理器驱逐(VM或者其他的),或者程序自身逻辑会触发崩溃。
(2) Java 垃圾回收(GC)速率加快,从而导致CPU使用率上升:由于CPU资源减少,请求处理速度变慢,内存使用率上升,导致GC触发次数增多,导致CPU资源的进一步减少。我们将此称之为“GC死亡螺旋 ”。
(3) 缓存命中率下降:可用内存的减少可能会导致应用层缓存的命中率降低,导致向后端发送更多的RPC,可能会导致后端任务过载。- 线程: 线程不足可能会导致错误或者导致健康检查失败。如果服务器为此增加更多线程,这些线程可能会占用更多内存。在极端情况下,线程不足可能会导致进程ID数不足(Linux的进程ID数是有限的)
- 文件描述符:文件描述符(file descriptor)不足可能会导致无法建立网络连接,进而导致健康检查失败。
- 资源之间的相互依赖:很多资源的耗尽都会导致其他资源出现问题—某个服务过载经常会出现一系列次级现象看起来很像根本问题,这会使定位问题更困难。
假设如下场景:
有点意思
- 某Java前端服务器GC参数没有被调优。
- 在高负载(但是在期待范围内)情况下,前端由于GC问题导致CPU不足。
- CPU不足导致请求处理变慢。
- 同时处理的请求增多导致内存使用上升。
- 内存压力上升,同时由于固定内存分配比例的原因,用于缓存的内存数量减少。
- 缓存数量降低意味着缓存中键值数量下降,从而导致命中率下降。
- 缓存命中率下降导致更多的请求被发往后端进行处理。
- 后端服务器CPU或者线程不足。
- CPU不足导致健康检查失败,从而触发了连锁故障。
(c)服务不可用:资源耗尽可能导致软件服务器崩溃。例如,这些服务器可能会由于内存超标而崩溃。一旦几个软件服务器由于过载而崩溃,其他软件服务器的负载可能会上升,从而使它们也崩溃。这种问题经常如同滚雪球一样越来越严重,不多久全部服务器就会进入崩溃循环。这种场景经常很难恢复,因为一旦某个软件服务器恢复正常,它就会接收到大量请求的轰炸,几乎立即再次崩溃。
- 防止软件服务器过载的策略
(a)使用负载压力测试得出服务器的极限,同时测试过载情况下的失败模式:除非在真实环境下测试,否则很难预测哪种资源会耗尽,以及资源耗尽产生的效果。
(b)提供降级结果
(c)在过载情况下主动拒绝请求
(d)上层系统应该主动拒绝请求
由于速率限制实现中一般不会考虑服务整体健康程度的因素,它可能并不能阻止一个已经发生了的连锁故障。简单的速率限制实现很有可能会导致一些容量的浪费。速率限制可以具体在以下位置实现:
- 在反向代理层,通过针对请求的某种特性进行数量限制(如IP地址),来缓解和避免拒绝服务攻击,避免攻击性客户端的影响。
- 在负载均衡器层,在服务进入全局过载时主动丢弃请求。取决于该服务的实现和复杂度,这种丢弃可能是针对所有请求的(丢弃超过X QPS 的所有请求),或者是有选择性的(丢弃非最近用户发来的请求,或者丢弃低优先级请求,如背景同步请求,但是保留用户交互型请求)。
- 在每个任务自身,避免负载均衡层的随机扰动导致软件服务器过载。
(e)进行容量规划:好的容量规划可以降低连锁反应发生的可能性。容量规划应该伴随着性能测试进行,以确定可能导致服务失败的负载程度。
- 队列管理:
大部分“每个请求一个线程”模型的软件服务器使用一个队列和一个线程池来处理请求。接收到的请求将会进入队列,线程池中的某线程会将请求从队列中取出,进行实际处理。通常情况下,当队列排满时,服务器会拒绝新的请求。
在队列中的排队请求消耗内存,同时使延迟升高。
对一个流量基本稳定的服务来说,队列长度比线程池大小更小会更好(如 50% 或更小)。当服务处理速度无法跟上请求到达速率时,尽早拒绝请求会更好。
- 流量抛弃 load shedding
软件服务器临近过载时,主动抛弃一定量的负载。目标是避免该软件服务器出现内存超限,健康检查失败,延迟大幅升高,或者其他过载造成的现象。
- 一种简单的流量抛弃实现方式是根据CPU使用量、内存使用量及队列长度等进行节流。
- 另外的做法包括将标准的先入先出(FIFO)队列模式改成后入先出(LIFO),以及使用可控延迟算法(CodDel)或者类似的方式更进一步地避免处理那些不值得处理的请求(如果某个用户的Web搜索请求由于RPC排队因素已经等待了10s,很有可能该用户已经放弃了,已经刷新了浏览器又发送了一个新请求:这时回复第一个RPC请求已经没有任何意义,因为它已经没用了!)
- 优雅降级 graceful degradation
在流量抛弃的基础上进一步减少服务器的工作量。在某些应用程序中,是可以通过降低回复的质量来大幅减少所需的计算量或者所需的计算时间的。
评估流量抛弃或者优雅降级时,需要考虑以下几点:
- 确定具体采用哪个指标作为流量评估和优雅降级的决定性指标(如,CPU用量、延迟、队列长度、线程数量、是否该服务可以自动进行降级,或者需要人工干预)。
- 当服务进入降级模式时,需要执行什么动作?
流量抛弃或者优雅降级应该在服务的哪一层实现?是否需要在整个服务的每一层都实现,还是可以选择某个高层面的关键节点来实现?当评估和具体实施的时候,还需要考虑以下几点:
- 优雅降级不应该经常被触发—通常触发条件显示了容量规划的失误,或者是出现了意料之外的负载转移。整个降级系统应该简单、易懂,尤其是在不经常使用的情况下。
- 记住,代码中平时不会使用的代码路径(通常来说)是不工作的。在稳定运行状态下,优雅降级不会经常触发,意味着在这个模式下的运维经验很少,对这个模式的问题也不够熟悉,这就会升高这种模式的危险性。我们可以通过定期针对一小部分的服务压力测试以便更多地触发这个模式,保证这个模式还能正常工作。
- 监控系统应该在进入这种模式的软件服务器过多时报警。
- 复杂的流量抛弃和优雅降级系统本身就可能造成问题—过于复杂的逻辑可能会导致软件服务器以外的服务进入降级模式,甚至进入反馈循环。设计时应该实现一种简单的关闭降级模式,或者是快速调节参数的方式。
- 重试
(a)限制每个请求的重试次数。不要将请求无限重试。
(b)使用明确的返回代码,同时详细考虑每个错误模式应该如何处理。
- 请求延迟和截止时间
(a)选择截止时间
(b)超过截止时间
(c)截止时间传递
- 慢启动和冷缓存
(a)进程在刚刚启动之后通常要比稳定状态下处理请求的速度慢一点,原因在于:1)必需的初始化过程在接收到第一个请求后,需要跟后端服务器建立连接。2)运行时性能优化,尤其是Java JIT 编译过程,热点优化,3)以及类延迟加载机制。
(b)有些服务器会在缓存没有充满之前效率很低,原因有可能:1)上线一个新的集群刚刚增加的集群的缓存是空的。2)在某个集群维护之后恢复服务状态缓存中的数据可能是过期的。3)重启。
- 保持调用栈永远向下
前端服务器和后端服务器通信,后端服务器随后和存储层通信。
假设后端服务器中的所有任务会彼此通信。这种在层内的交互通信可能会导致问题,原因有如下几个:
(a)导致分布式死锁。
(b)如果这种交互通信是由于某种失败因素或者过载情况导致的(如负载重新分布机制在负载很高的时候很活跃),这种交互通信可能在延迟上升的时候从不常见变得很常见。
(c)初始化整个系统可能会变得更复杂。
- 连锁故障的触发条件
(a)进程崩溃:进程可能会由于接收到致死请求(query of death,会触发进程崩溃的RPC)而崩溃,或者由于集群问题,代码中的断言错误
(b)进程更新:发布一个新版本或者更新配置文件时,可能由于大量任务同时受影响而触发连锁故障。
(c)新的发布:新的二进制文件、配置更改,或者底层架构的改变都可能导致请求特征的改变,资源使用和限制的改变、后端的改变和其他系统组件的改变可能导致连锁故障的发生。
(d)自然增长:使用量的天然上升,却没有进行对应的容量调整导致的。
(e)计划中或计划外的不可用:
- 连锁故障的测试
(a)测试直到出现故障,还要继续测试:理解服务在高负载情况下的行为模式可能是避免连锁反应最重要的一步。
我们还应该测试一个组件在过载之后再恢复到正常水平的行为状态。这种测试可能可以帮助回答下列问题:
- 如果一个组件在高负载条件下进入了降级模式,它是否能够在无人工干预的情况下退出该模式?
- 如果高负载情况下几个服务器崩溃,负载需要降低多少才能使系统重新稳定下来?
(b)测试最常用的客户端:
(c)测试非关键性后端:
- 某些原本为了降低服务背景错误率或者优化稳定状态的改变反而会让服务更容易出现事故。在请求失败的时候重试、负载自动转移、自动杀掉不健康的服务器、增加缓存以提高性能或者降低延迟:这些手段原本都是为了优化正常情况下的服务性能,但是也可能会提高大规模的服务故障的几率。
1万+



被折叠的 条评论
为什么被折叠?



