APDPlat提供了业务日志和监控日志,以便对用户操作进行审计、对系统性能进行调优。
业务日志主要包括数据的增删改日志、备份恢复日志以及用户登录注销日志。监控日志主要包括用户请求响应时间、内存使用情况、全文索引重建情况、系统启动关闭事件。
设计目标:
1、灵活,可以很容易地启用或停用
2、性能,不对正常的业务操作造成影响
3、开放,容易和第三方系统整合
下面阐述具体的设计及实现:
1、在灵活性方面,可以在配置文件config.properties或config.local.properties中指定选项来启用(true)或停用(false),如下所示:
配置:
#是否启用系统监控模块中的功能 monitor.memory=true monitor.performance=true monitor.runing=true monitor.login=true monitor.index=true monitor.backup=true #是否启用业务日志记录功能 log.create=true log.delete=true log.update=true
代码:
memoryMonitor=PropertyHolder.getBooleanProperty("monitor.memory"); if(memoryMonitor){ LOG.info("启用内存监视日志"); LOG.info("Enable memory monitor log", Locale.ENGLISH); }else{ LOG.info("禁用内存监视日志"); LOG.info("Disable memory monitor log", Locale.ENGLISH); }
2、在性能方面,使用内存缓冲区来临时存储日志对象,可以节省磁盘和网络开销,缓冲区的大小可以配置,当缓冲区满了或是人工强制执行的时候才会对日志进行持久化或其他处理,这样不但提高了吞吐量(批量提交、批量处理),而且对用户业务处理的影响非常小,因为产生日志对象之后只需要将日志对象加入缓冲区即可(无阻塞、内存操作)。除此之外,当对缓冲区中的日志对象进行持久化或其他处理的时候,会有独立的线程池中的线程来完成,不会阻塞用户业务处理线程(线程复用、异步非阻塞),如下所示:
MemoryState logger=new MemoryState(); try { logger.setServerIP(InetAddress.getLocalHost().getHostAddress()); } catch (UnknownHostException ex) { LOG.error("获取服务器地址出错",ex); LOG.error("Can't get server's internet address", ex, Locale.ENGLISH); } logger.setAppName(SystemListener.getContextPath()); logger.setRecordTime(new Date()); logger.setMaxMemory(max); logger.setTotalMemory(total); logger.setFreeMemory(free); logger.setUsableMemory(logger.getMaxMemory()-logger.getTotalMemory()+logger.getFreeMemory()); BufferLogCollector.collect(logger);
首先,构造了一个日志对象logger,设置相关信息,然后调用BufferLogCollector.collect(logger)将日志对象加入内存缓冲区,BufferLogCollector.collect方法如下:
public static <T extends Model> void collect(T t){ LOG.debug("将日志加入缓冲区:\n"+t.toString()); buffers.add(t); //判断缓冲区是否达到限制 if(buffers.size() > logBufferMax){ LOG.info("缓冲区已达到限制数:"+logBufferMax+" ,处理日志"); handleLog(); } }
buffers是类ConcurrentLinkedQueue的实例,不限制大小,不会有日志存不下而暂停阻塞的情况发生,支持多线程并发操作,链表结构,尤其适合增删操作。加入缓冲区之后就会判断缓冲区是否已满,如满则会处理,logBufferMax的值从哪里来的呢?
private static final int logBufferMax = PropertyHolder.getIntProperty("log.buffer.max");
log.buffer.max的值需要在配置文件config.properties或config.local.properties中指定,值越大,吞吐量越好,对用户的影响越小(除了批量处理的时候,发生次数很少),当然内存的占用也越大,需要根据实际情况权衡:
log.buffer.max=1000
如果缓冲区满了,怎么处理日志呢?看看handleLog方法:
public static void handleLog(){ if(shoudHandle()){ executorService.submit(handleLogRunnable); } }
先判断是否应该处理,看是否有logHandlers,对缓冲区大小做了检查,如下所示:
private static boolean shoudHandle(){ if(logHandlers.isEmpty()){ LOG.error("未找到任何LogHandler"); return false; } int len=buffers.size(); if(len==0){ LOG.info("没有日志需要保存"); LOG.info("No logs need to save:"+len, Locale.ENGLISH); return false; } return true; }
这里使用了ExecutorService来对线程进行管理,如检查通过,则将具体的日志处理逻辑(封装在handleLogRunnable中)提交给executorService
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
handleLog方法把具体的日志处理逻辑(封装在handleLogRunnable中)提交给executorService (线程执行服务),对线程的提交和执行做了解耦,复用线程池,提高了性能。由于具体的日志处理逻辑运行于独立的线程中,故不会阻塞用户业务处理线程。
3、怎么理解开放呢?先接着上面看看提交给线程执行服务的日志的处理逻辑:
private static final HandleLogRunnable handleLogRunnable = new HandleLogRunnable();
在线程中调用了私有静态内部类LogSaver的save方法,如下所示:
private static class HandleLogRunnable implements Runnable{ @Override public void run() { LOG_SAVER.save(); } }
private static final LogSaver LOG_SAVER = new LogSaver();
save里面究竟是怎么处理的呢?请看:
private static class LogSaver{ public void save(){ int len=buffers.size(); List<Model> list=new ArrayList<>(len); for(int i=0;i<len;i++){ list.add(buffers.remove()); } //把日志交给LogHandler处理 for(LogHandler logHandler : logHandlers){ logHandler.handle(list); } } }
首先把缓冲区里面的数据全部取出来(取出来之后缓冲区里面就没有了)构成一个链表,数目不超过规定的大小(配置文件中指定的log.buffer.max的值),然后把链表分别交给每一个注册的LogHandler来处理。注意这里不用多个线程来让多个LogHandler并行执行的原因:一是会增大系统负荷,对用户业务处理造成影响;二是多个LogHandler并行执行的话就满足不了需要先后执行顺序的要求了。
接着看LogHandler,这是一个接口:
/** * 日志处理接口: * 可将日志存入独立日志数据库(非业务数据库) * 可将日志传递到activemq\rabbitmq\zeromq等消息队列 * 可将日志传递到kafka\flume\chukwa\scribe等日志聚合系统 * @author 杨尚川 */ public interface LogHandler { public <T extends Model> void handle(List<T> list); }
那么logHandlers是怎么来的呢?
private static final List<LogHandler> logHandlers = new ArrayList<>();
@Service public class BufferLogCollector implements ApplicationListener {
BufferLogCollector实现了Spring的ApplicationListener接口,当Spring的所有对象正确完整地装配完成后会回调BufferLogCollector实现的方法:
@Override public void onApplicationEvent(ApplicationEvent event){ if(event instanceof ContextRefreshedEvent){ LOG.info("spring容器初始化完成,开始解析LogHandler"); String handlerstr = PropertyHolder.getProperty("log.handlers"); if(StringUtils.isBlank(handlerstr)){ LOG.info("未配置log.handlers"); return; } LOG.info("handlerstr:"+handlerstr); String[] handlers = handlerstr.trim().split(";"); for(String handler : handlers){ LogHandler logHandler = SpringContextUtils.getBean(handler.trim()); if(logHandler != null){ logHandlers.add(logHandler); LOG.info("找到LogHandler:"+handler); }else{ LOG.info("未找到LogHandler:"+handler); } } } }
怎么跟Spring扯上关系了呢?因为logHandlers是从Spring的容器中获得的。从这里可以得知,logHandlers是由配置文件config.properties或config.local.properties中的log.handlers选项指定的,如下所示:
#日志缓冲区的最大值,只有达到最大值或手工强制刷新时,日志才会被持久化 #当用户在管理界面查看任意一种日志时,会强制刷新 #log.handlers可指定多个,用;分割,值为spring的bean名称 #如:databaseLogHandler;fileLogHandler;consoleLogHandler log.buffer.max=1000 log.handlers=databaseLogHandler;
开放的含义体现在:LogHandler定义了统一的接口,允许任意的扩展,LogHandler的实现由Spring来管理,通过在配置文件中指定log.handlers的值为托管在Spring中的多个bean name,可以有序地调用多个LogHandler实现,调用顺序就是配置文件中指定的先后顺序。
最后来看几个LogHandler的实现:
1、DatabaseLogHandler(将日志保存到关系数据库,使用JPA)
@Service public class DatabaseLogHandler implements LogHandler{ private static final APDPlatLogger LOG = new APDPlatLogger(DatabaseLogHandler.class); //使用日志数据库 @Resource(name = "serviceFacadeForLog") private ServiceFacade serviceFacade; /** * 打开日志数据库em * @param entityManagerFactory */ private static void openEntityManagerForLog(EntityManagerFactory entityManagerFactory){ EntityManager em = entityManagerFactory.createEntityManager(); TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(em)); LOG.info("打开ForLog实体管理器"); } /** * 关闭日志数据库em * @param entityManagerFactory */ private static void closeEntityManagerForLog(EntityManagerFactory entityManagerFactory){ EntityManagerHolder emHolder = (EntityManagerHolder)TransactionSynchronizationManager.unbindResource(entityManagerFactory); LOG.info("关闭ForLog实体管理器"); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } @Override public <T extends Model> void handle(List<T> list) { int len = list.size(); LOG.info("需要保存的日志数目:"+len); LOG.info("The number of logs to be saved:"+len, Locale.ENGLISH); long start=System.currentTimeMillis(); EntityManagerFactory entityManagerFactoryForLog = SpringContextUtils.getBean("entityManagerFactoryForLog"); openEntityManagerForLog(entityManagerFactoryForLog); //保存日志 serviceFacade.create(list); closeEntityManagerForLog(entityManagerFactoryForLog); long cost=System.currentTimeMillis()-start; LOG.info("成功保存 "+len+" 条日志, 耗时: "+ConvertUtils.getTimeDes(cost)); LOG.info("Success to save "+len+" logs, elapsed: "+ConvertUtils.getTimeDes(cost), Locale.ENGLISH); } }
2、FileLogHandler(将日志保存到本地文件)
@Service public class FileLogHandler implements LogHandler{ private static int count = 1; @Override public <T extends Model> void handle(List<T> list) { StringBuilder str = new StringBuilder(); for(T t : list){ str.append(count++).append(":\n").append(t.toString()); } FileUtils.createAndWriteFile("/WEB-INF/logs/log-"+DateTypeConverter.toDefaultDateTime(new Date()).replace(" ", "-").replace(":", "-")+".txt", str.toString()); } }
3、ConsoleLogHandler(将日志在控制台输出)
@Service public class ConsoleLogHandler implements LogHandler{ private static int count = 1; @Override public <T extends Model> void handle(List<T> list) { for(T t : list){ System.out.println((count++) + ":"); System.out.println(t.toString()); } } }
APDPlat托管在Github