SRE Google 运维解密 具体实践一

一、基于时间序列数据进行有效报警
Borgmon是google使用的时间序列监控系统,在开源软件中Prometheus是非常类似的一款工具。这个软件将收集时间序列信息作为监控系统的首要任务,同时发展了一种丰富的时间序列信息操作语言,通过使用该语言将数据转化为图表和报警。

因为总是会有大批量的服务器在上线、下线,建立和维护一个有效的服务注册、发现系统是很有必要的。上线的主机需要能主动让Borgmon发现它的存在,进而纳入全规的监控体系中,下线的主机也需要有机制可以通知到Borgmon及时移除对该主机节点的监控条件。

Borgmon也从其他Borgmon实例上收集信息,一般来说运维团队会在每个集群中运行一个Borgmon实例,可以逐级汇总监控指标。

Google内部产生的每个二进制文件中都默认包含一个HTTP服务,在所有的应用软件中都设置监控埋点,使用/varz这个HTTP接口以文本方式对外提供监控数据的查询服务。

报警信息的处理:由Borgmon产生的报警信息全部发送给一个全局共享的报警管理服务,该系统负责对报警信息做适当的数据处理后,再转发到合适的通知渠道。报警管理服务的配置包括:当有其他报警触发的时候,抑制某些报警;将多个Borgmon发来的报警信息合排重;根据标签信息将收到的报警信息展开或将多个报警信息合并成一个。

严重级别的报警会直接发送给当前on-call的工程师,不紧急的报警则会被发送给工单系统。

监控系统的分片机制:受集群服务规模和地理分布的影响,使用一个Borgmon实例做监控数据收集会遇到性能问题,也存在单点故障。因此,google设计了一种流式传输协议,用于在Borgmon之间传输时间序列数据,然后在全球范围内部署了多个全局的Borgmon实例,在每个数据中心中运行一个本地Borgmon实例,形成了监控数据的收集层、汇总层的架构,上游Borgmon可以过滤从下游Borgmon收集来的信息,在全局Borgmon中不需要保留所有任务实例层面的time-series信息。

二、on-call轮值
SRE团队对on-call工作的质量和数量有明确的要求,管理者需要保证on-call轮值的工作压力在这两个维度上保持在一个可持续的水平上。

对于on-call轮值工作有一些最重要的资源是:清晰的问题升级路线;清晰定义的应急事件处理步骤;无指责,对事不对人的文化氛围。

避免运维压力过大:SRE最多只花50%的时间在常规运维工作上,当超出这一数值时需要及时采取措施,如补充人员、优化监控效果,针对特别不稳定的应用项目要求研发团队参与该应用的轮值或完全将该应用的运维交由研发团队负责。

三、有效的故障排查手段
在紧急情况中,正确的做法是:尽最大可能让系统恢复服务。这可能是切换到备用系统、抛弃部分流量或关闭某些功能以降低负载。
缓解问题是第一要务。

四、紧急事件响应
以google实际遇到过的3种不同的系统失败情况为例,一个由主动测试导致、一个由配置文件改动触发、一个由自动化程序触发。在处理这些事情时,一个共同特点是:响应者并没有惊慌失措。他们在必要的时候引入了其他人的帮助。他们都研究和学习过以前的事故记录。事故过后,他们都将系统改善为能更好地处理同类故障。

每次新的失败模式发生时,应急处理者都将这些模式记录了下来。依靠这些事后报告,他们帮助了其他的团队学习如何更好地进行故障排除,以及加固他们的系统以避免类似事故发生。同时这些处理者也主动测试了他们的系统,这些测试保障了相关修改确实修复了根源问题,同时在事故发生之前提前发现了其他的系统弱点。

随着我们的系统不断发展,每次事故和测试都让系统和流程不断进步。
这是一种可以通用的对待紧急事故的处理模式。

五、紧急事故管理
在一个无流程管理的事故处理过程中,容易犯这样的错误:
  1. 过于关注技术问题,而忽略了把时间与精力用在思考如何能够通过其他手段缓解当前服务的问题;
  2. 沟通不畅,处理故障的人员没有与其他人进行有效沟通,包括客户、自己的其它同事、部门领导,很多其它可以帮忙调试和处理问题的工程师却没有被充分利用起来;
  3. 不请自来,一些热心的同事也看到或发现了正在发生的问题,但没有通知任何人就出于善意的对系统进行了修改,这样的变更往往会把事件搞得更糟;

紧急事故的流程管理要素
  • 嵌套式职责分离,在事故处理中让每个人清楚自己的职责是非常重要的,以下是系统中可以分配给某个人的角色:
    • 事故总控,事故总控负责人掌握这次事故的概要信息,他们负责组建事故处理团队、分配任务、协调事务;
    • 事务处理团队,这个团队负责具体执行合适的事务来解决问题,他们也是在一次事故中唯一能对系统做修改的团队;
    • 发言人,向事故处理团队和所有关心的人发送周期性的通知,维护事故文档以保持其正确性和信息的及时性;
    • 规划负责人,负责为事务处理团队提供支持,处理保持团队持续投入修复故障所需要支持工作,如订餐、记录处理过程中对系统进行的特殊操作、安排职责交接记录等;
  • 控制中心,在必要的情况下可以设立一个作战室,将处理问题的全部成员挪到该地办公,当然队员留在自己的工位处理问题并通过IRC进行沟通也是很好的办法,同时IRC的讨论记录也能成为事后总结分析提供支持;
  • 实时事故状态文档,事故总控负责人最重要的职责就是要维护一个实时事故文档,这个文档可以以wiki的形式存在,但最好是能被多人同时编辑,可以设计一个相对固定的文档模板,然后按时间线补充、记录事件发展最新信息,新的信息应该发布在文档顶部(时间线部分),这个文档也是事后总结的一份重要材料;
  • 明确公开的职责交接,在超出或严重超出工作时间后,事故总负责人的职责能够明确、公开地进行交接是很重要的,可以通过电话或视频会议的形式进行公布,并通知到事故处理团队每个人;

事故流程管理最佳实践
  • 划分优先级,控制影响范围,恢复服务,同时为根源调查保存现场;
  • 事前准备,事先和所有事故处理参与者一起准备一套流程;
  • 信任,充分信任每个事故处理参与者,分配职责后让他们自主行动;
  • 反思,在事故处理过程中如果发现自己开始惊慌失措或感到压力过大,应该寻求更多帮助;
  • 考虑替代方案,周期性的重新审视目前的情况,评估目前的工作是该继续还是需要执行其它更重要的事情;
  • 练习
  • 换位思考,下一次的事故处理中你可以换一个职责试试;

六、事后总结:从失败中学习
事后总结是SRE的一个必要工具
一篇事后总结是一次事故的书面记录,包括该事故的影响,为缓解该事故采取的措施,事故的根本原因,以及防止未来问题重现的后续任务。
“对事不对人”,一篇事后总结必须重点关注如何定位造成这次事件的根本问题,而不是指责某个人或团队的错误或不恰当的举动。

什么情况下就应该进行事后总结了?
  • 用户可见的宕机时间或者服务质量降级程序达到一定标准;
  • 任何类型的数据丢失;
  • on-call工程师需要人工介入的事故(包括回滚、切换用户流量等);
  • 问题解决耗时超过一定限制;
  • 监控问题(即问题是由人工发现的,而非报警系统);

书写事后总结的过程需要注意做到以下几点:
协作和知识共享
  • 实时协作,使得写作过程可以很快地收集数据和想法,这在事后总结早期很有帮助;
  • 开放的评论系统,使大家都可以参与进来提供解决方案,以及提高对事故处理细节的覆盖和程度;
  • 邮件通知,可以在编辑文档时给别人发出通知消息,或引入其他人来共同填写文档;
对事后总结的评审,找一些资深工程师来评估文档的完整程度,所有的事后总结都需要评审,未经评审的事后总结还不如不写。
  • 关键的灾难数据是否已经被收集并保存起来了?
  • 本次事故的影响评估是否完整?
  • 造成事故的根源问题是否足够深入?
  • 文档记录中的任务优先级是否合理,能否及时解决了根源问题?
  • 这次事故处理的过程是否共享给了所有相关部门?

建立事后总结文化
  • 本月最佳事后总结,通过每月一次的新闻邮件,与整个组织共享一篇有趣并且质量很高的事后总结;
  • Google+ 事后总结小组,本小组共享和讨论内部与外部的事后总结,同时包括一些最佳实践和事后总结的评论文章;
  • 事后总结阅读俱乐部,大家共同开发式地讨论一篇有趣或有影响力的事后总结,包括事件的发生过程,学习到的经验教训,以及善后处理;
  • 命运之轮,即故障应急演练,在这种活动中将之前的某篇事后总结的场景再现,一批工程师负责扮演这篇文档中提到的角色,如果这个事件当时的总控负责人也参与其中,则演习的效果会更好;
注:一个提供管理事后总结的开源项目   https://github.com/etsy/morgue

下面是google使用的一个事后总结模板的示范:



七、跟踪故障
系统性地从过去发生过的问题中学习是服务运维的必要手段。虽然事后总结为单个故障提供了详细的信息,但是它们只是整个解决方案中的一部分。只有影响非常大的故障才会进行事后总结。所以小型但是频繁发生的故障经常不会被包含在内。
我们还可以从这些问题中获得如下信息:
  • 每次on-call轮值发生的报警次数是多少;
  • 上个季度中的可操作的报警和不可执行的报警的比例是多少;
  • 本团队管理的服务中,哪个消耗的人工最多;

提高可靠性的唯一可靠的方法论是建立一个基线(baseline),同时不断跟踪改变。
Google使用Outalator——一个故障跟踪工具来做这件事。Outalator被动收集监控系统发出的所有报警信息,同时提供聚合、标记、分组和数据分析功能。

八、测试可靠性
SRE的一项关键职责就是要定量地分析我们维护的某项服务的质量。SRE采用将经典的软件测试技术应用在分布式系统上来做到这一点。
软件测试基本分为2大类:传统测试和生产测试。
传统测试主要应用在软件开发过程中,离线评估软件的正确性,包括以下部分。
  • 单元测试
  • 集成测试
  • 系统测试,是一个在未部署的系统上运行的大型测试,包括以下几种类型:
    • 冒烟测试,名称来源于硬件产品测试,主要指针对软件的那些重要功能行为进行的检测;
    • 性能测试
    • 回归测试,对曾经发现和解决过的bug列表进行功能验证性测试;
生产测试是需要和一个已经部署在生产环境中的业务系统直接交互,包括以下部分。
  • 配置测试,在上线前通过一个配置测试系统对软件中使用的配置文件进行测试验证,以符合生产环境使用要求;
  • 压力测试
  • 金丝雀测试,这种测试将代码置于比较难以预测的生产环境的实时用户流量之下,看代码是否产生问题;


九、SRE部门中的软件工程实践
为什么软件工程项目对SRE很重要?
  • Google生产环境的复杂程度导致了很多内部工具必须由Google自己开发,因为很少有第3方工具可以适配这种复杂情况;
  • SRE是自己工具的直接使用者,能深刻得理解要开发的工具的重点在哪里;
  • 与这些工具的直接用户——其它SRE——的密切联系使得获取直接的和高质量的用户反馈变得很容易;
每个单独的SRE,以及整个SRE组织都会从这些SRE驱动的软件工程项目中获益
  • 完整的软件工程项目在SRE组织内部提供了一个职业发展的方向,也提供了一些磨炼编程技能的良好机会;
  • 长期的软件工程项目开发可以在on-call轮值之外提供平衡工作的选择,为同时想保持软件工程技能与系统工程技能的工程师提供了一个满意的工作机会;
  • 这些软件工程项目可以为SRE组织吸引和留住拥有很多不同技能的工程师;
为什么SRE驱动的软件开发项目对整 个公司也是很重要的?
  • SRE团队有独特的一手生产环境运维经验,可以用创新的手段处理老问题;
  • SRE团队经常开发用来简化低效流程或者自动化的工具,意味着SRE团队不需要和部署服务的规模同比线性增长;
  • SRE花在软件开发的精力会对公司、SRE团队以及SRE个人都产生回报。

Auxon案例分析——SRE内部开发的一个自动化容量规划工具
传统的容量规划方法
  • 收集对未来项目需求的预测
  • 制定资源的采购、构建和分配计划
  • 评审并且批准这个计划
  • 部署和配置对应的资源
传统的容量规划方法所存在的问题:不可靠性、耗时巨大且不够精确。

软件工程解决方案:基于意图的容量规划系统
意图,是服务负责人对如何运维该服务的一个理性表达。从具体的容量需求到背后理性原因的表达,通常需要跳过几个抽象级别:
  1. 我需要50个cpu的资源,必须在集群X,Y,Z中,为服务FOO使用;
  2. 我需要50个cpu的资源,在地理区域YYYY中的任意三个集群中,为服务FOO使用;
  3. 我想要满足FOO在每个地理区域的需求增长,同时保障N+2的冗余度;
  4. 我们想要将FOO以99.9999%的可靠度运行;
理想情况下,所有上述级别的意图都应该提供,服务提供的意图越多,它们得到的好处越大。

完整表达产品意图的先导条件:
  • 依赖关系,很多服务在运行时都要依赖于很多其它基础设施和服务,这些依赖服务的可用信息会极大影响某个服务的位置选择;
  • 性能指标,某个服务的需求最后可以分解为对一个或更多其它服务的容量需求;
  • 优先级,在资源不够的情况下,面对同时到达的多个服务的资源请求,哪些资源请求可以被牺牲掉;

Google Auxon软件设计思想
Auxon为收集基于意图的服务资源要求和依赖信息提供了工具。这些用户的意图通过一系列对服务的要求来表达。Auxon将这些信息通过用户配置信息或编程API收集起来,同时将这些人工指定的产品意图转化成机器可以使用的限定条件。这些需求可以指定优先级,这样在资源不够的情况下可以更好地分配资源。这些资源需求最后会形成一个巨大的线性规划表达式,Auxon通过对该表达式求解,并且利用一系列组合的最优压缩算法形成一个资源分配计划。
Auxon的主要组件有
  • 数据信息(Performance data),描述了某个服务的规模化能力;
  • 每个服务的需求预测数据(Per-Service Demand Forcecast Data);
  • 资源供给信息;
  • 资源价格,提供了资源的成本信息;
  • 意图配置信息,是向Auxon输入基于意图的信息的关键渠道,这里定义了每个服务,以及服务之间的依赖关系,使用限制、优先级要求。这个配置信息最终会成为其他组件的黏合剂。
  • Auxon配置语言引擎,完成将意图配置信息向机器格式的转换工作;
  • Auxon求解器,是整个工具的大脑;
  • 资源分配计划,是Auxon求解器的最后产物;

SRE内部的主要软件项目都是从一个小项目开始,随着采用率的提高而变得正式的。虽然专注的、没有干扰的项目开发是任何软件开发项目都必需的,但因为SRE不停地在多个任务间切换的时候,写代码几乎是不可能的。因此能够不受干扰地从事一部分软件工程工作,是SRE团队必须要时刻去保障的。

十、前端服务器的负载均衡
Google 是使用DNS负载均衡在数据中心之间调节用户流量的。DNS是最简单、最有效的负载均衡制度。
Google将权威DNS服务器和自己的全局负载均衡系统GSLB整合起来,并由内部的负载均衡系统负责跟踪服务的流量水平、可用容量和各种基础设备的状态。

DNS负载均衡功能存在的几个缺陷:
  1. RFC 1035将DNS回复限制为512字节,这也就限制了DNS回复能返回的地址数量,而对于一个大型互联网企业,这个上限是远小于企业使用的服务器数量的。为此企业需要自建内部负载均衡设备,而只对外部公共DNS服务暴露VIP地址。
  2. DNS中间人机制所还来的挑战,最终用户很少直接跟权限域名服务器进行联系,而是会由中间位置上的递归解析器代理请求。这样的设计使得在用户流量管理上有三个非常重要的影响:
    1. 递归解析IP地址,权威服务器收到的不是用户地址而是递归解析器的IP地址,以引作为用户流量调度的依据无疑会产生巨大的误差,一个可能的解决方案是使用EDNS0扩展协议(尚属草案,不是正式规范),它要求递归解析器发送的请求中包括了最终用户的地址。
    2. 不确定的回复路径;
    3. 额外的缓存问题;

十一、数据中心内部的负载均衡系统
在Google整个技术栈的多个部分上都应用了多种类型的负载均衡算法。大部分的外部HTTP请求都会由Google前端服务器(GFE)处理——Google的反向代理系统。GFE使用一个基于URL模式匹配的配置文件将某个特定请求转发给其他团队控制的后端服务器。为了处理这些请求,这些后端服务器都需要采用同样的负载均衡算法联系其他基础设施服务,或者他们所依赖的其他服务。有时候,整个依赖处理栈会非常深,单个HTTP请求可能会触发一长串的后端依赖请求,也可能在多个节点处拓展为多个并发请求。
在后端服务节点多到有几百个或几千个以后,负载均衡策略中的简单轮询算法、最闲轮询算法都都很多缺陷,造成实际上的最闲、最忙节点的负载相差达到2倍。
从实际使用经验上看,加权轮询是效果最好的,每个客户端为子集中每个后端任务保持一个“能力”值。最高和最低负载任务的差距得到有效的控制。

十二、应对过载
运维一个可靠系统的一个根本要求就是,能够优雅地处理过载情况。某个设计良好的后端程序,基于可靠的负载均衡策略的支持,应该仅仅接受它能处理的请求,而优雅地拒绝其他请求。
QPS陷阱
Google在多年的经验积累中得出,按QPS来规则服务容量,或者是按某种静态属性一般是错误的选择。这种不断变动的指标,使得设计和实现良好的负载均衡策略变得非常困难。
更好的解决方案是直接以可以资源来衡量可用容量。而且在绝大部分情况下,我们发现简单地使用CPU数量作为资源配给的主要信号就可以工作得很好。
给每个用户设置使用配额
过载应对策略设计的一个主要部分就是决定如何处理“全局过载”的情况。当发生全局过载时,使服务只针对某些“异常”客户返回错误是非常关键的,这样其它用户不会受影响。
这就需要运维该服务的团队和使用该服务的客户团队协商出一个合理的约定,同时使用这个约定来配置用户配额并且配置相应的资源。
例如,Google的某一个后端服务在全世界范围内分配了100000个cpu(分布在多个数据中心),它们的每用户限额可能与下面的类似:
  • 邮件服务允许使用4000cpu(每秒的用量);
  • 日历服务允许使用4000cpu;
  • 安卓服务允许使用3000cpu;
  • Google+服务允许使用2000cpu;
  • 其他用户允许使用500cpu;
上述的数字之和总是会超出实际的cpu容量的,这是因为所有用户都同时将他们的资源配额用满是一种非常罕见的情况。
SRE通过自研的管理系统从所有的后端任务中实时获取用量信息,并且使用这些数据将配额调整信息推送给每个后端任务。

客户端侧的节流机制
单纯依靠server端的使用配额控制并不足以解决全部的问题,还需要在客户端侧同时增加节流机制。即当某个客户端检测到最近的请求错误中的一大部分都是由于“配额不足”错误导致时,该客户端需要开始自行限制请求速度,限制它自己生成的请求数量。如果超出这个请求数量限制,则将额外的那些请求直接在本地回复失败,而不会真正发到网络层去。

Google 使用一种自适应节流的技术来实现客户端节流。具体地说,就是每个客户端记录过去2分钟内的“请求数量”和“请求接受数量”。常规情况下这两个数值是相等的。随着发生了后端服务过载,部分请求开始得不到响应,请求接接受数量开始比请求数量小了。客户端可以继续发送请求直到requests = K*accepts,一旦超过这个限制,客户端就开始自行节流。出于各种情况的综合考虑,一般建议K值设为2 。

重要性
这是一个在全局配额和限制机制中比较有用的信息。某个发往后端的请求都会标记以下4类中的一种,以说明请求的重要性。
  • 最重要CRITIAL_PLUS
  • 重要CRITICAL
  • 可丢弃的SHEDDABLE
  • 可丢弃的SHEDDABLE_PLUS

SRE将“重要性”属性当成RPC系统的一级属性,下了很多工夫将其集成进很多控制手段中,以便这些系统在处理过载情况时可以使用这些信息。
  • 当某个客户全局配额不够时,后端任务将会按请求优先级顺序分级拒绝请求;
  • 当某个任务开始进入过载时,低优先级的请求会先被拒绝;
  • 自适应节流系统也会根据每个优先级分别计数;

资源利用率信号
Google的任务过载保护是基于资源利用率实现的。而且在多数情况下,资源利用率仅仅是指目前CPU的消耗程序。当然某些情况下也会考虑内存的使用率。随着资源利用率的上升,我们开始根据请求的重要性来拒绝一些请求。
在Google的系统中可以接入后端自己定义的任意资源利用率信号,以CPU利用率最多,少部分使用内存利用率,同时该系统还可以配置为同时使用多个信号,并且在超过综合目标利用率阀值的时候开始拒绝请求。

处理过载错误
客户端在接收到过载相关的错误信息时应该如何应对。在过载错误中,主要有2种情况:
  1. 数据中心中的大量后端任务都处于过载状态,如果跨数据中心负载均衡系统在正常运行,那这种情况就不会出现;
  2. 数据中心中的一小部分后端任务处理于过载状态,这种情况一般是由负载均衡系统的不完美造成的,在这种情况下,很有可能该数据中心仍然有其他容量可以处理该请求。
如果大部分任务都处于过载状态,请求应该不再重试。在更常见的、小部分任务过载的情况下,我们更倾向于立即重试该请求。当某个后端服务发生轻微过载,也不需要区别对待重试请求和新请求,快速失败仍然会是最好的选择。这些请求可以立刻在另外一个可能空余资源的任务上重试。

怎样决定何时重试
当某个客户端接收到“任务过载”错误时,需要决定是否要重试这个请求。Google针对大量后端任务过载的情况有几个方法来避免进行重试。
  1. 增加了“每次请求重试次数限制”,限制重试3次,当某个请求已经失败了3次时,系统会将该错误返回给调用者;
  2. 实现了一个”每客户端的重试限制“,每个客户端都自行跟踪重试与请求的比例,一个请求在这个比例低于10%时才会重试,这种设计给后端服务能带来巨大的好处,缺少这种限制的客户端,在后端服务大面积过载时可能要发送出3倍量的请求重试,而应用了这个限制后则不会超过1.1倍量。
  3. 客户端在请求元数据中加入一个重试计数,以0开始,每次重试加1,到2为止,请求重试限制会导致不再重试该请求。客户端会记录和统计近期重试计数数据,当某个后端需要拒绝部分请求时,它可以从客户端侧获取上述统计数据,据此判断其它后端任务是否也处于过载的可能性。如果统计数据显示大部分请求都有重试,那么该后端会直接返回”过载;无须重试“的错误,而不是标准的”任务过载“错误信息。
Google的大型服务通常是由一个层次很深的系统栈组成的,这些系统可能互相依赖。在这种架构下,请求只应该在被拒绝的层面上面的那一层进行重试。如果在多层都要进行重试,会造成批量重试爆炸。

连接造成的负载
维护一个大型连接池的CPU和内存成本,或者是连接快速变动的成本,是连接造成的负载中最后一个值得注意的因素。这样的问题在小型系统中可以忽略不计,但在大型RPC系统中很快就会造成问题。
  1. 健康检查带来的负载,在google的系统中当某个连接空闲一段可配置的时间后,客户端会直接放弃TCP连接,转为UDP健康检查,以节省连接资源的消耗;
  2. 针对很多请求率很低的客户端又遇到的问题是:健康检查仍然是需要比实际处理请求更多的资源,这时就需要仔细调节客户端连接参数或动态创建、销毁连接;
  3. 突发性的新连接请求也是一类由连接造成的负载问题,例如那些超大规模的批处理任务会在短时间内建立大量的连接,协商和维护这些超大数量的连接可以造成整个后端的过载,Google采取以下办法消除这类问题:
    1. 将负载传递给跨数据中心负载均衡系统,即部分流量会被转移至其它数据中心;
    2. 强制要求批处理任务使用某些特定的批处理代理后端任务,这些代理仅转发批处理任务请求,同时将回复转发给客户端,在这种情况下,当有大型的批处理任务执行时,只有批处理代理任务会受影响,同时也更便于针对批处理任务的后端子集做更多的优化;

十三、处理连锁故障
如果请求没有成功,请以指数型延迟重试——Dan Sandler, Google软件工程师
连锁故障是由于正反馈循环导致的不断扩大规模的故障。即可能由于整个系统的一小部分出现故障而引发,进而导致系统其他部分也出现故障。

一个设计良好的系统应该考虑到几个典型的连锁故障产生场景,进而在设计上避免它们发生。
  1. 服务器过载,当一个服务器过载时,它收到了明显超出处理能力范围的请求数量,受此资源不足等原因影响会进一步引发崩溃、超时或者其它异常情况,结果一个过载状况下的服务器或集群所具备的处理能力会远低于它在正常负载范围时的水平;
  2. 资源耗尽,某一种资源的耗尽可以导致高延迟、高错误率或者低质量回复的发生。不同类型的资源耗尽会对软件服务器产生不同的影响。
    • CPU,在CPU资源不足时会造成一系列的副作用:
      • 正在处理的(in-flight)请求数量上升
      • 队列过长
      • 线程卡住
      • CPU死锁或者请求卡住
      • RPC超时
      • CPU缓存效率下降
    • 内存,内存耗尽可能导致如下情况的发生。
      • 任务崩溃
      • Java 垃圾回收速率加快,从而导致CPU使用率上升
      • 缓存命中率下降
    • 线程,线程不足可能会导致错误或者导致健康检查失败,如果服务器为此增加更多线程,这些线程可能会占用更多的内存。在极端情况下,线程不足可能会导致进程ID数不足。
    • 文件描述符,文件描述符不足会导致无法建立网络连接,进而导致健康检查失败。
    • 资源之间的相互依赖,很多资源的耗尽都会导致其他资源出现问题,会使得问题定位变得困难。
  3. 服务不可用,资源耗尽可能导致软件服务器崩溃,一旦几个软件服务器由于过载而崩溃,其他软件服务器的负载可能会上升,从而使它们也崩溃。这种问题经常如滚雪球一样越来越严重,不多久全部服务器就会进入崩溃循环,这种场景经常很难恢复。因为一旦某个软件服务器恢复正常,它就会收到大量请求的轰炸,几乎立即再次崩溃。

防止软件服务器过载
以下是几种避免过载的策略,按优先级排序:
  1. 使用负载压力测试得出服务器的极限,同时测试过载情况下的失败模式;
  2. 提供降级结果;
  3. 在过载情况下主动拒绝请求;
  4. 上层系统应该主动拒绝请求:
    • 在反射代理层,通过针对请求的某种特性进行数量限制(如IP地址);
    • 在负载均衡层,在服务进入全局过载时主动丢弃请求;
    • 在每个任务自身,避免负载层的随机扰动导致软件服务器过载;
  5. 进行容量规划,容量规划应该伴随着性能测试进行,以确定可能导致服务失败的负载程度;

队列管理
在队列中的排除请求消耗内存,同时使延时升高。如果队列处于满载状态,那每个请求所等待的大部分时间都消耗在排队过程中。
因此,对一个流量基本稳定的服务来说,队列长度比线程池大小更小些会更好,如50%或更小。
例如,Gmail通常使用无队列软件服务器,在线程满的时候尽早拒绝请求会更好,将负载转移到其他服务器上去。

流量抛弃和优雅降级
  1. 流量抛弃,是指在软件服务器临近过载时,主动抛弃一定量的负载,以避免该软件服务器出现内存超限,健康检查失败,延迟大幅升高,或其它过载造成的现象。
    • 一种简单的流量抛弃做法是根据CPU使用量、内存使用量以及队列长度等进行节流,例如当同时处理的请求超过一定量时,开始直接针对新请求返回HTTP503;
    • 另一种做法包括将标准的先入先出FIFO队列模式改成后入先出LIFO,以及使用可控延迟算法,或者类似的方式更进一步地避免处理那些已经不值得处理的请求;
    • 在基础性的共享服务中还经常采用精确识别客户端的试工来更有选择地丢弃部分任务,或者将请求按优先级排序,按优先级处理等;
  2. 优雅降级,在流量抛弃的基础上进一步减少服务器的工作量,在某些应用中是可以通过降低回复的质量来大幅减少所需的计算量或所需的计算时间的,例如一个搜索类型的应用在过载情况下可以仅仅搜索保存在内存中的数据,而不是全部数据,或者是可以采用一种不是那么精确的算法来进行结果排序。
当我们评估流量抛弃或优雅降级时需要考虑以下几点:
  • 确定具体采用哪个指标作为流量评估和优雅降级的决定性指标,如CPU用量、延迟、队列长度、线程数量、是否该服务可以自动进行降级或者需要人工干预;
  • 当服务进入降级模式时,需要执行什么动作?
  • 流量抛弃或优雅降级应该在服务的哪一层实现?
在实施流量抛弃或优雅降级时还需要考虑:
  • 优雅降级不该经常被触发,但整个降级系统应该简单、易懂,尤其是在不常使用的情况下;
  • 代码中平时不会使用的代码路径是不工作的,在稳定状态下,优雅降级不会经常触发,意味着在这个模式下的运维经验很少,也就提升了这种模式的危险性。这就需要我们主动安排一些压力测试以便更多地触发这个模式,保证还能使用;
  • 监控系统应该在进入这种模式的软件服务器过多时报警 ;
  • 复杂的流量抛弃和优雅降级系统本身就可能造成问题,设计时应该实现一种简单的关闭降级模式,或者是快速调节参数的方式;
重试,不加限制的重试机制是可以摧毁一个系统的,一个发生过载的系统可能会额外收到越来越多的重试请求,最终系统会在请求和重试的压力下崩溃。此时为了挽救我们的后端系统,必须大幅减少甚至彻底停止前端产生的负载,直到重试停止,后端稳定为止。

客户端在发送自动重试时,需要考虑到以下因素:
  • 大部分的后端保护策略都适用于此;
  • 一定要使用随机化的、指数型递增的重试周期;
  • 限制每个请求的重试次数;
  • 考虑使用一个全局重试预算;
  • 从多个视角重新审视该服务,决定是否要在某个级别上进行重试,尤其要避免同时在多个级别上重试导致的放大效应;
  • 使用明确的错误返回代码,同时详细考虑每个错误模式应该如何处理;

请求延迟和截止时间
当某个前端任务发送RPC给后端服务器时,前端需要消耗一定资源等待后端的回复。RPC截止时间定义了前端会等待多长时间,这限制了后端可以消耗的前端资源。
  1. 选择截止时间,设置一个恰当的截止时间是非常明智的,不设置或设置一个非常长的截止时间通常会导致某些短暂的、已经消失的问题继续消耗服务器资源,直到重启;
  2. 超过截止时间,很多连锁故障的一个常见问题是软件服务器正在消耗大量资源处理那些早已经超过客户端截止时间的请求,回复已经被取消的RPC是没有意义的,如果处理请求的过程有多个阶段,那该软件服务器应该在每个阶段开始前检查截止时间,以避免做无用功;
  3. 截止时间传递,与其在发送RPC给后端服务器自拟一个截止时间,不如让软件服务器采用截止时间传递和取消传递的策略:
    • 截止时间传递机制,在整个服务栈的高层设置,为一个请求触发的整个RPC树设置同样的绝对截止时间,每经过一层RPC调用则减法已经消耗的时间,并将剩余时间继续向下传递。当下层的服务收到的请求携带的截止时间要求超出服务处理能力范围时,直接拒绝该请求。
    • 截止时间的取消传递,有一些特例,如某个服务器在处理某个请求时需要执行很耗时的工作任务,此时就不适用请求的截止时间传递要求。
  4. 请求延迟的双峰分布,为请求设置了超长的截止时间时,例如前端服务器发送了1000QPS的请求,处理请求时间是100ms,而设置的请求截止时间高达100s,当有5%的请求因为后端系统某类事件影响而永远不会结束,结果这5%的请求需要一直到超时才能完成,5%的请求会消耗50QPS*100seconds=5000个线程,而95%的请求消耗950*0.1seconds=95个线程。在观察请求延迟的分布时,会看到100ms和100s的两个峰值。
    • 这种问题的检测很困难,只有当发生请求处理延迟问题时,注意观察延迟的分布情况进行判断;
    • 如果无法完成的请求能够尽早返回一个错误而不是等完整个截止时间,我们就可以避免这个问题,如果RPC层支持快速失败,一定要启用;
    • 将截止时间设置得比平均延迟大好几个数量级通常是不好的,在发生问题时会导致线程耗尽;
    • 可以考虑限制某一个客户端只能占用指定比例的线程总数,以便在个别客户端遇到异常时提供一些公平性;

慢启动和冷缓存
进程在刚刚启动之后通常要比稳定状态下处理请求的速度慢一点,有以下几个可能的原因:
  • 必需要初始化过程,在接收到第一个请求后需要跟后端服务器建立连接;
  • 运行时性能优化,尤其是java,JIT编译过程,热点优化,以及类延迟加载机制;
有些服务器会在缓存没有充满之前效率很低,当缓存是空的情况下,100%的请求都会非常耗时,而以下几种情况中可能会引发这种冷缓存情况:
  • 上线一个新的集群
  • 在某个集群维护之后恢复服务时
  • 重启
如果缓存对服务造成很大的影响,可能要采取以下几种策略中的一种或多种:
  • 过量配备该服务,同时特别注意区分延迟类缓存和容量型缓存服务,前者可以在空缓存情况下继续处理预期的请求负载,而后者服务将不能在空缓存下处理请求负载;
  • 使用通用的连琐故障避免手段,拒绝请求或优雅降级,同时应该对服务在大规模重启情况下的表现进行测试;
  • 当为一个集群增加负载时,需要缓慢增加,初期的小流量会加热缓存,确保所有集群都处理一定程度的负载以保证缓存随时是热的状态;

保持调用栈永远向下
如果后端服务器中的任务会彼此通信,后端服务器可能会彼此之间代理请求,这种在层内的交互通信可能会导致问题。
  1. 这种通信方式容易导致分布式死锁;
  2. 如果这种交互通信是由于某种失败因素或过载导致的,比如一个主后端在出现底层错误或延迟上升时会将请求代理给热备后端,这样的实现会给系统带来更多的负载;
  3. 初始化整个系统可能会变得更复杂,同层通信也就是说在通信路径中出现了环;

连琐故障的触发条件
  • 进程崩溃
  • 进程更新
  • 新的发布
  • 自然的流量增长
  • 计划中或计划外的不可用
  • 请求特征的变化
  • 资源限制

连琐故障的测试
应该针对服务进行压力测试,通过对重载下服务行为的观察可以确定该服务在负载很重的情况下是否会进入连琐故障模式。
  1. 测试直到出现故障,还要继续测试
    • 理解服务在高负载情况下的行为模式可能是避免连琐反应最重要的一步;
    • 压测每个组件直到它们崩溃,一个设计良好的组件应该可以拒绝一小部分请求而继续存活;
    • 压测得到的每个组件的临界点是进行容量规划的重要依据;
    • 需要考虑缓存的影响,区分测试逐渐升高的负载与瞬间升高的负载;
    • 在组件经受压测之下发生过载后,还需要继续测试这个组件再恢复到正常水平的行为状态:
      • 如果一个组件在高负载模式下进入了降级模式,它是否能够在无人工干预的情况下退出该模式?
      • 如果高负载情况下几个服务器崩溃,负载需要降低多少才能使系统重新稳定下来?
    • 每个组件都有不同的临界点,所以应该分别测试,因为我们不能知道哪个组件会先崩溃;
    • 如果你的系统有恰当的过载保护,那可以考虑在生产环境中对一小部分容量进行模拟故障测试,以发现在真实流量情况下系统中的哪个组件先出问题。可以考虑以下几种生产环境测试:
      • 快速或者缓慢地降低任务数量,超越之前预期的流量模式;
      • 快速去掉某一个集群的容量;
      • 屏蔽不同的后端(试验超时等因素对系统的影响);
  2. 测试最常用的客户端,理解最大的客户是如何使用服务的
    • 能够在服务中断的情况下排除;
    • 遇到错误时使用随机化的指数型延迟进行重试;
    • 是否会因为外部因素导致流量的突然变化(如因某些因素导致客户端清空了离线缓存);
  3. 测试非关键性后端,以确保他们的不可用不会影响到系统中的其他关键性组件
    • 通常情况下,一个请求同时需要关键性后端和非关键性后端的服务,请求可能会受非关键性后端影响而变慢;
    • 需要测试这些非关键性后端不返回结果时,前端如何表现,前端不应该因此而拒绝大量请求、资源过限或延迟大幅升高;

解决连琐故障的立即步骤
  1. 增加资源;
  2. 停止健康检查导致的任务死亡,例如Borg会周期性检查任务的健康程序,自动重启不健康的任务,如果半数以上的任务都因此被重启、重新初始化还不能工作,此时临时禁用健康检查是很有必要的;
  3. 重启软件服务器
    • Java 服务器处理GC死亡螺旋中时;
    • 某些正在处理中的请求因为没有截止时间设置而在消耗大量资源;
    • 死琐;
    • 最好是试验性地进行这种干预,如果故障根本原因是冷缓存,那简单的重启软件服务器只能使得问题更加严重;
  4. 丢弃流量,以恢复集群服务或保障部分请求的处理能力
  5. 进入降级模式
  6. 消除批处理负载,如搜索索引的更新、数据复制、请求处理过程中的资源统计等,可考虑关闭这些来降低负载;
  7. 消除有害的流量,如果某些类的请求造成了高负载或者是崩溃,可考虑将它们屏蔽掉,或者通过其它手段消除;

小结:当一个系统过载时,总是要有些东西被牺牲掉的。一旦一个服务越过了临界点,服务一些用户可见错误,或者低质量结果,要比尝试继续服务所有请求要好。理解这些临界点所在,以及超过临界点系统的行为模式,是所有想避免连锁故障的运维人员所必需的。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值