在上一篇的文章中https://blog.csdn.net/we_phone/article/details/79053472
我初步完成了整个RPC框架的搭建,从服务调用到服务发现再到负载均衡,这一篇开始进行的是一系列我所知的优化操作
这一篇我讲的是连接池,比较简单,详细代码已托管到github:https://github.com/wephone/MeiZhuoRPC
首先了解一下什么是连接池
连接池
在平时我们的数据库增删查改的业务中,应该绝大多数使用到了连接池技术,例如C3P0,Druid这类框架,帮我们完成了对数据库连接的池化。
当没有连接池时会遇到这样的问题:
要么每一次进行远程连接时都去创建一个连接,用完立即释放,也就是频繁的创建和销毁大量连接。
或者各个调用共用一个单一连接,但在多线程的情况下,需要加锁来避免争抢的问题,这个方案的问题是效率低下且复杂性高。
连接池的做法就是预先加载一定数量的连接放到资源池里,当需要连接时则从连接池中拿出一个来使用,用完则还回去,当并发量巨大,连接资源匮乏时,根据一定的策略来新建连接或者拒绝。
和jdk中的线程池类似,都是资源池化的思想。
在我前面的框架编写中,我采用的是单一连接的方式,并且用加锁来保证不会重复连接等等,其实在netty框架的一些版本中,有FixedChannelPool这个东西作为netty的连接池,但在我用的版本里没有发现这个,出于学习的目的,就自行用其他库写了一个来作为RPC框架的连接池。
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调用的优化