当前位置: 首页 > 科技观察

阿里两侧:大量对外接口超时,拖垮整个系统,引发雪崩!怎么解决?

时间:2023-03-21 10:38:03 科技观察

大家好,我是汤姆哥~互联网+时代,商业数字化已经渗透到你能想到的每一个行业。业务功能和营销方式越来越多样化,系统也越来越复杂。面对越来越复杂的业务系统,大脑越来越不够用,于是聪明人提出了微服务的设计思想。本着化繁为简的原则,我们将一个大系统拆分成若干个子系统,每个子系统职责单一,按照DDD的设计理念承载一个子域的业务构建。因此,人们可以集中精力,集中精力完成某个业务点的深度建设。多个微服务系统通过RPC框架(如dubbo、springcloud、gRPC等)串联起来,但是随着调用次数的增加,人们发现服务之间的稳定性越来越高一个重要的例子:服务D挂了,反应很慢。ServiceG和ServiceF都依赖ServiceD,都会被牵连。外部响应也会变慢,影响层层向上传递,服务A和服务B也会被拖到最后,触发雪崩效应,系统故障的影响会越来越大。为了解决这个问题,我们需要引入熔断机制。“破了就破了,不打扰了,破了就难了。”什么是保险丝?熔断器实际上是对调用环节中某个资源状态不稳定(如调用超时或异常比增高)的响应,限制对该资源的调用,使请求快速失败,避免影响其他资源而造成级联错误。当资源降级后,在下一个降级时间窗口内,对该资源的调用将自动中断(默认抛出BlockException)。目前,市面上有很多熔断器框架,比如:Sentinel、Hystrix、Resilience4j等,这些框架的框架设计理念都差不多。本文重点介绍如何在项目中使用Sentinel。Sentinel(分布式系统流量卫士)是阿里开源的服务容错综合解决方案。它以流量为切入点,从流量控制、断路器降级、系统负载保护等多个维度保障服务的稳定性。核心分为两部分:1.核心库(Java客户端):可以运行在所有Java环境,对Dubbo、SpringCloud等框架有很好的支持。2.Dashboard:基于SpringBoot开发,打包后可直接运行。Sentinel熔断类型:RT响应时间异常数量异常比例Sentinel安装首先,从官网下载sentinel控制台安装包。下载地址:https://github.com/alibaba/Sentinel/releases下载完Jar包后,打开终端,运行命令java-Dserver.port=8180-Dcsp.sentinel.dashboard.server=localhost:8180-Dproject.name=sentinel-dashboard-jarsentinel-dashboard-1.8.1.jar登录Sentinal控制台:默认用户和密码都是sentinel,登录成功后界面如下。首先我们直观感受下控制台配置熔断规则:这里表示熔断策略选择的慢调用比例,响应时间超过200毫秒,标记为慢请求。如果慢请求的比例超过30%,在1000ms的统计周期内超过3个(可自行调整),则后续请求熔断10秒,10秒后恢复正常。注解访问非常简单,只需要提前在控制台配置好资源规则,然后在代码中添加@SentinelResource注解即可。//资源名为handle1@RequestMapping("/handle1")@SentinelResource(value="handle1",blockHandler="blockHandlerTestHandler")publicStringhandle1(Stringparams){//业务逻辑处理return"success";}//接口方法handle1的自底向上方法publicStringblockHandlerTestHandler(Stringparams,BlockExceptionblockException){return"Backupreturn";}当达到阈值时,系统默认提示的是一段英文,非常不友好,我们可以自定义自下而上的方法。在@SentinelResource注解中,进一步配置blockHandler和fallback属性字段blockHandler:在主观层面,如果限流或者吹断,调用该方法处理fallback:业务的异常,比如抛出各种异常在执行过程中,再调用该方法进行自下而上的处理。通过以上两层bottom-up,可以使Sentinel框架更加人性化,体验更好。注意:注解式开发需要在方法中加入,范围比较固定。在后面的项目实战中,我们也可以通过显示形式灵活的划定代码块的范围。项目实战我们这里有个项目。考虑到客户的部署成本,我们想做一个轻量级的方案。需求如下:我们想引入框架的熔断功能,不想把console拦截点部署得比较封闭,类似于dubbo消费端远程访问一样,在做拦截处理代理类的远程通信位置。大纲方案--流程图:1.我们通过Proxy.newProxyInstance为所有的接口创建一个代理子类2.代理子类的所有方法调用都聚集在InvocationHandler3中。我们把类名和方法名拼接起来,然后去熔断规则表中查看是否配置了规则。4.如果没有,则按照常规的远程调用逻辑。5.如果是,将远程调用逻辑纳入Sentinel的监控权限6.如果触发熔断机制,直接抛出BlockException,上层业务拦截异常并做特殊处理,比如修改给用户更合适的复制提示。熔断状态机:核心代码逻辑,继续往下看首先介绍一下Sentinel的依赖包:com.alibaba.cspsentinel-core1.8.3熔断规则表设计:CREATETABLE`degrade_rule`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'primarykey',`resource_name`varchar(256)NOTNULLCOMMENT'资源名称',`count`doubleNOTNULLCOMMENT'慢调用时长,单位毫秒',`slow_ratio_threshold`doubleNOTNULLCOMMENT'慢调用比率阈值',`min_request_amount`intNOTNULLCOMMENT'熔断器',`stat_interval`intNOTNULLCOMMENT'统计持续时间,单位毫秒',`time_window`intNOTNULLCOMMENT'熔断持续时间,单位s',`created_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`updated_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间',PRIMARYKEY(`id`)USINGBTREE,UNIQUEKEY`uk_resource_name`(`resource_name`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb3COMMENT='破坏规则表';由于放弃了部署控制台,我们只能自己管理破坏规则的每个属性值。我们可以根据企业内部管理后台风格来管理这些规则,进行页面的开发。当然,前期可以使用更简单粗暴的方式,手动初始化数据库表中的数据。如果要调整规则,就去SQL修正。为了尽可能实时感知规则表中的数据变化,开发了一个定时任务,每10秒运行一次。@Scheduled(cron="0/10****?")publicvoidloadDegradeRule(){ListdegraderRuleDOList=degraderRuleDao.queryAllRule();如果(CollectionUtils.isEmpty(degradeRuleDOList)){返回;}StringnewMd5Hex=DigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList));如果(StringUtils.isBlank(newMd5Hex)||StringUtils.equals(lastMd5Hex,newMd5Hex)){返回;}Listrules=null;ListresourceNameList=newArrayList<>();rules=degradeRuleDOList.stream().map(degradeRuleDO->{//资源名称,即规则的对象DegradeRulerule=newDegradeRule(degradeRuleDO.getResourceName())//熔断策略,支持慢调用比/异常ratio/异常数strategy.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())//在慢调用比率模式下,为慢调用临界RT(超过这个值算慢调用);在异常比率/异常number模式,对应Threshold.setCount(degradeRuleDO.getCount())//熔断时长,单位s.setTimeWindow(degradeRuleDO.getTimeWindow())//慢调用率threshold.setSlowRatioThreshold(degradeRuleDO.getSlowRatioThreshold())//熔断触发的最小请求数。当请求数小于该值时,即使异常比例超过阈值,也不会熔断。);resourceNameList.add(degradeRuleDO.getResourceName());退货规则;}).collect(Collectors.toList());如果(CollectionUtils.isNotEmpty(rules)){DegradeRuleManager.loadRules(rules);ConsumerProxyFactory.resourceNameList=resourceNameList;lastMd5Hex=newMd5Hex;}log.error([DegradeRuleConfig]fuseruleload:"+rules);}考虑到规则的变化频率不会很高,不需要每次都重新加载规则DegradeRuleManager.loadRules这里设计了一个trickDigestUtils.md5Hex(JSON.toJSONString(degradeRuleDOList));JSON序列化查询规则内容,然后计算其md5摘要,如果结果与上次一致,则说明这段时间没有变化,不做处理直接返回。定义一个实现InvocationHandler接口的子类。通过Proxy.newProxyInstance为目标接口创建一个代理子类。这样每次调用接口方法时,实际上调用的是invoke方法@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{Classclazz=proxy.getClass().getInterfaces()[0];字符串urlCode=clazz.getName()+"#"+method.getName();if(resourceNameList.contains(urlCode)){//添加熔断处理入口entry=null;尝试{entry=SphU.entry(urlCode);//远程网络调用,获取结果responseString=HttpClientUtil.postJsonRequest(url,header,body);}catch(BlockExceptionblockException){//触发熔断log.error("degradetrigger!remoteurl:{}",urlCode);抛出新的DegradeBlockExcetion(urlCode);}finally{if(entry!=null){entry.exit();}}}else{//常规处理,无断路器判断逻辑//省略}}实验数据: