Java打造RPC框架(五):连接池化

在上一篇的文章中https://blog.csdn.net/we_phone/article/details/79053472

我初步完成了整个RPC框架的搭建,从服务调用到服务发现再到负载均衡,这一篇开始进行的是一系列我所知的优化操作

这一篇我讲的是连接池,比较简单,详细代码已托管到github:https://github.com/wephone/MeiZhuoRPC

首先了解一下什么是连接池

连接池

在平时我们的数据库增删查改的业务中,应该绝大多数使用到了连接池技术,例如C3P0,Druid这类框架,帮我们完成了对数据库连接的池化。

当没有连接池时会遇到这样的问题:

要么每一次进行远程连接时都去创建一个连接,用完立即释放,也就是频繁的创建和销毁大量连接

或者各个调用共用一个单一连接,但在多线程的情况下,需要加锁来避免争抢的问题,这个方案的问题是效率低下且复杂性高

连接池的做法就是预先加载一定数量的连接放到资源池里,当需要连接时则从连接池中拿出一个来使用,用完则还回去,当并发量巨大,连接资源匮乏时,根据一定的策略来新建连接或者拒绝。

和jdk中的线程池类似,都是资源池化的思想。

在我前面的框架编写中,我采用的是单一连接的方式,并且用加锁来保证不会重复连接等等,其实在netty框架的一些版本中,有FixedChannelPool这个东西作为netty的连接池,但在我用的版本里没有发现这个,出于学习的目的,就自行用其他库写了一个来作为RPC框架的连接池。

扫描二维码关注公众号,回复: 3873548 查看本文章

Commons-Pool

在后续的开发中我引入了这个库,用来做连接池的基本手脚架,这个框架已经包含了对象池的基本处理,例如创建对象,回收对象,最大数量控制等等。

<dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-pool2</artifactId>
       <version>2.2</version>
</dependency>

首先创建ConnectionPool类来作为我们的连接池,在他的构造方法里对Commons pool进行初始化配置

    private GenericObjectPool pool;
    private String fullIp;

    public ConnectionPool(String ip,Integer port) {
        ConnectFactory connectFactory=new ConnectFactory(ip, port);
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //最大空闲连接数
        config.setMaxIdle(RPC.getClientConfig().getPoolMaxIdle());
        //最大连接数
        config.setMaxTotal(RPC.getClientConfig().getPoolMaxTotal());
        pool=new GenericObjectPool(connectFactory,config);
        fullIp=ip+":"+port;
    }
GenericObjectPool就是基础的对象池手脚架,需要传入一个配置对象和一个对象工厂

在配置中设置我们需要的最大连接数和最大空闲连接数,其他的配置都用默认的

后面我们就需要用这个连接池来获取我们的连接,所以给他添加获取,释放连接,销毁连接的操作。

realease方法只是归还某一个连接到池中,而destroyChannel方法是对整个连接池的销毁,不单是关闭某个连接链路,所以它还需要对连接池内各个连接共用的netty线程池进行shutdown

    public Channel getChannel() throws Exception {
        return (Channel) pool.borrowObject();
    }

    public void releaseChannel(Channel channel){
        pool.returnObject(channel);
    }

    public void destroyChannel(){
        //关闭Netty线程资源及其注册的连接
        ((ConnectFactory)pool.getFactory()).getGroup().shutdownGracefully();
        pool.close();
        //移除引用
        RPCRequestNet.getInstance().connectionPoolMap.remove(fullIp);
    }

ConnectFactory类就是我们为连接池创建netty连接的地方 

public class ConnectFactory extends BasePooledObjectFactory<Channel> {

    private String ip;
    private Integer port;
    //netty线程组 同一个服务的连接池内各个连接共用
    private EventLoopGroup group=new NioEventLoopGroup();

    public ConnectFactory(String ip, Integer port) {
        this.ip = ip;
        this.port = port;
    }

    public EventLoopGroup getGroup() {
        return group;
    }

首先我们需要将他继承自手脚架的BasePooledObjectFactory并制定泛型为netty的channel

内部属性处理ip和端口之外,还需要配上netty的线程组,然后对BasePooledObjectFactory的几个核心方法进行重写

首先是创建对象的create方法,也就是基本的netty连接的创建并返回channel对象

    @Override
    public Channel create() throws Exception {
        //启动辅助类 用于配置各种参数
        Bootstrap b=new Bootstrap();
        b.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY,true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(2048));//以换行符分包 防止粘包半包 2048为最大长度 到达最大长度没出现换行符则抛出异常
                        socketChannel.pipeline().addLast(new StringDecoder());//将接收到的对象转为字符串
                        //添加相应回调处理和编解码器
                        socketChannel.pipeline().addLast(new RPCRequestHandler());
                    }
                });
        ChannelFuture f=b.connect(ip,port).sync();
        System.out.println("pool create channel "+ip+":"+port);
        return f.channel();
    }

再者是destroy方法,在我们创建了过多的连接后,连接需求下降时,对象池会回收我们的各个连接,这个方法则会被调用,这里我们做的处理是取出相应的channel对象,关闭这个连接链路。

    @Override
    public void destroyObject(PooledObject<Channel> p) throws Exception {
        System.out.println("destroy channel "+ip+":"+port);
        //销毁channel时释放资源 
        p.getObject().close();
    }

还有一个方法是给对象池内的对象再进行一次封装以增强功能用的,这里我们就使用默认的包装对象就行了

    @Override
    public PooledObject<Channel> wrap(Channel channel) {
        return new DefaultPooledObject<Channel>(channel);
    }

现在我们就完成了连接池的封装,接下来是把他接入之前的RPC调用中

接入连接池

在我们的核心调用端连接对象 RPCRequestNet对象中 加入一个单例的map

//每个ip对应一个连接池
    public Map<String,ConnectionPool> connectionPoolMap=new ConcurrentHashMap<String,ConnectionPool>();

key为ip,value就是我们的连接池,即一个服务端ip对应一个连接池,我们的连接服务端的connect方法从原来的单一连接加锁改成如下

    //负载均衡获取对应IP 从连接池中获取连接channel
    private Channel connect(String ip) throws Exception {
        String[] IPArr=ip.split(":");
        String host=IPArr[0];
        Integer port=Integer.valueOf(IPArr[1]);
        if (connectionPoolMap.get(ip)==null){
            ConnectionPool connectionPool = new ConnectionPool(host, port);
            connectionPoolMap.putIfAbsent(ip, connectionPool);
        }
        return connectionPoolMap.get(ip).getChannel();
    }

就是从单例的connectMap中获取连接池,再从连接池中get一个channel来供应RPC调用

        Channel channel=connect(ip);
        channel.writeAndFlush(requestBuf);
        connectionPoolMap.get(ip).releaseChannel(channel);

在我们获取channel,并发送我们的RPC请求后,我们就要归还我们的连接,给其他调用使用,也就是release操作

这样我们就完成了初步的连接池预加载和复用连接的操作,从池中取一个channel进行RPC调用,用完归还连接池

最后的问题就是,一个ip对应一个连接池,一个池中有多个channel连接,当这个ip宕机或者其他情况不可用时,我们需要对这整个ip对应的连接池进行销毁

在我们的负载均衡接口的changeIP方法中,我们可以获得到本次不可用的ip,在这里,我们获取这些IP,并销毁他们对应的连接池即可。

    //释放对应连接池
            ConnectionPool connectionPool=RPCRequestNet.getInstance().connectionPoolMap.get(oldIP);
            if (connectionPool!=null) {
                connectionPool.destroyChannel();
            }

到这里就完成了连接池化的优化,下一篇章我会继续讲述我对异步RPC调用的优化

猜你喜欢

转载自blog.csdn.net/we_phone/article/details/81874323
今日推荐