使用mina2框架编写分布式日志服务器

       日志服务器,那游戏服务器为实例,重要的数据库服务器,游戏服务器,网关服务器...往往不在意日志服务器,而通常情况下,日志简单的通过log4j,c下的log4c,cpp下的log4cpp等日志工具,当然,这类日志工具是主流的开源框架,性能,功能面都不错,但如果,我们需要记录几乎每个玩家,没个操作,那么这类在应用程序中添加的日志记录将会消耗大量系统资源,导致服务器卡慢.顾,分布式日志服务器在某些项目架构下非常有意义.

      我的分布式服务器,分为3个版本,c语言版本,原生java nio版本,以及依赖mina2框架实现,由于mina2框架的如日中天,相信,这篇文章对于java编程爱好者有一定帮助.

请看源码:

IoHandler的编写.log4j记录错误等日志,而LogWriter为真正的高频度,大数据日志记录工具.

package cn.vicky.mina.logserver;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Vicky.H
 */
public class LogIoHandler implements IoHandler {

    private static final Logger LOG = LoggerFactory.getLogger(LogIoHandler.class);
    private Set sessions = Collections.synchronizedSet(new HashSet(10));
    // 日志写入器
    private LogWriter logWriter;

    public LogIoHandler(LogWriter logWriter) {
        this.logWriter = logWriter;
    }

    public void sessionCreated(IoSession session) throws Exception {
        // Do nothing...
    }

    public void sessionOpened(IoSession session) throws Exception {
        sessions.add(session);
    }

    public void sessionClosed(IoSession session) throws Exception {
        sessions.remove(session);
    }

    public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
        // Do nothing...
    }

    public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
        LOG.error("exceptionCaught:", cause);
    }

    public void messageReceived(IoSession session, Object message) throws Exception {
        if (message instanceof IoBuffer) { // 确认数据类型
            logWriter.addLog((IoBuffer) message);
        }
    }

    public void messageSent(IoSession session, Object message) throws Exception {
        // Do nothing...
    }

}


 

LogWriter 其核心思想是,通过直接缓存区,以及缓存区交换区方式实现数据存储以及写入文件的高性能,高并发,并且通过5分钟左右的写入文件形式,在对日志时间要求不是非常精确下,可以直接通过日志文件名称方式,获得日志范围,可以精简日志文件大小,减少性能消耗.至于此处采用小文件方式记录日志,还有个原因就是方便数据统计.我在日志处理方面,没有编写额外的程序,通过正则表达式等方式拆解,而是通过linux shell命令拆解文件

package cn.vicky.mina.logserver;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.mina.core.buffer.IoBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Vicky.H
 * 日志写入定时器
 */
public class LogWriter extends Thread {

    private static final Logger LOG = LoggerFactory.getLogger(LogWriter.class);
    private static final DateFormat DF = new SimpleDateFormat("yyMMddHHmmss"); // 日志时间格式化
    
    private final Object syncObj = new Object(); // 缓冲区同步锁
    
    // 直接缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作的速度。
    // 但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并且长期存在,或者需要经常重用时,才使用这种缓冲区。
    // 故,此处使用直接缓冲区非常合适
    private ByteBuffer logcontent;
    private ByteBuffer logcontentswap;
    private boolean isRunning = true; // 日志线程运行状态
    
    private long interval = 180000; // 180秒间隔
    private int capacity = 1024 * 1024 * 18; // 缓冲区大小,预留足够,大约60秒6M
    
    public LogWriter () {
        this.logcontent = ByteBuffer.allocateDirect(capacity); // 10M缓冲区
        this.logcontentswap = ByteBuffer.allocateDirect(capacity); // 10M缓冲交换区
    }
    
    /**
     * 设置日志写入工具
     * @param capacity 缓冲区大小
     * @param interval 写入间隔(最好低于5分钟)
     * 缓冲区大小需要预留足够 大约100秒1M   capacity = interval / 10000
     */
    public LogWriter (int capacity,long interval) {
        this.logcontent = ByteBuffer.allocateDirect(capacity);
        this.logcontentswap = ByteBuffer.allocateDirect(capacity);
        this.interval = interval;
    }

    public long getInterval() {
        return interval;
    }

    /**
     * 该值默认为3分钟,由于通过文件创建时间方式和较短的时间间隔,在不精确每条日志时间的基础上
     * 便可以通过日志文件名的方式,大致获得每条日志的记录时间.
     * @param interval 
     */
    public void setInterval(long interval) {
        this.interval = interval;
    }
    
    public void addLog(IoBuffer log) {
        synchronized (syncObj) { // Buffer是非线程安全的
            logcontent.put(log.buf()); // 写入日志内容
        }
    }

    public void shutdown() {
        LOG.info("关闭日志记录器");
        this.isRunning = false;
    }

    @Override
    public void run() {
        LOG.info("开启日志记录器");
        while (isRunning) {
            LOG.info(new Date() + "写入日志");
            if (logcontent.position() == 0) {
                LOG.info(new Date() + "没有任何日志内容,跳过写入文件");
                continue;
            }
            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                LOG.error("线程休眠异常!", e);
            }

            try // 处理tempcontent
            {
                File file = new File("c:/log_" + DF.format(new Date()) + ".log");
                file.createNewFile();
                FileOutputStream fos = new FileOutputStream(file);
                FileChannel fileChannel = fos.getChannel();
                synchronized (syncObj) {
                    // 设置临时缓冲区指向当前接受数据的缓冲区
                    ByteBuffer tempcontent = logcontent;
                    // 当前缓冲区指向缓冲区交换区
                    logcontent = logcontentswap;
                    // 缓冲区交换区指向原来的缓冲区地址
                    logcontentswap = tempcontent;
                }
                logcontentswap.flip();
                fileChannel.write(logcontentswap, logcontentswap.position());
                fileChannel.close();
                fos.close();
                // 处理完毕后,重置缓冲区
                logcontentswap.clear();
            } catch (IOException e) {
                LOG.error("将日志写入文件异常!", e);
            }
        }
    }
}

 

简单的测试:

package cn.vicky.mina.logserver;

import java.util.Date;
import java.util.Scanner;
import org.apache.mina.core.service.IoAcceptor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 *
 * @author Vicky.H
 */
public final class MainApp {
    
    public static void main(String args[]) {
        if (System.getProperty("com.sun.management.jmxremote") != null) {
            System.out.println("JMX enabled.");
        } else {
            System.out.println("JMX disabled. Please set the "
                    + "'com.sun.management.jmxremote' system property to enable JMX.");
        }
        
        ConfigurableApplicationContext applicationContext = new ClassPathXmlApplicationContext("cn/vicky/mina/logserver/LogServerContext.xml");
        System.out.println("Listening ...");
        
        IoAcceptor ioAcceptor = (IoAcceptor) applicationContext.getBean("ioAcceptor");
        ioAcceptor.setCloseOnDeactivation(false); // 设置unbind()相应方式
        
        
        // TODO 开启测试
        class TestThread extends Thread {
            @Override
            public void run() {
                new TestClient().test();
            }
        }
        TestThread thread1 = new TestThread();
        TestThread thread2 = new TestThread();
        TestThread thread3 = new TestThread();
        TestThread thread4 = new TestThread();
        TestThread thread5 = new TestThread();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        
        
        System.out.println("输入exit关闭服务器!");
        Scanner scanner = new Scanner(System.in);
        String command = scanner.nextLine();
        while (!command.equals("exit")) {
            command = scanner.nextLine();
        }
        if (ioAcceptor.isCloseOnDeactivation()) {
            applicationContext.close(); // 关闭容器
            System.out.println("一旦unbind()将关闭所有连接");
        } else {
            // 需要2个步骤才能关闭服务器
            applicationContext.close();
            System.out.println("一旦unbind()将阻止之后的所有连接");
            ioAcceptor.dispose(true);
            System.out.println("关闭服务器");
        }
        System.out.println("服务器将在1分钟后完全关闭!" + new Date());
    }
}


测试客户端

package cn.vicky.mina.logserver;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.session.IoSessionConfig;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Vicky.H
 */
public class TestClient {

    public void test() {
        final IoConnector connector = new NioSocketConnector();

// 2.
        IoSessionConfig sessionConfig = connector.getSessionConfig();
        sessionConfig.setReaderIdleTime(60000);
        sessionConfig.setReadBufferSize(1024 * 2);

// 3
        DefaultIoFilterChainBuilder filterChain = connector.getFilterChain();
        ProtocolCodecFactory codecFactory = new TextLineCodecFactory(Charset.forName("UTF-8"),
                LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue());
        IoFilter filter = new ProtocolCodecFilter(codecFactory);
        filterChain.addLast("codec", filter);

// 5
        connector.setHandler(new IoHandler() {

            Logger LOGGER = LoggerFactory.getLogger(IoHandler.class);

            public void sessionCreated(IoSession session) throws Exception {
                LOGGER.debug("创建一个会话链接(未连接)");
            }

            public void sessionOpened(IoSession session) throws Exception {
                LOGGER.debug("服务器与客户端会建立连接");
            }

            public void sessionClosed(IoSession session) throws Exception {
                LOGGER.debug("服务器与客户端会话链接已经关闭");
            }

            public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
                LOGGER.debug("客户端端进入空闲状态");
                session.close(false);
            }

            public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
                LOGGER.error("Error:客户端捕获到异常", cause);
                session.close(true);
            }

            public void messageReceived(IoSession session, Object message) throws Exception {
                LOGGER.debug("接收到服务器端消息:" + message);
            }

            /**
             * 注意这里是消息发送后回调,非发送消息!!!
             */
            public void messageSent(IoSession session, Object message) throws Exception {
                // 发送返回消息后就断开与客户端的链接,这就类似HTTP请求一样的短链接模式
                // session.close(true)
            }
        });

// 6
        ConnectFuture connectFuture = connector.connect(new InetSocketAddress("192.168.1.77", 1234));

// 7

        connectFuture.awaitUninterruptibly();
        IoSession session = connectFuture.getSession();
        System.out.println("连接成功!");
        for (int i = 0; i < 999; i++) {
            session.write("测试日志服务器性能!!! " + i);
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String args[]) {
        new TestClient().test();
    }
}


 

 

 

关于SHELL拆解日志文件:

pid:72356 item:11217 num:1 cast:2
pid:72529 item:11217 num:1 cast:2
pid:52567 item:262 num:1 cast:20
pid:52567 item:262 num:1 cast:20
pid:72539 item:11217 num:1 cast:2
pid:72151 item:11217 num:1 cast:2
pid:70569 item:11318 num:3 cast:48

如以上内容的日志文件,shell脚本:

#!/bin/bash
# 根据日志文件统计出item:11218 的购买次数以及购买数量

 for item in 11218 11219 11217 230

do  
awk '$4~/item:'$item'/{count+=1;sub("num:","",$5);sum+=$5}END{print '$item',count == "" ? " 0 " : " "count" ",sum == "" ? 0 : sum}' log.2012-0*(日期范围) >> log.txt

done

你可能感兴趣的:(Java)