Lucene 实战教程第十五章索引的冷热备份以及恢复和修复

JAVA herman 447浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

任何存储到硬盘的数据基本上都需要备份,当然像 Redis 这类的可能也需要备份。备份的话,一般大致都分为两种,热备份和冷备份。备份完了之后一般都需要恢复。那么关于 Lucene 的冷热备份以及恢复是怎样的呢?请看本文,我们一起来学习学习!

冷备份面临的问题

所有的备份中,冷备份是最简单的。所谓的冷备份,就是停掉机器,更准确的说是停掉应用直接将索引文件拷贝多份即可。

冷备份反应到 Lucene 中就是关闭 IndexWriter,然后逐一拷贝索引文件,但是如果索引比较大,那么这种备份操作会持续较长时间。而在备份期间,程序无法对索引文件进行修改,很多搜索程序是不能接受索引操作期间如此长时间停顿的。这种备份对于全年邀请零故障的服务来说简直就是灾难,所以这种备份方式基本上都不会被采用。

那么怎么在不关闭 IndexWriter 的情况下,又能正常高效的进行索引备份呢?

答案就是热备份。

热备份面临的问题

相比冷备份,热备份同样存在一些问题。比如,在拷贝索引期间,如果索引文件发生变化,那么会导致备份的索引文件不全、损坏、数据过期等问题。还有一个问题就是如果原索引文件损坏的话,再备份它也毫无意义,所以一定要备份的是最后一次成功 commit 之后的索引文件。最后,热备份是增量备份呢还是全量备份?这些问题我们都得考虑。

Lucene 索引热备份

比较幸运的是,我们需要的这些问题,Lucene 都给我们想到了,帮助我们做好了。

Lucene 提供了一个热备策略,就是 SnapshotDeletionPolicy,这样就能在不关闭 IndexWriter 的情况下,对程序最近一次索引修改提交操作时的文件引用进行备份,从而能建立一个连续的索引备份镜像。那么你也许会有疑问,在备份期间,索引出现变化怎么办呢?这就是 SnapshotDeletionPolicy 的牛逼之处,在使用 SnapshotDeletionPolicy.snapshot() 获取快照之后,索引更新提交时刻的所有文件引用都不会被 IndexWriter 删除,只要 IndexWriter 并未关闭,即使 IndexWriter 在进行更新、优化操作等也不会删除这些文件。如果说索引拷贝过程耗时较长也不会出现问题,因为被拷贝的文件时索引快照,在快照的有效期,其引用的文件会一直存在于磁盘上。

所以在备份期间,索引会比通常情况下占用更大的磁盘空间,当索引备份完成后,可以调用 SnapshotDeletionPolicy.release (IndexCommit commit) 释放指定的某次提交,以便让 IndexWriter 删除这些已被关闭或下次将要更新的文件。

需要注意的是,Lucene 对索引文件的写入操作是一次性完成的。这意味着你可以简单通过文件名比对来完成对索引的增量备份备份,你不必查看每个文件的内容,也不必查看该文件上次被修改的时间戳,因为一旦程序从快照中完成文件写入和引用操作,这些文件就不会改变了。

segments.gen 文件在每次程序提交索引更新时都会被重写,因此备份模块必须要经常备份该文件,但是在 Lucene 6.X 中注意 segments.gen 已经从 Lucene 索引文件格式中移除,所以无需单独考虑 segments.gen 的备份策略了。在备份期间,write.lock 文件不用拷贝。

SnapshotDeletionPolicy 类的使用有两个限制:

  • 该类在同一时刻只保留一个可用的索引快照,当然你也可以解除该限制,方法是通过建立对应的删除策略来同时保留多个索引快照
  • 当前快照不会保存到硬盘,这意味着你关闭旧的 IndexWriter 并打开一个新的 IndexWriter,快照将会被删除,因此在备份结束前是不能关闭 IndexWriter 的,否则也会报 org.apache.lucene.store.AlreadyClosedException: this IndexWriter is closed 异常。不过该限制也是很容易解除的:你可以将当前快照存储到磁盘上,然后在打开新的 IndexWriter 时将该快照保护起来,这样就能在关闭旧的 IndexWriter 和打开新 IndexWriter 时继续进行备份操作。

下面看一个热备份的简单例子。

import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.FSDirectory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Collection;
import static org.apache.lucene.document.Field.Store.YES;
/**
 * 测试Lucene索引热备
 */
public class TestIndexBackupRecovery {
    public static void main(String[] args) throws IOException, InterruptedException {
        String f = "D:/index_test";
        String d = "D:/index_back";
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new StandardAnalyzer());
        indexWriterConfig.setIndexDeletionPolicy(new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()));
        IndexWriter writer = new IndexWriter(FSDirectory.open(Paths.get(f)), indexWriterConfig);
        Document document = new Document();
        document.add(new StringField("ID", "111", YES));
        document.add(new IntPoint("age", 111));
        document.add(new StoredField("age", 111));
        writer.addDocument(document);
        writer.commit();
        document = new Document();
        document.add(new StringField("ID", "222", Field.Store.YES));
        document.add(new IntPoint("age", 333));
        document.add(new StoredField("age", 333));
        writer.addDocument(document);
        document.add(new StringField("ID", "333", Field.Store.YES));
        document.add(new IntPoint("age", 555));
        document.add(new StoredField("age", 555));
        for (int i = 0; i < 1000; i++) {
            document = new Document();
            document.add(new StringField("ID", "333", YES));
            document.add(new IntPoint("age", 1000000 + i));
            document.add(new StoredField("age", 1000000 + i));
            document.add(new StringField("desc", "ABCDEFG" + i, YES));
            writer.addDocument(document);
        }
        writer.deleteDocuments(new TermQuery(new Term("ID", "333")));
        writer.commit();
        backupIndex(writer, f, d);
        writer.close();
    }
    public static void backupIndex(IndexWriter indexWriter, String indexDir, String
            backupIndexDir) throws IOException {
        IndexWriterConfig config = (IndexWriterConfig) indexWriter.getConfig();
        SnapshotDeletionPolicy snapshotDeletionPolicy = (SnapshotDeletionPolicy) config.getIndexDeletionPolicy();
        IndexCommit snapshot = snapshotDeletionPolicy.snapshot();
        //设置索引提交点,默认是null,会打开最后一次提交的索引点
        config.setIndexCommit(snapshot);
        Collection<String> fileNames = snapshot.getFileNames();
        File[] dest = new File(backupIndexDir).listFiles();
        String sourceFileName;
        String destFileName;
        if (dest != null && dest.length > 0) {
            //先删除备份文件中的在此次快照中已经不存在的文件
            for (File file : dest) {
                boolean flag = true;
                //包括文件扩展名
                destFileName = file.getName();
                for (String fileName : fileNames) {
                    sourceFileName = fileName;
                    if (sourceFileName.equals(destFileName)) {
                        flag = false;
                        break;//跳出内层for循环
                    }
                }
                if (flag) {
                    file.delete();//删除
                }
            }
            //然后开始备份快照中新生成的文件
            for (String fileName : fileNames) {
                boolean flag = true;
                sourceFileName = fileName;
                for (File file : dest) {
                    destFileName = file.getName();
                    //备份中已经存在无需复制,因为Lucene索引是一次写入的,所以只要文件名相同不要要hash检查就可以认为它们的数据是一样的
                    if (destFileName.equals(sourceFileName)) {
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    File from = new File(indexDir + File.separator + sourceFileName);//源文件
                    File to = new File(backupIndexDir + File.separator + sourceFileName);//目的文件
                    FileUtils.copyFile(from, to);
                }
            }
        } else {
            //备份不存在,直接创建
            for (String fileName : fileNames) {
                File from = new File(indexDir + File.separator + fileName);//源文件
                File to = new File(backupIndexDir + File.separator + fileName);//目的文件
                FileUtils.copyFile(from, to);
            }
        }
        snapshotDeletionPolicy.release(snapshot);
        //删除已经不再被引用的索引提交记录
        indexWriter.deleteUnusedFiles();
    }
}

更多关于热备份的知识和技术问题,建议大家去 Lucene 的官网查看和学习!

恢复备份的索引

有备份就有恢复。Lucene 的恢复备份索引步骤如下:

  • 关闭索引目录下的全部 reader 和 writer,这样才能进行文件恢复。对于 Windows 系统来说,如果还有其它进程在使用这些文件,那么备份程序仍然不能覆盖这些文件
  • 删除当前索引目录下的所有文件,如果删除过程出现“访问被拒绝”(Access is denied)错误,那么再次检查上一步是否已完成
  • 从备份目录中拷贝文件至索引目录。程序需要保证该拷贝操作不会碰到任何错误,如磁盘空间已满等,因为这些错误会损坏索引文件
  • 对于损坏索引,可以使用 CheckIndex(org.apache.lucene.index)进行检查并修复

通常 Lucene 能够很好地避免大多数常见错误,如果程序遇到磁盘空间已满或者 OutOfMemoryException 异常,那么它只会丢失内存缓冲中的文档,已经编入索引的文档将会完好保存下来,并且索引也会保持原样。这个结果对于以下情况同样适用:如出现 JVM 崩溃,或者程序碰到无法控制的异常,或者程序进程被终止,或者操作系统崩溃,或者计算机突然断电等。

恢复索引非常的简单,代码如下:

/**
 * @param source 索引源
 * @param dest 索引目标
 * @param indexWriterConfig 配置相关
 */
public static void recoveryIndex(String source, String dest, IndexWriterConfig indexWriterConfig) {
    IndexWriter indexWriter = null;
    try {
        indexWriter = new IndexWriter(FSDirectory.open(Paths.get(dest)), indexWriterConfig);
    } catch (IOException e) {
        log.error("", e);
    } finally {
        //说明IndexWriter正常打开了,无需恢复
        if (indexWriter != null && indexWriter.isOpen()) {
            try {
                indexWriter.close();
            } catch (IOException e) {
                log.error("", e);
            }
        } else {
            //说明IndexWriter已经无法打开,使用备份恢复索引
            //此处简单操作,先清空损坏的索引文件目录,如果索引特别大,可以比对每个文件,不必全部删除  try {
            FileUtils.deleteDirectory(new File(dest));
            FileUtils.copyDirectory(new File(source), new File(dest));
        } catch(IOException e){
            log.error("", e);
            //使用备份恢复出错,那么就使用最后一招修复索引
            log.info("Check index {} now!", dest);
            try {
                IndexUtils.checkIndex(dest);
            } catch (IOException | InterruptedException e1) {
                log.error("Check index error!", e1);
            }
        }
    }
}

修复索引

当其它所有方法都无法解决索引损坏问题时,你的最后一个选项就是使用 CheckIndex 工具了。该工具除了能汇报索引细节状况以外,还能完成修复索引的工作。该工具会强制删除索引中出现问题的段,需要注意的是,该操作还会全部删除这些段包含的文档,该工具的使用目标应主要着眼于能够在紧急状况下让搜索程序再次运行起来,一旦我们进行了索引备份,并且备份完好,应优先使用恢复索引,而不是修复索引。

/**
 * CheckIndex会检查索引中的每个字节,所以当索引比较大时,此操作会比较耗时
 * @throws IOException
 * @throws InterruptedException
 */
public void checkIndex(String indexFilePath) throws IOException, InterruptedException {
    CheckIndex checkIndex = new CheckIndex(FSDirectory.open(Paths.get(indexFilePath)));
    checkIndex.setInfoStream(System.out);
    CheckIndex.Status status = checkIndex.checkIndex();
    if (status.clean) {
        System.out.println("Check Index successfully!");
    } else {
        //产生索引中的某个文件之后再次测试
        System.out.println("Starting repair index files...");
        //该方法会向索引中写入一个新的segments文件,但是并不会删除不被引用的文件,除非当你再次打开IndexWriter才会移除不被引用的文件
        //该方法会移除所有存在错误段中的Document索引文件
        checkIndex.exorciseIndex(status);
        checkIndex.close();
        //测试修复完毕之后索引是否能够打开
        IndexWriter indexWriter = new IndexWriter(FSDirectory.open(Paths.get(indexFilePath)), new IndexWriterConfig(new
                StandardAnalyzer()));
        System.out.println(indexWriter.isOpen());
        indexWriter.close();
    }
}

执行上面的代码,如果输出的信息最后是 Check Index successfully!则表明索引完好;如果输出的信息是 Starting repair index files… true 则表明破坏的索引被修复。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:codedq(5000人已满),微信号3:xttblog(超2800)。备注:“xttblog”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Lucene 实战教程第十五章索引的冷热备份以及恢复和修复