索引优化(Optimized)

    Es在运行一段时间后,会出现分片数增多,删除的记录未及时清理,导致占用很多的存储空间,查询性能也下降;但是在优化过程中,其他的请求会被阻止,知道优化完成;如果http请求断开,优化的请求也会继续在后台执行;

$ curl -XPOST 'http://localhost:9200/twitter/_optimize'

管理索引优化

optimize API允许通过API优化一个或多个索引。优化过程的操作基本上优化的索引搜索速度更快(和涉及到Lucene索引内保存每个碎片的段数)。优化操作允许减少的段数,把它们合并。

$ curl -XPOST 'http://localhost:9200/twitter/_optimize'

名称

描述

max_num_segments

段数优化。要全面优化索引,将其设置为1。默认设置是只需检查是否需要执行合并,如果需要执行合并才执行合并操作。【经过测试越小速度越快】

only_expunge_deletes

优化过程中是否只合并带删除标记的段。在Lucene中,记录不会被删除,只是加了标记删除。索引会在在合并过程中,创建一个新的没有删除标记记录的分段。此标志只允许合并段删除。这个标记可以控制,只合并带删除标记的段,默认为false。【设置为true docs才会合并】

refresh

刷新。默认为true。

flush

数据优化后是否进行数据刷新进行优化。默认为true。

wait_for_merge

请求是否等待合并结束。默认为true。注意,合并有可能是一个非常繁重的操作,所以为了能有感官上的响应,需要把它设置为false;【最好设置为false,默认true请求就会阻塞在那里,直到完成】

force

Force a merge operation, even if there is a single segment in the shard with no deletions. [1.1.0]

强制优化操作,尽管只有单个块,且无删除标记的块,1.1.0版本后有该属性。

 

优化API一个调用,可以应用到多个索引,或者所有索引
$ curl -XPOST 'http://localhost:9200/kimchy,elasticsearch/_optimize'

$ curl -XPOST 'http://localhost:9200/_optimize'


清空带删除标记的记录:

http://localhost:9200/indexName/_optimize?max_num_segments =1&only_expunge_deletes=false&wait_for_merge=false&flush=true

Java API方式:

/**
 * <pre>
 * 索引优化方法
 * optimize API允许通过API优化一个或多个索引。
 * 优化过程的操作可以优化索引搜索速度<br>
 * (涉及到Lucene索引内保存每个碎片的段数)。
 * 优化操作合并Lucene段数和物理删除带删除标记的记录。
 * @param indexName 优化索引名
 * @return 是否优化成功,false:失败 true:成功
 *  <pre>
 */
public boolean indexOptimize(String indexName ) {
	logger.info("ES索引开始优化,索引名为:"+indexName);
	Client client = getClient();
	try {
		 OptimizeResponse response =  client.admin().indices().optimize(
				new OptimizeRequest(indexName)
				//合并段数量
				.maxNumSegments(1)
				//优化过程中是否只合并带删除标记的段,默认为false
				.onlyExpungeDeletes(false)
				.listenerThreaded(true)
				//合并完成后是否执行flush操作,默认为true
				.flush(true)
				).actionGet();
		
		if(response.getShardFailures().length == response.getTotalShards()){
			logger.info("ES索引优化失败"+response.getShardFailures());
			return false;
		}else if(response.getShardFailures().length>0){
			logger.info("ES索引优化部分分片失败"+response.getShardFailures());
		}
		logger.info("ES索引优化成功");
		return true;
	}catch (Exception e) {
		logger.error("ES优化失败", e);
		return false;
	} 
}

2.索引刷新(refresh)

ES索引过程,在写入文档时先写入内存,如下图所示:

 

新文档写入新的分片信息中,这时的分片是可读写的。只有到达一定时间和值后,才会flush到硬盘上,并进行合并;

 

 

  建立的索引,不会立马查到,这是为什么elasticsearch为near-real-time的原因
需要配置index.refresh_interval参数,默认是1s。可以修改conf/elasticsearch.yml文件中index.refresh_interval:1s的刷新频率,对于实时性要求不是很高的,可以设置刷新频率大点,这样会对索引的速度有提升;这样所有新建的索引都使用这个刷新频率。

刷新索引(使新加内容对搜索可见);

 单索引刷新命令:

curl -XPOST 'http://localhost:9200/twitter/_refresh'

 多索引刷新:

curl -XPOST 'http://localhost:9200/twitter/_refresh'

 所有索引刷新:

curl -XPOST 'http://localhost:9200/_refresh'

 

JAVA API:

(1)创建文档索引时可以通过设置setRefresh为true来实时使文档可查,但是
针对大量文档的操作,慎用,因为刷新会影响使用性能;
client.prepareIndex(IndexName, indexType, docId)
		    .setSource( XContentFactory.jsonBuilder()
		        .startObject()
		            // Register the query,添加查询记录
		            .field("query", qb) 
		        .endObject())
		     //Needed when the query shall be available immediately
		    .setRefresh(true)
(2)整个索引刷新方法
/**
 * <pre>
 * 索引刷新方法
 *@param indexName 刷新索引
 *@return 是否刷新成功
 * <pre>
 */
public boolean indexRefresh(String ...indexName ) {
	logger.info("ES索引开始刷新,索引名为:"+indexName);
	Client client = getClient();
	try {
		RefreshResponse response =  client.admin().indices()
				  .refresh(new RefreshRequest(indexName))
				  .actionGet();
		
		if(response.getShardFailures().length == response.getTotalShards()){
			logger.info("ES索引刷新失败"+response.getShardFailures());
			return false;
		}else if(response.getShardFailures().length>0){
			logger.info("ES索引刷新部分分片失败"+response.getShardFailures());
		}
		logger.info("ES索引刷新成功");
		return true;
	}catch (Exception e) {
		logger.error("ES刷新失败", e);
		return false;
	} 
}

3.Flush清空

     f lush API可以刷新一个或者多个索引。flush 操作将释放该索引所占用的内存,并将索引数据保存在磁盘上,并清除内部事务日志 transaction log。在默认情况下。ElasticSearch 使用以启发式自动清除内存,并保存索引。

Curl命令方式:

curl -POST localhost:9200/esfindex/_flush

多个索引操作命令:

curl -POST localhost:9200/zfindex,esfindex/_flush

 

接收参数:

wait_if_ongoing :是否等待其他的flush操作执行完毕,默认是false的,只要还有其他的进程还在执行,就会抛出异常信息,默认是false的;

force :是否强制执行,就算没有更新;例如没有任何文档提交到索引中;这样可以使操作日志的ID自增长,尽管没有更新索引(内部操作)。

 

Java API 方式:

/**
 * <pre>
 * 索引Flush方法
 *@param indexName 刷新索引
 *@return 是否刷新成功
 * <pre>
 */
public boolean indexFlush(String ...indexName ) {
	logger.info("ES索引开始刷新,索引名为:"+indexName);
	Client client = getClient();
	try {
		FlushResponse response =  client.admin().indices()
				  .flush(new FlushRequest(indexName))
				  .actionGet();
		//输出json格式的响应信息
		System.out.println(FastJSONHelper.serialize(response));
		if(response.getShardFailures().length == response.getTotalShards()){
			logger.info("ES索引刷新失败"+response.getShardFailures());
			return false;
		}else if(response.getShardFailures().length>0){
			logger.info("ES索引刷新部分分片失败" + response.getShardFailures());
		}
		logger.info("ES索引刷新成功");
		return true;
	}catch (Exception e) {
		logger.error("ES刷新失败", e);
		return false;
	} 
}

4.FileDescriptor sync方法 (强制所有系统缓冲区与基础设备同步)
    在读Lucene的源码过程中,发现FSDirectory类中有一个文件信息同步的方法,对其中的一行代码file.getFD().sync();不是很清楚(这也可见自己的基础有多么的差)。经过一番检索,终于明白了其大意。
 protected void fsync(String name) throws IOException {
    File fullFile = new File(directory, name);
    boolean success = false;
    int retryCount = 0;
    IOException exc = null;
    while (!success && retryCount < 5) {
      retryCount++;
      RandomAccessFile file = null;
      try {
        try {
          file = new RandomAccessFile(fullFile, "rw");
          file.getFD().sync();//强制所有系统缓冲区与基础设备同步
          success = true;
        } finally {
          if (file != null)
            file.close();
        }
      } catch (IOException ioe) {
        if (exc == null)
          exc = ioe;
        try {
          // Pause 5 msec
          Thread.sleep(5);
        } catch (InterruptedException ie) {
          throw new ThreadInterruptedException(ie);
        }
      }
    }
    if (!success)
      // Throw original exception
      throw exc;
  }
}

FileDescriptor文件描述符类的实例用作与基础机器有关的某种结构的不透明句柄,该结构表示开放文件、开放套接字或者字节的另一个源或接收者。文件描述符的主要实际用途是创建一个包含该结构的 FileInputStream 或 FileOutputStream。 应用程序不应创建自己的文件描述符。

大部分人都认为flush后,其他用户应该立即可见。但是在一些极端的情况下也需调用后还是无法看见以写入的数据。 

flush

   刷新此输出流并强制写出所有缓冲的输出字节。flush 的常规协定是:如果此输出流的实现已经缓冲了以前写入的任何字节,则调用此方法指示应将这些字节立即写入它们预期的目标。

     如果此流的预期目标是由基础操作系统提供的一个抽象(如一个文件),则刷新此流只能保证将以前写入到流的字节传递给操作系统进行写入,但不保证能将这些字节实际写入到物理设备(如磁盘驱动器)。

    OutputStream 的 flush 方法不执行任何操作。为什么会这样? 原因是,这个缓冲我们java自己实现的。 flush保证的是内部的缓冲写入到系统中。但是系统中文件也可能有缓冲,所以并不一定flush后立即可见。

 那么如何解决这个问题?在文件流或数据流中均可以看见getFD()这个方法, 它返回的是与此流有关的文件描述符。

所以调用文件描述符的sync的方法即可让实际文件强制同步了。JDK中描述如下:

sync

   强制所有系统缓冲区与基础设备同步。该方法在此 FileDescriptor 的所有修改数据和属性都写入相关设备后返回。特别是,如果此 FileDescriptor 引用物理存储介质,比如文件系统中的文件,则一直要等到将与此 FileDesecriptor 有关的缓冲区的所有内存中修改副本写入物理介质中,sync 方法才会返回。 sync 方法由要求物理存储(比例文件)处于某种已知状态下的代码使用。例如,提供简单事务处理设施的类可以使用 sync 来确保某个文件所有由给定事务造成的更改都记录在存储介质上。 sync 只影响此 FileDescriptor 的缓冲区下游。如果正通过应用程序(例如,通过一个 BufferedOutputStream 对象)实现内存缓冲,那么必须在数据受 sync 影响之前将这些缓冲区刷新,并转到 FileDescriptor 中(例如,通过调用 OutputStream.flush)。

  1. Flush与reflesh区别

   如果新添加一个文档到索引中,我们通过执行reflesh或者flush操作都能使该文档立即刻查询,表面上看两者是没有区别的,但是在设计上两者还是有区别的;

refresh操作有效地对Lucene index reader调用了reopen,使得在数据的那个时间快照进行了更新。这是Lucene拥有的近实时搜索api的特性。

ES refresh让文档可以搜索到,但是不保证这些信息被写入disk进入一个永久的存储状态,因为它并没有调用fsync,这就不能保证持久性了。让你数据获得持久性的是Lucene commit,这个操作代价比较大。

当你可以每秒都调用lucene reopen时,你不能这样使用lucene的commit。

借助lucene你可以尽可能频繁地调用reopen以使新的文档可以被搜索到,但是你仍然需要调用commit来确保数据写入disk并且fsynced,这样会安全。

ES通过增加了一个在每个shard(一个lucene的索引)上的事务解决这个问题,还未被commit的写操作会被存起来。事务log被fsynced,已经安全了,所以你每时每刻都获得了持久性,甚至对于那些没有被commit的文档,都是这样。因为refresh每秒自动地发生,所以你可以近实时地搜索文档,并且如果有不好的事件发生,事务log可以被替代从而恢复那些丢失的文档。事务log的优越性是它可以被用来做其他的事情,例如提供实时的get_by_id。

elasticsearch flush高效地触发lucene commit,并同时清空事务log,因为一旦数据在lucene层面提交,持久性将会由lucene保证。Flush同样是一个api,也可以进行微调,虽然通常没有必要这样。Flush自动发生取决于事务log增加了多少操作、它们有多大、最后一次flush何时发生。

  Flush操作触发Lucene的commit操作,并清空transaction log信息。直到索引文档被持久化到索引文件上,保证了数据的安全和持久化;flush操作api允许对flush操作进行配置,尽管普通情况下不是必要的,可以配置为新增多少条文档或提交的数据量达到多大时才进行flush操作;

flush操作与translog

   我们可能已经意识到如果数据在filesystem cache之中是很有可能在意外的故障中丢失。这个时候就需要一种机制,可以将对es的操作记录下来,来确保当出现故障的时候,保留在filesystem的数据不会丢失,并在重启的时候可以从这个记录中将数据恢复过来。elasticsearch提供了translog来记录这些操作。

当向elasticsearch发送创建document索引请求的时候,document数据会先进入到index buffer之后,与此同时会将操作记录在translog之中,当发生refresh时(数据从index buffer中进入filesystem cache的过程)translog中的操作记录并不会被清除,而是当数据从filesystem cache中被写入磁盘之后才会将translog中清空。而从filesystem cache写入磁盘的过程就是flush。可能有点晕,我画了一个图帮大家理解这个过程:

es younggc频繁 es频繁更新优化_数据

 

有关于translog和flush的一些配置项:

 

index.translog.flush_threshold_ops:当发生多少次操作时进行一次flush。默认是 unlimited。

index.translog.flush_threshold_size:当translog的大小达到此值时会进行一次flush操作。默认是512mb。

index.translog.flush_threshold_period:在指定的时间间隔内如果没有进行flush操作,会进行一次强制flush操作。默认是30m。

index.translog.interval:多少时间间隔内会检查一次translog,来进行一次flush操作。es会随机的在这个值到这个值的2倍大小之间进行一次操作,默认是5s。

 

Query context 查询上下文 这种语句在执行时既要计算文档是否匹配,还要计算文档相对于其他文档的匹配度有多高,匹配度越高,_score 分数就越高
Filter context 过滤上下文 过滤上下文中的语句在执行时只关心文档是否和查询匹配,不会计算匹配度,也就是得分。

 

总结和思考

Elasticsearch的索引思路:

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。

所以,对于使用Elasticsearch进行索引时需要注意:

  • 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
  • 同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
  • 选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询

关于最后一点,个人认为有多个因素:

其中一个(也许不是最重要的)因素: 上面看到的压缩算法,都是对Posting list里的大量ID进行压缩的,那如果ID是顺序的,或者是有公共前缀等具有一定规律性的ID,压缩比会比较高;

另外一个因素: 可能是最影响查询性能的,应该是最后通过Posting list里的ID到磁盘中查找Document信息的那步,因为Elasticsearch是分Segment存储的,根据ID这个大范围的Term定位到Segment的效率直接影响了最后查询的性能,如果ID是有规律的,可以快速跳过不包含该ID的Segment,从而减少不必要的磁盘读次数