文章目录
- 1.设置索引目录Directory
- 1.1Directory
- BaseDirectory类
- FSDirectory类
- SimpleFSDirectory
- NIOFSDirectory
- MMapDirectory
- 如何选择FSDirectory
- ByteBuffersDirectory
- RAMDirectory
- FilterDirectory
- SleepingLockWrapper
- TrackingTmpOutputDirectoryWrapper
- TrackingDirectoryWrapper
- LockValidatingDirectoryWrapper
- NRTCachingDirectory
- FileSwitchDirectory
- Lucene50CompoundReader
- 2.设置IndexWriter的配置:IndexWriterConfig
1.设置索引目录Directory
Directory用来维护索引目录中的索引文件,定义了创建、打开、删除、读取、重命名、同步(持久化索引文件至磁盘)、校验和(checksum computing)等抽象方法,索引目录中不存在多级目录,即不存在子文件夹的层次结构(no sub-folder hierarchy)。
1.1Directory
Directory类用来维护索引目录中的索引文件,定义了创建、打开、删除、读取、重命名、同步(持久化索引文件至磁盘)、校验和(checksum computing)等抽象方法。
索引目录中不存在多级目录,即不存在子文件夹的层次结构(no sub-folder hierarchy)。
其子类如下图所示,另外下图中只列出了Lucene7.5.0的core模块中的子类,在其他模块,比如在misc模块中还有很多其他的子类:
BaseDirectory类
BaseDirectory同样是一个抽象类,提供了其子类共有的获取索引文件锁的方法,即维护了一个LockFactory对象,主要提供索引文件锁。
FSDirectory类
FSDirectory作为一个抽象类,提供了其子类共有的创建、删除、重命名、同步(持久化索引文件至磁盘)、校验和(checksum computing)等方法。FSDirectory的三个子类主要的不同点在于它们各自实现了打开、读取索引文件的方法。
SimpleFSDirectory
打开索引文件:使用Files的newByteFileChannel方法来打开一个索引文件,比如说通过DirectoryReader.open(IndexWriter)读取索引文件信息会调用此方法
读取索引文件:使用FileChannelImpl读取索引文件,使得可以随机访问索引文件的一块连续数据。
随机访问索引文件的一块连续数据在Lucene中是很重要的,例如图4中画出了.doc索引文件的数据结构,索引文件按照域(field)划分,在读取阶段,Lucene总是按域逐个处理,所以需要获取每一个域在.doc索引文件中的数据区域。
使用SimpleFSDirectory有以下注意点:
-
1.该类不支持并发读取同一个索引文件,多线程读取时候会被处理为顺序访问(synchronized(FileChannelImpl))如果业务有这方面的需求,那么最好使用NIOFSDirectory或者MMapDirectory,很明显了,synchornized方法不太适合高并发的场景。
-
2.如果有多个线程读取同一个索引文件,当执行线程被打断(Thread.interrupt()或Future.cancel())后,该索引文件的文件描述符(file descriptor)会被关闭,那么阻塞的线程随后读取该索引文件时会抛出ClosedChannelException异常,不过可以使用RAFDirectory来代替SimpleFSDirectory,它使用了RandomAccessFIle来读取索引文件,因为它是不可打断的(not interruptible)。
-
RAFDirectory已经作为一个旧的API(legacy API)被丢到了misc模块中,它同样不支持并发读取索引文件,所以跟SimpleFSDirectory很类似。
NIOFSDirectory
-
打开索引文件:使用Files的FileChannel.open()方法来打开一个索引文件
-
读取索引文件:使用FileChannelImpl读取索引文件,使得可以随机访问索引文件的一块连续数据。
使用NIOFSDirectory有以下注意点:
该类支持并发读取同一个索引文件,但是它存在跟SimpleFSDirectory一样的多线程下执行线程被打断的问题,如果业务中存在这个情况,那么可以使用RAFDirectory来代替NIOFSDirectory
另外如果Lucene是运行在Windows操作系统上,那么需要注意在SUN’s JRE下的一个BUG。
MMapDirectory
-
打开索引文件:使用内存映射(memory mapping)功能来打开一个索引文件,例如初始化MMapDirectory时,如果索引目录中已存在合法的索引文件,那么将这些文件尽可能的都映射到内存中,或者通过DirectoryReader.open(IndexWriter)读取索引文件信息会打开IndexWriter收集的索引文件数据
-
读取索引文件:将索引文件全部读取到内存中(如果索引文件在磁盘上)
如果内存映射失败,导致的原因可能是内存中连续的虚拟地址空间的数量(unfragmented virtual address space)不足、操作系统的内存映射大小限制等,更多的原因可以看这里MapFailed,Lucene7.5.0中根据不同的情况提供了下面几种出错信息:
-
1.非64位的JVM:MMapDirectory should only be used on 64bit platforms, because the address space on 32bit operating systems is too small
-
2.Windows操作系统:Windows is unfortunately very limited on virtual address space. If your index size is several hundred Gigabytes, consider changing to Linux
-
3.Linux操作系统:Please review ‘ulimit -v’, ‘ulimit -m’ (both should return ‘unlimited’), and ‘sysctl vm.max_map_count’
-
4.内存不足:Map failed。JVM传递过来的内存不足的堆栈信息是各种嵌套OOM信息(nested OOM),容易让使用者困惑,所以Lucene将复杂的堆栈信息替换为一条简单的信息,即"Map failed"
延伸–内存映射I/O技术
如何选择FSDirectory
由于操作系统的多样性,Lucene无法用一个FSDirectory类来满足所有的平台要求,因此在FSDirectory类中提供了open()方法,让Lucene根据当前的运行平台来选择一个合适的FSDirectory对象,即为用户从SimpleFSDirectory、MMapDirectory、NIOFSDirectory中选出一个合适的FSDirectory对象,当然用户可以通过new的方式直接使用这些FSDirectory对象。
根据不同的条件使用对应的FSDirectory对象:
JRE环境是64位并且支持unmapHackImpl()方法:使用MMapDirectory,例如Linux、MacOSX、Solaris、Windows 64-bit JREs。(判断是否支持unmapHackImpl()方法的逻辑)
如果上面的条件都不满足并且当前平台是Windows:使用SimpleFSDirectory,例如Windows上其他的JREs
如果上面的条件都不满足:使用NIOFSDirectory
public static FSDirectory open(Path path, LockFactory lockFactory) throws IOException {
if (Constants.JRE_IS_64BIT && MMapDirectory.UNMAP_SUPPORTED) {
return new MMapDirectory(path, lockFactory);
} else {
return new NIOFSDirectory(path, lockFactory);
}
}
@SuppressForbidden(
reason =
"Needs access to private APIs in DirectBuffer, sun.misc.Cleaner, and sun.misc.Unsafe to enable hack")
// 操作的是直接内存,DirectBuffer,所以在JVM中,较难以监测到这块内存的使用
private static Object unmapHackImpl() {
final Lookup lookup = lookup();
try {
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
final MethodHandle unmapper =
lookup.findVirtual(
unsafeClass, "invokeCleaner", methodType(void.class, ByteBuffer.class));
// fetch the unsafe instance and bind it to the virtual MH:
final Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object theUnsafe = f.get(null);
return newBufferCleaner(ByteBuffer.class, unmapper.bindTo(theUnsafe));
} catch (SecurityException se) {
return "Unmapping is not supported, because not all required permissions are given to the Lucene JAR file: "
+ se
+ " [Please grant at least the following permissions: RuntimePermission(\"accessClassInPackage.sun.misc\") "
+ " and ReflectPermission(\"suppressAccessChecks\")]";
} catch (ReflectiveOperationException | RuntimeException e) {
return "Unmapping is not supported on this platform, because internal Java APIs are not compatible with this Lucene version: "
+ e;
}
}
ByteBuffersDirectory
ByteBuffersDirectory使用堆,即通过一个ConcurrentHashMap来存储所有存储索引文件,其实key是索引文件名。
private final ConcurrentHashMap<String, FileEntry> files = new ConcurrentHashMap<>();
ByteBuffersDirectory适合用于存储体积较小,不需要持久化的临时索引文件,在这种情况下比MMapDirectory更有优势,因为它没有磁盘同步的开销。
RAMDirectory
RAMDirectory使用自定义的byte[]数组来存储索引文件信息,并且该数组最多存放1024个字节,所以如果索引大小为indexSize个字节,那么内存中就会有( indexSize / 1024 )个byte[]数组,当indexSize超过hundred megabytes后时会造成资源浪费,比如回收周期(GC cycles)问题。RAMDirectory已经被置为@Deprecated,所以不详细展开。
FilterDirectory
FilterDirectory类作为一个抽象类,它的子类对封装的Directory类增加了不同的限制(limitation)来实现高级功能。
SleepingLockWrapper
索引目录同一时间只允许一个IndexWriter对象进行操作,此时另一个IndexWriter对象(不同的引用)操作该目录时会抛出LockObtainFailedException异常,使用了SleepingLockWrapper后,会捕获LockObtainFailedException异常,同时等待1秒(默认值为1秒)后重试,如果在重试次数期间仍无法获得索引文件锁,那么抛出LockObtainFailedException异常。
TrackingTmpOutputDirectoryWrapper
该类用来记录新创建临时索引文件,即带有.tmp后缀的文件。IndexWriter在调用addDocument()的方法时,flush()或者commit()前,就会生成.fdx、.fdt以及.tvd、.tvx索引文件,而如果IndexWriter配置IndexSort,那么在上述期间内就只会生成临时的索引文件,TrackingTmpOutputDirectoryWrapper会记录这些临时索引文件。
TrackingDirectoryWrapper
该类用来记录新生成的索引文件名,不会记录从已有的索引目录中读取的索引文件名,比如在初始化Directory对象阶段会先读取索引目录中的索引文件。
LockValidatingDirectoryWrapper
该类使得在执行创建、删除、重命名、同步(持久化索引文件至磁盘)的操作前都会先检查索引文件锁的状态是否有效的,比如说如果用户手动的把write.lock文件删除,那么会导致索引文件锁失效。
IndexWriter中使用了该类来维护索引文件。
NRTCachingDirectory
该类维护了一个RAMDirectory,并封装了另一个Directory类,使用该类需要定义两个重要参数:
- maxMergeSizeBytes:允许的段合并生成的索引文件大小最大值
- maxCachedBytes:RAMDirectory允许存储的索引文件大小总和最大值
NRTCachingDirectory的处理逻辑就是根据下面的条件来选择使用RAMDirectory或者使用封装的Directory来存储索引条件。
boolean doCache = (bytes <= maxMergeSizeBytes) && (bytes + cache.ramBytesUsed()) <= maxCachedBytes
其中bytes为索引文件的大小,cache.ramBytesUsed()为RAMDirectory已经存储的所有索引文件大小总和,当doCache为真,则继续使用RAMDirectory存储该索引文件,否则使用封装的Directory。
FileSwitchDirectory
在前面的介绍中我们知道,索引文件都存放同一个目录中,使用一个Directory对象来维护,而FileSwitchDirectory则使用两个Directory对象,即primaryDir跟secondaryDir,用户可以将索引文件分别写到primaryDir或者secondaryDir,使用primaryExtensions的Set对象来指定哪些后缀的索引文件使用primaryDir维护,否则使用secondaryDir维护,另外primaryDir或者secondaryDir可以使用同一个目录。
@Override
public void sync(Collection<String> names) throws IOException {
List<String> primaryNames = new ArrayList<>();
List<String> secondaryNames = new ArrayList<>();
for (String name : names)
if (primaryExtensions.contains(getExtension(name))) {
primaryNames.add(name);
} else {
secondaryNames.add(name);
}
primaryDir.sync(primaryNames);
secondaryDir.sync(secondaryNames);
}
primaryExtensions对象指定后缀为fdx、fdt、nvd、nvm的索引文件由primaryDir维护。
Lucene50CompoundReader
该类仅用来读取复合文件(Compound File),所以它仅支持打开、读取。比如当我们在初始化IndexWriter时,需要读取旧的索引文件,如果该索引文件使用了复合文件,那么就会调用Lucene50CompoundReader类中的方法来读取旧索引信息。
2.设置IndexWriter的配置:IndexWriterConfig
在调用IndexWriter的构造函数之前,我们需要先初始化IndexWriter的配置信息- IndexWriterConfig,IndexWriterConfig中的配置信息按照可以分为两类:
- 不可变配置(unmodifiable configuration):在实例化IndexWriter对象后,这些配置不可更改,即使更改了,也不会生效,因为仅在IndexWriter的构造函数中应用一次这些配置
- 可变配置(modifiable configuration):在实例化IndexWriter对象后,这些配置可以随时更改
配置项:
配置项 | 是否是不可变配置 | 作用 |
---|---|---|
OpenMode | 是 | OpenMode描述了在IndexWriter的初始化阶段,如何处理索引目录中的已有的索引文件,这里称之为旧的索引,OpenMode一共定义了三种模式,即:CREATE、APPEND、CREATE_OR_APPEND。CREATE:如果索引目录中已经有旧的索引(根据Segment_N文件读取旧的索引信息),那么会覆盖(Overwrite)这些旧的索引,但注意的是新的提交(commit)生成的Segment_N的N值是旧索引中最后一次提交生成的Segment_N的N值加一后的值。APPEND:该打开模式打开索引目录会先读取索引目录中的旧索引,新的提交操作不会删除旧的索引,注意的是如果索引目录没有旧的索引(找不到任何的Segment_N文件),并且使用当前模式打开则会报错,报错信息如下:throw new IndexNotFoundException("no segments* file found in " + directory + ": files: " + Arrays.toString(files));上述的异常中,directory即上文提到的索引目录Directory,而Arrays.toString(files)用来输出索引目录中的所有文件。CREATE_OR_APPEND:该打开模式会先判断索引目录中是否有旧的索引,如果存在旧的索引,那么相当于APPEND模式,否则相当于CREATE模式。OpenMode可以通过IndexWriterConfig.setOpenMode(OpenMode openMode)方法设置,默认值为CREATE_OR_APPEND。 |
IndexDeletionPolicy | 是 | IndexDeletionPolicy是索引删除策略,该策略用来描述当一个新的提交生成后,如何处理上一个提交NoDeletionPolicy:该策略描述了无论有多少次新的提交,旧的提交都不会被删除。KeepOnlyLastCommitDeletionPolicy:该策略是Lucene7.5.0中默认的策略,它描述了当有新的提交,则删除上一个提交,即索引目录中最多只存在一个segment_N文件。SnapshotDeletionPolicy:SnapshotDeletionPolicy用来保留提交的快照,它封装了其他的索引删除策略,由于NoDeletionPolicy保留了每一次的提交,所以封装该策略没有什么意义,当封装了KeepOnlyLastCommitDeletionPolicy,那么可以通过主动调用SnapshotDeletionPolicy.snapshot()的方法来实现快照功能,使得新的提交产生后,上一个提交能以快照的方式保留在内存中,这种策略的缺点在于需要额外一份索引信息大小的内存。PersistentSnapshotDeletionPolicy:该策略跟SnapshotDeletionPolicy一样提供快照功能,区别在于SnapshotDeletionPolicy的快照信息保留在内存中,而该策略则持久化(persist)到磁盘,并且生成snapshot_N文件。 |
IndexCommit | 是 | 行一次提交操作(执行commit方法)后,这次提交包含的所有的段的信息用IndexCommit来描述,其中至少包含了两个信息,分别是segment_N文件跟Directory,索引删除策略SnapshotDeletionPolicy,在每次执行提交操作后,我们可以通过主动调用SnapshotDeletionPolicy.snapshot()来实现快照功能,而该方法的返回值就是IndexCommit。如果设置了IndexCommit,那么在构造IndexWriter对象期间,会先读取IndexCommit中的索引信息,IndexCommit可以通过IndexWriterConfig.setIndexCommit(IndexCommit commit)方法设置,默认值为null。 |
Similarity | 是 | Similarity描述了Lucene打分的组成部分,在查询原理系列文章中详细介绍了Lucene如何使用BM25算法实现对文档的打分,这里不赘述。 |
MergeScheduler | 是 | MergeScheduler即段的合并调度策略,用来定义如何执行一个或多个段的合并,比如并发执行多个段的合并任务时的执行先后顺序,磁盘IO限制,Lucene7.5.0中提供了三种可选的段的合并调度策略,默认使用ConcurrentMergeScheduler。 |
Codec | 否 | Codec定义了索引文件的数据结构,即描述了每一种索引文件需要记录哪些信息,以及如何存储这些信息,在索引文件所有索引文件的数据结构。 |
DocumentsWriterPerThreadPool | 是 | DocumentsWriterPerThreadPool是一个逻辑上的线程池,它实现了类似Java线程池的功能,在Java的线程池中,新来的一个任务可以从ExecutorService中获得一个线程去处理该任务,而在DocumentsWriterPerThreadPool中,每当IndexWriter要添加文档,会从DocumentsWriterPerThreadPool中获得一个ThreadState去执行,故在多线程(持有相同的IndexWriter对象引用)执行添加文档操作时,每个线程都会获得一个ThreadState对象。如果不是深度使用Lucene,应该不会去调整这个配置吧。 |
ReaderPooling | 是 | ReaderPooling该值是一个布尔值,用来描述是否允许共用(pool)SegmentReader,共用(pool)可以理解为缓存,在第一次读取一个段的信息时,即获得该段对应的SegmentReader,并且使用ReaderPool来缓存这些SegmentReader,使得在处理删除信息(删除操作涉及多个段时效果更突出)、NRT搜索时可以提供更好的性能,ReaderPooling可以通过IndexWriterConfig.setReaderPooling(boolean readerPooling)方法设置,默认值为true。 |
FlushPolicy | 是 | FlushPolicy即flush策略,准确的说应该称为 自动flush策略,因为flush分为自动flush跟主动flush,即显式调用IndexWriter.flush( )方法,FlushPolicy描述了IndexWriter执行了增删改的操作后,将修改后的索引信息写入磁盘的时机。 |
RAMPerThreadHardLimitMB | 是 | 该配置在后面介绍可变配置中的MaxBufferedDocs、RAMBufferSizeMB时一起介绍。 |
InfoStream | 是 | InfoStream用来在对Lucene进行调试时实现debug输出信息,在业务中打印debug信息会降低Lucene的性能,故在业务中使用默认值就行,即不输出debug信息。 |
IndexSort | 是 | IndexSort描述了在索引阶段如何对segment内的文档进行排序,IndexSort可以通过IndexWriterConfig.setIndexSort(Sort sort)方法设置,默认值为null。 |
SoftDeletesField | 是 | oftDeletesField用来定义哪些域为软删除的域,关于软删除的概念在后面的文章中会用多篇文章的篇幅介绍,这里暂不展开。IndexSort可以通过IndexWriterConfig.setSoftDeletesField(String softDeletesField)方法设置,默认值为null。 |
MergePolicy | 否 | MergePolicy是段的合并策略,它用来描述如何从索引目录中找到满足合并要求的段集合(segment set),在前面的文章了已经介绍了LogMergePolicy、TieredMergePolicy两种合并策略,这里不赘述。MergePolicy可以通过IndexWriterConfig.setMergePolicy(MergePolicy mergePolicy)方法设置,在版本Lucene7.5.0中默认值使用TieredMergePolicy,如果修改了MergePolicy,那么下一次的段的合并会使用新的合并策略。 |
MaxBufferedDocs、RAMBufferSizeMB | 否 | RAMBufferSizeMB描述了索引信息被写入到磁盘前暂时缓存在内存中允许的最大使用内存值,而MaxBufferedDocs则是描述了索引信息被写入到磁盘前暂时缓存在内存中允许的文档最大数量,这里注意的是,MaxBufferedDocs指的是一个DWPT允许添加的最大文档数量,在多线程下,可以同时存在多个DWPT,而MaxBufferedDocs并不是所有线程的DWPT中添加的文档数量和值。每次执行文档的增删改后,会调用FlushPolicy(flush策略)判断是否需要执行自动flush,在Lucene7.5.0版本中,仅提供一个flush策略,即FlushByRamOrCountsPolicy,该策略正是依据MaxBufferedDocs、RAMBufferSizeMB来判断是否需要执行自动flush。不可配置值,即RAMPerThreadHardLimitMB,该值被允许设置的值域为0~2048M,它用来描述每一个DWPT允许缓存的最大的索引量。DWPT可以理解为一个容器,存放每一篇文档对应转化后的索引信息,在多线程下执行文档的添加操作时,每个线程都会持有一个DWPT,然后将一篇文档的信息转化为索引信息(DocumentIndexData),并添加到DWPT中。如果每一个DWPT中的DocumentIndexData的个数超过MaxBufferedDocs时,那么就会触发自动flush,将DWPT中的索引信息生成为一个段,如图1所示,MaxBufferedDocs影响的是一个DWPT。如果每一个DWPT中的所有DocumentIndexData的索引内存占用量超过RAMPerThreadHardLimitMB,那么就会触发自动flush,将DWPT中的索引信息生成为一个段,RAMPerThreadHardLimitMB影响的是一个DWPT。如果所有DWPT(例如图1中的三个DWPT)中的DocumentIndexData的索引内存占用量超过RAMBufferSizeMB,那么就会触发自动flush,将DWPT中的索引信息生成为一个段。 |
MergedSegmentWarmer | 否 | MergedSegmentWarmer即预热合并后的新段,它描述的是在执行段的合并期间,提前获得合并后生成的新段的信息,由于段的合并和文档的增删改是并发操作,所以使用该配置可以提高性能。 |
UseCompoundFile | 否 | UseCompoundFile是布尔值,当该值为true,那么通过flush、commit的操作生成索引使用的数据结构都是复合索引文件,即索引文件.cfs、.cfe。UseCompoundFile可以通过IndexWriterConfig.setUseCompoundFile(boolean useCompoundFile)方法设置,UseCompoundFile默认为true。注意的是执行段的合并后生成的新段对应的索引文件,即使通过上述方法另UseCompoundFile为true,但还是有可能生成非复合索引文件。 |
CommitOnClose | 否 | 该值为布尔值,它会影响IndexWriter.close()的执行逻辑,如果设置为true,那么会先应用(apply)所有的更改,即执行commit操作,否则上一次commit操作后的所有更改都不会保存,直接退出。CommitOnClose可以通过IndexWriterConfig.setCommitOnClose(boolean commitOnClose)方法设置,CommitOnClose默认为true。 |
CheckPendingFlushUpdate | 否 | 值为布尔值,如果设置为true,那么当一个执行添加或更新文档操作的线程完成处理文档的工作后,会尝试去帮助待flush的DWPT。 |