1、写入速度优化
在 ES 的默认设置下,是综合考虑数据可靠性、搜索实时性、写入速度等因素的。当离开默认设置、追求极致的写入速度时,很多是以牺牲可靠性和搜索实时性为代价的。
1.1、TranslogFulsh间隔调整
默认情况下,translog持久化策略为每个请求都flush,其保证了写入操作的可靠性。
对应配置为:
index.translog.durability: request
若系统接受一定概率的数据丢失,如系统日志数据等,则可以调整translog持久化策略为周期性和一定大小的时候flush。
//设置translog刷盘策略按sync_interval指定的时间周期进行
index.translog.durability: async
//加长translog的刷盘周期,默认为5s
index.translog.sync_interval: 120s
//超过这个大小会导致refresh操作,产生新的Lucene分段。默认值为512MB。
index.translog.flush_threshold_size: 1024mb
1.2、索引刷新间隔
默认索引的refresh_interval为1秒,即数据写入后最长1s就可被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为,如果不需要这么高的搜索实时性,应该降低索引refresh周期。
对应配置:
index.refresh_interval: 120s
1.3、段合并优化
段合并scheduler:
//concurrent或serial
index.merge.scheduler.type
serial为串行合并策略,此策略下只用一个线程来执行段合并任务。
concurrent为并行合并策略,会创建多个线程来执行段合并任务。
默认线程数为:
Math.max(1, math.min(4,Runtime.getRuntime().availableProcessors() / 2))
可通过设置:index.merge.scheduler.max_thread_count,来改变最大线程数。
段合并策略:
可通过:index.merge.policy.type来设置合并策略;
主要配置如下:
tiered(分层合并策略):
ES默认策略,其将大小相似的段放在一起合并,依据每层允许的段数量的最大值,可以区分出一次合并中段的数量。在索引过程中,此策略会计算索引中允许存在的段数量,即为预算,若索引中段数量大于预算值,此策略先按段大小降序排序,找出开销最小的合并方案,合并的开销会考虑比较小的段及删除文档较多的段。如果合并产生的段的大小大于index.merge.policy.max_merged_segment配置,则此合并会减少段合并的数量,以保持合并后新的段在预算之内。
- index.merge.policy.expunge_default_allowed:默认为10,指定段删除文档的比例,当删除文档比例超过此比例时将会被合并;
- index.merge,policy.max_merge_at_once:指定同一次合并的段的最大数量,默认为10。若调大此值,则一次合并将合并更多段,也会占用更多I/O等系统资源;
- index.merge.policy.max_merge_at_once_explicit:指定对索引进行optimize或expungeDelete操作时一次合并的最大数量,默认为30.
- index.merge.policy.max_merged_segment:默认5gb,指定索引过程中单个段的最大容量。
- index.merge.policy.segment_per_tier:指定每层段的数量,默认为10。较小的值代理较少的段。同时也意味着更多的合并操作和更低的索引性能。
- index.reclaim_deletes_weight:设定删除文档在合并操作的重要程度,默认为2.0。若设为0则表示删除文档对段合并无影响,值越大表示删除文档对段合并影响越大。
- index.compund_format:设定lucene中索引是否存储为压缩格式,默认为false。若为true,lucene会将索引所有数据存储在一个文档中,但会降低搜索和索引性能。
- index.merge.async:设定合并操作是否异步进行,默认为true;
- index.merge.async_interval:当index.merge.async为true时,设定两次合并的间隔时间,默认为1s。
log_byte_size(字节大小对数合并策略):
该策略将创建大小处于对数运算后大小在指定范围内的段组成的索引。当一个新段尝试并其与其他段不在一个数量级时,所有处于该数量级的段就会合并。索引中段的数量与经对数运算后新段字节的大小成比例。此策略通常能在段合并开销较小的情况下将索引中的段保持在一个较低的水平。
- merge_factor:该参数确定了索引期间索引段以多大的频率进行合并。该值越小,搜索的速度越快,消耗的内存也越少,而代价则是更慢的索引速度。如果该值越大,情形则正好相反,即更快的索引速度(因为索引合并更少),搜索速度更慢,消耗的内存更多。该参数默认为10,对于批量索引构建,可以设置较大的值,对于日常索引维护则可采用默认值。
- min_merge_size:该参数定义了索引段可能的最小容量(段中所有文件的字节数)。如果索引段大小小于该参数值,且merge_factor参数值允许,则进行索引段合并。该参数默认值为1.6MB,它对于避免产生大量小索引段是非常有用的。然而,用户应该记住,该参数值设置为较大值时,将会导致较高的合并成本。
- max_merge_size:该参数定义了允许参与合并的索引段的最大容量(以字节为单位)。默认情况下,参数不做设置,因而在索引合并时对索引段大小没有限制。
- maxMergeDocs:该参数定义了参与合并的索引段的最大文档数。默认情况下,参数没有设置,因此当索引合并时,对索引段没有最大文档数的限制。
- calibrate_size_by_deletes:该参数为布尔值,如果设置为true,则段中被删除文档的大小会用于索引段大小的计算。
- index.compund_format:该参数为布尔值,它确定了索引文件是否存储为复合文件格式,默认为false。可参考tiered合并策略配置中该选项的解释。
log_doc(文档对数合并策略):
与log_byte_size策略类似,只不过以文段数量的计算方式来合并段。
- merge_factor:与log_byte_size合并策略中该参数的作用相同,请参考前面的解释。
- min_merge_docs:该参数定义了最小索引段允许的最小文档数。如果某索引段的文档数低于该参数值,且- merge_factor参数允许,就会执行索引合并。该参数默认值为1000,它对于避免产生大量小索引段是非常有用的。但是用户需要记住,将该参数值设置过大会增大索引合并的代价。
- max_merge_docs:该参数定义了可参与索引合并的索引段的最大文档数。默认情况下,该参数没有设置,因而对参与索引合并的索引段的最大文档数没有限制。
- calibrate_size_by_deletes:该参数为布尔值,如果设置为true,则段中被删除文档的大小会在计算索引段大小时考虑进去。
- index.compund_format:该参数为布尔值,它确定了索引文件是否存储为复合文件格式,默认为false。可参考tiered合并策略配置中该选项的解释。
1.4、indexing buffer
indexing buffer在为doc建立索引时使用,当缓冲区满时会刷入磁盘,生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成新segment的地方。
//节点索引缓存的大小,默认为整个堆空间的10%
indices.memory.index_buffer_size
//节点索引缓存的最小值,默认为48MB
indices.memory.min_index_buffer_size
//节点索引缓存的最大值,默认为无限制
indices.memory.max_index_buffer_size
1.5、使用bulk请求
批量写比一个索引请求只写单个文档的效率高得多,但是要注意bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执行得更好。
1.6、bulk线程池和队列
建立索引的过程属于计算密集型任务,应该使用固定大小的线程池配置,来不及处理的任务放入队列。线程池最大线程数量应配置为CPU核心数+1,这也是bulk线程池的默认设置,可以避免过多的上下文切换。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致较高的GC压力,并可能导致FGC频繁发生。
1.7、并发执行bulk请求
bulk写请求是个长任务,为了给系统增加足够的写入压力,写入过程应该多个客户端、多线程地并行执行,如果要验证系统的极限写入能力,那么目标就是把CPU压满。磁盘util、内存等一般都不是瓶颈。如果 CPU 没有压满,则应该提高写入端的并发数量。但是要注意 bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求被拒绝,此时客户端会收到429错误(TOO_MANY_REQUESTS),客户端对此的处理策略应该是延迟重试。不可忽略这个异常,否则写入系统的数据会少于预期。即使客户端正确处理了429错误,我们仍然应该尽量避免产生reject。
1.8、自动生成docId
写入doc时如果外部指定了id,则ES会先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可以避免这个环节。
1.9、调整字段Mappings
- 减少字段数量,对于不需要建立索引的字段,不写入ES。
- 将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。
- 减少字段内容长度,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容。
- 使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。
1.10、对Analyzed的字段禁用Norms
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:
"title": {"type": "string","norms": {"enabled": false}}
1.11、index_options 设置
index_options用于控制在建立倒排索引过程中,哪些内容会被添加到倒排索引,例如,doc数量、词频、positions、offsets等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。不过在实际场景中,通常很难确定业务将来会不会用到这些信息,除非一开始方案就明确是这样设计的。
2、搜索速度优化
2.1、为文件系统cache保留足够内存
应用程序的读写都会被操作系统“cache”(除了direct方式),cache保存在系统物理内存中(线上应该禁用swap),命中cache可以降低对磁盘的直接访问频率。搜索很依赖对系统 cache 的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。应该至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。
2.2、使用更快的硬件
使用SSD会比旋转类存储介质好得多。尽量避免使用NFS 等远程文件系统,如果 NFS 比本地存储慢3倍,则在搜索场景下响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。
2.3、预索引数据
可以针对某些查询的模式来优化数据的索引方式。例如,如果所有文档都有一个 price字段,并且大多数查询在一个固定的范围上运行range聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚合来加快聚合速度。
PUT index/type/1
{
"designation": "spoon",
"price": 13
}
PUT index/type/1
{
"designation": "spoon",
"price": 13,
"price_range":"10-100"
}
2.4、字段映射
有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好。
2.5、优化日期搜索
在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配到的范围一直在变化。但是,从用户体验的角度来看,切换到一个完整的日期通常是可以接受的,这样可以更好地利用查询缓存。
2.6、为只读索引执行force-merge
为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。
2.7、预热文件系统cache
如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。可以使用index.store.preload设置,通过指定文件扩展名,显式地告诉操作系统应该将哪些文件加载到内存中。
PUT /my_index
{
"settings": {
"index.store.preload": ["nvd", "dvd"]
}
}
2.8、利用自适应副本选择(ARS)
为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力和健康水平。
ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每个副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到那里。ES7.0开始默认开启。
PUT /_cluster/settings
{
"transient": {
"cluster.routing.use_adaptive_replica_selection": true
}
}
3、磁盘使用量优化
3.1、索引映射参数
索引创建时可以设置很多映射参数:
- 控制字段值是否被索引。它可以设置为true或false,默认为true。未被索引的字段不会被查询到,但是可以聚合。除非禁用doc_values。
- doc values:values:默认情况下,大多数字段都被索引,这使得它们可以搜索。倒排索引根据term找到文档列表,然后获取文档原始内容。但是排序和聚合,以及从脚本中访问某个字段值,需要不同的数据访问模式,它们不仅需要根据term找到文档,还要获取文档中字段的值。这些值需要单独存储。doc_values 就是用来存储这些字段值的。它是一种存储在磁盘上的列式存储,在文档索引时构建,这使得上述数据访问模式成为可能。它们以面向列的方式存储与_source相同的值,这使得排序和聚合效率更高。几乎所有字段类型都支持doc_values,但被分析(analyzed)的字符串字段除外(即text类型字符串)。doc_values默认启用。
- store:默认情况下,字段值会被索引使它们能搜索,但它们不会被存储(stored)。意味着可以通过这个字段查询,但不能取回它的原始值。
3.2、禁用不需要的特性
禁用norms:
text 类型的字段会在索引中存储归一因子(normalizationfactors),以便对文档进行评分,如果只需要在文本字段上进行匹配,而不关心生成的得分,则可以配置 ES 不将 norms 写入索引。
PUT index
{
"mappings":{
"type":{
"properties":{
"foo":{
"type":"text",
"norms":false
}
}
}
}
}
修改index_options参数:
text类型的字段默认情况下也在索引中存储频率和位置。频率用于计算得分,位置用于执行短语(phrase)查询。如果不需要运行短语查询,则可以告诉ES不索引位置。
index_options参数:
- docs:只有被索引的文档数量。
- freqs:被索引的文档数量和索引词频。索引词频用来使重复索引词的得分高于单个索引词;
- positions:文档数量,词频、索引词位置。位置可用于临近或短语查询;
- offsets:文档数量、词频、位置及开始和结束字符偏移量,偏移量可用于高亮显示。
字符串使用positions作为默认值,其他字段使用docs为默认值;
PUT index
{
"mappings":{
"type":{
"properties":{
"foo":{
"type":"text",
"index_options":"freqs"
}
}
}
}
}
3.3、禁用doc values
所有支持doc value的字段都默认启用了doc value。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc value以节省磁盘空间:
PUT index
{
"mappings":{
"type":{
"properties":{
"foo":{
"type":"text",
"doc_values":false
}
}
}
}
}
3.4、使用best_compression
_source和设置为"store": true的字段占用磁盘空间都比较多。默认情况下,它们都是被压缩存储的。默认的压缩算法为LZ4,可以通过使用best_compression来执行压缩比更高的算法:EFLATE。但这会占用更多的CPU资源。
PUT index
{
"settings": {
"index": {
"codec": "best_compression"
}
}
}
3.5、Fource Merge
一个ES索引由若干分片组成,一个分片有若干Lucene分段,较大的Lucene分段可以更有效地存储数据。使用_forcemerge API来对分段执行合并操作,通常,我们将分段合并为一个单个的分段:max_num_segments=1。
4、综合优化
4.1、集群层优化
4.1.1、规划集群规模
集群规模和数据总量控制:
- 集群最大规模控制在100个节点左右,当节点更多时,节点间的连接数和通行量倍增,主节点管理压力较大;
- 单个分片不要超过50GB,最大集群的分片总数控制在几十万的级别,太多分片会增加主节点管理负担,且延长集群重启恢复时间;
- 建议为集群配置较好的硬件,这对搜索性能有较大提升;另外不要使用不同配置副服务器混合部署,否则搜索取决于最慢的那个节点,产生长尾效应。
4.1.2、多节点部署
ES不建议为JVM配置超过32GB的内存,超过32GB时,Java内存指针压缩失效,浪费一些内存,降低了CPU性能,GC压力也较大,因此推荐设置为31GB。确保堆内存最小值(Xms)与最大值(Xmx)大小相同,防止程序在运行时动态改变堆内存大小,这是很耗系统资源的过程。
当物理内存超过64GB时部署方案:
- 部署单个节点,JVM内存配置不超过32GB,配置全部数据盘。这种部署模式的缺点是多余的物理内存只能被cache使用,而且只要存在一个坏盘,节点重启会无法启动。
- 部署单个节点,JVM内存配置超过32GB,配置全部数据盘。接受指针压缩失效和更长时间的GC等负面影响。
- 有多少个数据盘就部署多少个节点,每个节点配置单个数据路径。优点是可以统一配置,缺点是节点数较多,集群管理负担大,只适用于集群规模较小的场景。
- 使用内存大小除以64GB来确定要部署的节点数,每个节点配置一部分数据盘,优点是利用率最高,缺点是部署复杂。
4.2、节点层
4.2.1、控制线程池的队列大小
不要为bulk和search分配过大的队列,队列并非越大越好,队列缓存的数据越多,GC压力越大,默认的队列大小基本够用了,即使在压力测试的场景中,默认队列大小也足以支持。除非在一些特别的情况下,例如,每个请求的数据量都非常小,可能需要增加队列大小。但是我们推荐写数据时组合较大的bulk请求。
4.2.2、为系统cache保留一半物理内存
搜索操作很依赖对系统cache的命中,标准的建议是把50%的可用内存作为ES的堆内存,为Lucene保留剩下的50%,用作系统cache。
4.3、系统层
4.3.1、关闭swap
在服务器系统上,无论物理内存多么小,哪怕只有1GB,都应该关闭交换分区。当服务程序在交换分区上缓慢运行时,往往会产生更多不可预期的错误,因此当一个申请内存的操作如
果真的遇到物理内存不足时,宁可让它直接失败。一般在安装操作系统的时候直接关闭交换分区,或者通过swapoff命令来关闭。
4.3.2、配置系统的OOM Killer
在Linux下,进程申请的内存并不会立刻为进程分配真实大小的内存,因为进程申请的内存不一定全部使用,内核在利用这些空闲内存时采取过度分配的策略,假如物理内存为1GB,则两个进程都可以申请1GB的内存,这超过了系统的实际内存大小。当应用程序实际消耗完内存的时候,怎么办?系统需要“杀掉”一些进程来保障系统正常运行。这就触发了OOM Killer,通过一些策略给每个进程打分,根据分值高低决定“杀掉”哪些进程。默认情况下,占用内存最多的进程被“杀掉”。
如果ES与其他服务混合部署,当系统产生OOM的时候,ES有可能会无辜被“杀”。为了避免这种情况,我们可以在用户态调节一些进程参数来让某些进程不容易被OOM Killer“杀掉”。
4.4、索引层
4.4.1、Force Merge
对冷索引执行Force Merge会有许多好处,可以选择在系统的空闲时间段对不再更新的只读索引执行ForceMerge。
- 单一的分段比众多分段占用的磁盘空间更小一些;
- 可以大幅减少进程需要打开的文件fd;
- 可以加快搜索过程,因为搜索需要检索全部分段;
- 单个分段加载到内存时也比多个分段更节省内存占用;
- 可以加快索引恢复速度。