PAYATHON 2026

Java gRPC 如何非阻塞地监听服务健康状态?

数据小许

可以实现。使用 HealthGrpc.newStub(channel) 创建异步 stub,通过 StreamObserver<HealthCheckResponse> 接收响应。每条响应到达时,gRPC 会调用 onNext()。如果希望主动查询新消息,可以在回调中将响应放入线程安全队列,再由业务线程调用 poll()。

为什么 hasNext() 会阻塞?

healthBlockingStub.watch(...) 返回的是服务端流式 RPC 的阻塞迭代器,不能用它检查“当前有没有已经收到的消息”。

对于这个迭代器:

  • 有下一条响应时,hasNext() 返回 true。
  • 尚无响应且流没有结束时,hasNext() 会等待。
  • 流正常结束且没有剩余响应时,hasNext() 返回 false。
  • RPC 失败时,读取操作会抛出 StatusRuntimeException。

next() 也可能等待下一条响应。这是 blocking stub 的预期行为,具体可参考 gRPC Java 的迭代器实现。

Watch 会先发送当前健康状态,再在状态变化时发送后续响应。因此,收到 SERVING 后长时间没有新消息,可能只是状态一直没有变化。Watch 不会定时发送心跳,具体语义见 Health 协议定义。

用异步 stub 接收响应

下面的示例复用已有的 Channel,启动监听,并将收到的响应放入队列。假设项目已经具备原问题中的 Health 生成类。

import io.grpc.Channel;
import io.grpc.Status;
import io.grpc.health.v1.HealthCheckRequest;
import io.grpc.health.v1.HealthCheckResponse;
import io.grpc.health.v1.HealthGrpc;
import io.grpc.stub.StreamObserver;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public final class HealthWatch {
    public static Queue<HealthCheckResponse> start(Channel channel) {
        Queue<HealthCheckResponse> updates =
                new ConcurrentLinkedQueue<>();

        HealthGrpc.HealthStub healthStub = HealthGrpc.newStub(channel);

        healthStub.watch(
                HealthCheckRequest.getDefaultInstance(),
                new StreamObserver<HealthCheckResponse>() {
                    @Override
                    public void onNext(HealthCheckResponse response) {
                        updates.offer(response);
                        // 如需即时通知,可在这里调用轻量的业务回调。
                    }

                    @Override
                    public void onError(Throwable t) {
                        System.err.println(
                                "Health Watch failed: "
                                        + Status.fromThrowable(t));
                    }

                    @Override
                    public void onCompleted() {
                        System.out.println("Health Watch completed");
                    }
                });

        return updates;
    }
}

异步调用通过 onNext() 接收响应,通过 onError() 或 onCompleted() 接收终止通知,业务线程无需循环等待迭代器。接口形式见 gRPC Java 生成代码说明。

主动查询是否收到新响应

业务代码可以保存启动监听时返回的队列,在需要时读取:

// channel 是已经创建好的 gRPC Channel。
Queue<HealthCheckResponse> updates = HealthWatch.start(channel);

// 在之后的业务处理或定时任务中执行:
HealthCheckResponse response = updates.poll();
if (response == null) {
    System.out.println("No pending health update");
} else {
    System.out.println("Health status: " + response.getStatus());
}

队列为空时,poll() 会立即返回 null,不会等待下一条网络响应。一次调用只取出一条消息;如果要处理所有待处理响应,可以循环读取,直到返回 null。

这里查询的是“当前是否有尚未取出的响应”。返回 null 后,新响应仍可能随时到达;它也不能说明监听连接仍然正常。应用需要单独记录 onError() 和 onCompleted() 表示的结束状态。

实际使用时需要注意什么?

  • 回调应保持简短。耗时计算、数据库访问等工作应交给业务执行器,避免拖延后续回调。
  • 示例使用无界队列。如果业务长期不读取,消息会持续占用内存。只关心最新状态时,可以改用 AtomicReference<HealthCheckResponse>,但这样会覆盖中间响应。
  • 示例仅打印终止信息,没有实现重连。需要持续监听时,应在非主动停止的情况下安排重连和退避;对于 UNIMPLEMENTED,Health 协议要求不要重试 Watch。
  • 不再需要监听时应取消该 RPC,可以使用可取消的 Context 或 ClientCallStreamObserver.cancel(...)。共享 Channel 不宜仅为停止一个监听而关闭。

如果继续使用原来的阻塞迭代器,通常应由专用线程执行 while (watch.hasNext()),再把响应转交给业务线程。这样可以避免业务线程等待,但专用线程仍会阻塞。如果希望响应到达时就收到通知,异步 stub 更直接。

备注:内容仅供参考。