最近我们团队对项目中的服务熔断降级代码做了一次重构原来的代码写得很烂到处都是重复的try-catch熔断逻辑散落在各个地方很难维护也很容易出问题重构之后,代码变得很优雅用注解+ AOP的方式统一处理熔断降级逻辑简洁清晰易维护也更可靠。
今天想记录一下这次重构的过程包括原来的烂代码有什么问题重构的思路和方案具体的实现以及,重构后的效果和经验教训希望能帮大家写出更优雅的熔断降级代码。
一、为什么需要熔断降级
先说说为什么需要熔断降级在微服务架构中服务之间,相互调用一个服务往往依赖多个其他服务,如果某个依赖的服务出现问题(比如响应慢,或者不可用)而调用方没有做保护就可能被拖垮导致请求堆积线程耗尽最终整个服务不可用甚至引发雪崩效应一个服务的故障蔓延到整个系统。
熔断降级就是为了防止这种情况发生的保护机制:
- 熔断(Circuit Breaker):当某个服务的错误率,或者响应时间超过阈值时自动熔断对该服务的调用后续请求直接返回降级结果不再调用该服务避免故障蔓延等服务恢复后再自动恢复调用就像电路的保险丝电流过大就熔断保护电路。
- 降级(Degradation):当服务不可用,或者响应慢时返回一个兜底的结果(比如默认值缓存数据简单的提示等等)保证核心功能可用用户体验不完全崩溃牺牲非核心功能保证核心功能可用。
熔断降级是微服务架构中非常重要的保护机制能提高系统的可用性和容错能力防止雪崩效应,所以每个微服务项目都应该有熔断降级机制。
二、原来的烂代码有什么问题
我们项目原来也有熔断降级,但是代码写得很烂有很多问题这里总结一下。
问题1:到处都是重复的try-catch:
原来的代码每个调用外部服务的地方都写了一大段try-catch逻辑大概是这样的:
try {
Result result = remoteService.call(params);
if (result.isSuccess()) {
return result.getData();
} else {
// 降级处理
return defaultResult;
}
} catch (Exception e) {
log.error("调用远程服务失败", e);
// 降级处理
return defaultResult;
}每个调用的地方都重复写这段逻辑非常冗余代码量大也很难维护,如果要改降级逻辑需要改很多地方容易遗漏出问题。
问题2:熔断逻辑散落在各处不统一:
原来的熔断逻辑是用一个简单的计数器实现的每个服务调用的地方都自己维护一个计数器统计错误率超过阈值就熔断,但是这些逻辑散落在各个地方实现方式也不一样有的地方有熔断有的地方没有有的阈值设置得合理有的设置得不合理非常混乱很难统一管理和监控。
而且,因为逻辑分散很容易出bug比如计数器没有正确重置熔断状态没有正确恢复等等导致熔断不生效,或者误熔断影响业务。
问题3:没有统一的监控和告警:
原来的代码没有统一的监控和告警熔断降级的情况没有被监控和记录出了问题很难排查不知道哪个服务被熔断了为什么被熔断降级了多少次等等运维和开发都很被动。
问题4:代码耦合严重难以测试:
原来的代码业务逻辑和熔断降级逻辑耦合在一起很难单元测试也很难维护要测试熔断降级逻辑需要模拟各种异常情况很麻烦,而且,因为逻辑分散测试覆盖也很难做全。
问题5:新人上手困难:
因为代码混乱逻辑分散没有统一的规范和文档新人上手很困难不知道熔断降级该怎么写该在哪里加容易写错,或者遗漏导致新代码没有熔断降级保护出了问题才发现。
这些问题积累到一定程度就必须重构了,否则代码会越来越烂越来越难维护最终成为技术债务影响业务发展。
三、重构的思路和方案
针对这些问题我们制定了重构的思路和方案核心思想是统一处理声明式配置关注点分离。
思路1:用注解+AOP统一处理熔断降级:
我们决定用注解+ AOP的方式统一处理熔断降级逻辑定义一个@CircuitBreaker注解标注在需要熔断降级的方法上,然后用AOP切面统一拦截这些方法处理熔断降级逻辑这样业务代码里就不用写重复的try-catch了只需要加一个注解就搞定简洁优雅。
注解里可以配置熔断的参数,比如降级方法名超时时间错误率阈值熔断恢复时间等等灵活配置不同的方法用不同的参数。
思路2:引入成熟的熔断框架:
我们没有自己造轮子实现熔断逻辑而是引入了成熟的熔断框架Hystrix(Netflix开源的熔断框架非常成熟广泛使用)用它来做底层的熔断实现我们只需要在上面封装一层注解+ AOP就行这样稳定可靠也不用自己维护复杂的熔断逻辑。
Hystrix提供了完整的熔断降级隔离监控等功能非常强大也有很好的生态和,文档用起来很放心。
思路3:统一的监控和告警:
重构的时候,我们加上了统一的监控和告警用Hystrix Dashboard+ Turbine做熔断监控能实时看到每个服务的熔断状态请求量错误率等等,同时把熔断降级的事件上报到监控系统配置告警规则熔断发生的时候,及时通知开发和运维快速响应。
思路4:统一的降级处理:
我们定义了统一的降级处理机制每个@CircuitBreaker注解可以指定降级方法熔断,或者异常的时候,自动调用降级方法返回兜底结果,同时也支持默认的降级处理,比如返回默认值,或者通用的错误提示减少重复代码。
思路5:完善的文档和规范:
重构完成后我们写了完善的文档和使用规范说明怎么使用@CircuitBreaker注解怎么配置参数怎么写降级方法注意事项等等让新人也能快速上手写出规范的代码。
四、具体实现
下面说说具体的实现我们用的是Java+ Spring Boot+ Hystrix的技术栈其他语言和,框架思路是一样的可以参考。
步骤1:引入Hystrix依赖:
首先,在pom.xml里引入Hystrix的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>然后在启动类上加@EnableCircuitBreaker注解开启熔断功能。
步骤2:定义@CircuitBreaker注解:
定义一个自定义的@CircuitBreaker注解用来标注需要熔断降级的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CircuitBreaker {
/**
* 降级方法名
*/
String fallbackMethod() default "";
/**
* 超时时间(毫秒)
*/
int timeout() default 1000;
/**
* 熔断错误率阈值(百分比)
*/
int errorThresholdPercentage() default 50;
/**
* 熔断后恢复时间(毫秒)
*/
int sleepWindowInMilliseconds() default 5000;
/**
* 熔断触发最小请求数
*/
int requestVolumeThreshold() default 20;
}这个注解里配置了熔断的各种参数用户可以根据需要调整。
步骤3:实现AOP切面:
然后实现一个AOP切面拦截标注了@CircuitBreaker的方法用Hystrix包装方法调用处理熔断降级:
@Aspect
@Component
public class CircuitBreakerAspect {
@Around("@annotation(circuitBreaker)")
public Object around(ProceedingJoinPoint joinPoint, CircuitBreaker circuitBreaker) throws Throwable {
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object target = joinPoint.getTarget();
Object[] args = joinPoint.getArgs();
// 构建HystrixCommand
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(target.getClass().getSimpleName()))
.andCommandKey(HystrixCommandKey.Factory.asKey(method.getName()))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(circuitBreaker.timeout())
.withCircuitBreakerErrorThresholdPercentage(circuitBreaker.errorThresholdPercentage())
.withCircuitBreakerSleepWindowInMilliseconds(circuitBreaker.sleepWindowInMilliseconds())
.withCircuitBreakerRequestVolumeThreshold(circuitBreaker.requestVolumeThreshold())
);
HystrixCommand<Object> command = new HystrixCommand<Object>(setter) {
@Override
protected Object run() throws Exception {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new Exception(throwable);
}
}
@Override
protected Object getFallback() {
// 调用降级方法
if (StringUtils.isNotBlank(circuitBreaker.fallbackMethod())) {
try {
Method fallbackMethod = target.getClass().getMethod(
circuitBreaker.fallbackMethod(), method.getParameterTypes());
return fallbackMethod.invoke(target, args);
} catch (Exception e) {
log.error("调用降级方法失败", e);
}
}
// 默认降级
return null;
}
};
return command.execute();
}
}这个切面拦截标注了@CircuitBreaker的方法用HystrixCommand包装方法调用自动处理熔断超时降级等等逻辑业务代码完全不用关心这些。
步骤4:业务代码使用注解:
重构后业务代码就很简洁了只需要在方法上加@CircuitBreaker注解指定降级方法就行:
@Service
public class OrderService {
@Autowired
private RemoteUserService remoteUserService;
@CircuitBreaker(fallbackMethod = "getUserDefault", timeout = 2000)
public User getUser(Long userId) {
// 调用远程服务
return remoteUserService.getUser(userId);
}
/**
* 降级方法
*/
public User getUserDefault(Long userId) {
// 返回兜底结果
User user = new User();
user.setId(userId);
user.setName("默认用户");
return user;
}
}看重构后的代码多简洁业务逻辑清晰熔断降级逻辑被注解和AOP统一处理了不用到处写try-catch了代码量大大减少也更易维护。
步骤5:配置监控和告警:
最后配置Hystrix Dashboard和Turbine做熔断监控能实时看到每个服务的熔断状态,同时把熔断事件上报到监控系统配置告警这里就不展开了网上有很多教程。
五、重构后的效果
重构完成后效果非常明显这里总结一下。
效果1:代码量大幅减少:
重构前每个调用外部服务的地方都要写十几行try-catch熔断逻辑重构后只需要加一个注解+ 一个降级方法代码量减少了70%以上简洁很多。
效果2:逻辑统一易维护:
熔断降级逻辑统一在AOP切面里处理要改逻辑只需要改一个地方不用到处改维护成本大大降低也不容易出bug。
效果3:监控完善问题可追溯:
加上了统一的监控和告警熔断降级的情况都能实时看到出了问题能快速定位和,响应运维和开发都更主动了。
效果4:新人上手快:
有了统一的注解和规范新人只需要看文档就知道怎么加熔断降级不用研究复杂的逻辑上手很快代码质量也有保障。
效果5:系统稳定性提升:
因为熔断降级逻辑更完善更可靠系统的稳定性也提升了外部服务故障的时候,能正确熔断降级不会被拖垮也不会引发雪崩系统可用性大大提高。
六、经验和教训
这次重构也让我们总结了一些经验和教训分享给大家。
教训1:不要重复造轮子用成熟的框架:
熔断降级这种通用的基础功能不要自己造轮子实现很容易出bug也维护,不过来用成熟的框架(比如HystrixResilience4jSentinel等等)稳定可靠功能完善也有社区支持省心很多。
我们原来自己写的简单的熔断计数器就有很多bug和不完善的地方后来用Hystrix就稳定很多,所以不要重复造轮子站在巨人的肩膀上更好。
教训2:横切关注点要用AOP统一处理:
熔断降级日志监控事务等等这些都是横切关注点和业务逻辑无关应该用AOP统一处理不要散落在业务代码里,否则代码会很混乱难维护用AOP统一处理业务代码简洁清晰关注点分离易维护。
教训3:重构要循序渐进有测试保障:
重构不要一次性大改风险太高要循序渐进先搭好框架和注解,然后逐个模块迁移每迁移一个模块就测试验证没问题再迁移下一个降低风险,同时要有完善的测试保障单元测试集成测试都要有确保重构不影响业务功能。
我们这次重构就是分模块逐步迁移的先迁移核心模块验证没问题再迁移其他模块整个过程很平稳没有出大问题。
教训4:文档和规范很重要:
重构完成后一定要写文档和规范告诉团队怎么使用新的方式注意事项是什么,否则新人还是会用老的方式写代码,或者用错新的方式重构的成果就不能保持文档和规范是重构成果的保障一定要重视。
教训5:技术债务要及时还:
原来的烂代码就是,因为一开始图快没有好好设计导致技术债务越积越多最后不得不花更多时间重构,所以技术债务要及时还不要拖越拖成本越高平时写代码就要注意质量做好设计不要写烂代码留技术债务。
七、写在最后
以上就是我们团队服务熔断降级代码重构的完整记录包括为什么需要熔断降级原来的烂代码有什么问题重构的思路和,方案具体的实现重构后的效果以及经验和教训。
代码重构是每个开发者都会遇到的事情烂代码不可怕可怕的是不去重构任由技术债务积累最后拖垮项目,只要我们重视代码质量及时重构用好的设计和模式就能把烂代码变成优雅代码让项目更健康更易维护。
熔断降级是微服务架构中非常重要的保护机制希望大家都能重视写出优雅可靠的熔断降级代码保护好自己的系统提高系统的可用性和容错能力。
最后用一句话结束这篇文章:"烂代码不可怕可怕的是不重构用好的设计和,模式把烂代码变成优雅代码是每个开发者的责任也是能力的体现。"
愿大家都能写出优雅的代码做快乐的程序员。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录