带着上篇文章的疑问,我们深入WAL实现的内部,我们看下记录日志的时候都有哪些操作。
跟着这个updateRepository方法,看到了NIFI文档所说的 FlowFileRepository
public interface FlowFileRepository extends Closeable {
/**
* 初始内容仓库,通过claimManager 可以将流文件的content部分写入内容仓库
*/
void initialize(ResourceClaimManager claimManager) throws IOException;
/**
*返回仓库容量
*/
long getStorageCapacity() throws IOException;
/**
* 返回可用容量
*/
long getUsableStorageSpace() throws IOException;
/**
* 返回存储文件地址
*/
String getFileStoreName();
/**
* 把处理器提交的记录更新入内容仓库
*/
void updateRepository(Collection<RepositoryRecord> records) throws IOException;
/**
* 把仓库中的flowfile加载进来,通过queueProvider 把流文件放入NIFI停止时其所在的队列
* 可以看出 是在NIFI启动时候调用的。
*/
long loadFlowFiles(QueueProvider queueProvider) throws IOException;
/**
* 仓库是否可用
*/
boolean isVolatile();
/**
* 返回一个按照顺序创建的ID
*/
long getNextFlowFileSequence();
/**
* 返回当前激活流文件的最大ID
*/
long getMaxFlowFileIdentifier() throws IOException;
/**
* Notifies the FlowFile Repository that the given identifier has been identified as the maximum value that
* has been encountered for an 'external' (swapped out) FlowFile.
* @param maxId the max id of any FlowFile encountered
*/
void updateMaxFlowFileIdentifier(long maxId);
/**
* 把流文件置换到磁盘
*/
void swapFlowFilesOut(List<FlowFileRecord> swappedOut, FlowFileQueue flowFileQueue, String swapLocation) throws IOException;
/**
* 把流文件从磁盘置换回内存
*/
void swapFlowFilesIn(String swapLocation, List<FlowFileRecord> flowFileRecords, FlowFileQueue flowFileQueue) throws IOException;
/**
* 验证置换文件前缀的合法性
*/
boolean isValidSwapLocationSuffix(String swapLocationSuffix);
/**
* 扫描仓库找到所有使用了指定 Resource Claims 的流文件.
default Map<ResourceClaim, Set<ResourceClaimReference>> findResourceClaimReferences(Set<ResourceClaim> resourceClaims, FlowFileSwapManager swapManager) throws IOException {
return null;
}
}
从以上内容也可以看出它的实现类,并不直接实现WAL的功能。进入 updateRepository,实现选择 WriteAheadFlowFileRepository,后边会看到这是配置文件默认的。
alwaysSync 表示每次都将更新同步到磁盘,读取的是配置文件,默认是 false.
再接下来是进行了一些校验,检查record的状态。 最后两行代码,一个是更新WAL,一个是更新ContentClaims。这个wal 从名字就能看出,功能的具体实现,就靠它了。来看看它的接口定义:
再看上边的功能描述
它将所有的更新都写入编辑日志。启动时,通过回放编辑日志中的所有的更新复原系统,但是这会写入大量的编辑日志,占用大量磁盘空间的同时需要花费很长的时间才能恢复。因此加入了Checkpoint 机制。把内存中当前状态刷入磁盘之后,把旧的日志删除掉。每次 Checkpoint 之后,日志继续写入编辑日志。在此基础上,若是系统重启,则先从Checkpoint 中恢复,然后重放编辑日志。
进入wal.update方法,因为如下默认配置的存在:
实现选择 SequentialAccessWriteAheadLog
提供的主要功能是能把所有操作通过写入单个日志文件的方式按顺序写入仓库。同时它也提到,使用了一个可回收复用的字节缓冲池,就是下图框起来的部分。
来到update方法
代码言简意赅,两个步骤,更新journal,一个是更新snapshot.。journal 就是操作日志,更新的结果就是在下边这个文件中增加记录:
snapshot笔者理解,就是文档里边提到的flowfile存在其中的hashmap.
它的更新操作更是简单。
注意到161-171的注释,翻一下是说,这个实现,保存了所有激活的records(激活指没被移除,也没被置换入磁盘),通过ID来索引。因此它只保存最新版的记录,203能看出来,如果ID已经有了,就直接覆盖掉了。
看到这里就让我想到了WAL的核心思想,先写日志再更新数据文件。
我们先暂停Update方法的深入,看下WAL的其他方法。
checkpoint
给日志加写锁,不再更新数据。
把缓存数据刷入磁盘
283行 生成下一个事务ID,事务ID的作用稍候解释。
具体作用稍候研究。
生成checkpoint时间点之后的事务日志要写的日志文件,并写入头部信息。然后释放锁
snapshot从内存写入磁盘。
进入 writeSnapshot 方法可以看到,最后是要写入到以下这个文件中
上边注释的意思是说,写的时候先会写入 partialFile 文件,写入完成,删掉checkpoint文件,把partialFile 重命名为checkpoint。假如NIFI在checkpoint的过程中停止运行会出现三情况:1、两个文件都存在,那说明partialFile 文件的写入还没完成。那用checkpoint就好。2、只有partialFile ,那说明partialFile 文件写入完成,已经删了旧的checkpoint,还没来得及重命名。3、只有checkpoint,直接用checkpoint就好。
接着checkpoint流程
dispose 方法进去能看到
释放旧的日志占用的所有资源。
所以checkpoint大流程可以总结为:把当前的 snapshot 持久化入磁盘,把旧的日志文件删掉,并建新的日志文件,checkpoint之后的日志写入新文件,并以当前最大事务ID命名。
这个操作是什么时候,由谁执行的呢?
专门起了一个后台线程,来执行checkpoint任务,默认两分钟执行一次。
recoverRecords
恢复 snapshot
checkpoint之后新增的记录,按照事务ID排序,
获取到已恢复记录的最大事务ID
上边的IF判断,是跳过小于已恢复事务最大ID的记录,下边就是重播事务日志了。
重置下一个事务ID
再执行一次checkpoint。
事务ID TransactionId
我们可以知道,系统掉电可能发生在上述问题的任何时候,如果NIFI停在执行了checkpoint之后,那根据上述过程,是可以很容易地就恢复到之前的状态的,但如果是发生在正在执行checkpoint的时候呢?旧的日志文件是在checkpoint的最后阶段删掉的。如果删之前,系统停掉了,那就会有两个id不一样的日志文件,此时恢复了snapshoot之后,找到以恢复记录的最大事务ID,那日志中,之后这个ID之后的更新才有效。所以此时的事务,指的是一次checkpoint,TransactionId用来保证一次操作的事务性。
小结
至此,关于NIFI WAL的实现流程,我们基本已经清楚了,后续我们会关注更细节的一些内容。