笔者参与过某线上项目,该项目的数据存储量巨大,大概每秒都有10W左右的数据进行推送过来,项目最初采用的是mysql作为存储,但是以传统型的数据库,这种数据量支撑不了太久,采用分库分表也只是临时方案,最终leader决定用Hadoop + HBase 进行数据存储,但是上面担心某天大数据平台面临崩溃,所以必须同时写入一份文件到txt中,这样作为最后的保障,日后极端问题出现恢复的基础。
这种级别的数据,写入到文件当中,必定对IO有很高的要求,当时代码开发是另一位同事,采用的是Java提供的基本文件API FileOutputStream 这种,去对文件进行写入,这种方式在正常写入确实没什么问题,但是数据量太大的情况,写入速度极慢,导致MQ中的数据一直不停的挤压阻塞。
当时项目组在讨论用什么样的方式,可以快速的将文件写入到txt中,笔者经过思考,发现我们项目的日志,不论访问多频繁,流量有多大,日志总能实时的写入到日志文件中去,日志框架正是SpringBoot默认支持的logback,所以笔者灵机一动,将采用此种方式的方案,提出到上面,经过讨论后,决定采用logback提供的API,对数据写入到txt中,我先简介一下数据流动流程 和 写入过程
client data -> netty -> rabbit mq -> logback -> txt
也就是客户端将数据通过长连接的方式,发送到服务端的netty中,netty接收到数据后,存入的MQ中,我的logback程序作为订阅者,读到数据后,进行分类写入txt下面看下代码实现方式,(只挂重点写入txt的代码,很多地方笔者做了删减,读者知道意思即可)
@Component
@RabbitListener
public class DataListener {
private static final Logger logger = LoggerFactory.getLogger(DataListener.class);
@RabbitHandler
public void getMessage(@Payload Data data) {
try {
data.setUuid(UUID.randomUUID().toString().replace("-", ""));
// 此处根据业务逻辑以数据的num后四位作为文件名称
String str4id = "0000";
if (data.getNum() != null && data.getNum().length() > 4) {
str4id = data.getNum().substring(data.getNum().length() - 4, data.getNum().length());
}
data.setStr4id(10000 + Long.parseLong(str4id));
String fileName = "data_" + data.getStr4id();
// 文件名称可以看做是一个客户端对象,这个客户端对象会不停的发送数据过来, 对每个文件名称都做了一个DataLogWriter对象的缓存,有就使用已经创建的写入,没有则创建并保存到map缓存中后写入
DataLogWriter dataSqlWriter = dataUtil.objMap.get(fileName);
if (dataSqlWriter == null) {
dataSqlWriter = new dataLogWriter(fileName);
dataUtil.objMap.put(fileName, dataSqlWriter);
}
dataSqlWriter.outLog(dataUtil.getdataLog(data));
} catch (Exception ex) {
ex.printStackTrace();
logger.error("gpswriter异常:", ex);
}
}
}
package cn.net.leadu.log;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.core.FileAppender;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class DataLogWriter {
private final Logger logbackLogger;
LoggerContext loggerContext = new LoggerContext();
public FileAppender fileAppender = new FileAppender();
private String dayDate ; //当前天
private String loggerName; //日志文件名称
public void outLog(String msg){
SimpleDateFormat yMd = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String nowDate = yMd.format(date);
//每次写入的时候,查看当前时间和对象中的天数是否相同,如果不相同则重置日志路径
if(!nowDate.equals(dayDate)) {
dayDate = nowDate;
fileAppender.setFile("/data/data_backup/" + dayDate + "/" + loggerName + ".txt");
fileAppender.start();
logbackLogger.debug(msg);
}else{
logbackLogger.debug(msg);
}
}
public DataLogWriter(String logfileName) {
SimpleDateFormat yMd = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
loggerName = logfileName;
fileAppender.setContext(loggerContext);
fileAppender.setName("logfile");
//文件路径 按照天 / 小时 / num后四位分文件
dayDate = yMd.format(date);
fileAppender.setFile("/data/data_backup/" + dayDate +"/" + loggerName + ".txt");
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern("%msg");
encoder.start();
fileAppender.setEncoder(encoder);
fileAppender.start();
logbackLogger = loggerContext.getLogger("nelogger");
logbackLogger.addAppender(fileAppender);
logbackLogger.isAdditive();
}
}
当时按照这么写后,上服务,开集群~ 发现不行… MQ依然堆积如山还是下不去?
为啥… 笔者思考一番后,发现原因是因为多个服务同时对一个文件写入时,会发现文件占用,相互等待,死锁问题,导致MQ的消息无法被读取写入,这里涉及到文件句柄问题,读者知道即可,解决方案就是,不开集群~ 哈哈,只开一个服务,
这时候读写速度堪比火箭~ 堆积几百万的数据,一分钟不到全部读写完~
这个服务在线上跑了巨久的时间,没有出现过问题,当然运维一定要关注你们的服务器磁盘,不要出现磁盘快被写满出现的各种异常问题~ 大数据和硬件存储息息相关~
这里不去探讨logback对文件写入到底作了何种优化,如果读者碰到同样的问题,可以参考本文章,采用logback方式,去解决这个问题~