Loading

Elasticsearch 性能调优总结

吞吐量(throughput)和延迟(latency)是评估 Elasticsearch 集群性能的指标,前者代表每秒写入(index)或查询(search)文档的数量,后者则代表单个请求的延迟。上述指标之间也有一定联系:延迟越低,吞吐量就越高。

20210608105408

JVM 内存压力

Elasticsearch 集群的吞吐量显然与节点的负载相关,尤其是大量的请求将引起节点的 JVM 内存压力升高。Elasticsearch 使用断路器(Circuit Breaker)来防止节点出现 JVM 堆内存溢出。如果 Elasticsearch 评估一项操作将触发断路器,那么便会返回一个 HTTP 错误码 429:

{
  'error': {
    'type': 'circuit_breaking_exception',
    'reason': '[parent] Data too large, data for [<http_request>] would be [123848638/118.1mb], which is larger than the limit of [123273216/117.5mb], real usage: [120182112/114.6mb], new bytes reserved: [3666526/3.4mb]',
    'bytes_wanted': 123848638,
    'bytes_limit': 123273216,
    'durability': 'TRANSIENT'
  },
  'status': 429
}

由于断路器有多个,首先应先查看其具体的触发情况:

GET _nodes/stats/breaker

// 返回
"breakers" : {
    "request" : {
        "limit_size_in_bytes" : 1278030643,
        "limit_size" : "1.1gb",
        "estimated_size_in_bytes" : 0,
        "estimated_size" : "0b",
        "overhead" : 1.0,
        "tripped" : 0
    },
    "fielddata" : {
        "limit_size_in_bytes" : 852020428,
        "limit_size" : "812.5mb",
        "estimated_size_in_bytes" : 1112,
        "estimated_size" : "1kb",
        "overhead" : 1.03,
        "tripped" : 0
    },
    ...
    "parent" : {
        "limit_size_in_bytes" : 2023548518,
        "limit_size" : "1.8gb",
        "estimated_size_in_bytes" : 1129775232,
        "estimated_size" : "1gb",
        "overhead" : 1.0,
        "tripped" : 0
    }
}

默认情况下,parent(父级)断路器在 JVM 内存达到 95%时触发。为了预防报错的产生,我们需要在其持续超过 85%时采取相应措施:

  • 如果 fielddata 断路器触发,则应减少 fielddata 的使用;
  • 清除 fielddata 缓存:POST _cache/clear?fielddata=true
  • 避免“昂贵的”搜索(expensive search);
  • 避免“映射爆炸”(mapping explosions);
  • 将批量请求(bulk request)拆分为多个小的请求;
  • 升级节点内存;
  • 减少索引分片的数量

详见官方文档:High JVM memory pressure

索引分片策略

上文提到,我们可以通过减少索引分片的数量来降低节点的 JVM 内存压力,这是因为:

  • 分片过多会导致底层的 Segment 过多,而 Segment 会消耗文件句柄、内存和 CPU 资源,并且每次搜索请求都必须轮流检查每个 Segment,进而导致开销增加;
  • 对于每个 Elasticsearch 索引,Mapping 和 State 的相关信息都保存在集群状态 (GET /_cluster/state) 中。它们存储在内存中,以便快速访问。因此,如果集群中的索引和分片数量过多,而 Mapping 又比较复杂的话,将占用大量内存。

官方博客 给出了分片策略的相关建议:

  • 建议将分片的平均大小控制在几 GB 到几十 GB 之间。对时序型数据用例而言,分片大小通常介于 20GB 至 40GB 之间;
  • 将分片数量/节点内存(GB)保持在 20 以下。例如,某个节点拥有 30GB 的堆内存,那其最多可有 600 个分片。在此限值范围内,设置的分片数量越少,性能就会越好;
  • 对于时序型索引,使用 shrink 或 rollover API 减少索引和分片的数量。

当然,分片的大小也不能无限制地扩大,因为会对集群的故障恢复造成不利影响。尽管并没有关于分片大小的固定限值,但通常将 50GB 作为分片大小的上限,而这一限值在各种用例中都已得到验证。

索引写入性能

优化索引的写入性能主要有以下途径:

  • 使用批量请求写入索引,而非单个文档写入;
  • 使用多个线程或进程向 Elasticsearch 发送数据;
  • 如果对实时搜索要求不高,可以将索引的 refresh 间隔时间 (index.refresh_interval) 从默认值 1s 提升到 30s 左右;
  • 先将索引的副本数 (index.number_of_replicas) 设置为 0,待写入全部完成后,再将其恢复到原始值;
  • 文件系统缓存(Filesystem Cache)将用于缓冲 I/O 操作,因此应确保节点的内存至少有一半分配给了文件系统缓存;
  • 若文档使用自定义的_id,那么 Elasticsearch 将在其写入分片时检查_id是否重复。这是一项代价高昂的操作,因此建议使用自动生成的_id
  • 物理存储设备使用 SSD 而非 HDD 或 NFS;
  • 减少磁盘的使用率,详见 Tune for disk usage

在硬件设备无法升级的情况下,第一个方法是我们提升索引写入性能的常用手段。然而,大量的批量请求可能引发 Elasticsearch 的 429 错误:Too many requests。因此在搭建集群前,我们需要对单节点单分片进行基准测试(benchmark),从而确认批量请求的最佳大小。首先尝试一次索引 100 个文档,然后提高到 200,400,等等。在每次基准测试中,批量请求中的文档数量加倍。当索引写入速度开始趋于平稳时,则说明批量请求达到了最佳大小。

那么如果生产环境的 Elasticsearch 向客户端返回 429 错误,我们有没有什么办法解决呢?这就要从批量请求的处理方式说起。Elasticsearch 节点使用线程池(Thread Pool)来管理内存消费,多个线程池队列使得客户端的请求能够在缓冲区保留而非丢弃。这样便可以防止客户端大量的写入请求造成集群的过载,进而提升集群的可靠性和稳定性。

当批量请求到达集群中的协调节点后,首先进入批量队列中,并交由线程池中的线程进行处理。由于其中的文档可能属于多个不同的索引和分片,因此需要根据分片对其进行拆分。随后拆分后的文档会被路由到其主分片所在的数据节点上,进入该节点的批量队列中。如果队列中没有多余空间,将会通知协调节点该子请求(sub-request)被拒绝。若索引的副本数不为 0,数据节点的线程池还要将文档发往其副本所在的节点上。待同步完成后,数据节点同样会向协调节点发送响应。一旦所有的子请求全部完成(或部分被拒绝),协调节点就会创建一个响应返回给客户端。

Elasticsearch API 可以查看节点线程池中各线程的批量队列配置:

Get _nodes/thread_pool

// 返回
...
"thread_pool": {
    "watcher": {
        "type": "fixed",
        "size": 50,
        "queue_size": 1000
    },
    "force_merge": {
        "type": "fixed",
        "size": 1,
        "queue_size": -1
    },
    "search": {
        "type": "fixed_auto_queue_size",
        "size": 25,
        "queue_size": 1000
    },
    "write": {
        "type": "fixed",
        "size": 16,
        "queue_size": 200
    }
}
...

其中,size 为线程数,queue_size 为待处理请求队列的大小。write线程负责处理每个文档的索引、删除、更新操作以及批量请求,若存在大量拒绝,则说明集群的写入性能达到了瓶颈:

GET _cat/thread_pool/write?v

// 返回
node_name           name  active queue rejected
instance-0000000015 write     16    75   912687
instance-0000000017 write      4     0   808414
instance-0000000016 write      4     0   514021

为解决这一问题,我们首先想到的是增加批量请求队列的大小。但实际上它并不会增大集群的吞吐量,只是让更多的数据在节点的内存中排队,甚至可能导致批量请求的处理时间增长。队列中的批量处理越多,被消耗的宝贵堆内存就越多。堆上压力过大将引起性能的下降,甚至导致集群的不稳定。

Cat Thread Pool 的返回结果可以看出拒绝发生在整个集群还是在单个节点,从而判断写入压力是否分布不均。根据 官方博客 中的测试结果,三节点集群的写入性能显著优于单节点和两节点集群。而两节点相比单节点提升不大,可能是因为两节点分担写入压力不够完美且副本同步操作增加了集群的负载。因此生产环境建议使用三节点部署,这样既实现了高可用,又大大提升了索引的写入性能。

我们还可以通过降低发送批量请求的频率来避免 Elasticsearch 出现 429 报错。以数据源 Logstash 为例,涉及的主要参数如下:

  • pipeline.batch.size:单个工作线程在尝试执行 filter,output 之前收集的最大事件数。数值越大,处理则通常更高效,但增加了内存开销;
  • pipeline.batch.delay:当前工作线程中接收到事件后等待新消息的最大时间(毫秒)。在此时间过后,Logstash 开始执行 filter 和 output。

Logstash 从接收事件到 filter 处理事件之间等待的最大时间是pipeline.batch.delaypipeline.batch.size的乘积。将上述两参数适当调大,可以增大每次批量请求的大小而降低发送的频率,防止 Elasticsearch 集群过载。

索引搜索性能

提升索引的写入性能有多种途径,如:

  • 对文档进行建模 (Document modeling);
  • 尽可能减少搜索的字段数量;
  • 使用 term 进行查询,速度较快;
  • 减少脚本的使用...

更多调优方案详见官方文档中的说明:Tune for search speed

参考文献

我在 Elasticsearch 集群内应该设置多少个分片?

Elasticsearch:针对日志和指标对 Elasticsearch 集群进行基准测试并确定集群规模

Why am I seeing bulk rejections in my Elasticsearch cluster?

Fix common cluster issues

Tune for indexing speed

Tune for disk usage

Tune for search speed

posted @ 2021-06-10 15:50  koktlzz  阅读(1459)  评论(0编辑  收藏  举报