Dubbo心跳逻辑演进

Dubbo 现有的心跳方案

1、通过自定义定时器(2.7.X之前版本)

在dubbo服务启动时,服务端会开启对应定时器,默认心跳间隔时间时60s,容错机制为失败次数超过3次将断开重连。核心代码如下:

//根据不同的client的canHandleIdle属性生成心跳定时器
   private void startHeartBeatTask(URL url) {
        if (!client.canHandleIdle()) {
            int heartbeat = getHeartbeat(url);
            long heartbeatTick = calculateLeastDuration(heartbeat);
            heartBeatTimerTask = new HeartbeatTimerTask(
                    () -> Collections.singleton(this), IDLE_CHECK_TIMER.get(), heartbeatTick, heartbeat);
        }
    }
    
//根据url的reconnect属性判断是否开启断开重连调度器,默认不配做reconnect属性,开启断开重连
    private void startReconnectTask(URL url) {
        if (shouldReconnect(url)) {
            long heartbeatTimeoutTick = calculateLeastDuration(idleTimeout);
            reconnectTimerTask = new ReconnectTimerTask(
                    () -> Collections.singleton(this),
                    IDLE_CHECK_TIMER.get(),
                    calculateReconnectDuration(url, heartbeatTimeoutTick),
                    idleTimeout);
        }
    }
    protected boolean shouldReconnect(URL url) {
        return !Boolean.FALSE.toString().equalsIgnoreCase(url.getParameter(Constants.RECONNECT_KEY));
    }

<1> HeartbeatTimerTask 主要用于定时发送心跳请求

protected void doTask(Channel channel) {
    Long lastRead = lastRead(channel);
    Long lastWrite = lastWrite(channel);
    if ((lastRead != null && now() - lastRead > heartbeat)
        || (lastWrite != null && now() - lastWrite > heartbeat)) {
            Request req = new Request();
            req.setVersion(Version.getProtocolVersion());
            req.setTwoWay(true);
            req.setEvent(Request.HEARTBEAT_EVENT);
            channel.send(req);
        }
    }
}

<2> ReconnectTimerTask 主要用于心跳失败之后处理重连,断连的逻辑

protected void doTask(Channel channel) {
    Long lastRead = lastRead(channel);
    Long now = now();
    if (lastRead != null && now - lastRead > heartbeatTimeout) {
        if (channel instanceof Client) {
            ((Client) channel).reconnect();
        } else {
            channel.close();
        }
    }
}

Dubbo 采取的是双向心跳设计,即服务端会向客户端发送心跳,客户端也会向服务端发送心跳,接收的一方更新 lastRead 字段,发送的一方更新 lastWrite 字段,超过心跳间隙的时间,便发送心跳请求给对端。这里的 lastRead/lastWrite 同样会被同一个通道上的普通调用更新,通过更新这两个字段,实现了只在连接空闲时才会真正发送空闲报文的机制。

2、使用netty框架的IdleStateHandler

public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {}

readerIdleTime:读超时时间
writerIdleTime:写超时时间
allIdleTime:所有类型的超时时间

IdleStateHandler 这个类会根据设置的超时参数,循环检测 channelRead 和 write 方法多久没有被调用。当在 pipeline 中加入 IdleSateHandler 之后,可以在此 pipeline 的任意 Handler 的 userEventTriggered 方法之中检测 IdleStateEvent 事件,很多服务治理框架都选择了借助 IdleStateHandler 来实现心跳。

IdleStateHandler 内部使用了 eventLoop.schedule(task) 的方式来实现定时任务,使用 eventLoop 线程的好处是还同时保证了线程安全

2.7.X之后版本的Dubbo默认使用IdleStateHandler 实现心跳,核心代码如下:

客户端
  protected void initBootstrap(NettyClientHandler nettyClientHandler) {
        bootstrap
                .group(EVENT_LOOP_GROUP.get())
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                // .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
                .channel(socketChannelClass());

        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(DEFAULT_CONNECT_TIMEOUT, getConnectTimeout()));
        SslContext sslContext = SslContexts.buildClientSslContext(getUrl());
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                int heartbeatInterval = UrlUtils.getHeartbeat(getUrl());

                if (sslContext != null) {
                    ch.pipeline().addLast("negotiation", new SslClientTlsHandler(sslContext));
                }

                NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
                ch.pipeline() 
                        .addLast("decoder", adapter.getDecoder())
                        .addLast("encoder", adapter.getEncoder())
                        //将对应的IdleStateHandler添加到nettyclient的pipeline中
                        .addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))
                        .addLast("handler", nettyClientHandler);

                String socksProxyHost =
                        ConfigurationUtils.getProperty(getUrl().getOrDefaultApplicationModel(), SOCKS_PROXY_HOST);
                if (socksProxyHost != null && !isFilteredAddress(getUrl().getHost())) {
                    int socksProxyPort = Integer.parseInt(ConfigurationUtils.getProperty(
                            getUrl().getOrDefaultApplicationModel(), SOCKS_PROXY_PORT, DEFAULT_SOCKS_PROXY_PORT));
                    Socks5ProxyHandler socks5ProxyHandler =
                            new Socks5ProxyHandler(new InetSocketAddress(socksProxyHost, socksProxyPort));
                    ch.pipeline().addFirst(socks5ProxyHandler);
                }
            }
        });
    }

   //客户端检测发现空闲超时,发出心跳事件,对应的事件处理器对服务端发出心跳请求
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // send heartbeat when read idle.
        if (evt instanceof IdleStateEvent) {//判断是否为心跳事件
            try {
                NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
                if (logger.isDebugEnabled()) {
                    logger.debug("IdleStateEvent triggered, send heartbeat to channel " + channel);
                }
                Request req = new Request();
                req.setVersion(Version.getProtocolVersion());
                req.setTwoWay(true);
                req.setEvent(HEARTBEAT_EVENT);
                channel.send(req);
            } finally {
                //结束之后判断链接是否可用,断开不可用的链接
                NettyChannel.removeChannelIfDisconnected(ctx.channel());
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    //重连
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
        handler.connected(channel);
        if (logger.isInfoEnabled()) {
            logger.info("The connection of " + channel.getLocalAddress() + " -> " + channel.getRemoteAddress()
                    + " is established.");
        }
    }

    //断连
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
        try {
            handler.disconnected(channel);
        } finally {
            NettyChannel.removeChannel(ctx.channel());
        }

        if (logger.isInfoEnabled()) {
            logger.info("The connection of " + channel.getLocalAddress() + " -> " + channel.getRemoteAddress()
                    + " is disconnected.");
        }
    }
服务端
protected void initServerBootstrap(NettyServerHandler nettyServerHandler) {
        boolean keepalive = getUrl().getParameter(KEEP_ALIVE_KEY, Boolean.FALSE);
        bootstrap
                .group(bossGroup, workerGroup)
                .channel(NettyEventLoopFactory.serverSocketChannelClass())
                .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childOption(ChannelOption.SO_KEEPALIVE, keepalive)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        int closeTimeout = UrlUtils.getCloseTimeout(getUrl());
                        NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                        ch.pipeline().addLast("negotiation", new SslServerTlsHandler(getUrl()));
                        ch.pipeline()
                                .addLast("decoder", adapter.getDecoder())
                                .addLast("encoder", adapter.getEncoder())
                                //将对应的IdleStateHandler添加到nettyclient的pipeline中,默认超时关闭空闲超时链接的时间时发送心跳时间的三倍url.getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartBeat * 3);
                                .addLast("server-idle-handler", new IdleStateHandler(0, 0, closeTimeout, MILLISECONDS))
                                .addLast("handler", nettyServerHandler);
                    }
                });
    }
    //超时关闭链接
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // server will close channel when server don't receive any heartbeat from client util timeout.
        if (evt instanceof IdleStateEvent) {
            NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
            try {
                logger.info("IdleStateEvent triggered, close channel " + channel);
                channel.close();
            } finally {
                NettyChannel.removeChannelIfDisconnected(ctx.channel());
            }
        }
        super.userEventTriggered(ctx, evt);
    }

Dubbo通过IdleStateHandler实现的心跳功能,主要依赖于客户端

<1>客户端检测空闲超时发送心跳包给到服务端,并且判断对应链接状态是否可以,当链接被服务端关闭。客户端也会关闭对应的链接,并重新发起新的链接请求。
<2>服务端接收到请求之后刷新空闲等待时间。如果服务端检测到空闲超时将直接关闭链接。

使用netty自带的IdleStateHandler实现心跳功能的优缺点

<1>优点:实现简单,不需要复杂的代码实现。主要依赖于netty框架本身的功能。性能和稳定性得到了提升
<2>缺点:依托于netty框架,容易受到netty框架版本冲突的影响,当出现问题时排查更困难。

实践中出现的Dubbo心跳失败案例

之前在我司系统升级的过程中,将对应的dubbo框架版本2.7.9升级到3.0.7,升级之后客户端和服务端之间的调用都一切正常,就存在一个问题当请求不频繁时,客户端出现报错:

No provider available for the service xxx.api.VehicleModelIdentifyFacade:1.0-online from registry RegistryDirectory(registry: 10.120.xxx.22:8848)-Directory(invokers: 2[10.120.xxx.26:11013, 10.120.xxx.28:11013], validInvokers: 0[], invokersToReconnect: 2[10.120.xxx.28:11013, 10.120.xx.26:11013]) on the consumer 10.120.110.24 using the dubbo version 2.0-SNAPSHOT. Please check if the providers have been started and registered.

同时发现同一时间段服务端出现报错:

IdleStateEvent triggered, close channel NettyChannel [channel=[id: 0x6f2fc1ed, L:/10.120.xxx.28:11013 - R:/10.120.xxx.27:48816]]

经过分析,重现问题确认是由于Dubbo客户端和服务端的心跳功能出现问题,服务端无法接收到客户端的心跳请求,导致每三个心跳间隔服务端断开客户端的链接。而在客户端发起重连之前,客户端出现调用请求,发现所有的服务都处于待重连阶段,报错无可用服务(No provider available)。最终翻看源码查询资料确认为netty版本冲突问题,导致Dubbo心跳功能出现BUG。最诡异的点在于接口请求调用正常,心跳功能不正常,而且只有在请求不频繁的情况下才会出现。当请求频繁时不会出现空闲超时,自然也不会触发超时关闭链接。

由于我司系统使用了dubbo、xxjob、rocketmq等一系列中间件,每个中间件使用的netty版本都各不相同,导致出现版本冲突。

解决方案

在项目的基础依赖项目的pom文件中添加netty版本限定,统一覆盖所有第三方框架的netty版本,保证版本统一。经过测试,发现心跳失败问题解决,各中间件功能正常。

           <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-bom</artifactId>
                <version>${netty.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
<think>嗯,用户问的是Java体系中的微服务和分布式是怎么实现的,还有单个服务的多实例是如何被发现的。我需要先理清楚这些概念,然后组织一个清晰的回答。 首先,微服务架构。微服务是一种架构风格,把应用拆分成多个小服务,每个服务独立运行和部署。Java中常用的框架有Spring Cloud、Micronaut、Quarkus,可能还需要提到Dubbo。这些框架提供了不同的工具和模块来支持微服务的各个部分。 然后是分布式系统,这涉及到服务之间的通信、协调和容错。需要提到的技术包括RESTful API、RPC(比如gRPC或Dubbo)、消息队列(Kafka、RabbitMQ)。另外,分布式事务和一致性,可能需要提到Seata或者Spring的解决方案,以及分布式配置中心如Apollo。 接下来是服务发现机制,这是用户问题的重点。单个服务多实例如何被发现。需要解释服务发现的基本概念,服务注册与发现的流程。常用的组件比如Eureka、Consul、Zookeeper、Nacos。特别是Spring Cloud中的Eureka,服务启动时注册到Eureka Server,客户端通过Eureka来发现服务实例。同时要提到负载均衡,比如Ribbon或者Spring Cloud LoadBalancer,它们如何与服务发现配合,选择可用的实例。 可能需要比较不同服务发现组件的区别,比如Eureka和Nacos的区别,但用户可能只需要基本实现方式。另外,健康检查机制也很重要,确保注册中心中的实例都是可用的,避免将请求发送到宕机的服务。 还有,关于服务发现的实现细节,比如客户端如何缓存服务列表,如何更新,服务下线时的处理(比如主动注销或者心跳检测失败)。这些细节可能对用户理解整个机制有帮助。 最后要确保回答结构清晰,分点说明,先介绍微服务和分布式的实现方式,再详细讲解服务发现的机制,包括注册中心和负载均衡。可能需要给出一个例子,比如使用Spring Cloud Eureka和Ribbon的组合,说明整个过程。 检查是否有遗漏的部分,比如是否需要提到容器化部署(如Docker和Kubernetes中的服务发现),但用户的问题可能更偏向于传统Java体系,所以可能不需要深入K8s的部分,但可以稍微提一下,作为扩展。 总结下来,回答的结构应该是: 1. 微服务架构的实现:框架、拆分原则、通信方式。 2. 分布式系统的关键技术:通信、消息队列、分布式事务、配置中心。 3. 服务发现机制:注册中心的作用,具体组件,注册与发现的流程,健康检查。 4. 多实例的负载均衡:客户端负载均衡器的工作原理。 5. 可能的例子:Spring Cloud Eureka + Ribbon。 6. 扩展:容器环境中的服务发现(可选)。 确保语言通俗易懂,避免过于技术化的术语,但又要准确。用户可能有一定的Java基础,但需要明确的概念解释。</think>在Java生态中,微服务与分布式的实现主要依赖一系列框架和技术,而服务实例的发现则通过**服务注册与发现机制**实现。以下是核心实现逻辑: --- ### 一、微服务架构的实现 1. **框架支持** - **Spring Cloud**:提供全套微服务解决方案(服务发现、配置中心、熔断器等)。 - **Apache Dubbo**:高性能RPC框架,支持服务治理。 - **Quarkus/Micronaut**:轻量级框架,适合云原生场景。 2. **服务拆分** - 按业务功能将单体应用拆分为独立服务(如订单服务、支付服务),每个服务可独立开发、部署和扩展。 3. **通信机制** - **RESTful API**:基于HTTP/JSON,通过Feign或RestTemplate实现。 - **RPC**:如Dubbo的远程调用或gRPC的高性能通信。 - **异步消息**:通过Kafka、RabbitMQ解耦服务。 --- ### 二、分布式系统的关键技术 1. **服务发现与注册** - **注册中心**(如Eureka、Nacos、Consul)负责记录服务实例的地址和元数据。 - **服务启动时**:向注册中心注册自身信息(IP、端口、健康状态)。 - **服务下线时**:主动注销或通过心跳超时自动移除。 2. **负载均衡** - **客户端负载均衡**(如Ribbon、Spring Cloud LoadBalancer):从注册中心获取实例列表,按策略(轮询、权重等)选择实例。 - **服务端负载均衡**(如Nginx、Gateway):通过反向代理分发请求。 3. **容错与高可用** - **熔断降级**:Hystrix或Resilience4j防止级联故障。 - **分布式配置**:通过Nacos或Spring Cloud Config统一管理配置。 - **分布式事务**:Seata或Saga模式保证数据一致性。 --- ### 三、多实例服务发现的实现流程 1. **服务注册** - 服务启动时,向注册中心发送注册请求(例如通过Spring Cloud的`@EnableEurekaClient`)。 - 注册信息包括:服务名(如`order-service`)、IP、端口、健康检查端点。 2. **健康检查** - 注册中心定期调用服务的健康检查接口(如`/actuator/health`),剔除不可用实例。 3. **服务发现** - 客户端(或其他服务)通过服务名查询注册中心,获取所有可用实例列表。 - 例如:订单服务调用支付服务时,通过Feign+Ribbon从Eureka获取支付服务的实例列表。 4. **动态更新** - 当新实例启动或旧实例下线时,注册中心实时更新列表,客户端缓存定期刷新。 --- ### 四、典型技术栈示例 - **服务注册中心**:Nacos(同时支持服务发现和配置管理)或Eureka。 - **通信**:OpenFeign声明式HTTP客户端 + Ribbon负载均衡。 - **API网关**:Spring Cloud Gateway统一路由和限流。 - **容器化部署**:Docker + Kubernetes(通过K8s Service实现服务发现)。 ```java // Spring Cloud 示例:服务注册与发现 @SpringBootApplication @EnableEurekaClient // 标记为Eureka客户端 public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } // 通过Feign调用其他服务 @FeignClient(name = "payment-service") public interface PaymentServiceClient { @PostMapping("/pay") String pay(@RequestBody Order order); } ``` --- ### 五、总结 - **核心逻辑**:服务实例通过注册中心上报自身状态,客户端动态发现可用实例并负载均衡。 - **优势**:实现水平扩展、故障自动恢复和高可用性。 - **演进趋势**:云原生场景下,Kubernetes内置的Service和Ingress逐渐替代传统注册中心,但原理相通。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值