作者: Adam Vanderbush
译者: 杨振涛
目录
文档建模
全局序列号和延迟
多代关系
为文件系统缓存分配内存
Elasticsearch 搜索调优权威指南, 是 QBOX 在其博客上发布的系列文章之一, 本文是该系列的第一篇, 主要从文档建模, 内存分配, 文件系统缓存, GC 和硬件等方面介绍了优化查询性能的一些经验.
Elasticsearch 5.0.0 确实是在 2.x 之后的一个大版本, 为大家带来了许多新东西. Elasticsearch 现在作为 Elastic Stack 中的一员, 与整个技术栈的其他产品的版本号已经对齐, 现在 Kibana,Logstash,Beats 和 Elasticsearch 全都是 5.0 版本了.
这个版本的 Elasticsearch 是目前为止最快, 最安全, 最弹性, 也是最易用的, 而且还带来了很多的改进和新特性.
我们已经通过 "Elasticsearch 性能调优权威指南" 系列, 介绍了一些性能调优的基本经验和方法, 解释了每一步最关键的系统设置和衡量指标. 该系列共分下列 3 个部分:
- The Authoritative Guide to Elasticsearch Performance Tuning (Part 1)
- The Authoritative Guide to Elasticsearch Performance Tuning (Part 2)
- The Authoritative Guide to Elasticsearch Performance Tuning (Part 3)
索引决策也很重要, 它对如何搜索数据有很大的影响. 如果是一个字符串字段, 是否需要分词或归一化? 如果是, 怎么做? 如果是一个数值型属性, 需要哪种精度? 还有很多其他类型, 比如 date-time,geospatial shape 以及父子关系等, 需要更多特别的考虑.
我们也通过一个系列教程讨论了 "Elasticsearch 索引性能优化", 介绍了一些通用的技巧和方法, 来最大化索引的吞吐量并降低监控和管理的负载. 该教程分如下 3 个部分:
- How to Maximize Elasticsearch Indexing Performance (Part 1)
- How to Maximize Elasticsearch Indexing Performance (Part 2)
- How to Maximize Elasticsearch Indexing Performance (Part 3)
本文旨在推荐一些搜索调优技术, 策略以及 Elasticsearch 5.0 及以上的推荐特性.
1. 文档建模
内部对象属性数组并不像期望的那样工作. Lucene 中没有内部对象的概念, 所以 Elasticsearch 把对象层次展开到一个由属性名称和属性值组成的简单列表中. 以下列文档为例:
- curl -XPUT 'localhost:9200/my_index/my_type/1?pretty' -H 'Content-Type: application/json' -d '{
- "group" : "fans",
- "user" : [
- {
- "first" : "John",
- "last" : "Smith"
- },
- {
- "first" : "Alice",
- "last" : "White"
- }
- ]
- }'
该请求会在内部转换为如下的文档形式:
- {
- "group" : "fans",
- "user.first" : [ "alice", "john" ],
- "user.last" : [ "smith", "white" ]
- }
如果需要索引对象数组, 并维护数组中每个对象的依赖关系, 应当使用内嵌数据类型而不是对象数据类型. 内嵌对象在内部会把数组中的每个对象当作单独的隐藏文档来索引, 即使用下述内嵌查询, 可以单独查询每个内嵌对象:
- curl -XPUT 'ES_HOST:ES_PORT/my_index?pretty' -H 'Content-Type: application/json' -d '{
- "mappings": {
- "my_type": {
- "properties": {
- "user": {
- "type": "nested"
- }
- }
- }
- }
- }'curl -XPUT'ES_HOST:ES_PORT/my_index/my_type/1?pretty'-H'Content-Type: application/JSON'-d'{
- "group" : "fans",
- "user" : [
- {
- "first" : "John",
- "last" : "Smith"
- },
- {
- "first" : "Alice",
- "last" : "White"
- }
- ]
- }'curl -XGET'ES_HOST:ES_PORT/my_index/_search?pretty'-H'Content-Type: application/JSON'-d'{
- "query": {
- "nested": {
- "path": "user",
- "query": {
- "bool": {
- "must": [
- { "match": { "user.first": "Alice" }},
- { "match": { "user.last": "Smith" }}
- ]
- }
- }
- }
- }
- }'curl -XGET'ES_HOST:ES_PORT/my_index/_search?pretty'-H'Content-Type: application/JSON'-d'{
- "query": {
- "nested": {
- "path": "user",
- "query": {
- "bool": {
- "must": [
- { "match": { "user.first": "Alice" }},
- { "match": { "user.last": "White" }}
- ]
- }
- },
- "inner_hits": {
- "highlight": {
- "fields": {
- "user.first": {}
- }
- }
- }
- }
- }
- }'
当有一个主实体比如一篇博客文章, 带有一些有一定关系但又不是非常重要的其他实体比如评论时, 内嵌对象会非常有用. 如果能根据评论内容来查询到博客文章, 那就很不错, 而且内嵌查询和过滤器一起提供了更快的 join 查询能力.
内嵌对象模型的缺点如下:
为了 增加 , 修改 或 删除 一个内嵌对象文档, 整个文档必须重建索引; 这就导致内嵌文档越多开销就越大.
搜索请求返回整个文档, 而不是只返回匹配的内嵌文档. 虽然已经以后计划支持返回根文档的部分最配内嵌文档, 但目前仍然不支持.
有时候可能需要把主文档和其关联实体分离, 这种分离由父子关系来提供.
通过建立另一个文档的父类型 mapping, 可以在相同索引的文档之间建立父子关系:
- curl -XPUT 'ES_HOST:ES_PORT/my_index?pretty' -H 'Content-Type: application/json' -d '{
- "mappings": {
- "my_parent": {},
- "my_child": {
- "_parent": {
- "type": "my_parent"
- }
- }
- }
- }'curl -XPUT'ES_HOST:ES_PORT/my_index/my_parent/1?pretty'-H'Content-Type: application/JSON'-d'{
- "text": "This is a parent document"
- }'curl -XPUT'ES_HOST:ES_PORT/my_index/my_child/2?parent=1&pretty'-H'Content-Type: application/JSON'-d'{
- "text": "This is a child document"
- }'curl -XPUT'ES_HOST:ES_PORT/my_index/my_child/3?parent=1&refresh=true&pretty'-H'Content-Type: application/JSON'-d'{
- "text": "This is another child document"
- }'curl -XGET'ES_HOST:ES_PORT/my_index/my_parent/_search?pretty'-H'Content-Type: application/JSON'-d'{
- "query": {
- "has_child": {
- "type": "my_child",
- "query": {
- "match": {
- "text": "child document"
- }
- }
- }
- }
- }'
父子 join 对管理实体关系非常有用, 尤其是在索引时间比检索时间很重要的情形下, 但是它会带来较大的开销; 父子查询比同等的内嵌查询要慢 5 到 10 倍.
2. 全局序列号和延迟
父子关系使用了全局序列号来加速 join 操作. 无论父子 map 是否使用了内存缓存或磁盘上的 doc value, 全局序列号仍然需要在索引发生任何改变时进行重建.
分片中的父代越多, 全局序列号构建就越耗时. 相对于需要父代和较少的子代, 父子关系最适合每个父代有很多子代的情形.
全局序列号默认是 延迟 构建: refresh 后的第一个父子查询或聚合请求将会触发构建全局序列号. 这会让用户感知到一个明显的潜在峰值. 可以使用 eager_global_ordinals 来把查询期构建全局序列号的成本转移到 refresh 期, 通过如下方式 mapping _parent 属性:
- curl -XPUT 'ES_HOST:ES_PORT/company -d'{
- "mappings": {
- "branch": {},
- "employee": {
- "_parent": {
- "type": "branch",
- "fielddata": {
- "loading": "eager_global_ordinals"
- }
- }
- }
- }
- }'
这里,_parent 属性的全局序列号将会在一个新的段搜索可见时被构建.
对于很多的父代, 全局序列号要花费数秒钟来构建. 此时, 需要增加 refresh_interval, 以便 refresh 的频率更低, 而全局序列号保持可用的时间更长. 这将大幅减少每秒钟重建全局序列号的 CPU 消耗.
3. 多代关系
对多代数据的 Join(参考 Grandparents and Grandchildren)能力听起来很吸引人, 但需要思考其代价:
Join 越多, 性能越差.
每一个父代都需要把自己的 string _id 属性保存在内存, 这可能会消耗大量 RAM.
当考虑关系型方案及父子关系是否适合时, 可参考下列关于父子关系的建议:
保守使用父子关系, 仅当子代比父代多很多时才考虑.
避免在单个查询中使用多父子关系来 join.
避免对使用 has_child 过滤器, 或 score_mode 为 none 的 has_child 查询来打分.
父 ID 尽量简短, 以便在 doc value 中更好地压缩, 从而在瞬时加载时消耗更少的内存.
4. 为文件系统缓存分配内存
对于运行中 Elasticsearch, 内存是需要密切监控的重要资源之一. Elasticsearch 和 Lucene 通过 JVM 堆内存和文件系统缓存两种方式来消耗内存. 由于 Elasticsearch 运行在 Java 虚拟机 (JVM) 中, 所以 JVM 的 GC 周期和频率也需要重点监控.
JVM 堆内存
对于 Elasticsearch 一个 "刚好合适" 的 JVM 堆大小是非常重要的 -- 不能设置过大或过小, 原因见后文. 一般来说 Elasticsearch 的经验值是分配少于 50% 的可用 RAM 给 JVM 堆, 且不要超过 32GB.
为 Elasticsearch 分配过少的堆内存, 那么就会留给 Lucene 更多内存, 而 Lucene 重度依赖于文件系统缓存来快速处理请求. 不管怎样也不能设置过小的堆内存, 因为当应用由于频繁 GC 而面临短时中断时, 可能会遭遇内存溢出错误或吞吐量下降.
Elasticsearch 默认安装时设置的 JVM 堆大小为 1GB, 这在大多数情况下都偏小. 可以通过环境变量来设置期望的对大小并重启 Elasticsearch:
export ES_HEAP_SIZE=10g
设置 JVM 堆大小的另一种方式(相当于设置一样的最小值和最大值, 以防止重新调整堆大小), 是在每次启动 Elasticsearch 时通过命令行参数指定:
ES_HEAP_SIZE="10g" ./bin/Elasticsearch
这两种示例方式都是设置了 10GB 的堆大小, 为了验证是否设置成功, 执行:
curl -XGET http://ES_HOST:9200/_cat/nodes?h=heap.max
返回的输出会显示已正确地更新了最大堆内存.
垃圾回收
Elasticsearch 依靠 GC 过程来释放堆内存. 由于 GC 本身也要消耗资源(为了释放资源!), 所以应当留意 GC 频率和持续时间, 以确认是否需要调整堆内存大小. 设置过大的堆内存, 换来的是更长的 GC 时间; 这种过多的停顿非常危险, 因为可能导致集群误认为该节点网络异常而失联.
因此, Elasticsearch 重度依赖文件系统缓存来加速搜索. 一般需要保证至少有一半的可用内存用于文件系统缓存, 这样 Elasticsearch 才能保持索引数据的热点区域都在物理内存中.
使用更快的硬件
如果搜索受限于 I/O, 应当考虑为文件系统缓存分片更多内存(参考前文), 或者购买更快的驱动. 特别地, SSD 公认地比机械磁盘性能好很多. 尽可能使用本地存储, 避免使用像 NFS 或 SMB 之类的远程或网络文件系统, 也要注意像 Amazon EBS 这样的虚拟化存储.
Elasticsearch 使用虚拟化存储工作是没有问题的, 它因为快速和安装简单而受欢迎, 但同样不幸的是, 在基础上与专用的本地存储相比它天生就比较慢. 如果在 EBS 上创建了一个索引库, 请确认使用预分配的 IOPS, 否则很快就会被限流.
如果搜索受限于 CPU, 那么应当考虑购买更快的 CPU.
来源: https://yq.aliyun.com/articles/705136