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).消费者

原文地址:https://www.cnblogs.com/uttu/p/2908685.html