1. 官网介绍

Dubbo 协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。

1.1. 特点

dubbo RPC 是dubbo体系中最核心的一种高性能、高吞吐量的远程调用方式,可以称之为 多路复用的TCP长连接调用

主要用于两个dubbo系统之间作远程调用,特别适合高并发、小数据的互联网场景。反之,Dubbo 协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

  • 长连接:避免了每次调用新建TCP连接,提高了调用的响应速度。
  • 多路复用:单个TCP连接可交替传输多个请求和响应的消息,降低了连接的等待闲置时间,从而减少了同样并发数下的网络连接数,提高了系统吞吐量
Q1:为什么要消费者比提供者个数多?

因为 dubbo 协议采用单一长连接,单连接的传输是有上限的,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样,供参考)。

假设网络为千兆网卡 1024Mbit=128MByte,理论上 1 个服务提供者需要 20 个服务消费者(对应 20 个连接)才能压满网卡。

Q2:为什么不能传大包?

因为 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡 1024Mbit=128MByte,每条连接最大 7MByte (不同的环境可能不一样),TPS 结果是:

  • 单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。
  • 单个消费者调用单个服务提供者的 TPS (每秒处理事务数)最大为:7MByte / 500KByte = 14。

如果能接受,可以考虑使用,否则网络将成为瓶颈。因为对于单个连接通道来看,倘若有一个超过7MByte的数据包请求,那么这一秒内就只有这一个请求执行了,其他请求都要阻塞等待。

Q3:为什么采用异步单一长连接?

因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务。比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨。

通过单一连接,保证单一消费者不会压死提供者。另外长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。

C10K问题:是关于如何在一个服务器上同时处理10,000个并发连接的问题。每个连接都需要分配一个独立的线程或进程来处理,都会占用一定的系统资源,而且线程或进程之间的上下文切换会带来额外的开销,影响系统的性能。
解决C10K问题的关键在于采用事件驱动编程、非阻塞I/O和轻量级线程等技术。通过这些技术,可以显著提高服务器处理并发连接的能力,从而更好地支持高并发场景下的应用。

1.2. 基本组成

image.png

  • Transporter: mina, netty, grizzy
  • Serialization: dubbo, hessian2, java, json
  • Dispatcher: all, direct, message, execution, connection
  • ThreadPool: fixed, cached

默认使用基于 netty 3.2.5.Final 和 hessian2 3.2.1-fixed-2(Alibaba embed version) 的 tbremoting 交互。

  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP
  • 传输方式:NIO 异步传输
  • 序列化:Hessian 二进制序列化
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。
  • 适用场景:常规远程服务方法调用

2. 源码实现

Dubbo 协议的实现逻辑涉及多个模块:

  • 协议接口 (Protocol):定义了服务导出和引用。
  • DubboProtocol:实现服务的导出和引用。
  • 网络传输:使用 Netty 实现高效的网络 IO。
  • 编解码 (Codec):负责请求和响应的序列化与反序列化。
  • 请求处理:通过 Invoker 实现远程调用。
  • 心跳检测和重连机制:保持连接的活跃性,并在异常时进行重试。

这些模块的协同工作,使得 Dubbo 协议能够在大规模分布式系统中高效地进行服务调用,并在连接异常时进行自动恢复。

2.1. Protocol 接口

Protocol 是 Dubbo 的核心接口之一,负责定义服务的导出和引用。

public interface Protocol {
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
}

2.2. DubboProtocol 类

DubboProtocolProtocol 接口的实现类,处理服务的导出和引用逻辑。

public class DubboProtocol implements Protocol {
    private static final DubboProtocol INSTANCE = new DubboProtocol();
    private final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<>();

    public static DubboProtocol getDubboProtocol() {
        return INSTANCE;
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();
        openServer(url);
        DubboExporter<T> exporter = new DubboExporter<>(invoker, url, exporterMap);
        exporterMap.put(url.getServiceKey(), exporter);
        return exporter;
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        DubboInvoker<T> invoker = new DubboInvoker<>(type, url, clients);
        return invoker;
    }

    private void openServer(URL url) {
        String key = url.getAddress();
        ExchangeServer server = serverMap.get(key);
        if (server == null) {
            server = Exchangers.bind(url, requestHandler);
            serverMap.put(key, server);
        }
    }
}

2.3. Transport 层

Dubbo 使用 Netty 作为底层网络传输框架,提供高效的网络 IO。

  • NettyTransporter:实现了 Transporter 接口,负责绑定和连接。
public class NettyTransporter implements Transporter {
    @Override
    public Server bind(URL url, ChannelHandler handler) throws RemotingException {
        return new NettyServer(url, handler);
    }

    @Override
    public Client connect(URL url, ChannelHandler handler) throws RemotingException {
        return new NettyClient(url, handler);
    }
}
  • NettyServer 和 NettyClient:负责服务端和客户端的网络通信。
public class NettyServer implements Server {
    private final ServerBootstrap bootstrap;
    private final Channel channel;

    public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
        bootstrap = new ServerBootstrap();
        bootstrap.group(new NioEventLoopGroup())
                 .channel(NioServerSocketChannel.class)
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) {
                         ch.pipeline().addLast(new NettyCodecAdapter(handler).getDecoder());
                         ch.pipeline().addLast(new NettyCodecAdapter(handler).getEncoder());
                         ch.pipeline().addLast(new NettyHandler(handler));
                     }
                 });
        channel = bootstrap.bind(url.getPort()).sync().channel();
    }
}

2.4. Codec 编解码

Codec 负责请求和响应的编解码,Dubbo 使用自定义的二进制协议。

  • DubboCodec:负责请求和响应的编码与解码。
public class DubboCodec extends ExchangeCodec {
    @Override
    protected void encodeRequestData(Channel channel, ObjectOutput out, Object data) throws IOException {
        Serialization serialization = getSerialization(channel);
        ObjectOutput output = serialization.serialize(channel.getUrl(), out);
        output.writeObject(data);
    }

    @Override
    protected Object decodeRequestData(Channel channel, ObjectInput in) throws IOException {
        Serialization serialization = getSerialization(channel);
        ObjectInput input = serialization.deserialize(channel.getUrl(), in);
        return input.readObject();
    }
}

2.5. Invoker 和 Exporter

  • Invoker:表示一个可调用的服务接口,是服务调用的核心模型。
  • Exporter:负责服务的导出。
public class DubboInvoker<T> extends AbstractInvoker<T> {
    private final ExchangeClient[] clients;

    @Override
    protected Result doInvoke(Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
        RpcResult result = new RpcResult();
        ResponseFuture future = clients[0].request(inv);
        return (Result) future.get();
    }
}

2.6. 心跳检测与异常处理

Dubbo 使用心跳机制来保持长连接的活跃,并检测连接状态。心跳异常处理通常涉及到连接的重试和恢复机制。

  • HeartbeatHandler:处理心跳检测。
public class HeartbeatHandler extends AbstractChannelHandlerDelegate {
    @Override
    public void received(Channel channel, Object message) throws RemotingException {
        if (message instanceof Heartbeat) {
            channel.send(new HeartbeatResponse());
        } else {
            super.received(channel, message);
        }
    }
}
  • 重试和恢复机制:在连接中断时,Dubbo 会尝试重新连接。这通常由底层的 Netty 实现,通过 ChannelHandler 的事件机制处理连接的中断和重连。

2.7. 连接管理

  • 重连机制:在连接中断后,Netty 可以通过 ChannelFutureListener 或定时任务来实现重连。
public class ReconnectHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        reconnect(ctx);
    }

    private void reconnect(ChannelHandlerContext ctx) {
        final EventLoop loop = ctx.channel().eventLoop();
        loop.schedule(() -> {
            // 重新连接逻辑
        }, 1L, TimeUnit.SECONDS);
    }
}

3. 连接池管理

3.1. 连接池作用

Dubbo协议中的连接池机制是为了提高网络通信效率和资源利用率而设计的。连接池可以复用已经建立的网络连接,减少频繁建立和关闭连接带来的开销,从而提高系统的性能和稳定性。

1. 减少连接开销
  • 减少建立连接的时间:每次建立TCP连接都需要经历三次握手的过程,这会消耗一定的时间和资源。
  • 减少关闭连接的时间:每次关闭TCP连接需要经历四次挥手的过程,同样会消耗时间和资源。
2. 提高性能
  • 复用连接:连接池可以复用已经建立的连接,避免频繁的连接建立和关闭,从而提高系统的吞吐量。
  • 资源管理:连接池可以统一管理和控制连接资源,避免资源浪费。
3. 提高稳定性
  • 连接重用:连接池中的连接可以被多次重用,减少了因频繁建立连接而导致的不稳定因素。
  • 故障恢复:连接池可以监控连接的状态,自动处理故障连接,提高系统的稳定性。

3.2. 连接池实现

Dubbo协议中的连接池主要通过以下几个方面实现:

  1. 连接池管理
  • 连接池大小:Dubbo允许配置连接池的最大连接数,以控制连接资源的使用。
  • 连接池维护:Dubbo会定期检查连接池中的连接状态,清理无效连接,并保持连接池的健康状态。
  1. 连接复用
  • 长连接:Dubbo默认使用长连接机制,即在服务消费者与服务提供者之间建立持久连接,并复用这些连接进行多次请求。
  • 短连接:虽然不常见,Dubbo也支持短连接机制,即每次请求都会建立新的连接并在请求结束后关闭连接。
  1. 连接生命周期管理
  • 连接建立:当服务消费者首次请求服务提供者时,Dubbo会建立一个新的连接并将其加入连接池。
  • 连接使用:服务消费者从连接池中获取连接,并通过该连接发送请求。
  • 连接释放:请求完成后,连接会被归还到连接池中,供后续请求复用。
  • 连接关闭:当连接池中的连接超过最大连接数或连接失效时,Dubbo会关闭部分连接,以保持连接池的健康状态。

3.3. 连接池配置参数

在 Dubbo 中,连接池的参数配置对于优化系统的性能和稳定性非常重要。通过合理配置这些参数,可以确保系统在高并发场景下能够高效地处理请求并保持良好的资源利用率。以下是 Dubbo 连接池的主要参数及其详细说明:

1. connections
  • 含义:指定每个消费者与每个提供者节点之间的最大连接数。
  • 默认值:通常默认值为 1 或 2。
  • 作用:控制每个提供者节点上的连接数量,以平衡性能和资源使用。
  • 示例配置

    <dubbo:consumer connections="10" />
2. lazy
  • 含义:是否懒加载连接。如果设置为 true,则在首次请求时才建立连接;如果设置为 false,则在启动时立即建立所有连接。
  • 默认值:默认值为 false
  • 作用:减少启动时的连接建立开销,适用于连接建立较为耗时的场景。
  • 示例配置

    <dubbo:consumer lazy="true" />
3. timeout
  • 含义:请求超时时间,单位为毫秒。
  • 默认值:通常默认值为 10000 毫秒(10 秒)。
  • 作用:设置请求的最大等待时间,超过该时间后请求将被取消。
  • 示例配置

    <dubbo:reference interface="com.example.Service" timeout="5000" />
4. retries
  • 含义:请求失败后的重试次数。
  • 默认值:通常默认值为 2。
  • 作用:在网络不稳定或提供者暂时不可用时自动重试请求。
  • 示例配置

    <dubbo:reference interface="com.example.Service" retries="3" />
5. loadbalance
  • 含义:负载均衡策略。
  • 默认值:默认值为 roundrobin(轮询)。
  • 作用:选择不同的负载均衡算法来分发请求到不同的提供者节点。
  • 可选值roundrobin(轮询)、random(随机)、leastactive(最少活跃调用数)、consistenthash(一致性哈希)等。
  • 示例配置

    <dubbo:reference interface="com.example.Service" loadbalance="leastactive" />
6. threadpool
  • 含义:线程池类型。
  • 默认值:默认值为 fixed
  • 作用:控制处理请求的线程池类型,影响并发处理能力。
  • 可选值fixed(固定大小线程池)、cached(可缓存线程池)、scheduled(定时任务线程池)等。
  • 示例配置

    <dubbo:protocol name="dubbo" threadpool="fixed" threads="20" />
7. threads
  • 含义:线程池中的线程数量。
  • 默认值:默认值为 200。
  • 作用:控制线程池中处理请求的线程数量。
  • 示例配置

    <dubbo:protocol name="dubbo" threads="100" />

3.4. 连接复用限制

既然是连接池,当现有连接达到上限之后,就会触发创建新连接。那么既然 dubbo协议 是基于 tcp 连接实现的,复用一个 tcp 连接,怎么判断是否达到连接复用的限制呢?

在 Dubbo 中,连接复用限制通常涉及到单个连接上可以同时处理的请求数量。尽管 Dubbo 使用了 Netty 的异步非阻塞 I/O 模型来支持高并发,但在实际应用中,单个连接的复用能力可能受到一些因素的限制。以下是一些可能的限制因素:

  1. 协议层限制
    单连接并发请求数:某些协议(如 HTTP/1.1)在单个连接上可能会有并发请求数的限制。虽然 Dubbo 默认使用的协议是基于长连接和异步通信的,但在某些情况下,协议层面的限制可能会影响单个连接的复用能力。
  2. 应用层限制
    请求处理能力:即使在一个连接上可以同时发送多个请求,服务端处理这些请求的能力也是有限的。如果一个连接上积压了过多的请求,可能会导致延迟增加或超时。
  3. 网络层限制
    TCP 窗口大小:TCP 连接的窗口大小可能限制单个连接上传输的数据量,从而间接影响并发请求的处理能力。
  4. Dubbo 配置和实现
    连接池配置:虽然 Dubbo 的连接池并没有明确限制单个连接的并发请求数,但通过合理的连接池配置(如设置最大连接数)可以间接影响连接的复用策略。

资源管理:为了避免单个连接上的过载,Dubbo 可能会在现有连接达到一定负载时创建新的连接。这种负载可以是请求数量、请求大小或其他资源使用指标。

实际应用中的上限
在实际应用中,"达到上限"通常指的是当一个连接的负载(如并发请求数或请求处理时间)超出某个阈值时,系统会选择创建新的连接来分担负载。这种策略可以帮助系统在高并发场景下保持稳定的性能。

4. 连接池实现

4.1. 每个提供者独立连接池

首先,Dubbo 的连接池和 OkHttp 等不同,每个服务消费者对应请求每个服务提供者,就是一个连接池。

另外当服务提供者以集群方式部署时,服务消费者会与集群中的每个提供者节点建立连接。

具体来说,Dubbo 的连接池机制会为每个提供者 IP(或节点)维护一组连接。这意味着,对于每一个提供者实例,消费者都会有独立的连接池来管理与该实例的连接。

  1. 每个提供者节点独立的连接池

    • 对于每个服务提供者节点(IP:Port),消费者会维护一个独立的连接池。这样可以确保请求能够被均匀地分发到集群中的不同节点。
    • 这种方式有助于实现负载均衡,因为消费者可以根据负载均衡策略(如随机、轮询、最少活跃调用等)选择合适的提供者节点。
  2. 连接数配置

    • 可以通过 Dubbo 的配置参数(如 connections)来指定每个消费者到每个提供者节点的最大连接数。这有助于控制系统的资源使用。
    • 例如,设置 connections=10 意味着每个消费者到每个提供者节点最多可以建立 10 个连接。
  3. 连接复用与长连接

    • Dubbo 使用长连接机制,这意味着一旦连接建立,消费者会尽量复用这些连接来处理多个请求,而不是频繁地创建和销毁连接。
    • 连接池的使用使得连接的管理更加高效,减少了连接建立和关闭的开销。

4.2. 代码实现

在Dubbo中,当消费者需要调用多个不同的服务提供者(可能是集群中的多个服务器)时,连接池的管理变得更为复杂。为了更好地理解和解决这个问题,我们需要详细探讨连接池的管理和负载均衡策略。

展示一个示例代码,展示如何在Dubbo中管理连接池,并处理多个服务提供者的情况。

1. NettyChannelPool
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class NettyChannelPoolManager {
    private final Map<String, NettyChannelPool> poolMap = new ConcurrentHashMap<>();
    private final int maxConnections;

    public NettyChannelPoolManager(int maxConnections) {
        this.maxConnections = maxConnections;
    }

    public synchronized Channel getChannel(String host, int port) {
        String key = host + ":" + port;
        NettyChannelPool pool = poolMap.computeIfAbsent(key, k -> new NettyChannelPool(host, port, maxConnections));
        return pool.acquire();
    }

    public synchronized void releaseChannel(Channel channel) {
        String key = extractKeyFromChannel(channel);
        NettyChannelPool pool = poolMap.get(key);
        if (pool != null) {
            pool.release(channel);
        }
    }

    public synchronized void shutdown() {
        for (NettyChannelPool pool : poolMap.values()) {
            pool.shutdown();
        }
        poolMap.clear();
    }

    private String extractKeyFromChannel(Channel channel) {
        InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress();
        return remoteAddress.getHostString() + ":" + remoteAddress.getPort();
    }

    private static class NettyChannelPool {
        private final Bootstrap bootstrap;
        private final EventLoopGroup eventLoopGroup;
        private final FixedChannelPool channelPool;
        private final int maxConnections;

        public NettyChannelPool(String host, int port, int maxConnections) {
            this.maxConnections = maxConnections;
            eventLoopGroup = new NioEventLoopGroup();
            bootstrap = new Bootstrap()
                    .group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new MyChannelHandler());
                        }
                    });

            channelPool = new FixedChannelPool(maxConnections) {
                @Override
                protected Channel newChannel() {
                    try {
                        Future<Channel> future = bootstrap.connect(host, port).addListener((GenericFutureListener<Future<Channel>>) f -> {
                            if (!f.isSuccess()) {
                                f.cause().printStackTrace();
                            }
                        });
                        return future.sync().channel();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
        }

        public Channel acquire() {
            return channelPool.acquire();
        }

        public void release(Channel channel) {
            channelPool.release(channel);
        }

        public void shutdown() {
            channelPool.shutdown();
            eventLoopGroup.shutdownGracefully();
        }

        private static class MyChannelHandler extends ChannelInboundHandlerAdapter {
            @Override
            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                System.out.println("Channel active: " + ctx.channel().remoteAddress());
            }

            @Override
            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                System.out.println("Channel inactive: " + ctx.channel().remoteAddress());
            }

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                System.out.println("Received message: " + msg);
            }

            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                cause.printStackTrace();
                ctx.close();
            }
        }
    }
}
2. 服务消费者端
public class NettyClient {
    public static void main(String[] args) {
        int maxConnections = 100;
        NettyChannelPoolManager poolManager = new NettyChannelPoolManager(maxConnections);

        // 服务提供者 A
        String hostA = "localhost";
        int portA = 20880;

        // 服务提供者 B
        String hostB = "localhost";
        int portB = 20881;

        // 调用服务提供者 A
        for (int i = 0; i < 10; i++) {
            Channel channelA = poolManager.getChannel(hostA, portA);
            channelA.writeAndFlush("Hello, Service A!");
            poolManager.releaseChannel(channelA);
        }

        // 调用服务提供者 B
        for (int i = 0; i < 10; i++) {
            Channel channelB = poolManager.getChannel(hostB, portB);
            channelB.writeAndFlush("Hello, Service B!");
            poolManager.releaseChannel(channelB);
        }

        poolManager.shutdown();
    }
}
解释
  1. 连接池管理

    • NettyChannelPoolManager 类管理多个服务提供者的连接池。
    • 一个 NettyChannelPool对象其实就对应一个连接池,一个 Channel 对应一个连接。
    • 使用一个ConcurrentHashMap来存储每个服务提供者的NettyChannelPool对象。
  2. 获取连接

    • getChannel 方法根据服务提供者的IP地址和端口号获取一个空闲连接。
    • 如果连接池中没有空闲连接,会新建一个连接。
  3. 释放连接

    • releaseChannel 方法将使用过的连接归还到对应的连接池中。
  4. 关闭连接池

    • shutdown 方法关闭所有连接池和EventLoopGroup

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论