容错机制实现
什么是容错机制?
容错机制是分布式系统设计中非常重要的一部分,它的目标是在部分组件或节点出现故障时,仍能保证系统整体的正常运转和服务可用性。
这种能力对于构建高可用、高可靠的分布式系统非常关键。
容错机制主要包括以下几个方面:
-
故障检测和隔离:
- 系统需要能够及时发现和定位故障,并将故障节点或服务隔离,防止故障扩散。
- 可以使用心跳监测、状态检查等手段来检测故障。
- 当发现故障时,可以采用服务熔断、容器隔离等方式将故障节点隔离。
-
请求重试和超时控制:
- 对于临时性的网络或服务异常,可以采用重试机制来提高成功概率。
- 重试策略可以是固定时间间隔、指数退避、随机等不同形式。
- 同时需要设置合理的超时时间,超时后放弃重试,防止无限重试耗尽资源。
-
容错路由和负载均衡:
- 当某个服务节点出现故障时,可以通过容错路由将请求重新分配到其他可用节点。
- 负载均衡策略也需要考虑容错因素,例如剔除故障节点、动态调整权重等。
-
服务降级和业务兜底:
- 当依赖的关键服务出现故障时,可以采用服务降级,临时使用备用方案或返回默认响应。
- 通过业务层面的兜底措施,保证核心功能的可用性。
-
资源隔离和限流:
- 将不同服务或模块的资源进行隔离,例如使用容器、虚拟机等技术。
- 对关键服务实施限流,防止被大量请求冲垮。
-
数据备份和恢复:
- 定期备份系统状态和业务数据,以便在发生故障时快速恢复。
- 备份方案包括数据备份、日志备份、配置备份等。
-
监控报警和自愈机制:
- 建立完善的监控体系,实时检测系统运行状态,及时发现并报警异常。
- 结合其他容错手段,设计自动化的故障修复和自愈流程。
为什么要容错机制?
分布式系统需要使用容错机制主要有以下几个原因:
-
提高系统可用性
- 分布式系统由多个独立组件构成,任何一个组件的故障都可能导致整个系统不可用。
- 容错机制可以在部分组件出现故障时,保证系统整体仍能正常提供服务,提高可用性。
-
降低故障影响
- 在分布式环境下,一个故障可能会通过调用链在系统中传播,导致级联故障。
- 容错机制可以及时隔离故障,阻止其扩散,降低故障对整个系统的影响。
-
增强系统弹性
- 分布式系统面临各种不确定因素,如网络延迟、服务器故障等。
- 容错机制可以让系统在面对这些不确定性时,仍能保持稳定和可靠的运行。
-
支持高并发和扩展性
- 分布式系统通 常需要支持高并发访问和动态扩展。
- 容错机制可以在系统扩展或负载增大时,确保服务质量不会下降。
-
满足业务连续性要求
- 许多关键业务系统需要实现7*24小时的持续运行。
- 容错机制可以确保业务在出现故障时仍能快速恢复,减少业务中断。
-
降低运维成本
- 容错机制可以自动化地处理和修复故障,减少人工介入。
- 这可以降低系统维护的人力和时间成本。
容错机制是构建高可用、高可靠分布式系统的关键所在。它可以显著提高系统的抗风险能力,确保业务连续性,为用户提供稳定可靠的服务。这对于许多关键性的分布式应用来说是非常必要的。
容错机制有哪些?
参考Dubbo的实现,
在Dubbo的文档中也介绍了这些容错策略:https://cn.dubbo.apache.org/zh-cn/blog/2018/08/22/dubbo-%e9%9b%86%e7%be%a4%e5%ae%b9%e9%94%99/
-
Failover (失败自动切换):
- 当某个服务提供者出现故障时,自动切换到备用的服务提供者。
- 这样可以在某个节点出现问题时,保证服务的可用性。
- 通常会配合负载均衡策略使用,确保流量能够自动分配到可用的节点上。
-
Failsafe (失败安全):
- 当服务调用出现异常时,直接返回一个安全的默认值或者空值。
- 这 种策略适用于一些对结果容忍度较高的场景,比如日志记录、缓存预热等。
- 通过快速返回,可以避免阻塞调用链,提高系统的整体可用性。
-
Failfast (快速失败):
- 当服务调用出现异常时,立即抛出异常,不进行重试或降级。
- 这种策略适用于对响应时间敏感的场景,比如用户交互界面。
- 快速失败可以减少系统资源的占用,但需要在上层进行更好的异常处理。
-
Failback (失败自动恢复):
- 当服务提供者恢复正常后,自动恢复对该服务的调用。
- 这种策略通常与Failover一起使用,可以在故障恢复后,自动切换回正常的服务节点。
- 这样可以最大程度地减少服务中断的时间。
-
Forking (并行调用):
- 当调用一个服务时,同时向多个服务提供者发起并行调用。
- 只要有一个调用成功,就返回结果,其他的调用则会被取消。
- 这种策略可以提高服务的可靠性,但会增加资源消耗。适用于对响应时间有严格要求的场景。
-
Broadcast (广播调用):
- 当调用一个服务时,向所有已知的服务提供者发起调用。
- 所有提供者的响应都会被收集和合并,返回给调用方。
- 这种策略可以提高服务的可用性,但会增加网络开销。适用于需要聚合多个服务结果的场景。
容错方案的设计
-
先容错再重试
-
先重试再容错
容错机制实现
除了上述的策略之外,很多 技术都可以算得上是容错,例如:
- 重试:重试本身就是容错的降级策略,系统出现错误后再重试
- 限流:如果系统压力过大,已经出现部分错误,那么可以限制请求的频率数量来进行保护
- 降级:出现错误之后,可以变成执行其他更稳定 的操作,也称兜底,
- 熔断:出现故障或者异常,暂停服务,避免连锁故障
- 超时控制:长时间没有处理完成,就中断,防止阻塞和资源占用
容错策略接口定义
我们定义的 TolerantStrategy
接口主要有两个参数:
-
Map<String, Object> context
- 这个参数是一个上下文对象,用于在容错处理过程中传递一些数据。
- 在分布式系统中,当一个远程调用出现异常时,我们需要根据当前的上下文信息来决定如何进行容错处理。
- 这个上下文可以包含一些关键信息,例如:
- 当前请求的参数
- 调用链路信息
- 服务实例的元数据
- 重试次数等
- 通过这个上下文对象,容错策略实现可以获取到更丰富的信息,从而做出更加合理的容错决策。
-
Exception e
- 这个参数表示在执行远程调用时出现的异常。
- 容错 策略需要根据异常的类型、错误信息等,来决定采取什么样的容错措施。
- 例如,对于网络异常可以选择重试,而对于业务异常可能需要降级或返回默认响应。
- 通过分析异常信息,容错策略可以更有针对性地进行容错处理。
-
RpcResponse doTolerant(Map<String, Object> context, Exception e)
- 这个方法定义了容错处理的具体实现。
- 它接收上下文信息和异常对象作为参数,并返回一个
RpcResponse
作为处理结果。 - 容错策略的实现者需要根据具体的业务需求和故障情况,编写相应的容错逻辑,并返回一个合适的响应结果。
总的来说,这个 TolerantStrategy
接口为容错处理提供了一个标准的抽象和扩展点。通过传入上下文信息和异常对象,容错策略的实现者可以更灵活地根据不同的场景,制定出适合自己系统的容错机制。
/**
* @author houyunfei
*/
public interface TolerantStrategy {
/**
* 容错处理
* @param context 上下文,用于传递数据
* @param e 异常
* @return
*/
RpcResponse doTolerant(Map<String, Object> context, Exception e);
}
快速失败策略
打印一个日志,直接抛异常出去。
/**
* @author houyunfei
* 快速失败
*/
@Slf4j
public class FailFastTolerantStrategy implements TolerantStrategy {
/**
* 快速失败 -立刻通知调用方失败
*
* @param context 上下文,用于传递数据
* @param e 异常
* @return
*/
@Override
public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
log.error("FailFastTolerantStrategy doTolerant", e);
throw new RuntimeException("FailFastTolerantStrategy doTolerant", e);
}
}
静默处理策略
静默处理策略提供了一种安静而高效的容错处理方式,再需要容错的时候,我们返回一个默认的RpcResponse即可,可以通过构造函数传入
静默处理策略的特点是:
-
不通知调用方失败:
- 当服务调用出现异常时,不会抛出异常,也不会返回错误响应。
- 而是直接返回一个默认的响应结果。
-
只记录日志:
- 异常信息仅仅通过日志的形式记录下来,方便事后排查问题。
- 但不会将异常信息直接返回给调用方。
-
适用场景:
- 这种策略适用于对最终结果不太敏感的场景,比如日志记录、缓存预热等。
- 即使服务调用失败,也不会影响业务的核心逻辑。
-
优缺点:
- 优点是简单易实现,对系统负载影响小。
- 缺点是可能会丢失一些有价值的业务信息,无法保证最终一致性。
/**
* @author houyunfei
* 静默处理
*/
@Slf4j
public class FailSilentTolerantStrategy implements TolerantStrategy {
private final RpcResponse defaultResponse;
public FailSilentTolerantStrategy(RpcResponse rpcResponse) {
this.defaultResponse = rpcResponse;
}
public FailSilentTolerantStrategy() {
this.defaultResponse = new RpcResponse();
}
/**
* 静默处理 - 不通知调用方失败,只记录日志
*
* @param context 上下文,用于传递数据
* @param e 异常
* @return
*/
@Override
public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
log.info("FailSafeTolerantStrategy doTolerant", e);
return defaultResponse;
}
}
故障恢复策略
故障恢复是容错机制的一个重要组成部分,它的目的是在服务出现故障时,能够快速恢复服务的正常运行,减少业务中断时间。
我们在重试策略失败的时候,这个时候触发容错策略,可以把我们的上下文传过来 ,
// 发送TCP请求
// 使用重试策略
RpcResponse response ;
try {
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
response = retryStrategy.doRetry(() -> {
return VertxTcpClient.doRequest(rpcRequest, metaInfo);
});
} catch (Exception e) {
TolerantStrategy strategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
// 构造上下文
Map<String, Object> context = new HashMap<>();
context.put(TolerantStrategyConstant.SERVICE_LIST, serviceMetaInfos);
context.put(TolerantStrategyConstant.CURRENT_SERVICE, metaInfo);
context.put(TolerantStrategyConstant.RPC_REQUEST, rpcRequest);
response = strategy.doTolerant(context, e);
}
return response.getData();
然后去获取所有的服务列表,只要不是当前的服务,都可以重试一次,如果都失败,那就直接抛异常,不重试了。
/**
* @author houyunfei
* 故障转移
*/
@Slf4j
public class FailOverTolerantStrategy implements TolerantStrategy {
/**
* 故障转移 - 重试其他服务
*
* @param context 上下文,用于传递数据
* @param e 异常
* @return
*/
@Override
public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
List<ServiceMetaInfo> metaInfos = (List<ServiceMetaInfo>) context.get(TolerantStrategyConstant.SERVICE_LIST);
ServiceMetaInfo metaInfo = (ServiceMetaInfo) context.get(TolerantStrategyConstant.CURRENT_SERVICE);
RpcRequest rpcRequest = (RpcRequest) context.get(TolerantStrategyConstant.RPC_REQUEST);
if (metaInfos == null || metaInfos.isEmpty()) {
log.error("FailOverTolerantStrategy doTolerant metaInfos is empty");
return null;
}
// 重试metaInfo之外的其他服务
for (ServiceMetaInfo serviceMetaInfo : metaInfos) {
if (serviceMetaInfo.equals(metaInfo)) {
continue;
}
// 重试
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(RpcApplication.getRpcConfig().getRetryStrategy());
try {
return retryStrategy.doRetry(() -> {
return VertxTcpClient.doRequest(rpcRequest, metaInfo);
});
} catch (Exception ex) {
// 如果重试再失败,继续重试下一个
log.error("FailOverTolerantStrategy doTolerant retry fail");
}
}
// 所有服务都重试失败
throw new RuntimeException("FailOverTolerantStrategy doTolerant all retry fail");
}
}
失败恢复策略
这种策略和故障恢复差不多,都是尝试其他服务,只不过这个在故障服务恢复正常后触发,目的是将流量切换回原来的服务实例。
/**
* @author houyunfei
* 降级到其他服务
*/
@Slf4j
public class FailBackTolerantStrategy implements TolerantStrategy {
/**
* 降级到其他服务 - 重试其他服务
*
* @param context 上下文,用于传递数据
* @param e 异常
* @return
*/
@Override
public RpcResponse doTolerant(Map<String, Object> context, Exception e) {
List<ServiceMetaInfo> metaInfos = (List<ServiceMetaInfo>) context.get(TolerantStrategyConstant.SERVICE_LIST);
ServiceMetaInfo metaInfo = (ServiceMetaInfo) context.get(TolerantStrategyConstant.CURRENT_SERVICE);
RpcRequest rpcRequest = (RpcRequest) context.get(TolerantStrategyConstant.RPC_REQUEST);
if (metaInfos == null || metaInfos.isEmpty()) {
log.error("FailOverTolerantStrategy doTolerant metaInfos is empty");
return null;
}
// 重试metaInfo之外的其他服务
for (ServiceMetaInfo serviceMetaInfo : metaInfos) {
if (serviceMetaInfo.equals(metaInfo)) {
continue;
}
// 重试
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(RpcApplication.getRpcConfig().getRetryStrategy());
try {
return retryStrategy.doRetry(() -> {
return VertxTcpClient.doRequest(rpcRequest, metaInfo);
});
} catch (Exception ex) {
// 如果重试再失败,继续重试下一个
log.error("FailOverTolerantStrategy doTolerant retry fail");
}
}
// 所有服务都重试失败
throw new RuntimeException("FailOverTolerantStrategy doTolerant all retry fail");
}
}