• 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栈会因为太多的变量而溢出。

    findAllData
    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;
        }
        
    }
    storeToEnd()
    /**这里是读取到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时,要调用此方法,更新对应的变量

    setNextProcessIndex
    public void setNextProcessIndex(int tmp) {
        preProcessIndex = nextProcessIndex;
        nextProcessIndex = tmp;
        if(nextProcessIndex < preProcessIndex){
            isReadBeforeProcess = true;
        }
    }

    5.总结:

      通过此模块的coding和磨砺,使自己明白模块的入口、出口、接收、处理、出错的数据条数必须要记录清楚,否则总有很多的问题要去解决,而如果没有记录,哪怕问题不在此模块上,但是领导不知道。。。仅以此献给做公用模块的码农!

    6.整体的代码:

    package app.consumer.impl.block;
    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).消费者

  • 相关阅读:
    Oracle Sql优化之日期的处理
    python excel转xml
    3、vue项目里引入jquery和bootstrap
    1、vue.js开发环境搭建
    2、vue-router2使用
    go 初步
    一个全局变量引起的DLL崩溃
    在linux用gdb查看stl中的数据结构
    gdb看core常用命令
    redis常用命令
  • 原文地址:https://www.cnblogs.com/uttu/p/2908685.html
Copyright © 2020-2023  润新知