为了避免给Elasticsearch带来过高的负载,在Kibana中需要注意的事项是什么

本文是2018年Elastic stack (Elasticsearch) Advent Calendar中第24天的文章。

在进行访问日志分析等工作时,使用Kibana的时候,有时会听到它的界面突然卡住的故事。今天我想介绍一下这样的反模式。

使用通配符查询的第一个查询项。

在访问日志中表示用户代理的字段上匹配 *GoogleBot*

使用「包含〜」条件对未被分析的字段进行过滤时,容易误用,但如果在开头有通配符,Elasticsearch需要逐个检查已知的每个term是否匹配,这样会变慢。如果数据规模较小就可以,但在处理大规模日志等情况下,基本上应避免使用。

作为对策,Query String查询选项中有一个名为 allow_leading_wildcard 的选项,将其设为 false,就可以禁用首部通配符。在Kibana的设置界面中有一个控制该选项的项目,因此可以考虑提前禁用它。

使用通配符也不会对性能造成太大压力的情况是在足够缩小范围的前缀后使用通配符。Elasticsearch(或者说Lucene)以字典顺序管理term,因此擅长列举具有特定前缀的term。

对于基数非常高的字符串类型字段的聚合操作

虽然这些是一些特殊的情况,但我还是介绍给你们。

将访问日志中的请求URI字段作为桶(bucket)进行可视化。

有时候我们会尝试对不同的终端进行请求时间等数据的汇总统计。然而,只要该字段的基数并不高,这并没有什么问题。问题出现在请求URI中包含的查询参数中,如果客户端随机生成了一次性的值,并且该字段的基数可能会与数据量成正比。

虽然如此,由于我也不太明白,所以我会提供验证代码。

version: "3.5"

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.1
    volumes:
      - esdata:/usr/share/elasticsearch/data
    environment:
      - "bootstrap.memory_lock=true"
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - "9200:9200"
  kibana:
    image: docker.elastic.co/kibana/kibana-oss:6.5.1
    environment:
      ELASTICSEARCH_URL: http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
volumes:
  esdata:
    driver: local

使用以下的Compose文件在本地启动Elasticsearch和Kibana,并使用下面的脚本插入验证数据。数据总量为1000万条,执行耗时超过十几分钟。

from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk as es_bulk
from datetime import datetime
import random
import sys

settings = {
    'index': {
        'number_of_shards': 1,
        'number_of_replicas': 0,
        'refresh_interval': -1,  # インデキシングを高速に行うため自動refreshは止める
    },
}

mappings = {
    '_doc': {
        'properties': {
            'uri': {'type': 'keyword'},  # Request-URI
            'timestamp': {'type': 'date'},
        },
        'dynamic': 'strict',
    },
}


index = 'huge_cardinality_test'
es = Elasticsearch(hosts='localhost:9200')
if es.indices.exists(index):
    es.indices.delete(index)
es.indices.create(index, body={
    'settings': settings,
    'mappings': mappings,
})

def gen_random_docs(n):
    for _ in range(n):
        yield {
            '_index': index,
            '_type': '_doc',
            'uri': f'/?_={random.randint(0, sys.maxsize)}',
            'timestamp': datetime.utcnow().isoformat(),
        }

num_of_docs = 10000000
es_bulk(es, gen_random_docs(num_of_docs))
es.indices.refresh(index)

请将以下内容的中文更能概括,并提供一个选项:
假设客户端始终以类似于/?_=XXXXXX(其中XXXXXX是随机数字)的随机查询参数进行请求,您可以将其视为预期的访问日志数据。在这种情况下,字段的基数与数据数量相同。

Screen Shot 2018-12-24 at 23.17.13.png

现在我们来尝试将数据量增加一位,变为10亿件。如果尝试进行类似的聚合操作的话……

elasticsearch_1  | java.lang.OutOfMemoryError: Java heap space

顺便提一下,在启动时,我们使用环境变量 ES_JAVA_OPTS=-Xms512m -Xmx512m 来设置Elasticsearch使用的堆大小为512MB。

现在,发生了什么呢?我推测这是在构建一个巨大的全局序号。之前提到过,term是按照字典顺序进行管理的,具体来说,term被分配了一个按照字典顺序的编号,并在内部用该编号表示。这些编号或者term与编号之间的映射被称为序数。首先,在每个分片中管理着序数,而全局序数则是每个分片的序数之间的映射关系,它是按需在内存中创建的。例如,在执行聚合操作时,会进行该字段全局序数的构建,为此需要引用所有分片的序数,并且创建的全局序数的大小与唯一term的数量成正比。结果是,需要相应于字段基数的内存和计算资源,并且当字段的基数异常高时,可能无法响应请求。此外,全局序数的构建是针对整个索引的term,而不是聚合的范围,因此,即使将聚合周期设为15分钟,似乎也无法节约资源消耗。

在处理措施方面,如果对于高基数的数据进行聚合,很难得到有意义的结果,所以基本上可以说是不进行聚合。如果需要进行聚合,有效的方法是通过一些预处理或数据创建技巧来控制基数。例如,在之前提到的访问日志示例中,如果目标是对每个终端点进行汇总,可以考虑将希望在汇总中使用的终端点标识信息包含在数据中,或者仅使用最基本的路径信息而忽略查询参数等。

最后

根据我所了解的情况,发送了一些在Kibana中导致Elasticsearch集群整体响应延迟的查询模式主要归结为上述两种情况。因此,考虑到这些,请您使用后可能会感到放心。

顺便说一下,今天我在公司的博客上也写了一篇关于Elasticsearch使用案例的文章,如果你有兴趣的话,也可以去那边看看。

通过横断搜索加速内部信息共享 – Hatena 开发者博客

bannerAds