阿里一面:RPC框架容错层应该怎么考虑

0 阅读5分钟

前言

大家好,我是田螺。

分享一道阿里面试题:RPC框架容错层应该怎么考虑呢?

想象一下这样的场景:你在淘宝下单时点击"支付"按钮,却因为某个服务节点故障导致整个支付功能崩溃,这是多么糟糕的体验!RPC框架的容错层就像系统的"防弹衣",专门处理各种意外情况。今天田螺哥跟大家聊聊设计这个防护层需要考虑的关键点。

  • 服务调用失败的三重防护
  • 服务端防护的四大金刚
  • 进阶防护
  • 公众号捡田螺的小男孩
  • github地址,感谢每颗star:github

1. 服务调用失败的三重防护

1.1 异常信息避免透传

当服务端出现异常时,容错层需要像"快递员"一样准确传递包裹(异常信息)。这里有个关键点:把Java的Throwable序列化为JSON时,要像"海关检查"一样过滤敏感信息。比如堆栈跟踪这种"内部机密"需要脱敏处理,只返回错误码和友好提示

json
{
  "code": "0001",
  "message": "当前服务繁忙,请稍后再试",
  "timestamp": 1627548399000
}

image.png

2.重试策略设计

重试机制就像追女生,死缠烂打可能适得其反。我们需要设计智能策略:

  • 超时重试(默认等待1秒)
  • 异常类型判断(数据库连接失败重试,参数错误不重试)
  • 指数退避策略(第1次等1秒,第2次等2秒,第3次等4秒)
  • 最大重试次数控制(通常不超过3次)

有关于重试,田螺哥推荐大家可以使用guava的Retryer组件,挺好用的~

public class RetryDemo {

    // 定义需要重试的异常白名单
    private static final Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
            // 遇到IO异常或超时重试
            .retryIfExceptionOfType(IOException.class)
            .retryIfRuntimeException()
            // 参数错误不重试(自定义判断)
            .retryIfResult(result -> result == false)
            // 最大重试3次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            // 指数退避(1s, 2s, 4s)
            .withWaitStrategy(WaitStrategies.exponentialWait(
                    1000,  // 初始等待1秒
                    5000,  // 最大等待5秒
                    TimeUnit.MILLISECONDS))
            // 超时控制(单次调用超时1秒)
            .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(1, TimeUnit.SECONDS))
            .build();

    public static void main(String[] args) {
        try {
            Boolean result = retryer.call(new Callable<Boolean>() {
                int count = 0;
                @Override
                public Boolean call() throws Exception {
                    System.out.println("尝试第" + (++count) + "次调用");
                    // 模拟不同异常场景
                    if(count == 1) throw new IOException("数据库连接失败"); // 触发重试
                    if(count == 2) throw new IllegalArgumentException("参数错误"); // 不重试
                    return true;
                }
            });
            System.out.println("最终结果:" + result);
        } catch (RetryException | ExecutionException e) {
            System.err.println("所有重试失败,最后异常:" + e.getMessage());
        }
    }
}

1.3 熔断器模式

借鉴电路保险丝的智慧,当失败率超过阈值(如50%)时自动"跳闸"。此时:

  • 直接返回降级结果
  • 定时尝试恢复(如每隔30秒试探请求)
  • 半开状态监测(允许少量请求测试服务状态)

2. 服务端防护的四大金刚

2.1 连接数限制

像餐厅限制就餐人数保证服务质量,我们可以:

// 使用Semaphore控制最大连接数
public class ConnectionLimiter {
   private static final int MAX_CONNECTIONS = 200;
   private final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);

   public boolean tryAcquire() {
       return semaphore.tryAcquire();
   }

   public void release() {
       semaphore.release();
   }
}

2.2 多维限流策略

限流就像高速公路的匝道控制

  • 令牌桶算法:匀速发放通行证(适合平滑流量)
  • 漏斗算法通过固定速率的"漏水"过程控制流量,无论请求多突发,系统处理速率始终保持恒定,类似水从漏斗中匀速流出
  • 滑动窗口计数:实时统计最近1秒请求数(反应灵敏)

我们来实现一个简单的漏铜算法吧:

image.png

public class FunnelRateLimiter {
    static class Funnel {
        int capacity;        // 漏斗容量
        float leakingRate;   // 漏水速率(单位:quota/s)
        int leftQuota;       // 剩余容量
        long leakTs;         // 上次漏水时间

        void makeSpace() {
            long now = System.currentTimeMillis();
            long deltaTs = now - leakTs;
            int deltaQuota = (int)(deltaTs * leakingRate / 1000);
            if (deltaQuota < 1) return;
            leftQuota = Math.min(capacity, leftQuota + deltaQuota);
            leakTs = now;
        }

        boolean tryAcquire(int quota) {
            makeSpace();
            if (leftQuota >= quota) {
                leftQuota -= quota;
                return true;
            }
            return false;
        }
    }
    
    private Map<String, Funnel> funnels = new ConcurrentHashMap<>();
    
    public boolean allowRequest(String key, int capacity, float leakingRate) {
        Funnel funnel = funnels.computeIfAbsent(key, 
            k -> new Funnel(capacity, leakingRate));
        return funnel.tryAcquire(1);
    }
}

有关限流的,我们可以使用guava的RateLimiter 做简单限流,也可以使用Sentinel组件。

2.3 服务降级

准备多种"Plan B"应对突发情况:

  • 返回缓存数据
  • 提供简化版服务
  • 抛出标准化的业务异常

2.4 隔离舱设计

把不同服务分配到独立"船舱",避免一个服务故障引发连锁反应。可以通过线程池隔离或信号量隔离实现,就像把不同品类的海鲜分池养殖。

image.png

3. 进阶防护手段

3.1 流量灰度发布

给测试流量打上特殊标记,像给实验小白鼠染色一样,让它们只路由到指定服务器,避免影响线上用户。

3.2 混沌工程实践

测试阶段,或者灰度模拟验证阶段,主动制造一些"捣乱",来验证系统韧性,比如:

  • 随机杀掉服务节点
  • 模拟网络延迟
  • 制造磁盘IO故障

3.3 监控

  • 建立立体的监控网络:
  • 实时流量热力图
  • 异常调用链追踪(类似DNA检测)
  • 历史故障模式分析

最后

好的容错设计就像中医调理,需要"望闻问切"全方位考虑。从基础的重试、限流到高级的混沌工程,每个环节都需要精心设计。记住:容错不是追求绝对不失败,而是让失败变得优雅可控,就像飞机引擎失效时还有备用系统能安全着陆。