Skip to main content

容错机制实现


容错机制实现

什么是容错机制?

容错机制是分布式系统设计中非常重要的一部分,它的目标是在部分组件或节点出现故障时,仍能保证系统整体的正常运转和服务可用性。

这种能力对于构建高可用、高可靠的分布式系统非常关键。

容错机制主要包括以下几个方面:

  1. 故障检测和隔离:

    • 系统需要能够及时发现和定位故障,并将故障节点或服务隔离,防止故障扩散。
    • 可以使用心跳监测、状态检查等手段来检测故障。
    • 当发现故障时,可以采用服务熔断、容器隔离等方式将故障节点隔离。
  2. 请求重试和超时控制:

    • 对于临时性的网络或服务异常,可以采用重试机制来提高成功概率。
    • 重试策略可以是固定时间间隔、指数退避、随机等不同形式。
    • 同时需要设置合理的超时时间,超时后放弃重试,防止无限重试耗尽资源。
  3. 容错路由和负载均衡:

    • 当某个服务节点出现故障时,可以通过容错路由将请求重新分配到其他可用节点。
    • 负载均衡策略也需要考虑容错因素,例如剔除故障节点、动态调整权重等。
  4. 服务降级和业务兜底:

    • 当依赖的关键服务出现故障时,可以采用服务降级,临时使用备用方案或返回默认响应。
    • 通过业务层面的兜底措施,保证核心功能的可用性。
  5. 资源隔离和限流:

    • 将不同服务或模块的资源进行隔离,例如使用容器、虚拟机等技术。
    • 对关键服务实施限流,防止被大量请求冲垮。
  6. 数据备份和恢复:

    • 定期备份系统状态和业务数据,以便在发生故障时快速恢复。
    • 备份方案包括数据备份、日志备份、配置备份等。
  7. 监控报警和自愈机制:

    • 建立完善的监控体系,实时检测系统运行状态,及时发现并报警异常。
    • 结合其他容错手段,设计自动化的故障修复和自愈流程。

为什么要容错机制?

分布式系统需要使用容错机制主要有以下几个原因:

  1. 提高系统可用性

    • 分布式系统由多个独立组件构成,任何一个组件的故障都可能导致整个系统不可用。
    • 容错机制可以在部分组件出现故障时,保证系统整体仍能正常提供服务,提高可用性。
  2. 降低故障影响

    • 在分布式环境下,一个故障可能会通过调用链在系统中传播,导致级联故障。
    • 容错机制可以及时隔离故障,阻止其扩散,降低故障对整个系统的影响。
  3. 增强系统弹性

    • 分布式系统面临各种不确定因素,如网络延迟、服务器故障等。
    • 容错机制可以让系统在面对这些不确定性时,仍能保持稳定和可靠的运行。
  4. 支持高并发和扩展性

    • 分布式系统通常需要支持高并发访问和动态扩展。
    • 容错机制可以在系统扩展或负载增大时,确保服务质量不会下降。
  5. 满足业务连续性要求

    • 许多关键业务系统需要实现7*24小时的持续运行。
    • 容错机制可以确保业务在出现故障时仍能快速恢复,减少业务中断。
  6. 降低运维成本

    • 容错机制可以自动化地处理和修复故障,减少人工介入。
    • 这可以降低系统维护的人力和时间成本。

容错机制是构建高可用、高可靠分布式系统的关键所在。它可以显著提高系统的抗风险能力,确保业务连续性,为用户提供稳定可靠的服务。这对于许多关键性的分布式应用来说是非常必要的。

容错机制有哪些?

参考Dubbo的实现,

在Dubbo的文档中也介绍了这些容错策略:https://cn.dubbo.apache.org/zh-cn/blog/2018/08/22/dubbo-集群容错/open in new window

  1. Failover (失败自动切换):

    • 当某个服务提供者出现故障时,自动切换到备用的服务提供者。
    • 这样可以在某个节点出现问题时,保证服务的可用性。
    • 通常会配合负载均衡策略使用,确保流量能够自动分配到可用的节点上。
  2. Failsafe (失败安全):

    • 当服务调用出现异常时,直接返回一个安全的默认值或者空值。
    • 这种策略适用于一些对结果容忍度较高的场景,比如日志记录、缓存预热等。
    • 通过快速返回,可以避免阻塞调用链,提高系统的整体可用性。
  3. Failfast (快速失败):

    • 当服务调用出现异常时,立即抛出异常,不进行重试或降级。
    • 这种策略适用于对响应时间敏感的场景,比如用户交互界面。
    • 快速失败可以减少系统资源的占用,但需要在上层进行更好的异常处理。
  4. Failback (失败自动恢复):

    • 当服务提供者恢复正常后,自动恢复对该服务的调用。
    • 这种策略通常与Failover一起使用,可以在故障恢复后,自动切换回正常的服务节点。
    • 这样可以最大程度地减少服务中断的时间。
  5. Forking (并行调用):

    • 当调用一个服务时,同时向多个服务提供者发起并行调用。
    • 只要有一个调用成功,就返回结果,其他的调用则会被取消。
    • 这种策略可以提高服务的可靠性,但会增加资源消耗。适用于对响应时间有严格要求的场景。
  6. Broadcast (广播调用):

    • 当调用一个服务时,向所有已知的服务提供者发起调用。
    • 所有提供者的响应都会被收集和合并,返回给调用方。
    • 这种策略可以提高服务的可用性,但会增加网络开销。适用于需要聚合多个服务结果的场景。

容错方案的设计

  1. 先容错再重试

  2. 先重试再容错

容错机制实现

除了上述的策略之外,很多 技术都可以算得上是容错,例如:

  1. 重试:重试本身就是容错的降级策略,系统出现错误后再重试
  2. 限流:如果系统压力过大,已经出现部分错误,那么可以限制请求的频率数量来进行保护
  3. 降级:出现错误之后,可以变成执行其他更稳定 的操作,也称兜底,
  4. 熔断:出现故障或者异常,暂停服务,避免连锁故障
  5. 超时控制:长时间没有处理完成,就中断,防止阻塞和资源占用

容错策略接口定义

我们定义的 TolerantStrategy 接口主要有两个参数:

  1. Map<String, Object> context

    • 这个参数是一个上下文对象,用于在容错处理过程中传递一些数据。
    • 在分布式系统中,当一个远程调用出现异常时,我们需要根据当前的上下文信息来决定如何进行容错处理。
    • 这个上下文可以包含一些关键信息,例如:
      • 当前请求的参数
      • 调用链路信息
      • 服务实例的元数据
      • 重试次数等
    • 通过这个上下文对象,容错策略实现可以获取到更丰富的信息,从而做出更加合理的容错决策。
  2. Exception e

    • 这个参数表示在执行远程调用时出现的异常。
    • 容错策略需要根据异常的类型、错误信息等,来决定采取什么样的容错措施。
    • 例如,对于网络异常可以选择重试,而对于业务异常可能需要降级或返回默认响应。
    • 通过分析异常信息,容错策略可以更有针对性地进行容错处理。
  3. 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即可,可以通过构造函数传入

静默处理策略的特点是:

  1. 不通知调用方失败:

    • 当服务调用出现异常时,不会抛出异常,也不会返回错误响应。
    • 而是直接返回一个默认的响应结果。
  2. 只记录日志:

    • 异常信息仅仅通过日志的形式记录下来,方便事后排查问题。
    • 但不会将异常信息直接返回给调用方。
  3. 适用场景:

    • 这种策略适用于对最终结果不太敏感的场景,比如日志记录、缓存预热等。
    • 即使服务调用失败,也不会影响业务的核心逻辑。
  4. 优缺点:

    • 优点是简单易实现,对系统负载影响小。
    • 缺点是可能会丢失一些有价值的业务信息,无法保证最终一致性。
/**
 * @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");
    }
}