在我们应用经常需要统计一些计数,比如调用次数等。典型代码如下:
Keys keys = newKeys("XXX","doSomething");
MonitorLog.addStat(keys, System.currentTimeMillis()-begin, 1L);
在监控系统的页面上就能看到准实时的数据。
这是一个典型的计数器应用,实现也是比较经典。
类图
处理流程:
1. 初始化:
MonitorLog被加载时执行其static区代码:
static { /** 动态创建日志记录的配置 */
String userHome = System.getProperty(MonitorConstants.USER_HOME);
if(!userHome.endsWith(File.separator)) {
userHome+= File.separator;
}
String filePath = userHome+ MonitorConstants.DIR_NAME + File.separator;
File dir = newFile(filePath);
if (!dir.exists()) {
dir.mkdirs();
}
……
String classLoader =MonitorLog.class.getClassLoader().toString();
classLoader =classLoader.split("@")[0];
initAppLog4j(filePath,classLoader);
initMiddlewareLog4j(filePath,classLoader);
setHostName();
runWriteThread();
}
1)在用户的根目录下创建日志目录,linux下是/home/$user/logs/monitor
2)初始化应用日志文件,Logger和对应Appender,使用classloader名称作为文件名,使用DailyRollingFileAppender
3)同样方式初始化中间件的日志文件和Appender
4)启动flush线程
2. 应用方调用MonitorLog.addStat()方法,写日志
1) 从cache中获取key对应的ValueObject,如果没有,则创建之,同时如果cache的size超过默认100000,则该key被丢弃
使用RetreenLock+doublecheck实现。此处代码略有多余,使用ConcurrentHashMap的putIfAbsent即可。
2) 调用ValueObject. addCount()递增计数,代码:
long[]current;
long[] update = new long[NUM_VALUES];
do {
current= values.get();
update[0] = current[0] + value1;
update[1] = current[1] + value2;
} while(!values.compareAndSet(current, update));
使用CAS实现并发递增,使用AtomicReference实现对计数同步更新
3. flush线程,实现cache数据的定期刷新,2分钟一次
1) 周期控制,代码:
while (true) {
timerLock.lock();
try {
condition.await(waitTime, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("wait error", e);
} finally {
timerLock.unlock();
}
使用Condition的await实现。此处单线程用Thread.sleep更加简单实现。如果是多线程场景,condition.await可以提供另一种调度控制方式,对开发比较友好(await/signal)。
2) writeLog()调用,使用log4j刷日志到磁盘.
a. 使用临时Map存放待刷磁盘的数据快照
b. 遍历原始map数据,拼装成一个SringBuilder,并将Key数据快照存入临时Map
c. 将日志刷磁盘
if (tmp.size() > 0 && writeLog) {
appStatLog.info(sb);
}
d. 根据之前key的数据快照递减原始计数
// 循环把已经写入文件的数据从datas中减少
for (Keys key: tmp.keySet()) {
long[]values = tmp.get(key).getValues();
appDatas.get(key).deductCount(values[0],values[1]);
}
e. 同样方式刷中间件的日志数据
f. MonitorLog刷到磁盘的数据,会由哈勃的agent定期回收到哈勃服务端存储
小结:
1. 使用Map务必注意map引起的内存泄漏问题,size检查必不可少
2. Flush线程和业务线程并发控制很重要,使用CAS以提高并发性能
3. 需要对同一个对象的不同属性进行原子更新时,可以使用AtomicReference+CAS实现