优化检索-使用缓存
在文档评分那一篇中,我简短的叙述了针对过滤器缓存的一点知识,我们知道在使用filtered查询会比查询后置过滤,在性能上有较为可观的提神,会减少非必要的文档打分。
字段级别的缓存Doc Values
字段级别的缓存,难以绕过的ES的一座山Doc Values
当你对一个字段进行排序时,Elasticsearch 需要访问每个匹配到的文档得到相关的值。倒排索引的检索性能是非常快的,但是在字段值排序时却不是理想的结构。在搜索的时候,我们能通过搜索关键词快速得到结果集。当排序的时候,我们需要倒排索引里面某个字段值的集合。换句话说,我们需要 转置
倒排索引。默认情况下每个字段的 Doc Values
都是激活的,Doc Values
是在索引时创建的,当字段索引时,Elasticsearch 为了能够快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 Doc Values
也就是双写.
例如存在如下倒排索引
对于以下倒排索引:
Term Doc_1 Doc_2 Doc_3
------------------------------------
brown | X | X |
dog | X | | X
dogs | | X | X
fox | X | | X
foxes | | X |
in | | X |
jumped | X | | X
lazy | X | X |
leap | | X |
over | X | X | X
quick | X | X | X
summer | | X |
the | X | | X
------------------------------------
如果我们想要获得所有包含 brown
的文档的词的完整列表
GET /my_index/_search
{
"query" : {
"match" : {
"body" : "brown"
}
},
"aggs" : {
"popular_terms": {
"terms" : {
"field" : "body"
}
}
}
}
倒排索引是根据项来排序的,所以我们首先在词项列表中找到 brown ,然后扫描所有列,找到包含 brown
的文档。我们可以快速看到 Doc_1
和 Doc_2
包含 brown
这个 token
。
然而对于聚合部分,我们需要找到 Doc_1
和 Doc_2
里所有唯一的词项。 用倒排索引做这件事情代价很高: 我们会迭代索引里的每个词项并收集 Doc_1
和 Doc_2
列里面 token
。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。
Doc values
通过转置两者间的关系来解决这个问题。倒排索引将词项映射到包含它们的文档,doc values
将文档映射到它们包含的词项:
Doc Terms
-----------------------------------------------------------------
Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 | dog, dogs, fox, jumped, over, quick, the
-----------------------------------------------------------------
当数据被转置之后,想要收集到 Doc_1 和 Doc_2 的唯一 token 会非常容易。获得每个文档行,获取所有的词项,然后求两个集合的并集。
因此,搜索和聚合是相互紧密缠绕的。搜索使用倒排索引查找文档,聚合操作收集和聚合 doc values 里的数据。
为什么说Doc Values
会很快,那是因为不占用ES的堆内存。Doc Values
通过序列化把数据结构持久化到磁盘,我们可以充分利用操作系统的内存,而不是 JVM
的Heap
如果你的物理机器的内存十分的庞大,那么你可以适当的预留堆外内存给Doc Values
,同时禁止使用swap交换内存。
分片级别的缓存
分片缓存默认是禁用的,可以在索引的配置项index.cache.query.enable=true
来开启,或者通过_settings
接口来设置,在检索阶段使用设置query_cache=true
来开启分片缓存。比较好的一点是分片缓存会自动失效和更新,当分片内容发生改变,ES会自动更新缓存内容,默认情况每个节点最多使用1%的堆内存来作为分片缓存,当然你也可以通过参数来更改indices.cache.query.size
来调整百分比,也可以手动设置过期时间indices.cache.query.expire
断路器
哦断路器,一个陌生又熟悉的字眼,在Hystrix中提到过,用于保护服务的一种机制,在ES中针对查询的预估内存特别大,那么断路器就会拒绝这次查询。如果某个查询的内存占用高过了预估值。这个断路器的配置值为indices.breaker.fielddata.limit=60%
,这就意味着JVM堆的最多60的空间用于字段数据的缓存。Elasticsearch 包括一个字段数据断熔器 ,这个设计就是为了处理上述情况。 断熔器通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的字段数据是否会导致 fielddata 的总量超过堆的配置比例。
如果估算查询的大小超出限制,就会 触发 断路器,查询会被中止并返回异常。这都发生在数据加载 之前 ,也就意味着不会引起 OutOfMemoryException
优化ES-JVM优化
对es使用的JVM参数进行调优,在超大堆毫无意外直接使用G1垃圾回收器
优化ES-热点线程
GET /_nodes/hot_threads
::: {g1-sys-es-15}{bscbTjq_S4e0lxIdmUbKHg}{wIOUk45xSwebMOtx2UmMCQ}{192.168.2.1}{192.168.2.1:9300}{ml.machine_memory=67450658816, ml.max_open_jobs=20, xpack.installed=true, box_type=hot, ml.enabled=true}
Hot threads at 2021-01-30T08:27:26.988, interval=500ms, busiestThreads=3, ignoreIdleThreads=true:
94.7% (473.4ms out of 500ms) cpu usage by thread 'elasticsearch[g1-sys-es-15][write][T#7]'
4/10 snapshots sharing following 2 elements
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
java.base@10.0.2/java.lang.Thread.run(Thread.java:844)
94.7% (473.2ms out of 500ms) cpu usage by thread 'elasticsearch[g1-sys-es-15][write][T#12]'
2/10 snapshots sharing following 16 elements
org.joni.ByteCodeMachine.matchAt(ByteCodeMachine.java:254)
org.joni.Matcher.matchCheck(Matcher.java:304)
org.joni.Matcher.searchInterruptible(Matcher.java:480)
org.joni.Matcher.search(Matcher.java:318)
org.elasticsearch.grok.Grok.captures(Grok.java:247)
org.elasticsearch.ingest.common.GrokProcessor.execute(GrokProcessor.java:66)
app//org.elasticsearch.ingest.CompoundProcessor.execute(CompoundProcessor.java:100)
app//org.elasticsearch.ingest.Pipeline.execute(Pipeline.java:58)
app//org.elasticsearch.ingest.PipelineExecutionService.innerExecute(PipelineExecutionService.java:155)
app//org.elasticsearch.ingest.PipelineExecutionService.access$100(PipelineExecutionService.java:43)
app//org.elasticsearch.ingest.PipelineExecutionService$1.doRun(PipelineExecutionService.java:78)
app//org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:723)
app//org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
java.base@10.0.2/java.lang.Thread.run(Thread.java:844)
3/10 snapshots sharing following 17 elements
org.joni.ByteCodeMachine.opAnyCharStar(ByteCodeMachine.java:883)
org.joni.ByteCodeMachine.matchAt(ByteCodeMachine.java:230)
org.joni.Matcher.matchCheck(Matcher.java:304)
org.joni.Matcher.searchInterruptible(Matcher.java:480)
org.joni.Matcher.search(Matcher.java:318)
org.elasticsearch.grok.Grok.captures(Grok.java:247)
org.elasticsearch.ingest.common.GrokProcessor.execute(GrokProcessor.java:66)
app//org.elasticsearch.ingest.CompoundProcessor.execute(CompoundProcessor.java:100)
app//org.elasticsearch.ingest.Pipeline.execute(Pipeline.java:58)
app//org.elasticsearch.ingest.PipelineExecutionService.innerExecute(PipelineExecutionService.java:155)
app//org.elasticsearch.ingest.PipelineExecutionService.access$100(PipelineExecutionService.java:43)
app//org.elasticsearch.ingest.PipelineExecutionService$1.doRun(PipelineExecutionService.java:78)
app//org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:723)
app//org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
java.base@10.0.2/java.lang.Thread.run(Thread.java:844)
4/10 snapshots sharing following 19 elements
org.joni.StackMachine.pop(StackMachine.java:324)
org.joni.ByteCodeMachine.opFail(ByteCodeMachine.java:1749)
org.joni.ByteCodeMachine.opExact1(ByteCodeMachine.java:428)
org.joni.ByteCodeMachine.matchAt(ByteCodeMachine.java:203)
org.joni.Matcher.matchCheck(Matcher.java:304)
org.joni.Matcher.searchInterruptible(Matcher.java:480)
org.joni.Matcher.search(Matcher.java:318)
org.elasticsearch.grok.Grok.captures(Grok.java:247)
org.elasticsearch.ingest.common.GrokProcessor.execute(GrokProcessor.java:66)
app//org.elasticsearch.ingest.CompoundProcessor.execute(CompoundProcessor.java:100)
app//org.elasticsearch.ingest.Pipeline.execute(Pipeline.java:58)
app//org.elasticsearch.ingest.PipelineExecutionService.innerExecute(PipelineExecutionService.java:155)
app//org.elasticsearch.ingest.PipelineExecutionService.access$100(PipelineExecutionService.java:43)
app//org.elasticsearch.ingest.PipelineExecutionService$1.doRun(PipelineExecutionService.java:78)
app//org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:723)
app//org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
java.base@10.0.2/java.lang.Thread.run(Thread.java:844)
unique snapshot
org.joni.StackMachine.pop(StackMachine.java:324)
org.joni.ByteCodeMachine.opFail(ByteCodeMachine.java:1749)
org.joni.ByteCodeMachine.opCClass(ByteCodeMachine.java:720)
org.joni.ByteCodeMachine.matchAt(ByteCodeMachine.java:220)
org.joni.Matcher.matchCheck(Matcher.java:304)
org.joni.Matcher.searchInterruptible(Matcher.java:480)
org.joni.Matcher.search(Matcher.java:318)
org.elasticsearch.grok.Grok.captures(Grok.java:247)
org.elasticsearch.ingest.common.GrokProcessor.execute(GrokProcessor.java:66)
app//org.elasticsearch.ingest.CompoundProcessor.execute(CompoundProcessor.java:100)
app//org.elasticsearch.ingest.Pipeline.execute(Pipeline.java:58)
app//org.elasticsearch.ingest.PipelineExecutionService.innerExecute(PipelineExecutionService.java:155)
app//org.elasticsearch.ingest.PipelineExecutionService.access$100(PipelineExecutionService.java:43)
app//org.elasticsearch.ingest.PipelineExecutionService$1.doRun(PipelineExecutionService.java:78)
app//org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:723)
app//org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
java.base@10.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
java.base@10.0.2/java.lang.Thread.run(Thread.java:844)
可以通过热点现场来定位当前es阻塞在那个阶段,用于我们判断es的瓶颈,
查询参数通过type来制定 如cpu,wait当前阻塞线程,最后通过interval来控制时间间隔。
常用的读写调优
a、调优思路
写入调优:增加refresh和flush时间间隔以及index buffer区和translog区空间,来减少segment生成,segment merge 以及刷盘的频率;
查询调优:一定程度上增加副本个数来增大查询吞吐量,定期对冷索引执行force meger操作,是其合并segment。
b、搜索调优实战
reindex:针对分片分配不均,我们可以通过重建索引的方式使分配均匀分配
cluster.routing.use_adaptive_replica_selection = true:开启自适应副本分片开关,使节点的路由更为智能
cluster.routing.allocation.same_shard.host=true :单机多节点部署,打开此开关,可以保证同一分片的主副本不落在同一台物理机上
number_of_replicas = 2 :一定程度上增大副本数目可以增大查询的吞吐量
垂直拓展和水平拓展
简单的理解想象关系学数据库的分表分库,垂直扩展就相当于专库专用,水平拓展就是分表,大表化作小表。在ES当中呢垂直扩展就是机器加内存呗,加机器,水平拓展呢就是使用副本分片,加机器,让主分片和副本分片均匀的分布在集群内的每个节点,避免同一个索引的主分片和副本分片共处于一个节点。
还有就是河里分配集群内节点的职责,如制定主节点,数据节点 已经专门用来做数据聚合查询的聚合节点。
在高负载场景使用ES
- 基于高负载场景 第一选择合适的存储,如在Linux上使用NioFSDirector,在linux上使用64位的JVM也可以使用mmap的方式,直接使用虚拟内存映射。速度大大的。
- 控制索引刷新的频率,在前面的章节我们看到,一个文档被索引到lucene段中到可以被Search实例检索到 有个1S的延时,在实时性要求不是很高的场景,可以调整这个刷新的时间,用来减轻压力
- 线程池调优,在高并发查询的场景,es默认的搜索线程配比完全不够,默认search线程大小为((cpu个数 * 3) / 2) + 1;可以使用下列的配置来更改
#queue_size允许控制没有线程执行它们的挂起请求队列的初始大小。
thread_pool.search.size: 200
#size参数控制线程数,默认为核心数乘以5。
thread_pool.search.min_queue_size:10
#min_queue_size设置控制queue_size可以调整到的最小量。
thread_pool.search.max_queue_size: 1000
#max_queue_size设置控制queue_size可以调整到的最大量。
thread_pool.search.auto_queue_frame_size: 2000
#auto_queue_frame_size设置控制在调整队列之前进行测量的操作数。它应该足够大,以便单个操作不会过度偏向计算。
thread_pool.search.target_response_time: 6s
#target_response_time是时间值设置,指示线程池队列中任务的目标平均响应时间。如果任务通常超过此时间,则将调低线程池队列以拒绝任务。
- 调整段合并的配置
- 更换SSD 升级磁盘的能力。
- 增加副本的数量
在高检索场景使用ES 多读
- 使用缓存 过滤器缓存 或者是分片缓存
- 查询使用到过滤器,避免后置过滤
- 使用路由,在索引期间把对于的数据放到某个节点,在查询时候根据路由参数,可以直接定位到对应的节点,减少fetch的分片数目。
- 并行查询,也就是为你的每个索引的主分片在创建时预分配。
- 使用Doc Vlaue 来进行聚合和排序
- 对于聚合查询,控制不反悔size
在高索引场景使用ES 多写
- 批量索引
- 判断是否启用doc value 你肯定知道开启doc value 会作双写,如果你的索引压根就不会进行聚合查询 和排序,你可以选择禁用doc vlaue。
- 还有就是你要控制你的文档的字段
- 你需要权衡索引所需非最合理的副本和速度上的区别,你要控制可用性和可靠性的天平
- 针对多写入的场景 你要把事务日志的默认时间开启延长,或者把事务文件的大小调整,避免flush动作。
- 更换SSD升级磁盘