ES(三)配置与优化

重要的配置

在进行参数配置前,先捡重点的来,不要修改什么配置:在权威指南中的建议不要触碰这些配置。总结下就是jvm的垃圾回收器配置和线程池的数目,与很多高性能的软件一样,线程的数量最好是cpu核心的数量,最多就是2倍的cpu核心的数目,避免线程的切换开销。

而且权威指南给出的数据:

在现代的 CPUs 上,开销估计高达 30 μs

什么概念,微妙数量级的,内存操作也就纳秒数量级的,如果有1000个线程,每秒全部切换一次,cpu就浪费了30毫秒,0.03秒!

批量插入时相关配置

批量插入有两个关键参数,参考使用批量请求并调整其大小,一批数量消息的多少和有多少并发的线程,没有统一的值,不同场景下单条消息大小和复杂度都不一样。对于高写入量的集群,如记录大量日志,需要多测试验证,而对于写入速度不大的集群,能用就好了。

较小分片数目,取消自动刷新,注意,此两项设置需要在插入结束后修改会原数值。尤其是第一项number_of_replicas,在生产环境中refresh_interval也可适当调节大些,可以十几秒刷新一次。

PUT http://10.45.157.55:9200/demo_index/_settings
{   
    "settings" : {   
        "number_of_replicas" : 0   
    }   
} 

PUT http://10.45.157.55:9200/demo_index/_settings
{   
    "index" : {   
        "refresh_interval" : "-1"   
    }  
}

refresh_interval按照官网建议修改为-1。但是目前测试看,从索引为空开始增加文档,这个参数修改并没有明显提高,不知道是不是测试的问题。毕竟这只是个从内存刷新到内存(文件系统缓存)的过程。可能是因为测试的数据单条太小了不到1k,而cpu又比较好(8核心),内存拷贝的代价不是很大,对于批量插入影响不大额。

此外,为了不影响搜索的性能ES对导入的数据有20M/s的限制,但是这对于机械硬盘是够用的,但对于SSD要额外配置。还有在数据初始导入的时候,不管是机械的还是SSD的,可以先去掉这个限制。

PUT /_cluster/settings
{
    "transient" : {
        "indices.store.throttle.type" : "none" 
    }
}

设置限流类型为 none 彻底关闭合并限流。等完成了导入,记得改回 merge 重新打开限流。

还有就是段合并时候,并发的线程数目,机械硬盘并发很差,平时拷贝文件时候感觉明显,多个文件一起拷贝还不如一个个拷贝来的快。这个配置对于只部署在一个机器上的ES很明显,因为默认有5个shard,对应的是5个索引,而如果又只有一个盘放索引的话,如果是多核心,默认值很可能大于1,假设是2,这样就有了5*2个线程在并发的争夺磁盘IO。

如果你使用的是机械磁盘而非 SSD,你需要添加下面这个配置到你的 elasticsearch.yml 里:

index.merge.scheduler.max_thread_count: 1 机械磁盘在并发 I/O 支持方面比较差,所以我们需要降低每个索引并发访问磁盘的线程数。这个设置允许 max_thread_count + 2 个线程同时进行磁盘操作,也就是设置为 1 允许三个线程。

对于 SSD,你可以忽略这个设置,默认是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2) ,对 SSD 来说运行的很好。

堆大小配置

设置堆大小,也就是ES能利用的堆的大小,需要修改config下的jvm.options文件,增加:

-Xms8g
-Xmx8g

最大与最小heap为物理内存的一半。

停用Swapping

sudo swapoff -a,其他方式可以参考:swapping_是性能的坟墓。操作磁盘的响应是毫秒级别的,而操作内存只有纳秒,引用Mongodb上的形象比喻,一个操作你在内存上,可能只用了一秒钟,而在磁盘上,需要花上一天的时间。对于追求高性能而且不差钱的我们,肯定是直接关掉这磁盘交换的功能。

关闭NUMA的

在/etc/sysctl.conf 中增加配置vm.zone_reclaim_mode = 0 , 然后执行 sysctl -p 使配置生效。在Mongodb的权威指南中,说关闭NUMA会对Mongodb对性能有很大提高,相当于按下了“magic key”。

master节点

此设置应该始终被配置为 master 候选节点的法定个数(大多数个)。法定个数就是 ( master 候选节点个数 / 2) + 1。 如果你有两个节点,你遇到难题了。法定数当然是 2 ,但是这意味着如果有一个节点挂掉,你整个集群就不可用了。 设置成 1 可以保证集群的功能,但是就无法保证集群脑裂了,像这样的情况,你最好至少保证有 3 个节点。 你可以在你的 elasticsearch.yml 文件中这样配置:

discovery.zen.minimum_master_nodes: 2

参考,最小主节点数这里,虽然建议至少有3个master节点也是针对集群的情况。如果只有一个节点,那就不用折腾了,这节点即是数据节点又是master节点就可以了,如果有两个节点,额,可以在额外专门加个master节点,但是也不是必须的,两个节点脑裂的概率其实很小,还是默认是1也无关紧要。

如果集群规模比较大,比如有100个数据节点,那么有单独的配置的master节点还是有必要的,一般3个也够了,如果单算长期没人维护5个也行。如果100个数据节点没有master节点,所有的都即是master节点又是data节点,首先master没有那么多工作要做,只是建立索引,监控状态,均衡数据的时候使用,其次100个master他们之间的交互也麻烦,造成额外的不必要的通信。

多个节点集群中,使用单独的master节点还能一定程度上提高集群的性能甚至稳定性。防止与data节点在一起data节点负载过高而master功能影响。

集群恢复方面的配置

参考集群恢复方面的配置

gateway.recover_after_nodes: 8 这将阻止 Elasticsearch 在存在至少 8 个节点(数据节点或者 master 节点)之前进行数据恢复。 这个值的设定取决于个人喜好:整个集群提供服务之前你希望有多少个节点在线?这种情况下,我们设置为 8,这意味着至少要有 8 个节点,该集群才可用。

现在我们要告诉 Elasticsearch 集群中应该有多少个节点,以及我们愿意为这些节点等待多长时间: gateway.expected_nodes: 10 gateway.recover_after_time: 5m 这意味着 Elasticsearch 会采取如下操作:

等待集群至少存在 8 个节点 等待 5 分钟,或者10 个节点上线后,才进行数据恢复,这取决于哪个条件先达到。 这三个设置可以在集群重启的时候避免过多的分片交换。这可能会让数据恢复从数个小时缩短为几秒钟。 注意:这些配置只能设置在 config/elasticsearch.yml 文件中或者是在命令行里(它们不能动态更新)它们只在整个集群重启的时候有实质性作用。

这三个参数一个也不能少。在kafka和Hbase类似的集群的系统都有相关配置。

上面的参数针对的场景是整个集群离线要重启的时候的配置,另外,针对集群运行中,单个节点的配置也需要考虑。在推迟分片分配中说明了单个节点离线后,延迟进行分片转移的配置,一般是等待节点一会,如果不上线在进行分片转移。可以动态修改:

PUT /_all/_settings 
{
  "settings": {
    "index.unassigned.node_left.delayed_timeout": "5m" 
  }
}

注意:延迟分配不会阻止副本被提拔为主分片。集群还是会进行必要的提拔来让集群回到 yellow 状态。缺失副本的重建是唯一被延迟的过程。

这个原因么也很简单,如果副本不被提升为主分片,那么新加入的数据如果要写入这个分片,就会缓存等待原来暂时离线的Node的主分片上线,如果配置为5分钟,对于一个大的吞吐量的集群,会有很多数据,引入很多问题。所以,直接提拔一个其他Node上副本分片为主分片,只是一个流程的复用,没有其他额外的工作。

单播配置

以前ES支持组播的,但是方便性也引起了很多麻烦,很多没有改默认字的ES的节点会乱入已存在的集群。现在都是默认单播发现,组播需要额外的插件支持。一般单播列表是所有master的地址,一般三个master地址就够了。

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]

测试环境

虚拟机centos64为操作系统,cpu为Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz 8核心,16G内存,机械硬盘。

ES官网推荐配置内存为16G、32G、64G,由于机器限制,目前只在单机在部署,如部署多台,响应时间和吞吐量会有近似线性的增长。

数据量为1亿数据,数据为单卡口每天1w数据,100卡口在三个月内的数据。数据为模拟数据,车牌号随机生成,数据平均分布在三个月的时间段内。

测试结果

文档的结构为ES(二)车牌搜索中的完整格式。 数据插入速度:批量插入,单线程同步,使用Restful的接口,每次1w条,同步发送,速度大概在1w条每秒。插入了1亿的数据,只有车牌是需要分析的,其他字段要么是整型,要么是keyword。

1亿的数据测试过程中,大部分响应是在1s内,加上term过滤也是差不多的时间。

然后就继续批量插入到10亿,插入速度的问题后面有提到。10亿的数据,模糊查询时候车牌字段的分析器使用whitespace,速度也非常快,平均三秒。ES性能确实不错。

注意

  1. 上述测试没有考虑大并发量,只是单客户端情况
  2. 上述测试没有考虑搜索时有大量数据插入ES的情况
  3. 上述测试没有考虑同时有聚合操作的情况

如内网用户不多,并发量应该不是问题。大量数据插入的情况下,可能会有一定影响,可以通过批量,适当延长批量写入周期方式缓解此问题。对于聚合操作,是非常消耗性能的,如果使用ES做聚合操作,建议上固态硬盘,减少对于搜索的影响。

批量插入问题

使用单个生产者,多个消费者bulk写,在自己电脑上写一个空的index,每个批量1w条数据,插入的速度有1.3w/s,1w条数据的大小也就0.3M,应该不是带宽的问题,看看本机CPU(双核),一会高一会低,而且内存也快满了,可能与配置有关系。由于是模拟的数据,把模拟数据放在ES的部署机器上,有3w多每秒的速度,看来CPU影响还是很大。

但是超过1亿的数据后插入速度明显变慢,2亿后,虚拟机上甚至只有几十条每秒的速度。使用top命令查看,其中cpu的wa百分比将近30(iostat统计也差不多),这个wa的意思是“wa: io wait cpu time (or) % CPU time spent in wait (on disk)”,也就是CPU等待IO上浪费的时间,这也是固体硬盘能大大提升系统的原因。这30是个总的统计,top下按个”1”能显示各个cpu的时间详细信息,主要有两个cpu在工作,第一个的CPU的wa竟然有98%,基本是在等待IO,其他的什么也没干。

然后,换成一个基本没有文档的索引插入,轻轻松松每秒1w条,在看看cpu的wa时间,竟然只有0.3%。使用 iostat -c 1 10命令,iowait也是只有0.2%。又跑了一次,发现速度是一点点慢下来的,cpu的wa时间也是一点点上升的,大概查了下,有的原因是因为有update操作,id用的是32位的uid,重复概率基本没有,所以更新操作的概率也非常小。

想起前两天看Mongodb的id,默认的是12个字节,为时间+机器hash+PID+自增计数,而我这里测试的用的id是32长度的uuid,Mongodb权威指南解释是,使用时间最为前缀,因为Mongodb文档一般是顺序存下的,如果id在以时间顺序排序,是有利于批量获取的。太长了?顺序的原因?自己试了下,插入ES数据的id为自增的整数然后转为string,果然速度有明显提升,而且比开始的空索引时候还要快,有4w每秒的速度。

什么原因呢?在MongoDB中有说明使用单调的shard key可能会限制插入的吞吐量,参考Monotonically Increasing Shard Keys Can Limit Insert Throughput。因为MongoDB中shard是按照range划分的,与Hbase一样,如果是连续的话,会一直写入到一个shard,这样在分布式的情况下是非常不利的,写的时候就没有负载均衡的效果,解决的方式就是使用hash index。

但是这ES就是使用的hash的方式进行分片的额,理论上看,id是不是单调的,都会均匀地写入到不同的shard上。ES也是数据库么,影响插入速度,首先想到的就是索引,那么,ES的倒排索引构建的速度与id是否单调有什么关系?

这篇blogChoosing a fast unique identifier (UUID) for Lucene中说明了使用java的uuid在查询效率上的低效。虽然没有说对插入的影响,但是简单分析下,在构建索引的时候,需要找到自己合适的位置,而这个找位置的过程,其实也是查询的过程。

主要的分析过程如下,可是看的不是很懂。。

The purpose of the terms dictionary is to store all unique terms seen during indexing, and map each term to its metadata (docFreq, totalTermFreq, etc.), as well as the postings (documents, offsets, postings and payloads). When a term is requested, the terms dictionary must locate it in the on-disk index and return its metadata.

The default codec uses the BlockTree terms dictionary, which stores all terms for each field in sorted binary order, and assigns the terms into blocks sharing a common prefix. Each block contains between 25 and 48 terms by default. It uses an in-memory prefix-trie index structure (an FST) to quickly map each prefix to the corresponding on-disk block, and on lookup it first checks the index based on the requested term’s prefix, and then seeks to the appropriate on-disk block and scans to find the term.

In certain cases, when the terms in a segment have a predictable pattern, the terms index can know that the requested term cannot exist on-disk. This fast-match test can be a sizable performance gain especially when the index is cold (the pages are not cached by the the OS’s IO cache) since it avoids a costly disk-seek. As Lucene is segment-based, a single id lookup must visit each segment until it finds a match, so quickly ruling out one or more segments can be a big win. It is also vital to keep your segment counts as low as possible!

Given this, fully random ids (like UUID V4) should perform worst, because they defeat the terms index fast-match test and require a disk seek for every segment. Ids with a predictable per-segment pattern, such as sequentially assigned values, or a timestamp, should perform best as they will maximize the gains from the terms index fast-match test.

虽然不懂,强行分析下,这blocktree数据结构存储数据的时候,可以提取公共的前缀,数据前缀越相似,公共的前缀越多,存储的blocks越少,blocks越少,相当于复用效率越高,检索速度越快。与前缀索引很像,在Mysql中也有前缀索引。例如123,124,125,126,127,这几个数,如果使用前缀索引,1和2都能够复用,blocktree可能在一个block中,而如果是123,456,789,023,这几个数,可能就分别在不同的block中。而且这block是有大小限制的,如果使用UUIDv4,32个字节,随着数据量的增加,可能前20个字节为一个block,而且这样的block有非常多个,这个时候,就需要把前20个字节相同的所有block都遍历一遍,才能判断id是否存在。

结论就是使用UUIDv4是最差的:

if you are free to assign your own ids then what works best for Lucene? One obvious choice is Java’s UUID class, which generates version 4 universally unique identifiers, but it turns out this is the worst choice for performance: it is 4X slower than the fastest.

不光是ES,Mysql,主键最好是单调(增加或减少)的,而且是越短越好,固定长度。能用数值(十进制或二进制)就不用字符串。对于Mysql而言,尤其是使用innodb搜索引擎的,主键长度很关键,可以参考这篇MySQL索引背后的数据结构及算法原理

检索分析器问题

如果按照默认的检索输入“苏A12345”,ES会使用对应的字段上的默认的分析器对检索词分词,也就是会对“苏A12345”使用ngram分析器分成28个词,在进行match查询的时候,ES就需要根据这个28个词计算相关性,这显然会比较慢。所以可以对索引时使用的分析器和检索时使用的分析器单独设置,可以直接在配置map的时候进行设置,这里进行车牌检索,按照一般人使用的习惯,直接按照空格分析就可以了。设置map时候可以配置:

...
"plate": {
	"type": "text",
	"fields": {
		"keyword": {
			"type": "keyword"
		}
	},
	"analyzer": "my_analyzer",
    "search_analyzer": "whitespace"
}
...

// my_analyzerd的配置
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "my_tokenizer"
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 8,
          "token_chars": [
            "letter",
            "digit"
          ]
        }
      }
    }
  }
}

简单测试了下,如果使用默认的ngram分析器,检索返回如果长度比较长,如“苏A12345”,10亿数据在上述配置下,有8-10秒的延迟,而使用whitespace分析器,由于没有空格,只有一个词,相当于是keyword,也就是精确查询,结果是毫秒级的。一般的使用情况,有两个空格三个词如“苏A 12 34”平均大概3秒,一个空格两个词平均大概2秒。响应时间会有大幅度的提高。 更加完整的表述参考官网对search_analyzer的说明:search_analyzer

OOM问题

测试的时候出现OOM导致ES挂掉的问题,”[o.e.t.n.Netty4Utils] fatal error on the n etwork layer”,在论坛上也有人报过着问题:Elasticsearch crashed with “fatal error on the network layer”,但是还没有close掉。

引发问题的原因是因为在批量插入数据的时候,多线程并发bulk,ES响应慢,超时了又继续发送,最后导致netty中消息堆积,然后就OOM了。猜测是这样,上面的stackoverflow上的问题也是在批量插入的时候发生的。

避免的方式,如果初始批量插入结束了,增加数据最好是单线程同步批量插入,异常的时候要处理,不要不管异常再继续插入了。

参考

Day19 ES内存那点事
亿级规模的Elasticsearch优化实战
Elasticsearch as a Time Series Data Store
降维打击!使用ElasticSearch作为时序数据库
ES VS mongoDB
英文版官方指南

Table of Contents