java 大块内存做数据缓存 大数据的高效收发
马上就要过年了,同事该撤的都撤了,我的心也开始飞了,趁此机会将以前做的数据缓存总结下,包含三个部分:模块简介、概要设计、详细设计、核心思想和代码。
1.模块简介:
模块名称为数据总线,功能主要是封装数据传送层,使得数据生产者、消费者不再关心数据的传输,让其只需关心逻辑的处理。
2.概要设计:
数据的生产者通过调用write()接口将要发送的数据交给数据总线,数据的消费者实现read()接口,当数据总线接收到消费者需求的数据时,回调消费者的read()方法处理数据,见末尾图:
3.详细设计:
生产者设计图见末尾图;数据按组存储到DataFlow中,然后在缓存至DistributeData中,在DistributeData中将数据根据全量模式和均衡模式进行发送,其中均衡模式要处理均衡的算法问题,使数据均衡的发送到需求机器上
消费者设计图见末尾图;
4.核心思想和代码:
数据量每天大约在1TB左右,而且socket都是长连接,所以采用了block io来处理接收的数据(ps:这里我试过nio总是在第二天抛出GC overhead limit exceeded);这里的关键点在于数据流过模块的时候不能消耗太多的系统资源,因为机器上还有更加重要的解码程序,再加上要处理的数据非常大,所以流经模块的时候只做内存拷贝,不能新开辟内存。
每个连接每秒流过的数据量在2M/s,连接数为生产者的个数(ps:数据生产者为C++解码程序,经常会进行调整,连接数多的时候有24个,少的时候有5个),本文详细介绍的是为数据总线消费者端为每个连接开辟50-80M内存接收数据的问题。
通信协议:1byte(5e)1byte(7a)14byte(4byte dataLen 2byte dataType 8byte key),协议为比较常见的tlb的变换, 5e7a为包的标志位,其后是14字节的包头信息,再后面是数据
思想:接收数据做缓存,然后分析接收到的数据是否是完整的一条数据,是则将其push到消费队列中去。通常C++程序做这些比较多,因为有指针可以方便操作,但是咱java里的引用不就是一个不用delete的指针么!想明白这点,下面就好操作了。
1).几个重要的引用(指针):
//对底层的socket的简单封装对象 private TCPSession session; //开辟的大块内存 public byte[] bytes; //Node<ConsumerPo>一条完整数据的在大块内存中位置和自身信息的对象 private List<Node<ConsumerPo>> outList; /**这里是标识dataQueue处理这个 BusReciver对象的byte[]的位置,用来保证数据的准确性,只有上接受的数据被处理了才能继续读取*/ private int processIndex = 0; //consumer the data's end index private volatile int nextProcessIndex = 0; private int preProcessIndex = 0; /**一条数据的长度*/ private int dataLen = 0; /**分析数据的起始位置:1、头文件14个字节 2、dataLen数据*/ private int parseOff = 0; /**顺序读取中,bytes已经存放了的字节数*/ private int readOff = 0; /**是否解析了数据头14个字节*/ private boolean header = false; /**判断是否解析出了包头5E7A*/ private boolean finded = false; /**一次读取的长度*/ private int readOnce = 0; /**业务处理不及时,读取的数据存放在这里,业务没有处理完可以重复覆盖*/ private byte[] abandonBytes; private volatile boolean isReadBeforeProcess = true; public CallbackHookThread nextProcess; private String groupName ;
2).几个逻辑重要点:
processIndex = nextProcessIndex; if(readOff == processIndex && !isReadBeforeProcess){ readOnce = session.read(abandonBytes); if(-1 == readOnce){ destory(); break; } } else { /* * 否则,判断readOff和processIndex的大小,如果readOff<processIndex,则说明本次存储只能存储在bytes中的readOff到processIndex否则,可以存储在bytes中的readOff到bytes.length中 */ if(readOff < processIndex){ readOnce = session.read(bytes, readOff, processIndex - readOff); } else { readOnce = session.read(bytes, readOff, bytes.length - readOff); } readOff += readOnce; //数据存储到末尾和没有存储到末尾,处理方法不同 if(readOff != bytes.length){ findAllData(); } else { storeToEnd(); } }
first:读取数据前要判断是否还有存储空间,如果没有,则此次读取的数据要扔掉,并且做日志记录;首先将消费者最近一次处理的数据的end index变量nextProcessIndex赋值给processIndex,这里大家肯定犯牢骚了,为什么不用同一个变量?一是因为接收数据和处理数据是两个线程,并发中对下标用同一个变量处理肯定会出现i++的问题,二是如果用同一个变量加锁也不行,效率太低,用过sync关键字和ReentrantLock锁测试过,直接不能满足需求(ps:千兆网环境,要求收发在500Mbps,即跟普通STAT盘的存盘速度接近)。基于上述两点,想到新增一个变量nextProcessIndex,让消费者去控制,接收线程每次从寄存器(volatile关键字)里取出来这个变量值,并赋值给本地变量即可(ps:为什么不直接用nextProcessIndex,因为volatile关键字修辞的变量在多核cpu时是不能进行指令重排的,所以在此增加一个变量,最大限度的提升并行处理的能力)
然后比较判断readOff == processIndex && !isReadBeforeProcess是否为true,如果为true,说明消费者处理数据太慢,内存里没有空间了,此时要扔数据,并进行记录;否则按照相应的条件进行数据的存储。这里的对于isReadBeforeProcess的作用必须详细说明下:此变量为true用来标识第一次存储或者消费者消费到bytes.len,并且又从bytes的index=0开始消费了一条数据;为false,用来标识数据接收到bytes.len,然后从index=0重新开始存储数据(在有空间即readOff != processIndex时),只有当readOff == processIndex && !isReadBeforeProcess为true,即接收者存储赶上了消费者才;这样做保证了存储数据的完整性和安全性,接收和存储转头要解决数据不能被覆盖的问题,大家可以用图纸画出可能出现的情况,并分析之。如有更好的思路,可以发我邮件(lifeonhadoop@gmail.com)一起探讨。
second:接下来要做的就是解析接收到的数据,主要是递归思想的应用,这里有个特别需要注意的地方,如果用到递归方法,则方法内部不能有局部变量,因为在包长度特别小,如100byte时,java栈会因为太多的变量而溢出。
private void findAllData(){ //通过finded来标识本次读取的数据是否需要寻找包头 while(true){ if(!header){ //是否需要寻找包的标志 if(!finded){ //读取的数据最后一个字节的没有解析 finded = findHeader(parseOff, readOff - 2); } //这里有两重逻辑:如果此次进来finded为true,和finded进来为false,但findHeader()后为TRUE,则都要其后的数据解析。 if(finded){ if(readOff - parseOff >= 14){ parseProtol(bytes, parseOff, 14); parseOff += 14; outList.get(0).value.setOff(parseOff); header = true; } } } if(header){ //不需要寻找包头,直接进行数据长度的判断 if(readOff - parseOff >= dataLen){ parseOff += dataLen; outList.get(0).value.setEnd(parseOff - 1); outList.get(0).value.setRecv(this); outList.get(0).value.setGroupName(groupName); nextProcess.push(outList.get(0)); outList.remove(0); finded = false; header = false; continue; } } break; } }
/**这里是读取到bytes最后一位时的处理方式*/ public void storeToEnd(){ isReadBeforeProcess = false; while(true){ //寻找找到标志位 if(!finded){ finded = findHeader(parseOff, readOff - 2); //移动最后以为到头部 if(!finded){ if(processIndex >= 1){ bytes[0] = bytes[bytes.length - 1]; parseOff = 0; readOff = 1; return; } else { this.abondanAndLog(1); readOff = 0; return; } } } if(!header){ //找到标识位后,判断剩下的长度是否够14位,够,找包头;不够则移动数据 if(readOff - parseOff >= 14){ parseProtol(bytes, parseOff, 14); parseOff += 14; outList.get(0).value.setOff(parseOff); header = true; } else { if(processIndex >= 13){ //移动不够14字节的数据到头部 int moveLen = readOff - parseOff; System.arraycopy(bytes, parseOff, bytes, 0, moveLen); parseOff = 0; readOff = moveLen; return; } else { abondanAndLog(readOff - parseOff); readOff = 0; return; } } } if(header){ if(readOff - parseOff < dataLen){ if(processIndex >= dataLen){ //移动已经存放的数据体到头部,方便应用读取 int moveLen = readOff - parseOff; System.arraycopy(bytes, parseOff, bytes, 0, moveLen); outList.get(0).value.setOff(0); parseOff = 0; readOff = moveLen; return; } else { abondanAndLog(readOff - parseOff); readOff = 0; return; } } else { parseOff += dataLen; outList.get(0).value.setEnd(parseOff - 1); outList.get(0).value.setRecv(this); outList.get(0).value.setGroupName(groupName); nextProcess.push(outList.get(0)); outList.remove(0); finded = false; header = false; continue; } } } }
findAllData()没有太多可以说的地方,storeToEnd()方法里注意一些逻辑:如果存储到末尾的是5e,则要把5e放到bytes[0]中;如果找到包的标志位,但是到bytes.length()不够14个字节的包头信息,则要把接收到的部分搬迁到bytes开头;同时在相应的位置要更新isReadBeforeProcess parseOff readOff finded header的值。
three:提供给消费者端修改nextProcessIndex的setNextProcessIndex(int)方法,当消费者从其队列中消费一个element时,要调用此方法,更新对应的变量
public void setNextProcessIndex(int tmp) { preProcessIndex = nextProcessIndex; nextProcessIndex = tmp; if(nextProcessIndex < preProcessIndex){ isReadBeforeProcess = true; } }
5.总结:
通过此模块的coding和磨砺,使自己明白模块的入口、出口、接收、处理、出错的数据条数必须要记录清楚,否则总有很多的问题要去解决,而如果没有记录,哪怕问题不在此模块上,但是领导不知道。。。仅以此献给做公用模块的码农!
6.整体的代码:
package app.consumer.impl.block; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import po.ConsumerPo; import app.consumer.IProcessData; import app.consumer.impl.CallbackHookThread; import context.StarfishContext; import core.log.CrontabLog; import core.log.ManagerLog; import core.pool.IThreadObject; import core.pool.PoPool; import core.struct.Node; import core.tcpsocket.TCPSession; public class DistributeProcessDataBlockImpl implements IThreadObject, CrontabLog, IProcessData{ private TCPSession session; public byte[] bytes; private List<Node<ConsumerPo>> outList; public Object dequeued = new Object(); /**这里是标识dataQueue处理这个 BusReciver对象的byte[]的位置,用来保证数据的准确性,只有上接受的数据被处理了才能继续读取*/ private int processIndex = 0; private volatile int nextProcessIndex = 0; private int preProcessIndex = 0; /**一条数据的长度*/ private int dataLen = 0; /**分析数据的起始位置:1、头文件14个字节 2、dataLen数据*/ private int parseOff = 0; /**顺序读取中,bytes已经存放了的字节数*/ private int readOff = 0; /**是否解析了数据头14个字节*/ private boolean header = false; /**判断是否解析出了包头5E7A*/ private boolean finded = false; /**一次读取的长度*/ private int readOnce = 0; /**业务处理不及时,读取的数据存放在这里,业务没有处理完可以重复覆盖*/ private byte[] abandonBytes; private volatile boolean isReadBeforeProcess = true; public CallbackHookThread nextProcess; private String groupName ; public volatile boolean started = true; private String remoteIp ; private String remoteAddr; private volatile int msgAbandonSize = 0; private long recvSize = 0; private long dayRecvSize = 0; private Logger log; private long threadId = 0; private StringBuilder logRes = new StringBuilder(); public DistributeProcessDataBlockImpl(TCPSession session, int len, CallbackHookThread consumer) { this.session = session; bytes = new byte[len*1024*1024]; outList = new ArrayList<Node<ConsumerPo>>(); remoteIp = session.getSock().getInetAddress().getHostAddress(); remoteAddr = remoteIp + "/" + session.getSock().getPort(); abandonBytes = new byte[1024*1024]; this.nextProcess = consumer; } public void process(){ log = ManagerLog.newInstance(this); log.info("started a new thread-"+" to recv data"); threadId = Thread.currentThread().getId(); while(started){ /* * 第一次从头存储的时候不用考虑processIndex的范围,直接存储,但是第二次从头开始存储的范围要小于processIndex;如果processIndex等于readOff,并且 * 队列dataQueue不为空,说明上次接受的数据还没有被应用处理,为保证数据的准确性,不能存储数据到bytes中,扔掉数据 */ processIndex = nextProcessIndex; if(readOff == processIndex && !isReadBeforeProcess){ readOnce = session.read(abandonBytes); if(-1 == readOnce){ destory(); break; } recvSize += readOnce; this.abondanAndLog(readOnce); } else { /* * 否则,判断readOff和processIndex的大小,如果readOff<processIndex,则说明本次存储只能存储在bytes中的readOff到processIndex * 否则,可以存储在bytes中的readOff到bytes.length中 */ if(readOff < processIndex){ readOnce = session.read(bytes, readOff, processIndex - readOff); } else { readOnce = session.read(bytes, readOff, bytes.length - readOff); } //处理读取异常和连接超时的问题 if(readOnce < 0){ if(readOnce == -2){ //连接超时,判断此连接是否有效;如果出现网络异常,要停掉此处理线程 try { InetAddress address = InetAddress.getByName(remoteIp); if(address.isReachable(5000)){ continue; } else { log.error("the connection to "+remoteIp+"is unreachable , stop this receive thread"); } } catch (UnknownHostException e) { log.error(e); } catch (IOException e) { log.error(e); } } destory(); break; } readOff += readOnce; recvSize += readOnce; dayRecvSize += readOnce; //数据存储到末尾和没有存储到末尾,处理方法不同 if(readOff != bytes.length){ findAllData(); } else { storeToEnd(); } } } } public void abondanAndLog(int size){ finded = false; header = false; if(!outList.isEmpty()){ outList.remove(0); } parseOff = readOff; msgAbandonSize += size; } /**这里是读取到bytes最后一位时的处理方式*/ public void storeToEnd(){ isReadBeforeProcess = false; while(true){ //寻找找到标志位 if(!finded){ finded = findHeader(parseOff, readOff - 2); //移动最后以为到头部 if(!finded){ if(processIndex >= 1){ bytes[0] = bytes[bytes.length - 1]; parseOff = 0; readOff = 1; return; } else { this.abondanAndLog(1); readOff = 0; return; } } } if(!header){ //找到标识位后,判断剩下的长度是否够14位,够,找包头;不够则移动数据 if(readOff - parseOff >= 14){ parseProtol(bytes, parseOff, 14); parseOff += 14; outList.get(0).value.setOff(parseOff); header = true; } else { if(processIndex >= 13){ //移动不够14字节的数据到头部 int moveLen = readOff - parseOff; System.arraycopy(bytes, parseOff, bytes, 0, moveLen); parseOff = 0; readOff = moveLen; return; } else { abondanAndLog(readOff - parseOff); readOff = 0; return; } } } if(header){ if(readOff - parseOff < dataLen){ if(processIndex >= dataLen){ //移动已经存放的数据体到头部,方便应用读取 int moveLen = readOff - parseOff; System.arraycopy(bytes, parseOff, bytes, 0, moveLen); outList.get(0).value.setOff(0); parseOff = 0; readOff = moveLen; return; } else { abondanAndLog(readOff - parseOff); readOff = 0; return; } } else { parseOff += dataLen; outList.get(0).value.setEnd(parseOff - 1); outList.get(0).value.setRecv(this); outList.get(0).value.setGroupName(groupName); nextProcess.push(outList.get(0)); outList.remove(0); finded = false; header = false; continue; } } } } private void findAllData(){ //通过finded来标识本次读取的数据是否需要寻找包头 while(true){ if(!header){ //是否需要寻找包的标志 if(!finded){ //读取的数据最后一个字节的没有解析 finded = findHeader(parseOff, readOff - 2); } //这里有两重逻辑:如果此次进来finded为true,和finded进来为false,但findHeader()后为TRUE,则都要其后的数据解析。 if(finded){ if(readOff - parseOff >= 14){ parseProtol(bytes, parseOff, 14); parseOff += 14; outList.get(0).value.setOff(parseOff); header = true; } } } if(header){ //不需要寻找包头,直接进行数据长度的判断 if(readOff - parseOff >= dataLen){ parseOff += dataLen; outList.get(0).value.setEnd(parseOff - 1); outList.get(0).value.setRecv(this); outList.get(0).value.setGroupName(groupName); nextProcess.push(outList.get(0)); outList.remove(0); finded = false; header = false; continue; } } break; } } /**寻找5E7A包头,找到要初始化一个ArrayList*/ private boolean findHeader(int off, int end){ int i; for (i = off; i <= end; i++) { if((bytes[i] & 0xff) == 0x5E && (bytes[i+1] & 0xff) == 0x7A){ parseOff = i + 2; Node<ConsumerPo> node = PoPool.getCPo(); ConsumerPo po ; if(null == node){ po = new ConsumerPo(); node = new Node<ConsumerPo>(po); } else { po = node.value; } outList.add(node); return true; } } //这里是寻找到传入的倒数第二个字节没有找到包头的情况,下次从end+1下表开始读取 parseOff = i; return false; } /**解析14个字节的包头信息*/ private void parseProtol(byte[] byteTmp, int off, int len){ dataLen = (makeInt(byteTmp[off], byteTmp[off+1], byteTmp[off+2], byteTmp[off+3])) - 14; off += 4; outList.get(0).value.setDataType(makeShort(byteTmp[off], byteTmp[off+1])); off += 2; outList.get(0).value.setKey(makeLong(byteTmp[off], byteTmp[off+1], byteTmp[off+2], byteTmp[off+3], byteTmp[off+4], byteTmp[off+5], byteTmp[off+6], byteTmp[off+7])); } private static short makeShort(byte b2,byte b1){ return (short) ((b2 << 8) | (b1 & 0xff)); } private static int makeInt(byte b3, byte b2, byte b1, byte b0) { return (int) ((((b3 & 0xff) << 24) | ((b2 & 0xff) << 16) | ((b1 & 0xff) << 8) | ((b0 & 0xff) << 0))); } private static long makeLong(byte b7, byte b6, byte b5, byte b4, byte b3, byte b2, byte b1, byte b0) { return ((((long) b7 & 0xff) << 56) | (((long) b6 & 0xff) << 48) | (((long) b5 & 0xff) << 40) | (((long) b4 & 0xff) << 32) | (((long) b3 & 0xff) << 24) | (((long) b2 & 0xff) << 16) | (((long) b1 & 0xff) << 8) | (((long) b0 & 0xff) << 0)); } public void destory() { started = false; session.close(); ManagerLog.unregister(this); log.warn("the recv session-"+remoteIp+" is closed"); } public void setNextProcessIndex(int tmp) { preProcessIndex = nextProcessIndex; nextProcessIndex = tmp; if(nextProcessIndex < preProcessIndex){ isReadBeforeProcess = true; } } @Override public void cleanMsgVariable() { dayRecvSize = 0; } @Override public String writeLog() { logRes.delete(0, logRes.capacity()); if(0 != msgAbandonSize){ logRes.append("warn "); } dayRecvSize += recvSize; logRes.append("starfish today, blockIO, DistributeProcessDataBlockImpl connect to"+this.remoteAddr+", thread-" + threadId + " --> (abandonNum : " + msgAbandonSize/(double)(1024*1024) + "MB), (recvSpeed : " + (double)recvSize/(1024*1024*(Integer.parseInt(StarfishContext.context.getProperty("log_interval")))*60) + "MB/s), (totleRecvSize:"+(double)dayRecvSize/(1024*1024)+"MB)"); recvSize = 0; msgAbandonSize = 0; return logRes.toString(); } @Override public byte[] getBytes() { return this.bytes; } public String getRemoteAddr(){ return remoteAddr; } public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } }
7.测试结果:
两个生产者时,消费者端的接收情况如下(千兆网,并且是现场测试的,网络环境非常复杂):
256b-pkg:61.8MB/s
1k-pkg:85.25MB/s
2k-pkg:98.44MB/s
1M-pkg:105.3MB/s
64M-pkg:95.3MB/s
8.设计图:
1).概要设计
2).生产者
3).消费者