前言:
自Zookeeper-3.4.0版本开始,就提供了自动清理事务日志和快照日志的功能。
我们可以想一下,如果不清理这些日志会怎样?貌似短期也不会怎样,但是由于这些日志是直接落入当前磁盘的,所以长期以往,磁盘肯定会被占满,导致zookeeper服务无法正常提供。
本文就介绍下这个自动清理日志的功能。
1.配置自动清理
配置的方式很简单,就是在zoo.cfg中添加以下两个配置即可,示例如下:
# 保存3个快照,3个日志文件
autopurge.snapRetainCount=3
# 间隔1个小时执行一次清理
autopurge.purgeInterval=1
autopurge.snapRetainCount指定的是最多保留几个日志文件
autopurge.purgeInterval指定的是多久去清理一次
需要注意的是:这个配置默认是不开启的,所以需要我们手动添加。
2.源码分析清理功能实现
那么这个清理功能时如何实现的呢?我们直接在源码中查找配置出现的地方,最终发现在DatadirCleanupManager中有相关的使用,那么我们就来分析下这个类
2.1 DatadirCleanupManager结构分析
public class DatadirCleanupManager {
// 任务状态
public enum PurgeTaskStatus {
NOT_STARTED, STARTED, COMPLETED;
}
// 默认状态
private PurgeTaskStatus purgeTaskStatus = PurgeTaskStatus.NOT_STARTED;
// 快照日志路径
private final String snapDir;
// 事务日志路径
private final String dataLogDir;
// 最多保留的日志文件数
private final int snapRetainCount;
// 执行频率
private final int purgeInterval;
// 调度器
private Timer timer;
// 构造器
public DatadirCleanupManager(String snapDir, String dataLogDir, int snapRetainCount,
int purgeInterval) {
this.snapDir = snapDir;
this.dataLogDir = dataLogDir;
this.snapRetainCount = snapRetainCount;
this.purgeInterval = purgeInterval;
LOG.info("autopurge.snapRetainCount set to " + snapRetainCount);
LOG.info("autopurge.purgeInterval set to " + purgeInterval);
}
}
那么这个构造器是在哪里被调用的呢?
源码一通调用,发现入口是:QuorumPeerMain.initializeAndRun()方法,具体如下
public class QuorumPeerMain {
protected void initializeAndRun(String[] args) throws ConfigException, IOException {
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
// 在这里开启自动清理任务
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
...
}
}
2.2 DatadirCleanupManager.start()
public class DatadirCleanupManager {
public void start() {
// 任务状态及参数校验
if (PurgeTaskStatus.STARTED == purgeTaskStatus) {
LOG.warn("Purge task is already running.");
return;
}
if (purgeInterval <= 0) {
LOG.info("Purge task is not scheduled.");
return;
}
// 创建调度器
timer = new Timer("PurgeTask", true);
TimerTask task = new PurgeTask(dataLogDir, snapDir, snapRetainCount);
// 以purgeInterval执行频率执行PurgeTask
timer.scheduleAtFixedRate(task, 0, TimeUnit.HOURS.toMillis(purgeInterval));
purgeTaskStatus = PurgeTaskStatus.STARTED;
}
}
start()方法主要是启动了一个Timer定时调度器,执行频率为purgeInterval小时,执行任务为PurgeTask
2.3 PurgeTask
static class PurgeTask extends TimerTask {
private String logsDir;
private String snapsDir;
private int snapRetainCount;
public PurgeTask(String dataDir, String snapDir, int count) {
logsDir = dataDir;
snapsDir = snapDir;
snapRetainCount = count;
}
@Override
public void run() {
LOG.info("Purge task started.");
try {
// 最终交由PurgeTxnLog来执行
PurgeTxnLog.purge(new File(logsDir), new File(snapsDir), snapRetainCount);
} catch (Exception e) {
LOG.error("Error occurred while purging.", e);
}
LOG.info("Purge task completed.");
}
}
2.4 PurgeTxnLog.purge()
public class PurgeTxnLog {
public static void purge(File dataDir, File snapDir, int num) throws IOException {
if (num < 3) {
throw new IllegalArgumentException(COUNT_ERR_MSG);
}
FileTxnSnapLog txnLog = new FileTxnSnapLog(dataDir, snapDir);
// 获取最新的num个快照文件
List<File> snaps = txnLog.findNRecentSnapshots(num);
int numSnaps = snaps.size();
if (numSnaps > 0) {
// 方法处理就是:清理这num个文件之外的其他文件
purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
}
}
static void purgeOlderSnapshots(FileTxnSnapLog txnLog, File snapShot) {
// 最新的zxid(num个文件中最小的那个)
// 只要比这个zxid小的文件都需要被清理
final long leastZxidToBeRetain = Util.getZxidFromName(
snapShot.getName(), PREFIX_SNAPSHOT);
// retainedTxnLogs中是需要被保留的日志
final Set<File> retainedTxnLogs = new HashSet<File>();
retainedTxnLogs.addAll(Arrays.asList(txnLog.getSnapshotLogs(leastZxidToBeRetain)));
class MyFileFilter implements FileFilter{
private final String prefix;
MyFileFilter(String prefix){
this.prefix=prefix;
}
public boolean accept(File f){
if(!f.getName().startsWith(prefix + "."))
return false;
if (retainedTxnLogs.contains(f)) {
return false;
}
long fZxid = Util.getZxidFromName(f.getName(), prefix);
if (fZxid >= leastZxidToBeRetain) {
return false;
}
return true;
}
}
// files中添加的是zxid比leastZxidToBeRetain小的所有事务日志文件,是需要被删除的
List<File> files = new ArrayList<File>();
File[] fileArray = txnLog.getDataDir().listFiles(new MyFileFilter(PREFIX_LOG));
if (fileArray != null) {
files.addAll(Arrays.asList(fileArray));
}
// fileArray中添加的是zxid比leastZxidToBeRetain小的所有快照文件,需要被删除
fileArray = txnLog.getSnapDir().listFiles(new MyFileFilter(PREFIX_SNAPSHOT));
if (fileArray != null) {
files.addAll(Arrays.asList(fileArray));
}
// 删除老文件
for(File f: files)
{
final String msg = "Removing file: "+
DateFormat.getDateTimeInstance().format(f.lastModified())+
"\t"+f.getPath();
LOG.info(msg);
System.out.println(msg);
if(!f.delete()){
System.err.println("Failed to remove "+f.getPath());
}
}
}
}
总结:
代码不算复杂,直接保留对应数目的文件后,直接把其他的全部删除。
这个其他的意思是通过zxid进行比较过的,zxid较小的所有文件(事务日志和快照日志的后缀就是zxid,所以这里直接通过zxid进行比较是合适的)。