前言
大家好,我是田螺。
分享一道阿里面试题:RPC框架容错层应该怎么考虑呢?
想象一下这样的场景:你在淘宝下单时点击"支付"按钮,却因为某个服务节点故障导致整个支付功能崩溃,这是多么糟糕的体验!RPC框架的容错层就像系统的"防弹衣",专门处理各种意外情况。今天田螺哥跟大家聊聊设计这个防护层需要考虑的关键点。
- 服务调用失败的三重防护
- 服务端防护的四大金刚
- 进阶防护
- 公众号:捡田螺的小男孩
- github地址,感谢每颗star:github
1. 服务调用失败的三重防护
1.1 异常信息避免透传
当服务端出现异常时,容错层需要像"快递员"一样准确传递包裹(异常信息)。这里有个关键点:把Java的Throwable序列化为JSON时,要像"海关检查"一样过滤敏感信息。比如堆栈跟踪这种"内部机密"需要脱敏处理,只返回错误码和友好提示:
json
{
"code": "0001",
"message": "当前服务繁忙,请稍后再试",
"timestamp": 1627548399000
}
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秒请求数(反应灵敏)
我们来实现一个简单的漏铜算法吧:
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 隔离舱设计
把不同服务分配到独立"船舱",避免一个服务故障引发连锁反应。可以通过线程池隔离或信号量隔离实现,就像把不同品类的海鲜分池养殖。
3. 进阶防护手段
3.1 流量灰度发布
给测试流量打上特殊标记,像给实验小白鼠染色一样,让它们只路由到指定服务器,避免影响线上用户。
3.2 混沌工程实践
测试阶段,或者灰度模拟验证阶段,主动制造一些"捣乱",来验证系统韧性,比如:
- 随机杀掉服务节点
- 模拟网络延迟
- 制造磁盘IO故障
3.3 监控
- 建立立体的监控网络:
- 实时流量热力图
- 异常调用链追踪(类似DNA检测)
- 历史故障模式分析
最后
好的容错设计就像中医调理,需要"望闻问切"全方位考虑。从基础的重试、限流到高级的混沌工程,每个环节都需要精心设计。记住:容错不是追求绝对不失败,而是让失败变得优雅可控,就像飞机引擎失效时还有备用系统能安全着陆。