文章 62
浏览 15135
自研网关day08-高可用

自研网关day08-高可用

高可用技术方案


首先,我们来看看,什么叫高可用,高可用(High availability,即 HA)的主要目的是为了保障「业务的连续性」,即在用户眼里,业务永远是正常(或者说基本正常)对外提供服务的。高可用主要是针对架构而言,那么要做好高可用,就要首先设计好架构,
第一步我们一般会采用分层的思想将一个庞大的 IT 系统拆分成为应用层,中间件,数据存储层等独立的层,每一层再拆分成为更细粒度的组件,
第二步就是让每个组件对外提供服务,毕竟每个组件都不是孤立存在的,都需要互相协作,对外提供服务才有意义。要保证架构的高可用,就要保证架构中所有组件以及其对外暴露服务都要做高可用设计,任何一个组件或其服务没做高可用,都意味着系统存在风险。
那么这么多组件该怎么做高可用设计呢,其实任何组件要做高可用,都离不开「冗余」和「自动故障转移」,众所周知单点是高可用的大敌,所以组件一般是以集群(至少两台机器)的形式存在的,这样只要某台机器出现问题,集群中的其他机器就可以随时顶替,这就是「冗余」。简单计算一下,假设一台机器的可用性为 90%,则两台机器组成的集群可用性为 1-0.1*0.1 = 99%,所以显然冗余的机器越多,可用性越高。但光有冗余还不够,如果机器出现问题,需要人工切换的话也是费时费力,而且容易出错,所以我们还需要借助第三方工具(即仲裁者)的力量来实现「自动」的故障转移,以达到实现近实时的故障转移的目的,近实时的故障转移才是高可用的主要意义怎样的系统可以称之为高可用呢,业界一般用几个九来衡量系统的可用性,如下

业内通常用多少 9 来衡量网站的可用性,例如 QQ 的可用性是 4 个 9,也就是 QQ 能够保证在一年里,服务在 99.99% 的时间是可用的,只有 0.01% 的时间不可用,大约最多 53 分钟。对于大多数网站,2 个 9 是基本可用;3 个 9 是叫高可用;4 个 9 是拥有自动恢复能力的高可用。实现高可用的主要手段是数据的冗余备份和服务的失效转移,这两种手段具体可以怎么做呢,在网关里如何体现,我们接下来看看

集群部署


保障服务可用是网关的一个重要职责,服务通过网关开放出去,如果不是集群部署,整个网关只有一个节点,这个节点挂了,网关就相当于挂了,这样网关存在的意义其实不大,所以一般网关会跟根据服务器性能进行集群部署。虽然网关可以只在一个地方部署集群,相当于是单数据中心部署,但是企业可以根据服务性质进行地域性的数据中心部署,每个数据中心包含几个网关节点,这样每一个数据中心既可以当作是地区用户的访问中心,也能够当作是数据的灾备中心。这样部署已经能够保障网关的正常可用。

负载均衡

一套完整的网关应该包含一个控制台与多个网关节点,控制台内的配置项对所有的节点生效。通常一台服务器只部署一个网关节点,并且通过 IP 地址注册在控制台中,节点会通过主动/被动更新的方式获取控制台上的最新配置信息。这里说的负载均衡不是架设在网关前的负载设备(nginx 或 f5),而是网关节点本身的负载,网关的每个节点都能够对所有后端进行负载。如下图所示,每个网关节点都能够将请求分发到服务 1、服务 2 和服务 3。也就是说,一个请求从客户端过来,首先被负载设备(nginx)分发到某个节点,再由节点(网关)将请求分发到具体后端。

主备热切换

尽管上面已经加了两层负载,但是,假设我们的某个节点出了问题,或者说某个后端服务出了问题。nginx 有可能会把请求负载到有问题的节点,节点也有可能会把请求负载到有问题的后端,这时候服务最终的结果仍是不可用,如果能及时把有问题的节点和有问题的后端移出负载范围就好了。如何及时知道节点出了问题或者说是后端出了问题?其实也不难,心跳检查实现主备热切换,像是监控检查一样,定期去检查目标对象,对象没有返回结果就是有问题了。
健康检查这里有两种,一种是 nginx 对网关节点的健康检查,另一种是网关节点对后端服务的健康检查。
nginx 如何对节点进行健康检查,网上有很多相关教程。这里主要探讨的是网关节点对服务后端的健康检查,我们可以对后端设定正常返回的结果(根据请求状态码、超时期限、或是其他条件),定期访问后端服务,若发现返回异常,则控制台将该后端从负载列表里移除。发现异常时网关同样会产生告警。移除后网关也会定期访问该后端服务,若发现后端服务已恢复,则恢复对该后端的负载。

节点自动启动:

网关针对异常情况导致停止运行的节点会进行自动重启
控制台每隔 30 秒去访问一遍运行中的节点列表,若发现节点返回异常,则进行重试,若重试过程拿到正常返回,则视为节点正常;若重试 3 次后节点仍返回异常,则视为节点异常,自动重启节点。

熔断

由于某些接口或服务的不可控因素,比如网络连接缓慢,资源被占用或者暂时不可用等,导致对这些服务的调用失败,一般来说可以在一定时间内恢复的,但是如果某些原因导致整个服务的完全不可用。比如由并发请求引起的阻塞,这种对请求的阻塞可能会占用宝贵的系统资源,如内存,线程,数据库连接等等消耗的资源使其他系统不相关的部分受影响甚至拖累整个系统。
在这种情况下,对客户端立即返回错误可能是一种更好的选择,等到发现服务可用的时候再恢复访问。判断服务不可用就切断对服务的访问,这种机制像是电路的保护机制,我们都形象地称其为熔断。熔断跟心跳检测不太一样,心跳检测是主动地去探测接口是否正常,而熔断是使用过程中才会触发的。熔断是指接口在一定时间内访问失败达到一定的次数,就触发熔断。熔断启动后,网关不会对该接口进行转发,而是直接返回预先设定的内容。每隔一段时间网关会检测接口是否恢复正常,等到接口恢复正常,网关才会恢复对该接口的转发。在 EOLINKER AGW(GOKU API Gateway)里熔断是根据接口返回的状态码触发的,异常的状态码我们能设置多个,比如说常见的 404 或 500。
所以熔断这个机制可以分为三个部分:

  • 第一个部分是熔断条件:它包含 monitorPeriod:监控期(秒),监控服务的单位时间,matchStatusCodes:触发熔断的异常状态码,一般是 404、500 等,minimumRequests:触发熔断的请求阈值,超过该阈值才会触发熔断机制,因为单位时间内请求次数过少不一定有必要触发熔断,failurePercent:监控期内,总请求次数的错误百分比,一般是 50% 几个参数
  • 第二部分是:开启熔断:breakPeriod:熔断期(秒),此期间内网关不转发请求,直接由网关返回下面预设的内容,statusCode:熔断启动,网关返回的状态码,header:熔断启动,网关返回的头部信息,body:熔断启动,网关返回的 body 信息
  • 第三个部分是:关闭熔断,恢复服务: successCounts:熔断期后,判断请求是否恢复正常的条件,若连续请求成功次数达标,则恢复转发,服务自动转入监控期;否则,继续进入熔断期。如此反复。

服务降级:

接下来是服务降级有点像熔断的其中一部分,但是使用上没有熔断那么苛刻,我们可以根据服务的返回来判断是否需要进行服务降级。例如根据 HTTP 状态码,只要接口返回异常状态码就可以进行服务降级,接口返回正常的状态码,网关就会正常转发。服务降级时,由网关返回服务降级预先设定的内容。

服务降级也能分为三个部分:

  • 降级条件:
    matchStatusCodes:异常状态码:一般是 404、500 等
  • 启动服务降级,返回预设内容:
    statusCode:服务降级时返回的状态码
  • header:服务降级时返回的头部信息
  • body:服务降级时返回的 body 信息
    关闭服务降级,恢复服务:
    接口返回正常状态码即可恢复服务

接口重试

虽然有很多机制保障接口的可访问,但是一个请求报错的原因有很多,偶然一次报错不一定是服务不可用,最简单的,第一次不行,应该再访问一次或几次,以确定结果。

请求重试可以说是网关对接口转发的基本要求,每个接口都应该可以设置重试次数。当请求失败后,网关应立即再次请求,直到拿到正常返回,或是达到重试阈值,再将结果返回给客户端。

隔离

我们要保障系统的高可用还可以运用隔离,隔离是指将系统或资源分割开,系统隔离是为了在系统发生故障时,能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他服务还是可用的。资源隔离通过隔离来减少资源竞争,保障服务间的相互不影响和可用性
老师遇到比较多的隔离手段有线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、快慢隔离、动静隔离、爬虫隔离等。出现系统问题时,可以考虑负载均衡路由、自动/手动切换分组或者降级等手段来保障可用性。关于具体实施方法,我们这里就不进行详细讲解,我们来聊一下线程隔离,线程隔离主要是通过线程池实现的,在实际使用中,我们会把请求进行分类,然后交给不同的线程池处理。当一种业务请求处理发生异常时,不会将故障扩散到其他线程池,从而保障其他服务可用,


我们就是根据服务等级划分成核心业务和非核心业务两个线程池,具体实施方法,大家可以看图,其他的隔离技术,我们就不详细介绍在企业中的运用了,大家可以根据他们的优缺点
,用我教大家分析的方法,确认使用什么隔离技术,当然也有开源的框架可以供大家使用,就是 Hystric,它提供线程和信号量两种隔离技术,还有就是基于 Servlet3 实现请求隔离,这个特别适用于页面异步加载很多服务的场景,比如商品详情页,

压测与预案

  • 第一步:定期对现有系统进行梳理,发现系统瓶颈和问题,并且正对性进行优化

我们高可用系统,还要考虑防范未然,在大促来临之前,研发人员需要对现有系统进行梳理,发现系统瓶颈和问题,然后进行系统调优来提升系统的健壮性和处理能力。一般通过系统压测来发现系统瓶颈和问题,然后进行系统优化和容灾(如系统参数调优、单机房容灾、多机房容灾等)。
压测一般指性能压力测,用来评估系统的稳定性和性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容或缩容。压测之前要有压测方案(如压测接口、并发量、压测策略(突发、逐步加压、并发量)、压测指标(机器负载、 QPS / TPS 、响应时间)),之后要产出压测报告(压测方案、机器负载、 QPS / TPS 、响应时间(平均、最小、最大)、成功率、相关参数( JVM 参数压缩参数)等),最后根据压测报告分析的结果进行系统优化和容灾。

即使已经把系统优化和容灾做得非常好了,但也存在一些不稳定因素,如网络、依赖服务的 SLA 不稳定等,这就需要我们制定应急预案,在出现这些因素后进行路由切换或降级处理。在大促之前需要进行预案演习,确保预案的有效性。
在系统压测之后会发现一些系统瓶颈,在系统伊化之后会提升系统行重于降低编应时间,容灾之后的系统可用性得以保障,但还是会存在一些风险,如网络世型、菜台机器负载过高、某个服务变慢、数据库 Lond 值过高等,为了防止因为愿些网题面出理系统雪崩,需要针对这些情况制定应急预室,从而在出现突发情况时,有相应的措施*解决掉这些问题。
应急预案可按照如下几步进行:首先进行系统分级,然后进行全链路分析、配置
控报警,最后制定应急预案。
系统分级可以按照交易核心系统和交易支撑系统进行划分。交易核心系统,如购物车,如果挂了,将影响用户无法购物,因此需要投入更多资源保障系统质量,将系统优化到极致,降低事故率。而交易支撑系统是外围系统,如商品后台,即使挂了也不影响前台用户购物,这些系统允许暂时不可用。实际系统分级要根据公司特色进行,目的是对不同级别的系统实施不同的质量保障,核心系统要投入更多资源保障系统高可用,外围系统要投入较少资源允许系统暂时不可用。
系统分级后,接下来要对交易核心系统进行全链路分析,从用户入口到后端存储,梳理出各个关键路径,对相关路径进行评估并制定预案。即当出现问题时,该路径可以执行什么操作来保证用户可下单、可购物,并且也要防止问题的级联效应和雪崩效应。

  • 第二步:制定应急预案应对突发情况
    即使已经把系统优化和容灾做得非常好了,但也存在一些不稳定因素,如网络、依赖服务的 SLA 不稳定等,这就需要我们制定应急预案,在出现这些因素后进行路由切换或降级处理。在大促之前需要进行预案演习,确保预案的有效性。在系统压测之后会发现一些系统瓶颈,在系统伊化之后会提升系统行重于降低编应时间,容灾之后的系统可用性得以保障,但还是会存在一些风险,如网络世型、菜台机器负载过高、某个服务变慢、数据库 Lond 值过高等,为了防止因为愿些网题面出理系统雪崩,需要针对这些情况制定应急预室,从而在出现突发情况时,有相应的措施*解决掉这些问题。
    应急预案可按照如下几步进行:首先进行系统分级,然后进行全链路分析、配置监控报警,最后制定应急预案。系统分级可以按照交易核心系统和交易支撑系统进行划分。交易核心系统,如购物车,如果挂了,将影响用户无法购物,因此需要投入更多资源保障系统质量,将系统优化到极致,降低事故率。而交易支撑系统是外围系统,如商品后台,即使挂了也不影响前台用户购物,这些系统允许暂时不可用。实际系统分级要根据公司特色进行,目的是对不同级别的系统实施不同的质量保障,核心系统要投入更多资源保障系统高可用,外围系统要投入较少资源允许系统暂时不可用。
    系统分级后,接下来要对交易核心系统进行全链路分析,从用户入口到后端存储,梳理出各个关键路径,对相关路径进行评估并制定预案。即当出现问题时,该路径可以执行什么操作来保证用户可下单、可购物,并且也要防止问题的级联效应和雪崩效应。
    制定好预案后,应对预案进行演习,来验证预案的正确性,在制定预案时也要设定故障的恢复时间。有一些故障如数据库挂掉是不可降级处理的,对于这种不可降级的关键链路更应进行充分演习。演习一般在零点之后,这个时间点后用户量相对来说较少,即使出了问题影响较小。

多机房灾备以及双活数据中心

  • 灾备: 是指容灾和备份
  • 解决方案-“两地三中心”、同城灾备、异地灾备
  • 双活数据中心
    以上的方案只是面临可预见的情况,但是有一种我们不可预见的情况,我们也需要重视,就是灾备,容灾是为了在遭遇灾害,,比如火灾。地址等灾害时能保证信息系统能正常运行,帮助企业实现业务 7*24 小时连续性的目标,备份是为了应对灾难来临时造成的数据丢失问题。容灾备份产品的最终目标是帮助企业应对人为误操作、软件错误、病毒入侵等“软”性灾害以及硬件故障、自然灾害等“硬”性灾害。目前使用比较多的技术方案就是两地三中心、同城灾备、异地灾备,我们来聊一下我们曾经使用的,“两地三中心”,“两地三中心”的意思是生产中心、同城容灾中心、异地容灾中心,
    从意义上来看就是,兼具同城容灾和异地容灾,结合起来就叫两地三中心。
    我曾经服务的公司已经构建了北京主中心、上海一期和二期数据中心,形成两地三中心的灾备架构模式,其中一个上海中心只是专门作为 备份的数据中心,为全国系统带来灾备服务。与此同时,上海外高桥数据中心作为灾备中心自然可以与上海一期数据中心形成同城灾备关系,与北京主中心形成异地灾备关系,从而可以防范公司在异地、同城情况下的灾难情况。两地三中心的技术选择度量标准:

在构建容灾系统时,首先考虑的是结合实际情况选择合理的数据复制技术。 在选择合理的数据复制技术时主要考虑以下因素:

**1、**灾难承受程度 :明确我们计算机系统需要承受的灾难类型,系统故障、通信故障、长时间断电、火灾及地震等各种意外情况所采取的备份、保护方案不尽相同。

**2、**业务影响程度 :必须明确当计算机系统发生意外无法工作时,导致业务停顿所造成的损失程度,也就是定义用户对于计算机系统发生故障的最大容忍时间。这是设计备份方案的重要技术指标。

所谓 “ 双活 ” 或 “ 多 活 ” 数据中心,区别于 传统 数据中心 和 灾备中心的模式,前者 多个 或两个数据中心都处于运行当中, 运行相同的应用,具备同样的数据,能够提供跨中心业务负载均衡运行能力,实现持续的应用可用性和灾难备份能力, 所以称为 “双活 ” 和 “ 多 活 ” ;后者是 生产 数据中心投入运行, 灾备 数据中心处在不工作状态,只有当灾难发生时,生产数据中心瘫痪,灾备中心才启动。
“ 双活 ” 数据中心最大的特点是 : 一、充分利用资源,避免了一个数据中心常年处于闲置状态而造成浪费 , 通过资源整合, “ 双活 ” 数据中心的服务能力是 翻 倍的 ;
二 、 “ 双活 ” 数据中心如果断了一个数据中心, 其 业务可以迅速切换到另外一个 正在 运行的数据中心, 切换 过程对用户来说是不可感知的。
在 “ 双活 ” 的模式中,两地数据中心同时接纳交易,技术难度很大,需要更改众多底层程序 , 因而在现实中,国内还没有 真正 “ 双活 ” 数据中心 的成功 应用 案例。

异常处理机制

为什么需要异常处理机制?


异常信息受众有两类:人和机器.

人:用户、运营、开发人员等
机器:根据状态码程序进行处理
针对受众,各方的需求可能有:

用户:给用户看得懂的反馈,例如:密码错误
运营:根据信息能判断 哪个用户、在什么时候、做什么操作时、发生了什么问题
开发人员:根据信息能判断用户环境(用户、Token 等)、请求信息(请求方式、数据格式、请求的数据、URL、请求时间)、异常信息(stacktrace、异常状态码、异常消息)、日志信息(报错时的关键 ID、单据等)、服务信息(哪个服务、哪个实例、在哪台机器)等

异常预警要根据团队及业务情况来,初创团队简单处理,有余力再好好整。

简单处理:出现异常发个邮件通知或者发送消息到公司自己的 IM 工具比如钉钉即可
稳妥处理:由于非工作时间大家不一定看邮件,严重异常除普通预警,还可以进行:短信通知、微信通知、自动语音拨号等措施,做到哪怕人在睡觉,只要通讯正常,也能把人抓起来
有余力可以自建异常处理平台,有一套异常处理流程,有个炫酷且实用的 Dashboard。


出于以下几个考虑,使用了网关来处理异常:

若异常交给具体服务处理,那么各个团队在代码中处理异常的方式将 “形色各异”,不好统一管理
开发人员应该专注于业务,知道合理的抛出异常即可,具体服务不应该重复做相同的事情
异常状态码对应的异常消息应该统一读取,具体的服务不允许直接访问缓存服务器
异常处理流程本身较为复杂,例如:持久化、预警等,各个服务不需要做同样的事情
这个思路和 AOP 的理念有点类似

高容错性-重试


最容易也最简单被人想到的容错方式,当然就是“失败重试”,当我们单体应用时,所有的逻辑计算都在单一的进程中,除了进程断电外几乎不可能有处理失败的情况。
然而,当我们把单体应用拆分为一个个细分的子服务后,服务间的互相调用无论是 RPC 还是 HTTP,都是依赖于网络。网络是脆弱的,不时请求会出现抖动失败。例如我们的 Server1 调用 Server2 进行下单时,可能网络超时了,这个时候 Server1 就需要返回给用户提示「网络错误」,这样我们的服务质量就下降了,可能会收到用户的投诉吐槽,降低产品竞争力。

这也是为什么很多产品内部都建设接口维度的 SLA 指标,当成功率低于一定程度时需要和负责人绩效挂钩以此来推进产品的稳定性。

对于网络抖动这种情况,解决的最简单办法之一就是重试。我们接下来聊一下,我们怎么去做重试的,

首先是简单重试,简单是指它的实现通常很简单,粗暴则是指使用不当,很可能会带来系统“雪崩”的风险,因为重试意味着对后端服务的双倍请求。我们请求一个服务,如果服务请求失败,则重试一次。假设,这个服务在常规状态下是 99.9% 的成功率,因为某一次波动性的异常,成功率下跌到 95%,那么如果有重试机制,那么成功率大概还能保持在 99.75%。而简单重试的缺陷也很明显,如果服务真的出问题,很可能带来双倍流量,冲击服务系统,有可能直接将服务冲垮。而在实际的真实业务场景,往往更严重,一个功能不可用,往往更容易引起用户的“反复点击”,反而制造更大规模的流量冲击。比起服务的成功率比较低,系统直接被冲击到“挂掉”的后果明显更严重。


既然单一服务的重试,可能会给该带来双倍的流量冲击,而最终导致更严重的后果,那么我们不如将场景变为主备服务的自动重试或者切换。
例如,我们搭建了两套获取后台仓储的服务,如果服务 A 获取失败,则尝试从服务 B 中获取。因为重试的请求压力是压到了服务 B 上,
服务 A 通常不会因为重试而产生双倍的流量冲击。
这种重试的机制,看似比较可用,而实际上也存在一些问题:
(1)通常会存在“资源浪费”的问题。因为备份服务系统,很可能长期处于闲置状态,只有在主服务异常的时候,
它的资源才会被比较充分地使用。不过,如果对于核心的服务业务(例如核心数据、营收相关)进行类似的部署,虽然会增加一些机器成本和预算,但这个付出通常也是物有所值的。
(2)触发重试机制,对于用户的请求来说,耗时必然增加。主服务请求失败,然后再到备份服务请求,这个环节的请求耗时就至少翻倍增长,假设主服务出现连接(connect)超时,
那么耗时就更是大幅度增加。一个服务在正常状态下,获取数据也许只要 50ms,而服务的超时时间通常会设置到 500-1000ms,甚至更多,一旦出现超时重试的场景,请求耗时必然大幅度增长,很可能会比较严重地影响用户体验。
(3)主备服务一起陷入异常。如果是因为流量过大问题导致主服务异常,那么备份服务很可能也会承受不住这种级别的流量而挂掉。
重试的容错机制,在我们原先系统上有使用,但是相对比较少,因为我们认为主备服务,还是不足够可靠

在我们的系统里,我们的后端涉及数以百计的各类服务,来支撑整个运营系统的正常运作。所有后端服务或者存储,首先是部署为无状态的方式提供服务(一个服务通常很多台机器),
然后,通过公司内的一个公共的智能路由服务 L5,纳入到我们系统中。
(1)所有服务与存储,无状态路由。这样做的目的,主要是为了避免单点风险,就是避免某个服务节点挂了,导致整个服务就瘫痪了。
实际上,即使像一些具有主备性质(主机器挂了,支持切换到备份机器)的接入服务,也是不够可靠的,毕竟只有 2 台,它们都挂了的情况,还是可能发生的。我们后端的服务,通常都以一组机器的形式提供服务,彼此之间没有状态关系,支撑随机分配请求。

(2)支持平行扩容。遇到大流量场景,支持加机器扩容。

(3)自动剔除异常机器。在我们的路由服务,发现某个服务的机器异常的时候(成功率低于 50%),就会自动剔除该机器,后续,会发出试探性的请求,确认等它恢复正常之后,再重新加回到服务机器组。
例如,假如一组服务下拥有服务机器四台(ABCD),假设 A 机器的服务因为某种未知原因,完全不可用了,这个时候 L5 服务会主动将 A 机器自动从服务组里剔除,只保留 BCD 三台机器对外提供服务。而在后续,假如 A 机器从异常中恢复了,那么 L5 再主动将机器 A 加回来,最后,又变成 ABCD 四台机器对外提供服务。
在过去的 3 年里,我们逐步将我们系统内的服务,渐渐从写死 IP 列表或者主备状态的服务,全部升级和优化为 L5 模式的服务,慢慢实现了后端服务的自我容错能力。至少,我们已经比较少遇到,再因为某一台机器的软件或者硬件故障,而不得不人工介入处理的情况。我们也慢慢地从疲于奔命地处理告警的苦难中,被解放出来。


调用任何一个服务或者存储,一个合理的超时时间(超时时间,就是我们请求一个服务时,等待的最长时间),是非常重要的,而这一点往往比较容易被忽视。通常 Web 系统和后端服务的通信方式,是同步等待的模式。这种模式,它会带来的问题比较多。

对于服务端,影响比较大的一个问题,就是它会严重影响系统吞吐率。假设,我们一个服务的机器上,启用了 100 个处理请求的 worker,worker 的超时时间设置为 5 秒,1 个 worker 处理 1 个任务的平均处理耗时是 100ms。那么 1 个 work 在 5 秒钟的时间里,能够处理 50 个用户请求,然而,一旦网络或者服务偶尔异常,响应超时,那么在本次处理的后续整整 5 秒里,它仅仅处理了 1 个等待超时的失败任务。一旦比较大概率出现这类型的超时异常,系统的吞吐率就会大面积下降,有可能耗尽所有的 worker(资源被占据,全部在等待状态,直到 5s 超时才释放),最终导致新的请求无 worker 可用,只能陷入异常状态。


算上网络通信和其他环节的耗时,用户就等待了超过 5s 时间,最后却获得一个异常的结果,用户的心情通常是崩溃的。

解决这个问题的方式,就是设置一个合理的超时时间。例如,回到上面的的例子,平均处理耗时是 100ms,那么我们不如将超时时间从 5s 下调到 500ms。从直观上看,它就解决了吞吐率下降和用户等待过长的问题。然而,这样做本身又比较容易带来新的问题,就是会引起服务的成功率下降。因为平均耗时是 100ms,但是,部分业务请求本身耗时比较长,耗时超过 500ms 也比较多。例如,某个请求服务端耗时 600ms 才处理完毕,然后这个时候,客户端认为等待超过 500ms,已经断开了连接。处理耗时比较长的这类型业务请求会受到比较明显的影响。

超时时间设置过短,会将很多本来处理成功的请求,当做服务超时处理掉,进而引起服务成功率下降。将全部业务服务,以一刀切的方式设置一个超时时间,是比较不可取的。优化的方法,我们分为两个方向。
(1)快慢分离
根据实际的业务维度,区分对待地给各个业务服务配置不同的超时时间,同时,最好也将它们的部署服务也分离出来。例如,天天酷跑的查询服务耗时通常为 100ms,那么超时时间我们就设置为 1s,某新手游的查询服务通常耗时为 700ms,那么我们就设置为 5s。这样的话,整体系统的成功率,就不会受到比较大的影响。

(2)解决同步阻塞等待
“快慢分离”可以改善系统的同步等待问题,但是,对于某些耗时本来就比较长的服务而言,系统的进程/线程资源仍然在同步等待过程中,无法响应其他新的请求,只能阻塞等待,它的资源仍然是被占据,系统的整体吞吐率仍然被大幅度拉低。

解决的思路,当然是利用 I/O 多路复用,通过异步回调的方式,解决同步等待过程中的资源浪费。AMS 的一些核心服务,采用的就是“协程”(又叫“微线程”,简单的说,常规异步程序代码里嵌套比较多层的函数回调,编写复杂。而协程则提供了一种类似写同步代码的方式,来写异步回调程序),以解决同步等待的问题。异步处理的简单描述,就是当进程遇到 I/O 网络阻塞时,就保留现场,立刻切换去处理下一个业务请求,进程不会因为某个网络等待而停止处理业务,进而,系统吞吐率即使遇到网络等待时间过长的场景,通常都能保持在比较高的水平。

值得补充一点的是,异步处理只是解决系统的吞吐率问题,对于用户的体验问题,并不会有改善,用户需要等待的时间并不会减少。

3.防重入,防止重复业务
前面我们提到,我们设置了一个比较“合理的超时时间”,简而言之,就是一个比较短的超时时间。而在数据写入的场景,会引起新的问题,比如数据重复问题,此时我们后端服务就要做好幂等性校验,一般可以通过 Redis,唯一业务主键实现

1、针对服务核心环节,我们要重点监控,并且设置较长的超时时间
2、针对服务非核心环节,我们设置较短超时时间


虽然,大家都知道一个服务的设计,要尽可能小和分离部署,如此,服务之间的耦合会比较小,一旦某个模块出问题,受到影响的模块就比较少,容错能力就会更强。可是,从设计之初,就将每一个服务有序的切割地很小,这个需要设计者具备超前的意识,能够提前意识到业务和系统的发展形态,而实际上,业务的发展往往是比较难以预知的,因为业务的形态会随着产品的策略的改变而变化。在业务早期流量比较小的时候,通常也没有足够的人力和资源,将服务细细的切分。我们系统从日请求百万级的 Web 系统,逐渐成长为亿级,在这个过程中,流量规模增长了 100 倍,我们经历了不少服务耦合带来的阵痛。
我们经历了以下几个阶段,
第一个 阶段服务分离,大服务变成多个小服务
DataLog 以前是一个比较小的系),因此,很多服务和存储在早起都是部署在一起的,查询和发货服务都放在一起,不管哪一个出问题,都相互影响。后来,我们逐渐的将这些核心的服务和存储,慢慢地分离出来,细细切分和重新部署。在数据存储方面,我们将原来 3-5 个存储的服务,慢慢地切为 20 多个独立部署的存储。
这样做带来了很多好处:

(1)原来主存储的压力被分流。

(2)稳定性更高,不再是其中一个出问题,影响整个大的模块。

(3)存储之间是彼此物理隔离的,即使服务器硬件故障,也不会相互影响。

第二个阶段,轻重分离,物理隔离
我们对于一些核心的业务,进行“轻重分离”
轻重分离的这个部署方式,可以给我们带来一些好处:

(1)查询集群即使出问题,也不会影响发货集群,保证用户核心功能正常。

(2)两边的机器和部署的服务基本一致,在紧急的情况下,两边的集群可以相互支援和切换,起到容灾的效果。

(3)每个集群里的机器,都是跨机房部署,例如,服务器都是分布在 ABC 三个机房,假设 B 机房整个网络故障了,反向代理服务会将无法接受服务的 B 机房机器剔除,然后,剩下 AC 机房的服务器仍然可以正常为外界提供服务

如果系统架构设计层面的“容错”我们都搭建完善了,那么再继续下一层容错,就需要根据实际的业务来进行,因为,不同的业务拥有不同的业务逻辑特性,也能够导致业务层面的各种问题。而在业务层面的容错,简而言之,避免“人的失误”。不管一个人做事性格多么谨慎细心,也总有“手抖”的时候,在不经意间产生“失误”。例如我们上线配置,一个月都有几百次配置,完善的监控系统能够及时发现问题,防止影响面的进一步扩大和失控,但是,它并不能杜绝现网问题的发生。而真正的根治之法,当然是从起源的地方杜绝这种场景的出现,回到上面“配置错误”的例子场景中,用户在内部管理端发布活动配置时,就直接提示运营同学,这个配置规则是不对的。在业界,因为配置参数错误而导致的现网重大事故的例子,可以说是多不胜数,“配置参数问题”几乎可以说是一个业界难题,对于解决或者缓解这种错误的发生,并没有放之四海而皆准的方法,更多的是需要根据具体业务和系统场景,亦步亦趋地逐步建设配套的检查机制程序或者脚本。

因此,我们建设了一套强大并且智能的配置检查系统,里面集合了数十种业务的搭配检查规则,并且检查规则的数目一直都在增加。这里规则包括检查礼包日限量之类比较简单的规则,也有检查各种关联配置参数、相对比较复杂的业务逻辑规则。
另外一方面,流程的执行不能通过“口头约定”,也应该固化为平台程序的一部分,例如,活动上线之前,我们要求负责活动的同事需要验证一下真实的业务操作。然而,这只是一个“口头约定”,实际上并不具备强制执行力,如果这位同事因为验证流程太麻烦太多而忽略了某个场景,这种事情也的确偶尔会发生,这个也算是“人的失误”的另外一种场景。为了解决问题,这个流程在我们系统的内部管理端中,是通过程序去保证的,确保这位同事实际操作过整个流程。做法其实挺简单的,就是让负责活动的同事设置一个验证活动的账号,然后,程序在进行活动时,程序会自动检查每一个子活动项目中,是否有这个账号的活动参与记录。如果都有参与记录,则说明这位同事完整地做完了流程。同时,其他模块的验证和测试,我们也都采用程序和平台来保证,而不是通过“口头约定”,通过程序和系统对业务逻辑和流程的保证,尽可能防止“人的失误”。

这种业务配置检查程序,除了可以减少问题的发生,实际上也减轻了测试和验证活动的工作,可以起到节省人力的效果。不过,业务配置检查规则的建设并不简单,逻辑往往比较复杂,因为要防止误杀。

无论是人还是机器,都是会产生“失误”,只是对于单一个体,发生的概率通常并不大。但是,如果一个系统拥有数百台服务器,或者有一项工作有几百人共同参与,这种“失误“的概率就被大大提升,失误很可能就变为一种常态了。机器的故障,尽可能让系统本身去兼容和恢复,人的失误,尽可能通过程序和系统流程来避免,都尽可能做到”不依赖于人


标题:自研网关day08-高可用
作者:xiaohugg
地址:https://xiaohugg.top/articles/2024/05/06/1715003527984.html

人民有信仰 民族有希望 国家有力量