源码分析mycat1.6之网络篇---前端线程模型下篇(读写事件篇)
来源:互联网 发布:js回调函数是闭包吗 编辑:程序博客网 时间:2024/06/07 20:14
1、mycat前端读事件处理
程序的入口处:NIOReactor的run方法
直接调用的方法为AbstractConnection的aysnRead方法。
NIOSocketWR,IO处理的核心入口:
@Override public void asynRead() throws IOException { ByteBuffer theBuffer = con.readBuffer; //@1 if (theBuffer == null) { //@2 theBuffer = con.processor.getBufferPool().allocate(con.processor.getBufferPool().getChunkSize()); con.readBuffer = theBuffer; } int got = channel.read(theBuffer); //@3 con.onReadData(got); //@4 }
代码@1,首先,一个Connection与一个SocketWR一对一。首先获取该连接的reader buffer。
代码@2,如果reader buffer为空,则像内存池中申请一块内存(内存的具体分配,将在后文的内存篇重点讲解)
代码@3,从SocketChannel中读取数据到 readerBuffer中。
代码@4,主要是对读入的数据进行处理。具体代码见AbstractConnection的onReadData。
/** * 读取可能的Socket字节流 */ public void onReadData(int got) throws IOException { //@1 if (isClosed.get()) { return; } lastReadTime = TimeUtil.currentTimeMillis(); if (got < 0) { this.close("stream closed"); return; } else if (got == 0 && !this.channel.isOpen()) { this.close("socket closed"); return; } // @2 netInBytes += got; processor.addNetInBytes(got); //@3 // 循环处理字节信息 int offset = readBufferOffset, length = 0, position = readBuffer.position(); // @4 for (;;) { length = getPacketLength(readBuffer, offset); //@5 if (length == -1) { //@6 if (offset != 0) { this.readBuffer = compactReadBuffer(readBuffer, offset); } else if (readBuffer != null && !readBuffer.hasRemaining()) { throw new RuntimeException( "invalid readbuffer capacity ,too little buffer size " + readBuffer.capacity()); } break; } if (position >= offset + length && readBuffer != null) { // @7 // handle this package readBuffer.position(offset); byte[] data = new byte[length]; readBuffer.get(data, 0, length); handle(data); // @8 // maybe handle stmt_close if(isClosed()) { // @9 return ; } // offset to next position offset += length; // reached end if (position == offset) { // @10 // if cur buffer is temper none direct byte buffer and not // received large message in recent 30 seconds // then change to direct buffer for performance if (readBuffer != null && !readBuffer.isDirect() && lastLargeMessageTime < lastReadTime - 30 * 1000L) { // used temp heap if (LOGGER.isDebugEnabled()) { LOGGER.debug("change to direct con read buffer ,cur temp buf size :" + readBuffer.capacity()); } recycle(readBuffer); readBuffer = processor.getBufferPool().allocate(processor.getBufferPool().getConReadBuferChunk()); } else { if (readBuffer != null) { readBuffer.clear(); } } // no more data ,break readBufferOffset = 0; break; } else { // @11 // try next package parse readBufferOffset = offset; if(readBuffer != null) { readBuffer.position(position); } continue; } } else { // @12 // not read whole message package ,so check if buffer enough and // compact readbuffer if (!readBuffer.hasRemaining()) { readBuffer = ensureFreeSpaceOfReadBuffer(readBuffer, offset, length); } break; } } }
首先,mycat处理读事件的接收缓存区为readBuffer,每个Connection只有一个。接下来,主要的思路是如果接收缓存区中包含一个完整的数据包,则对数据包进行处理,如果没有,确保接收缓存区能够容纳一个包的大小,然后等更多数据到达。整体浏览了该方法的实现,有些异议,本文先按照作者的思路进行分析,然后提出自己的优化建议供大家交流讨论。
代码@1,参数got,就是本次SocketChannel读入的字节数据。
代码@2,如果读取的数据为-1,或为0,并且通道已经关闭了,直接返回。
代码@3,NIOProcessor的 netInBytes 主要是用来统计信息的。
代码@4,offset,上次读到readerBuffer的偏移量。length,当前mysql请求包的数据长度(包括协议头),position,readerBuf当前可写,可读的位置,当前的reader ByteBuffer处于可写状态。
代码@5,开始循环解析数据包。从当前reader buffer中获取数据包的长度。
protected int getPacketLength(ByteBuffer buffer, int offset) { int headerSize = getPacketHeaderSize(); if ( isSupportCompress() ) { headerSize = 7; } //@51 if (buffer.position() < offset + headerSize) { // @52 return -1; } else { // @53 int length = buffer.get(offset) & 0xff; length |= (buffer.get(++offset) & 0xff) << 8; length |= (buffer.get(++offset) & 0xff) << 16; return length + headerSize; } }
代码@51,首先mysql协议包长度,如果没有启用压缩,协议头部长度固定为4字节,如果启用了压缩,则为7个字节。
代码@52,判断该readerBuffer中数据是否有一个完整的数据包头部,当前position为第一个可写的位置,readerBuffer中第一个有效数据为offset。如果不够一个完整的数据包,则返回-1。
代码@53,读取头部的前3个字节,表示数据包(报文体)的长度,由于使用了小端序列。然后返回加上头部长度,得出数据包的最终长度。
代码@6,如果readerBuffer中没有包含一个完整的数据包,并且offset不为0,则压缩该read buffer,节省空间。
代码@7、@8,如果该readerBuf中包含一个完整的mysql数据包。准备从readerbuf中读取一个完整的数据包,这里没有使用flip方法,而是手动改变position的值。
首先设置position的值为offset,然后在堆里创建一个与待解析数据包相同大小的byte[],然后就数据读入到该数组中。在这里我觉得这样做不妥,既然是用的堆外内存,在处理数的时候,为什么需要将数据从堆外内存拷贝到堆内呢?关于优化点先放到文章的末尾。然后将一个完整的数据包交给NIOHandler进行处理。
代码@9,处理完一个完整的数据包后,再次检查连接是否已经关闭。
代码@10,如果ReaderBuffer读取完毕,进行一次优化,如果使用的Reader ByteBuffer是一个堆内Buffer,则使用直接内存进行替换。
代码@11,尝试继续解析下一个数据包。
代码@12,如果readBuffer中不包含一个完整的数据包,则判断是否需要扩容当前的ByteBuffer,如果需要,则扩容,否则结束本次读任务,等待更多数据到达。
onReadData中,每解析一个数据包,将转发给NIOHandler进行处理(单线程中)。
压缩readBuffer的实现:
private ByteBuffer compactReadBuffer(ByteBuffer buffer, int offset) { if(buffer == null) { return null; } buffer.limit(buffer.position()); buffer.position(offset); buffer = buffer.compact(); readBufferOffset = 0; return buffer; }readBuf扩容的实现:
private ByteBuffer ensureFreeSpaceOfReadBuffer(ByteBuffer buffer, int offset, final int pkgLength) { // need a large buffer to hold the package if (pkgLength > maxPacketSize) { throw new IllegalArgumentException("Packet size over the limit."); } else if (buffer.capacity() < pkgLength) { ByteBuffer newBuffer = processor.getBufferPool().allocate(pkgLength); lastLargeMessageTime = TimeUtil.currentTimeMillis(); buffer.position(offset); newBuffer.put(buffer); readBuffer = newBuffer; recycle(buffer); readBufferOffset = 0; return newBuffer; } else { if (offset != 0) { // compact bytebuffer only return compactReadBuffer(buffer, offset); } else { throw new RuntimeException(" not enough space"); } } }
至此,mycat读事件的解决就分析到这里了,提出如下4个性能优化点:
性能优化点:
1)首先 该类的 readBufferOffset 属性其实完成可以不需要,依然能够合理的解析出数据包。readBufferOffset是voliate类型的字段,有一定的性能损坏。
2)第二个重点,在解析数据包的时候:
if (position >= offset + length && readBuffer != null) {
// handle this package
readBuffer.position(offset);
byte[] data = new byte[length];
readBuffer.get(data, 0, length);
handle(data);
// 其他代码省略
readBuffer是 堆外内存,现在在处理数据的时候,又从堆外内存,拷贝一次到堆内存(byte[]),这里多了一本从堆内存拷贝到堆内存的步骤,抵消了直接内存的优势;是否可 以实现一个从ReadBuffer slice(int startIndex, int posistion),使用readerBuffer的内存,但用SliceByteBuffer进行后面的数据包解析等等。
这样的理解是否合理。
3)当Reader Buffer中没有一个足够的mysql数据包时,此时的扩容条件,可以进一步优化为
if (!readBuffer.hasRemaining() || (readBuffer.limit - offset + 1 ) < length ) {
readBuffer = ensureFreeSpaceOfReadBuffer(readBuffer, offset, length);
// readBuffer.limit - offset + 1 表示readBuffer可以写入的总长度,如果可以写入的总长度小于数据包的长度,则需要扩容
}
4)readBuffer只有扩容,没有容量压缩,这里不同于上面compactReadBuffer的实现。举个例子,默认readBuffer的容量为4K,突然一个数据包,用了16M,,但以后每个包的容量又只需要4K,但该连接的readBuffer始终占有16M的空间,导致内存空间的浪费。
2、mycat前端写事件处理
入口:
AbstractConnection的doNextWriteCheck()
public void doNextWriteCheck() throws IOException {
this.socketWR.doNextWriteCheck();
}
在讲解写事件之前,我们不妨再看看NIOSocketWR的相关属性,我们从前文已经知道NIOSocketWR与AbstractConnection一一对应。NIOSocketWR是负责Connection网络的读写。
private SelectionKey processKey;
private static final int OP_NOT_READ = ~SelectionKey.OP_READ;
private static final int OP_NOT_WRITE = ~SelectionKey.OP_WRITE;
private final AbstractConnection con;
private final SocketChannel channel;
private final AtomicBoolean writing = new AtomicBoolean(false); //@1通道是否正在处理写事件,默认为false。
接下来重点关注doNextWrite
public void doNextWriteCheck() { if (!writing.compareAndSet(false, true)) { // @1 return; } try { boolean noMoreData = write0(); //@2 writing.set(false); if (noMoreData && con.writeQueue.isEmpty()) { //@3 if ((processKey.isValid() && (processKey.interestOps() & SelectionKey.OP_WRITE) != 0)) { disableWrite(); } } else { // @4 if ((processKey.isValid() && (processKey.interestOps() & SelectionKey.OP_WRITE) == 0)) { enableWrite(false); } } } catch (IOException e) { if (AbstractConnection.LOGGER.isDebugEnabled()) { AbstractConnection.LOGGER.debug("caught err:", e); } con.close("err:" + e); } }
代码@1,如果有写操作正在进行,则直接退出。
代码@2,具体的通道写,稍后查看。
代码@3,如果没有数据待写,并且写任务队列为空,并且关注了写事件,则取消写事件。noMoreData为true,表示没有更多数据。
代码@4,如果有更多数据待写,并且没有关注写事件,重新关注写事件。
接下来重点关注写操作的具体执行逻辑:
private boolean write0() throws IOException { int written = 0; ByteBuffer buffer = con.writeBuffer; if (buffer != null) { //@1 while (buffer.hasRemaining()) { written = channel.write(buffer); if (written > 0) { con.netOutBytes += written; con.processor.addNetOutBytes(written); con.lastWriteTime = TimeUtil.currentTimeMillis(); } else { break; } } if (buffer.hasRemaining()) { con.writeAttempts++; return false; } else { con.writeBuffer = null; con.recycle(buffer); } } while ((buffer = con.writeQueue.poll()) != null) { //@2 if (buffer.limit() == 0) { con.recycle(buffer); con.close("quit send"); return true; } buffer.flip(); try { while (buffer.hasRemaining()) { written = channel.write(buffer);// java.io.IOException: // Connection reset by peer if (written > 0) { con.lastWriteTime = TimeUtil.currentTimeMillis(); con.netOutBytes += written; con.processor.addNetOutBytes(written); con.lastWriteTime = TimeUtil.currentTimeMillis(); } else { break; } } } catch (IOException e) { con.recycle(buffer); throw e; } if (buffer.hasRemaining()) { con.writeBuffer = buffer; con.writeAttempts++; return false; } else { con.recycle(buffer); } } return true; }
经典的写处理操作,while( buffer.hasRemaining()),在循环中,调用通道的write方法,然后判断写入的字节数,如果大于0,则继续写,否则跳出,然后再次检测待写缓存区是否有剩余空间,如果没有,则回收该ByteBuffer,如果没有,待下次继续写入(直接结束本次写入操作)。如果成功将AbstractConnection的writeBuffer写入后,继续处理写任务队列中的ByteBuffer,如果全部写完,则返回true,表示没有更多数据,否则返回false。写事件的处理就分析到到,既然有任务缓存区,我们肯定也要关注一下,缓存区中的待写ByteBuffer是从哪来的。关注一下呗:
关注一下AbstractConnection
protected volatile ByteBuffer writeBuffer;protected final ConcurrentLinkedQueue<ByteBuffer> writeQueue = new ConcurrentLinkedQueue<ByteBuffer>();//写任务队列private final void writeNotSend(ByteBuffer buffer) { // 放入写任务队列,但不立即写出 if (isSupportCompress()) { ByteBuffer newBuffer = CompressUtil.compressMysqlPacket(buffer, this, compressUnfinishedDataQueue); writeQueue.offer(newBuffer); } else { writeQueue.offer(buffer); } } //该方法先将写任务放入到写任务队列中,然后触发一次写操作,doNextWriteCheck 支持重入不产生副作用。 @Override public final void write(ByteBuffer buffer) { if (isSupportCompress()) { ByteBuffer newBuffer = CompressUtil.compressMysqlPacket(buffer, this, compressUnfinishedDataQueue); writeQueue.offer(newBuffer); } else { writeQueue.offer(buffer); } // if ansyn write finishe event got lock before me ,then writing // flag is set false but not start a write request // so we check again try { this.socketWR.doNextWriteCheck(); } catch (Exception e) { LOGGER.warn("write err:", e); this.close("write err:" + e); } } public ByteBuffer checkWriteBuffer(ByteBuffer buffer, int capacity, boolean writeSocketIfFull) { // @1 if (capacity > buffer.remaining()) { //@2 if (writeSocketIfFull) { writeNotSend(buffer); return processor.getBufferPool().allocate(capacity); } else {// Relocate a larger buffer //@3 buffer.flip(); ByteBuffer newBuf = processor.getBufferPool().allocate(capacity + buffer.limit() + 1); newBuf.put(buffer); this.recycle(buffer); return newBuf; } } else { return buffer; } }
代码@1,首先checkWriteBuffer的调用者,一般都是mysql协议包,比如RowDataPacket。buffer,待写入的缓存区,capacity,需要继续往缓存区buffer写入的字节数,writeSocketIfFull,如果buffer无法容纳待写入的字节数,是创建一个新的,还是扩容。
代码@2,如果待写入字节数大于ByteBuffer可以容纳空间,此时要么重新分配一个新的,将原先的直接放入到任务队列中,要么采取扩容。这两种方式都有优劣
如果直接放入到任务队列,重新按容量创建一个,可能会浪费一部分内存,但扩容,会涉及数据的移动,损耗性能。综合来说,个人还是倾向于第一种,重新分配一个新的,然后将原先的ByteBuffer丢入到写任务队列中。
附上checkWriteBuffer的调用示例(io.mycat.net.mysql.RowDataPacket):
@Override public ByteBuffer write(ByteBuffer bb, FrontendConnection c, boolean writeSocketIfFull) { bb = c.checkWriteBuffer(bb, c.getPacketHeaderSize(), writeSocketIfFull); BufferUtil.writeUB3(bb, calcPacketSize()); bb.put(packetId); for (int i = 0; i < fieldCount; i++) { byte[] fv = fieldValues.get(i); if (fv == null ) { bb = c.checkWriteBuffer(bb, 1, writeSocketIfFull); bb.put(RowDataPacket.NULL_MARK); }else if (fv.length == 0) { bb = c.checkWriteBuffer(bb, 1, writeSocketIfFull); bb.put(RowDataPacket.EMPTY_MARK); } else { bb = c.checkWriteBuffer(bb, BufferUtil.getLength(fv), writeSocketIfFull); BufferUtil.writeLength(bb, fv.length); bb = c.writeToBuffer(fv, bb); } } return bb; }
0 0
- 源码分析mycat1.6之网络篇---前端线程模型下篇(读写事件篇)
- 源码研读Mycat1.6之网络篇---前端线程模型(应用程序与mycat交互篇)上篇
- 源码分析mycat1.6之网络篇----前后端连接交互设计(mycat命令处理流程)
- 源码分析MyCat之SQL解析篇-----sql解析引擎druid之抽象语法树(mycat1.6)
- 源码分析mycat1.6之mysql通信协议篇之COM_QUERY(SELECT语句报文解析)
- 源码分析mycat1.6之mysql通信协议篇之存储过程调用
- Memcached源码分析之网络模型篇
- Memcached源码分析之网络模型篇
- ZMQ源码分析(二)-- 网络&线程模型
- Mycat源码篇 : MyCat线程模型分析
- HBase源码分析之事件处理模型
- memcached源码分析之线程模型
- memcached源码分析之线程模型 【转】
- PHP源码分析之线程安全模型
- Memcached源码分析之线程模型
- netty源码分析 之十三 线程模型
- Netty源码分析之Reactor线程模型
- Mycat1.6之三大配置文件加载源码阅读
- POJ-3621-01分数规划,spfa判负环
- Android app 内存分配
- pthread_cond_broadcast与pthread_cond_signal使用区别
- 图解之TCP三次握手与四次挥手
- 阻止click点击事件
- 源码分析mycat1.6之网络篇---前端线程模型下篇(读写事件篇)
- 【GIT 基础篇五】绝杀技之远程仓库(搭建)
- 解题报告:HDU 4111 Alice and Bob 递推|博弈
- iOS9 & iOS10 HTTP 不能正常使用的解决办法
- js == 与 === 的区别[转]
- PHP网站优化的6个基础方法
- Android 源码分析——Looper,Messagequeue,Message,handler 初始化及 handler 机制简介
- 随笔(迷茫的程序员)-0
- SURF源码解析