前言
在读完谷歌 SRE 运维解密之后,觉得谷歌对过载问题解决能让人学到很多东西,这里就做个笔记。
处理过载问题
QPS陷阱
QPS只能反应请求的个数,但是每个请求消耗服务器的资源不同,谷歌大部分情况下采用服务器负载而不是QPS来衡量应用的负载。
客户端限流
每个客户端都是请求的 quota ,超过之后请求就会本机失败,就是请求不会发出去,而是客户端直接返回失败。
每个客户端都保留2分钟内的总请求数,和后端接受的请求数。客户端根据这两个参数来设定客户端直接返回失败的比率。
优先级
给每个应用设定优先级,
一共有四种,不多不少,
当一个用户用完配额的时候,后端服务器只接受指定优先级的请求。
当一个任务过载的时候,它会拒绝低优先级的请求。
限流系统为每个优先级各自存着统计值。
利用率信号
处理过载错误
一种是这个机房大部分服务都不行了
一种是这个机房小部分服务不行
决定重试
首先,per request restry budget of up to three attemps, 如果请求三次失败,就直接返回失败
其次,per client retry budget ,一个重试请求只有小于10%的时候才会发出去
第三种方法,后端保存重试次数的直方图,如果直方图反应出其他的后端也过载了,那就返回过载不要重试,而不是过载来避免客户端的重试。
如果一个请求会经过很多层的服务,只有最靠近失败的一层重试,其他的层应该收到的是过载不要重试或者一个降级的回复。
来自连接的负载
把负载暴露给跨机房负载均衡
在批处理客户端和后端之间加一层代理
处理连锁故障
服务器过载
一般连锁故障都是有服务器过载或者过载引起的
资源耗尽
各种资源耗尽的影响都不相同
CPU
- 增加当前的请求数
- 超长的队列
- 线程饿死
- cpu或者请求饿死
- 超过了RPC最长的等待时间
- 减少cpu的缓存优势
内存
- 将死的任务
- java应用GC频繁,cpu使用率也随之增高
- cache命中降低
线程
文件描述符
依赖的资源
当一个应用发生故障的时候,它的依赖资源也会出现问题,有时候这也像根本原因,使得排查问题更难。
服务不可用
资源耗尽可能导致服务器挂掉,然后剩下的服务器就会承受更多的负载。
服务器可能不健康,这时候负载均衡层次的容量就会降低,也可能出现跛脚鸭的状态或者健康检查失败但是并没有挂掉。
上面的情况容易雪崩
阻止服务过载
测试服务容量的极限,为过载测试失败的模式
服务降级
拒绝请求当过载的时候
在高级别的系统上拒绝请求,而不是在过载的服务器上
做好容量规划
队列管理
如果请求平稳,请求队列最好设置小点儿,这样如果他就能更早的失败,接受更少的请求
负载去除和优雅降级
去除负载的目的就是在服务过载的时候还能尽量接受一些请求。
如果用户等了10秒服务器还没有返回,一般用户就会刷新浏览器,这样之前的请求再返回就没有任何意义
优雅降级的一个例子就是当服务过载的时候,用户使用搜索服务,直接放回在内存中的数据子集而不是磁盘上的整个数据,或者返回一个不那么精确的所有结果
当你实施的时候,你要记住以下几点:
- 优雅降级不应该经常发生,保持系统简单可理解,特别是它不经常被使用的时候。
- 要保证这个优雅降级的系统能正常工作,你可以经常让一小部分服务过载来测试这个系统
- 当好多服务器进入这个模式的时候监控和告警
- 复杂的负载去除和优雅降级系统本身就可能产生问题,要设计一个方法能很快的关闭这个模式。
重试
当实施重试的时候需要注意以下几点
- 大部分的后端保护策略在阻止服务器过载那段描述的这里面都适用
要以随机指数的时间重试 - 一个请求重试的次数要有限制
- 考虑服务端的重试预算,比如一个进程一分钟只能重试60次,如果超过了预算就不要重试了
- 要全局的考虑是否需要重试,如果一个前端的请求重试可能导致多个后端的请求重试,
- 用明确的返回码,考虑怎样处理不同的失败模式,比如区分可重试和不可重试的情况。返回一个特定的状态,让客户端或者其他的服务不要重试了
延迟和截止时间
选择一个截止时间
大的截止时间当低层次的服务有问题的时候可能导致高层次的资源消耗,小的截止时间可能导致一些更贵的请求一直失败。平衡这些约束是一种艺术
错过截止时间
当一个请求经过很多的服务的时候,在每个阶段处理这个请求的时候检查一下是否剩下足够的时间。
截止时间传播
当调用链是A->B->C的时候A的截止时间是20秒,A处理用10秒,如果B处理也用10秒,那么这时候C接到这个请求是没有必要处理的。
取消传播
取消传播通过建议服务器在一个RPC调用栈中他的工作已经不需要了 减少不必要的或者灾难性的工作。
双向延迟
如果一个服务只有5%的请求有问题,这5%就会消耗这个服务的资源导致超过5%的请求都会有问题,这样可能会产生雪球效应,以下方法可以帮助解决问题
- 查看每段RT时间的数量而不是RT时间的平均值(长尾效应)
- 如果有问题就尽早失败返回错误而不是等待截止时间
- 截止时间不要大于平均RT几个数量级,这样容易导致线程耗尽
- 如果有可能被关键应用耗尽的共享资源,考虑限制这个应用同时请求的次数或者用一些其他的滥用追踪方法。比如如果你的应用又很多种客户端,限定每种客户端最多占用25%的线程,这样这个客户端就算有问题,也不会影响太大
慢启动和冷缓存
应用在刚启动的时候比稳定后的响应要慢,可能有以下原因
- 需要初始化,比如刚开始的时候建立连接
- 运行时性能提升,比如java中的延迟类加载,just-in-time 编译,热点优化
下面的一些场景中可能导致冷缓存
- 开启一个新的集群
- 集群从维护状态到正常
- 重启
如果服务严重依赖缓存,可以考虑以下策略
- 要区分latency cache和capacity cache详细可以看这个文章
- 采用综合的连锁故障预防技术,服务需要拒绝请求当过载或者进入了降级模式,需要进行测试来看服务是否在重启后正常
- 当给一个集群加负载的时候,慢慢的加,这样这个缓存就能热起来了。
在调用栈中永远向下
同层的调用是有问题的,
这个调用很容易导致分布式死锁。如果A的线程池慢了,B调用A,B也会满,这样线程池问题就会扩大
如果同层调用为响应某种故障或者高负载而增加,那么当负载增加到一定程度的时候同层调用可以迅速从底层切换到高层
如果有同层的调用启动一个系统会更复杂,如果A->B->C,如果BC同层,那么B应该让A去请求C而不是代理A去请求A。
连锁故障的触发条件
- 进程死亡
- 进程更新
- 服务更新,服务应该提供能查到最近的变更的途径
- 正常增长,好多时候连锁故障不是由变更引起的而是,流量正常增长,但是容量没有响应的调整
- 计划的变更,关机,节流(drained)
- 请求的配置文件变更
- 资源限制,在执行负载测试时,请确保不超出承诺的资源限制。
为连锁故障添加测试
测试直到错误和以后
一个好的设计组件在过载的时候,不应该很容易就挂掉或者返回很多错误,应该能拒绝掉一些请求,然后活下去
做这个测试应该能回答以下问题
- 如果一个组件进入降级模式在高负载下,它能在没有人干预的情况下抗住降级模式吗
- 如果一些服务器在高负载下挂了,需要丢掉多少负载才能使系统恢复正常
每个组件都有不同的极限点,所以要对每个组件单独测试
测试大部分的客户端
了解大部分使用你服务的客户端,比如你想知道客户端是否
- 当服务挂掉的时候,把请求放入队列
- 用随机指数规避错误
- 易受外部引起大负载的触发器的影响(比如软件更新导致客户端缓存清空)
你需要知道客户端怎么处理后端的失败
测试不关键的后端
测试不关键的后端,确定它们挂掉的时候不会影响关键的服务
当不关键的后端有问题的时候,系统不应该拒绝大部分的请求,耗尽资源,RT时间过长。
迅速解决连锁故障
- 增加资源
- 停止健康检查
- 重启服务
- 在重启之前要确定连锁故障的根源,确保重启不会扩大问题。逐渐的重启。重启可能导致问题变得更严重,比如冷缓存。
- 丢掉流量,当你想不到其他的方法的时候使用。要确定丢掉流量之后,集群正常在接受整个流量的时候,问题不会重现。
- 进入降级模式
- 消除批处理负载
- 消除错误的流量
写在最后
当系统过载的时候,一些信息需要给出用来改善这个情况。当一个服务超过它能承受的量的时候,最好允许一些用户可见的错误或者低质量的结果而不是尽量处理每一个请求。理解系统它能承受的量,已经超过时候的表现对想要避免连锁故障的服务管理员来说很重要。
如果不注意的话,一些系统变更意味着减少后端的错误或者提高稳定性,但是这些变更可能把系统暴露在了更大的风险面前。当评估变更的时候,要保证处理一个中断不会导致另一个。