RPC调用在分布式系统中无处不在,但超时和异常处理却是很多开发者最容易忽视的部分。想象一下,你在调用一个远程服务时,对方服务挂了或者网络卡了,你的程序会一直等着等到什么时候?这就是超时控制要解决的问题。而异常处理,则是当事情出了岔子时,你的程序该怎么优雅地应对,而不是直接崩溃。
一、RPC调用超时控制的基本概念
1.1 什么是RPC超时
RPC(Remote Procedure Call,远程过程调用)说白了就是让本地代码去调用远程服务器的方法,就像你在本地调函数一样简单。但问题是,这个远程服务器可能不在线,或者网络太慢,你的调用就会一直等下去。超时控制就是给这个等待设一个上限时间,超过这个时间就不等了,直接告诉调用方"超时了"。
打个比方,你给快递员打电话问快递到哪了,你不可能一直等对方接电话吧?你会等个十几秒,如果对方没接,你就会挂掉,然后再打或者换个方式联系。这个"等个十几秒"就是超时时间。
1.2 超时的常见场景
在实际开发中,超时可能出现在这些地方:
- 网络延迟:服务端和客户端之间的网络不稳定,数据包传输慢
- 服务端处理慢:服务端正在处理大量请求,队列满了,你的请求排队等了很久
- 服务端宕机:服务端直接挂了,你的请求石沉大海
- 数据库查询慢:服务端在执行SQL时,查询走了全表扫描,执行了很长时间
二、超时控制的实现方案
2.1 客户端超时配置
客户端超时是最直接的超时控制方式。你在调用远程方法时,告诉框架"我最多等多久",超过这个时间框架就会主动中断等待,抛出一个超时异常。
以gRPC为例,你可以通过拦截器或者调用选项来设置超时:
/**
* gRPC 客户端超时配置示例
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
@Component
public class GrpcClientConfig {
/**
* 创建带有超时设置的拦截器
* 每个RPC调用都会带上这个超时时间
*/
public ClientInterceptor timeoutInterceptor(long timeoutMs) {
return (call, headers, next) -> {
ClientCall<ByteString, ByteString> clonedCall = call;
clonedCall.setDeadline(System.nanoTime() + Duration.ofMillis(timeoutMs).toNanos(), TimeUnit.NANOSECONDS);
return next.startCall(call, headers);
};
}
/**
* 全局超时拦截器
* 所有RPC调用统一设置超时时间
*/
@Bean
public ClientInterceptor globalTimeoutInterceptor() {
return new io.grpc.ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions,
Channel next) {
// 如果没有设置超时,则使用默认超时时间5秒
if (callOptions.getDeadlineNanoTime() == 0) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
callOptions = callOptions.withDeadline(deadline, TimeUnit.NANOSECONDS);
}
return next.newCall(method, callOptions);
}
};
}
}
2.2 服务端超时处理
服务端超时是站在服务提供方的角度做控制。即使客户端没设超时,服务端也应该设一个"最大处理时间",避免某个请求占用资源太久。
/**
* gRPC 服务端超时处理示例
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
private final UserServiceMapper userMapper;
public UserServiceImpl(UserServiceMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 获取用户信息
* 服务端设置最大处理时间3秒
*/
@Override
public void getUser(GetUserRequest request, StreamObserver<GetUserResponse> responseObserver) {
try {
// 检查是否超时
long deadline = responseObserver.getDeadline();
if (deadline != 0) {
long remaining = deadline - System.nanoTime();
if (remaining < 0) {
throw Status.DEADLINE_EXCEEDED
.withDescription("服务端处理超时,请重试")
.asRuntimeException();
}
}
// 执行业务逻辑
User user = userMapper.findById(request.getUserId());
if (user == null) {
responseObserver.onError(Status.NOT_FOUND
.withDescription("用户不存在")
.asRuntimeException());
return;
}
GetUserResponse response = GetUserResponse.newBuilder()
.setUserId(user.getId())
.setUsername(user.getUsername())
.setEmail(user.getEmail())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
// 已经是gRPC状态异常,直接传递
responseObserver.onError(e);
} catch (Exception e) {
// 未知异常,包装为INTERNAL错误
responseObserver.onError(Status.INTERNAL
.withDescription("服务端内部错误:" + e.getMessage())
.withCause(e)
.asRuntimeException());
}
}
}
2.3 超时传播与链路追踪
在分布式系统中,一个请求可能经过多个服务。如果第一个服务设了3秒超时,但它调用的第二个服务要4秒才能返回,第一个服务就会超时。这时候就需要超时传播——第一个服务把自己的剩余超时时间传给第二个服务,让第二个服务知道自己最多只能用多少时间。
/**
* 超时传播示例 - 服务A调用服务B时传递剩余超时时间
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
@Service
public class OrderService {
private final PaymentServiceGrpc.PaymentServiceBlockingStub paymentStub;
private final InventoryServiceGrpc.InventoryServiceBlockingStub inventoryStub;
public OrderService(
PaymentServiceGrpc.PaymentServiceBlockingStub paymentStub,
InventoryServiceGrpc.InventoryServiceBlockingStub inventoryStub) {
this.paymentStub = paymentStub;
this.inventoryStub = inventoryStub;
}
/**
* 创建订单 - 包含超时传播逻辑
* 总超时时间5秒,分配给下游服务
*/
public OrderResult createOrder(CreateOrderRequest request) {
long startTime = System.nanoTime();
long totalTimeoutMs = 5000;
// 第一步:检查库存(分配1秒超时)
long inventoryTimeout = Math.min(1000, totalTimeoutMs - elapsed(startTime));
try {
InventoryResponse invResponse = inventoryStub
.withDeadlineAfter(inventoryTimeout, TimeUnit.MILLISECONDS)
.checkStock(request.getProductId());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
return OrderResult.failure("库存查询超时,请稍后重试");
}
throw e;
}
// 第二步:支付处理(分配剩余时间)
long paymentTimeout = totalTimeoutMs - elapsed(startTime);
try {
PaymentResponse payResponse = paymentStub
.withDeadlineAfter(paymentTimeout, TimeUnit.MILLISECONDS)
.processPayment(request.getPaymentInfo());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
return OrderResult.failure("支付处理超时,请检查支付状态");
}
throw e;
}
return OrderResult.success("订单创建成功");
}
/**
* 计算已消耗的时间(毫秒)
*/
private long elapsed(long startTime) {
return (System.nanoTime() - startTime) / 1_000_000;
}
}
三、异常处理的完整方案
3.1 异常分类与处理方式
在RPC调用中,异常可以分为几大类:
- 超时异常:调用时间超过预设的超时时间
- 网络异常:连接不上服务端,DNS解析失败等
- 业务异常:服务端返回的业务错误,比如用户不存在、余额不足
- 序列化异常:请求或响应数据格式不对,无法解析
- 未知异常:其他未预见的异常
不同的异常类型需要不同的处理策略。比如网络异常可以重试,但业务异常重试也没用。
/**
* 异常分类处理示例
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
public class RpcExceptionClassifier {
/**
* 根据gRPC状态码分类异常,并返回对应的处理策略
*/
public static ExceptionStrategy classify(StatusRuntimeException e) {
Status.Code code = e.getStatus().getCode();
switch (code) {
case DEADLINE_EXCEEDED:
return new ExceptionStrategy(true, "请求超时,正在重试...", ExceptionType.TIMEOUT);
case UNAVAILABLE:
return new ExceptionStrategy(true, "服务暂时不可用,请稍后重试", ExceptionType.UNAVAILABLE);
case NOT_FOUND:
return new ExceptionStrategy(false, "请求的资源不存在", ExceptionType.NOT_FOUND);
case INVALID_ARGUMENT:
return new ExceptionStrategy(false, "请求参数错误:" + e.getStatus().getDescription(), ExceptionType.INVALID_ARGUMENT);
case PERMISSION_DENIED:
return new ExceptionStrategy(false, "权限不足,无法访问该资源", ExceptionType.PERMISSION_DENIED);
case INTERNAL:
return new ExceptionStrategy(true, "服务内部错误,正在重试...", ExceptionType.INTERNAL);
default:
return new ExceptionStrategy(false, "未知错误:" + code.name(), ExceptionType.UNKNOWN);
}
}
}
/**
* 异常处理策略
*/
public class ExceptionStrategy {
private final boolean retryable;
private final String userMessage;
private final ExceptionType type;
public ExceptionStrategy(boolean retryable, String userMessage, ExceptionType type) {
this.retryable = retryable;
this.userMessage = userMessage;
this.type = type;
}
public boolean isRetryable() { return retryable; }
public String getUserMessage() { return userMessage; }
public ExceptionType getType() { return type; }
}
/**
* 异常类型枚举
*/
public enum ExceptionType {
TIMEOUT,
UNAVAILABLE,
NOT_FOUND,
INVALID_ARGUMENT,
PERMISSION_DENIED,
INTERNAL,
UNKNOWN
}
3.2 重试机制
重试是处理临时性故障最有效的手段。但重试不是随便重试,需要遵循一些原则:
- 指数退避:第一次等1秒,第二次等2秒,第三次等4秒……这样不会把所有请求集中在一起
- 最大重试次数:最多重试3次,防止无限循环
- 幂等性检查:只有幂等操作才重试,比如查询可以重试,但"扣款"这种操作不能随便重试
/**
* 带重试机制的RPC调用工具类
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
public class RetryableRpcClient {
private static final int MAX_RETRIES = 3;
private static final long INITIAL_DELAY_MS = 1000;
private static final double BACKOFF_MULTIPLIER = 2.0;
private static final long MAX_DELAY_MS = 10000;
/**
* 带重试的RPC调用方法
*/
public <T> T callWithRetry(Supplier<T> call, String operationName, boolean isIdempotent) {
int attempt = 0;
long delay = INITIAL_DELAY_MS;
while (true) {
attempt++;
try {
return call.get();
} catch (StatusRuntimeException e) {
ExceptionStrategy strategy = RpcExceptionClassifier.classify(e);
if (!strategy.isRetryable() || !isIdempotent || attempt > MAX_RETRIES) {
throw new RpcCallException(operationName, strategy, e);
}
log.warn("RPC调用失败,正在第{}次重试: operation={}, error={}, delay={}ms",
attempt, operationName, strategy.getUserMessage(), delay);
try {
Thread.sleep(Math.min(delay, MAX_DELAY_MS));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RpcCallException(operationName, strategy, ie);
}
delay = (long) (delay * BACKOFF_MULTIPLIER);
} catch (Exception e) {
throw new RpcCallException(operationName,
new ExceptionStrategy(false, "系统错误", ExceptionType.UNKNOWN), e);
}
}
}
}
/**
* RPC调用异常
*/
public class RpcCallException extends RuntimeException {
private final String operationName;
private final ExceptionStrategy strategy;
public RpcCallException(String operationName, ExceptionStrategy strategy, Throwable cause) {
super(operationName + " failed: " + strategy.getUserMessage(), cause);
this.operationName = operationName;
this.strategy = strategy;
}
public String getOperationName() { return operationName; }
public ExceptionStrategy getStrategy() { return strategy; }
}
3.3 熔断与降级
熔断机制的思想是:当一个服务连续失败多次后,不再调用它,直接返回错误或者降级结果。这样可以避免在下游服务已经明显有问题的情况下继续浪费资源。
/**
* 熔断器实现 - 基于滑动窗口的熔断策略
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
public class CircuitBreaker {
/** 熔断状态枚举 */
public enum State {
CLOSED,
OPEN,
HALF_OPEN
}
private volatile State state = State.CLOSED;
private final AtomicInteger failureCount = new AtomicInteger(0);
private final AtomicInteger successCount = new AtomicInteger(0);
private volatile long lastFailureTime = 0;
private volatile long lastOpenTime = 0;
private final int failureThreshold;
private final long recoveryTimeoutMs;
private final int halfOpenMaxCalls;
private final AtomicInteger halfOpenCalls = new AtomicInteger(0);
public CircuitBreaker(int failureThreshold, long recoveryTimeoutMs, int halfOpenMaxCalls) {
this.failureThreshold = failureThreshold;
this.recoveryTimeoutMs = recoveryTimeoutMs;
this.halfOpenMaxCalls = halfOpenMaxCalls;
}
/**
* 执行受熔断保护的RPC调用
*/
public <T> T execute(Supplier<T> call, Supplier<T> fallback) {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastOpenTime >= recoveryTimeoutMs) {
transitionToHalfOpen();
} else {
log.warn("熔断器打开,执行降级逻辑");
return fallback.get();
}
}
if (state == State.HALF_OPEN) {
if (halfOpenCalls.incrementAndGet() > halfOpenMaxCalls) {
halfOpenCalls.decrementAndGet();
return fallback.get();
}
}
try {
T result = call.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
throw e;
}
}
private void onSuccess() {
int success = successCount.incrementAndGet();
if (state == State.HALF_OPEN && success >= halfOpenMaxCalls) {
transitionToClosed();
}
}
private void onFailure() {
int failure = failureCount.incrementAndGet();
lastFailureTime = System.currentTimeMillis();
if (failure >= failureThreshold) {
transitionToOpen();
}
}
private void transitionToHalfOpen() {
state = State.HALF_OPEN;
halfOpenCalls.set(0);
log.info("熔断器进入半开状态,开始试探调用");
}
private void transitionToClosed() {
state = State.CLOSED;
failureCount.set(0);
successCount.set(0);
log.info("熔断器关闭,恢复正常调用");
}
private void transitionToOpen() {
state = State.OPEN;
lastOpenTime = System.currentTimeMillis();
log.warn("熔断器打开,拒绝新的调用");
}
}
/**
* 熔断器使用示例
*/
@Service
public class PaymentService {
private final CircuitBreaker circuitBreaker;
private final PaymentServiceGrpc.PaymentServiceBlockingStub paymentStub;
public PaymentService(PaymentServiceGrpc.PaymentServiceBlockingStub paymentStub) {
this.paymentStub = paymentStub;
this.circuitBreaker = new CircuitBreaker(5, 30000, 3);
}
/**
* 带熔断保护的支付处理
*/
public PaymentResult processPayment(PaymentRequest request) {
return circuitBreaker.execute(
() -> {
PaymentResponse response = paymentStub
.withDeadlineAfter(3000, TimeUnit.MILLISECONDS)
.processPayment(convertToProto(request));
return PaymentResult.success(response.getTransactionId());
},
() -> {
log.warn("支付服务熔断,执行降级逻辑");
return PaymentResult.pending("支付处理中,请稍后查询结果");
}
);
}
}
四、完整实战示例
4.1 技术栈说明
以下示例统一使用以下技术栈:
- Java 17
- Spring Boot 3.2
- gRPC 1.59
- Protobuf 3.24
- Lombok
4.2 Protobuf 定义
/**
* 用户服务 gRPC 接口定义
* 文件:proto/user_service.proto
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59 + Protobuf 3.24
*/
syntax = "proto3";
package user_service;
option java_package = "com.example.user.grpc";
option java_outer_classname = "UserServiceProto";
message User {
int64 user_id = 1;
string username = 2;
string email = 3;
string phone = 4;
bool enabled = 5;
}
message GetUserRequest {
int64 user_id = 1;
}
message GetUserResponse {
User user = 1;
}
message CreateUserRequest {
string username = 1;
string email = 2;
string phone = 3;
}
message CreateUserResponse {
int64 user_id = 1;
string message = 2;
}
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
}
4.3 服务端完整实现
/**
* 用户服务 gRPC 服务端实现
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59 + Protobuf 3.24
*/
@GrpcService
@Slf4j
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
private final UserRepository userRepository;
private final ObjectMapper objectMapper;
public UserServiceImpl(UserRepository userRepository, ObjectMapper objectMapper) {
this.userRepository = userRepository;
this.objectMapper = objectMapper;
}
/**
* 获取用户信息
*
* 超时处理:
* - 检查客户端设置的deadline
* - 处理数据库查询超时的情况
* - 处理业务异常(用户不存在)
*/
@Override
public void getUser(GetUserRequest request, StreamObserver<GetUserResponse> responseObserver) {
long startTime = System.currentTimeMillis();
log.info("收到获取用户请求, userId={}, traceId={}", request.getUserId(), TraceIdUtils.get());
try {
// 1. 参数校验
if (request.getUserId() <= 0) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("用户ID必须大于0")
.asRuntimeException());
return;
}
// 2. 检查是否超时(客户端设置的deadline)
long deadlineNanos = responseObserver.getDeadline();
if (deadlineNanos != 0) {
long remainingMs = (deadlineNanos - System.nanoTime()) / 1_000_000;
if (remainingMs < 100) {
throw Status.DEADLINE_EXCEEDED
.withDescription("剩余时间不足,请求被拒绝")
.asRuntimeException();
}
}
// 3. 查询数据库
User user = userRepository.findById(request.getUserId())
.orElse(null);
// 4. 处理业务逻辑
if (user == null) {
responseObserver.onError(Status.NOT_FOUND
.withDescription("用户不存在: userId=" + request.getUserId())
.asRuntimeException());
return;
}
// 5. 构建响应
UserProto userProto = UserProto.newBuilder()
.setUserId(user.getId())
.setUsername(user.getUsername())
.setEmail(user.getEmail())
.setPhone(user.getPhone())
.setEnabled(user.isEnabled())
.build();
GetUserResponse response = GetUserResponse.newBuilder()
.setUser(userProto)
.build();
// 6. 发送响应
responseObserver.onNext(response);
responseObserver.onCompleted();
log.info("获取用户成功, userId={}, 耗时={}ms",
request.getUserId(), System.currentTimeMillis() - startTime);
} catch (StatusRuntimeException e) {
log.error("获取用户失败, userId={}, error={}",
request.getUserId(), e.getStatus().getDescription());
responseObserver.onError(e);
} catch (DataAccessException e) {
log.error("数据库查询失败, userId={}, error={}",
request.getUserId(), e.getMessage());
responseObserver.onError(Status.UNAVAILABLE
.withDescription("数据库服务暂时不可用,请稍后重试")
.withCause(e)
.asRuntimeException());
} catch (Exception e) {
log.error("获取用户发生未知异常, userId={}",
request.getUserId(), e);
responseObserver.onError(Status.INTERNAL
.withDescription("系统内部错误")
.withCause(e)
.asRuntimeException());
}
}
/**
* 创建用户 - 非幂等操作,不应该随意重试
*/
@Override
public void createUser(CreateUserRequest request, StreamObserver<CreateUserResponse> responseObserver) {
long startTime = System.currentTimeMillis();
log.info("收到创建用户请求, username={}, traceId={}",
request.getUsername(), TraceIdUtils.get());
try {
// 1. 参数校验
validateCreateRequest(request);
// 2. 检查用户名是否已存在
Optional<User> existing = userRepository.findByUsername(request.getUsername());
if (existing.isPresent()) {
responseObserver.onError(Status.ALREADY_EXISTS
.withDescription("用户名已存在: " + request.getUsername())
.asRuntimeException());
return;
}
// 3. 保存用户
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setEnabled(true);
userRepository.save(user);
// 4. 构建响应
CreateUserResponse response = CreateUserResponse.newBuilder()
.setUserId(user.getId())
.setMessage("用户创建成功")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
log.info("创建用户成功, userId={}, username={}, 耗时={}ms",
user.getId(), user.getUsername(), System.currentTimeMillis() - startTime);
} catch (StatusRuntimeException e) {
log.error("创建用户失败, username={}, error={}",
request.getUsername(), e.getStatus().getDescription());
responseObserver.onError(e);
} catch (Exception e) {
log.error("创建用户发生未知异常, username={}",
request.getUsername(), e);
responseObserver.onError(Status.INTERNAL
.withDescription("系统内部错误")
.withCause(e)
.asRuntimeException());
}
}
/**
* 校验创建用户请求参数
*/
private void validateCreateRequest(CreateUserRequest request) {
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
throw Status.INVALID_ARGUMENT
.withDescription("用户名不能为空")
.asRuntimeException();
}
if (request.getUsername().length() > 50) {
throw Status.INVALID_ARGUMENT
.withDescription("用户名长度不能超过50个字符")
.asRuntimeException();
}
if (!request.getEmail().matches("^[\\w.-]+@[\\w.-]+\\.\\w+$")) {
throw Status.INVALID_ARGUMENT
.withDescription("邮箱格式不正确")
.asRuntimeException();
}
}
}
4.4 客户端完整实现
/**
* 用户服务 gRPC 客户端实现
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59 + Protobuf 3.24
*/
@Service
@Slf4j
public class UserServiceClient {
private final UserServiceGrpc.UserServiceBlockingStub blockingStub;
private final CircuitBreaker circuitBreaker;
private final RetryableRpcClient retryClient;
private static final long DEFAULT_TIMEOUT_MS = 5000;
private static final long READ_TIMEOUT_MS = 3000;
private static final long WRITE_TIMEOUT_MS = 2000;
public UserServiceClient(
ManagedChannel channel,
CircuitBreaker circuitBreaker,
RetryableRpcClient retryClient) {
this.blockingStub = UserServiceGrpc.newBlockingStub(channel);
this.circuitBreaker = circuitBreaker;
this.retryClient = retryClient;
}
/**
* 获取用户信息 - 带完整的超时控制和异常处理
*/
public UserProto getUser(long userId) {
return circuitBreaker.execute(
() -> retryClient.callWithRetry(
() -> {
long startTime = System.currentTimeMillis();
log.debug("调用获取用户, userId={}", userId);
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(userId)
.build();
GetUserResponse response = blockingStub
.withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.getUser(request);
log.debug("获取用户成功, userId={}, 耗时={}ms",
userId, System.currentTimeMillis() - startTime);
return response.getUser();
},
"GetUser",
true
),
() -> {
log.warn("获取用户熔断,返回降级结果, userId={}", userId);
return UserProto.newBuilder()
.setUserId(userId)
.setUsername("未知用户")
.setEnabled(false)
.build();
}
);
}
/**
* 创建用户 - 不重试(非幂等操作)
*/
public CreateUserResponse createUser(String username, String email, String phone) {
return circuitBreaker.execute(
() -> {
long startTime = System.currentTimeMillis();
log.debug("调用创建用户, username={}", username);
CreateUserRequest request = CreateUserRequest.newBuilder()
.setUsername(username)
.setEmail(email)
.setPhone(phone)
.build();
CreateUserResponse response = blockingStub
.withDeadlineAfter(WRITE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.createUser(request);
log.debug("创建用户成功, 耗时={}ms", System.currentTimeMillis() - startTime);
return response;
},
() -> {
throw new RpcCallException("CreateUser",
new ExceptionStrategy(false, "用户创建服务暂时不可用,请稍后重试",
ExceptionType.UNAVAILABLE),
new RuntimeException("熔断器打开"));
}
);
}
/**
* 批量获取用户 - 并行调用优化
*/
public List<UserProto> getUserBatch(List<Long> userIds, long timeoutMs) {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
List<Future<UserProto>> futures = new ArrayList<>();
for (long userId : userIds) {
futures.add(CompletableFuture.supplyAsync(() -> {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(userId)
.build();
GetUserResponse response = blockingStub
.withDeadline(deadline, TimeUnit.NANOSECONDS)
.getUser(request);
return response.getUser();
}));
}
List<UserProto> results = new ArrayList<>();
for (Future<UserProto> future : futures) {
try {
UserProto user = future.get(timeoutMs, TimeUnit.MILLISECONDS);
if (user != null) {
results.add(user);
}
} catch (TimeoutException e) {
log.warn("批量获取用户超时, future已取消");
future.cancel(true);
} catch (Exception e) {
log.warn("批量获取用户失败", e);
}
}
return results;
}
}
4.5 配置文件
# application.yaml - gRPC 超时和重试配置
# 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
grpc:
server:
port: 9090
max-inbound-message-size: 10485760
max-inbound-metadata-size: 8192
client:
default-timeout: 5s
connect-timeout: 3s
deadline: 10s
retry:
max-attempts: 3
initial-backoff: 1s
max-backoff: 30s
backoff-multiplier: 2.0
circuit-breaker:
failure-threshold: 5
recovery-timeout: 30s
half-open-max-calls: 3
logging:
level:
com.example.user: INFO
io.grpc: WARN
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
五、应用场景分析
5.1 微服务间调用
在微服务架构中,每个服务都是独立部署的。当订单服务调用库存服务时,如果库存服务响应慢,订单服务不应该一直等下去。通过设置合理的超时时间(比如2秒),订单服务可以在超时后执行降级逻辑,返回"库存查询中"的友好提示,而不是让整个订单流程卡死。
实际案例:一个电商系统的下单流程需要调用库存服务、支付服务、物流服务。如果每个调用设置2秒超时,整个流程最多等6秒。如果某个服务超时,返回降级结果,用户依然可以看到订单状态,而不是看到"系统错误"。
5.2 第三方接口调用
调用第三方API时,对方服务的稳定性你控制不了。比如调用地图服务获取地址,如果地图服务挂了,你的系统不应该崩溃。通过超时控制,你可以快速判断第三方服务不可用,然后切换备用方案或者提示用户稍后重试。
实际案例:外卖平台需要调用地图服务计算配送距离。设置3秒超时,如果超时则使用缓存的距离数据或者提示用户"正在计算距离,请稍后查看"。
5.3 内部RPC通信
即使是在内网环境中,RPC调用也可能因为网络抖动、GC停顿等原因超时。比如数据库连接池满了,查询排队等了很久。通过超时控制,可以避免线程被长时间占用,导致线程池耗尽。
六、技术优缺点分析
6.1 超时控制的优缺点
优点:
- 防止请求无限等待,保护系统资源
- 让调用方快速失败,而不是默默等待
- 可以通过监控超时次数来判断服务健康状况
- 为降级和熔断提供触发条件
缺点:
- 超时时间设置需要平衡,太短会导致误判,太长会占用资源
- 超时后请求可能还在执行,需要处理超时后的状态不一致
- 在链路中传递超时需要额外的心跳机制
- 超时异常需要仔细区分是网络问题还是服务问题
6.2 异常处理的优缺点
优点:
- 让系统在面对故障时依然可用
- 重试机制可以自动恢复临时性故障
- 熔断器可以防止故障蔓延到整个系统
- 分类处理可以让不同异常有不同的应对策略
缺点:
- 实现复杂度较高,需要维护状态机
- 重试可能造成重复操作,需要幂等性保证
- 熔断器的配置需要经验积累
- 降级逻辑需要额外开发和维护
七、注意事项
7.1 超时时间设置原则
超时时间不是拍脑袋决定的,需要根据实际场景来设置。一般来说:
- 快速接口(简单查询):1-3秒
- 普通接口(涉及数据库操作):3-5秒
- 复杂接口(涉及多服务调用):5-10秒
- 超时时间应该是服务端P99响应时间的1.5-2倍
不要设置太短的超时(比如100毫秒),因为网络抖动、GC停顿等都可能导致请求在100毫秒内无法完成,这样会导致大量误判超时。
7.2 重试次数控制
重试次数不是越多越好。每次重试都会带来额外的资源消耗,而且如果服务真的出了问题,重试只会加重负担。建议:
- 最多重试3次:经验值,大多数临时故障3次内可以恢复
- 使用指数退避:避免所有重试同时发生
- 只对幂等操作重试:写操作不要重试,除非有幂等性保证
- 设置最大重试时间:整体重试时间不超过原始超时时间的2倍
7.3 线程池管理
超时控制离不开线程池。每个RPC调用都会占用一个线程,如果大量调用超时但线程没有及时释放,线程池就会耗尽。
/**
* 线程池配置示例 - 为RPC调用配置独立的线程池
* 技术栈:Java 17 + Spring Boot 3.2 + gRPC 1.59
*/
@Configuration
public class ThreadPoolConfig {
@Bean("grpcExecutor")
public ExecutorService grpcExecutor() {
return new ThreadPoolExecutor(
20,
50,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "grpc-call-" + counter.incrementAndGet());
t.setDaemon(true);
t.setUncaughtExceptionHandler((thread, ex) ->
log.error("RPC线程异常, thread={}", thread.getName(), ex));
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
7.4 监控与告警
超时和异常处理需要配合监控使用。你需要关注这些指标:
- 超时次数和比例:判断服务是否响应变慢
- 重试次数:判断是否有大量临时故障
- 熔断器状态变化:判断是否有服务异常
- 降级触发次数:判断降级逻辑是否合理
建议将这些指标上报到监控系统(比如Prometheus + Grafana),设置告警规则。当超时比例超过阈值时,及时通知开发团队排查问题。
八、文章总结
RPC调用的超时控制和异常处理是分布式系统稳定性的基石。通过合理设置超时时间,你可以防止系统资源被慢请求耗尽;通过完善的异常处理机制,你可以让系统在面对故障时优雅降级,而不是直接崩溃。
核心要点:
- 超时时间要合理:根据接口的实际响应时间设置,不要拍脑袋
- 异常要分类处理:不同异常类型有不同的处理策略
- 重试要有策略:只对幂等操作重试,使用指数退避
- 熔断要配合使用:连续失败后熔断,防止故障蔓延
- 监控要到位:超时、重试、熔断等指标都要监控
在实际开发中,建议将这些机制封装成通用的工具类或框架组件,让业务开发者可以轻松使用,而不需要每次都重新实现。这样既能保证系统的稳定性,又能提高开发效率。
评论
围绕“Photon服务端RPC调用,远程方法执行的超时控制与异常处理方案”参与讨论