Elasticsearch基准测试:性能验证的工具与技术

使用真实工作负载、Rally测试集、可重复测试以及正确的索引和搜索指标,对Elasticsearch进行基准测试。

Elasticsearch基准测试:性能验证的工具与技术

基准测试Elasticsearch回答了一个实际问题:你的集群能否处理用户实际产生的索引和搜索负载?没有可重复的测试,你可能会将热缓存、安静的网络或一次幸运的查询误认为是真正的性能提升。

有用的基准测试是那种在更改映射、分片数量、硬件、JVM设置或查询代码后可以再次运行的测试。

为什么基准测试至关重要

基准测试不仅仅是运行几个查询。它是一个系统性的过程,用于衡量Elasticsearch集群在不同工作负载下的性能。以下是其不可或缺的原因:

  • 客观衡量:提供可量化的数据来评估性能。无需猜测,你就能确切知道某个更改使性能提升了多少或降低了多少。
  • 识别瓶颈:帮助定位系统中阻碍性能的具体区域,例如慢查询、过载节点或低效的索引。
  • 验证优化:对于确认性能调优期间所做的更改(例如索引设置、分片分配、硬件升级)是否达到预期效果至关重要。
  • 容量规划:通过了解集群的当前限制以及在负载增加时的行为,为扩展集群的决策提供信息。
  • 回归测试:确保新的代码部署或配置更改不会对性能产生负面影响。

需要监控的关键指标

进行基准测试时,重点关注直接反映用户体验和系统健康状况的指标。这些通常可以分为以下几类:

索引指标

  • 索引吞吐量:每秒索引的文档数量。通常越高越好。
  • 索引延迟:文档被索引后变为可搜索所需的时间。越低越好。
  • 刷新间隔影响refresh_interval 设置的更改如何影响索引速度和搜索可见性。

搜索指标

  • 搜索吞吐量:每秒处理的搜索请求数量。
  • 搜索延迟:响应搜索查询所需的时间。这通常细分为:
    • 总延迟:端到端时间。
    • 查询延迟:执行搜索查询本身所花费的时间。
    • 获取延迟:检索实际文档所花费的时间。
  • 错误率和超时:失败的请求与快速成功的请求同样重要。

集群健康指标

  • CPU使用率:高CPU可能表示查询或索引效率低下。
  • 内存使用率:对JVM堆和操作系统文件系统缓存至关重要。
  • 磁盘I/O:此处的瓶颈会严重影响索引和搜索。
  • 网络流量:在分布式环境中很重要。
  • JVM堆使用率:监控可能导致暂停的垃圾回收活动。

流行的Elasticsearch基准测试工具

有多种工具可以帮助模拟负载并衡量Elasticsearch性能。选择合适的工具取决于你的具体需求和技术专长。

1. Rally

Rally是Elasticsearch的官方基准测试工具。它功能强大、灵活,旨在模拟真实的用户工作负载。

主要特点:

  • 工作负载定义:允许使用Rally DSL定义复杂的索引和搜索任务。
  • 数据生成:可以生成合成数据或使用现有数据集。
  • 指标收集:在测试运行期间收集详细的性能指标。
  • 集成:与Elasticsearch和OpenSearch无缝协作。

示例:运行基本Rally基准测试

Rally通常运行命名的测试集和挑战。要对现有本地集群运行标准基准测试,请从内置测试集开始:

esrally race --pipeline=benchmark-only --target-hosts=localhost:9200 --track=geonames

在选择测试集之前,先列出可用的测试集:

esrally list tracks

对于特定于应用程序的工作负载,创建一个自定义Rally测试集,以反映你的映射、文档和常见查询。除非你已根据Rally版本的测试集格式检查过临时JSON片段,否则请避免使用它们。

2. 用于摄取负载的Logstash或Beats

虽然主要是一个摄取工具,但当你想要测试为Elasticsearch提供数据的管道时,Logstash可用于基本的索引负载。

主要特点:

  • 输入插件:可以模拟来自各种来源的数据摄取。
  • 输出插件elasticsearch 输出插件用于将数据发送到Elasticsearch。
  • 过滤:允许在索引之前进行数据转换。

示例:模拟索引负载

你可以配置一个Logstash管道来生成随机数据并将其发送到Elasticsearch:

logstash_indexer.conf

input {
  generator {
    count => 1000000
    type => "event"
  }
}

filter {
  mutate {
    add_field => {
      "timestamp" => "%{+YYYY-MM-dd'T'HH:mm:ss.SSSZ}"
      "message" => "This is a test log message %{random}"
    }
    remove_field => ["random", "host"]
  }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
    index => "logstash-benchmark-%{+YYYY.MM.dd}"
    # 考虑使用批量API以获得更好的性能
    # 如果需要更新,考虑设置document_id
  }
}

使用此配置运行Logstash:

bin/logstash -f logstash_indexer.conf

监控Elasticsearch和Logstash日志以及集群指标,以评估性能。

3. 自定义脚本(Python、Java等)

对于高度特定或复杂的场景,使用Elasticsearch客户端编写自定义脚本是一个可行的选择。

主要特点:

  • 最大灵活性:根据应用程序的查询模式和索引需求精确定制负载生成。
  • 客户端库:Elasticsearch为多种流行语言(Python、Java、Go、.NET等)提供官方客户端库。

示例:用于搜索负载的Python脚本

from elasticsearch import Elasticsearch
import time
import threading

# 配置你的Elasticsearch连接
ES_HOST = "localhost:9200"
es = Elasticsearch([ES_HOST])

# 定义你的搜索查询
SEARCH_QUERY = {
    "query": {
        "match": {
            "content": "example data"
        }
    }
}

NUM_THREADS = 10
QUERIES_PER_THREAD = 100

results = []

def perform_search():
    for _ in range(QUERIES_PER_THREAD):
        start_time = time.time()
        try:
            response = es.search(index="my-index-*", body=SEARCH_QUERY, size=10)
            end_time = time.time()
            results.append({
                "latency": (end_time - start_time) * 1000, # 以毫秒为单位
                "success": True,
                "hits": response['hits']['total']['value']
            })
        except Exception as e:
            end_time = time.time()
            results.append({
                "latency": (end_time - start_time) * 1000,
                "success": False,
                "error": str(e)
            })
        time.sleep(0.1) # 查询之间的小延迟

threads = []
for i in range(NUM_THREADS):
    thread = threading.Thread(target=perform_search)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

# 分析结果
successful_searches = [r for r in results if r['success']]
failed_searches = [r for r in results if not r['success']]

if successful_searches:
    avg_latency = sum(r['latency'] for r in successful_searches) / len(successful_searches)
    total_hits = sum(r['hits'] for r in successful_searches)
    print(f"平均延迟: {avg_latency:.2f} ms")
    print(f"总命中数: {total_hits}")
    print(f"成功搜索数: {len(successful_searches)}")
else:
    print("未执行成功搜索。")

if failed_searches:
    print(f"失败搜索数: {len(failed_searches)}")
    for r in failed_searches:
        print(f"  - 错误: {r['error']} (延迟: {r['latency']:.2f} ms)")

此脚本使用Python的elasticsearch-py客户端模拟并发搜索请求并测量其延迟。

设计可重复的负载测试

为了获得有意义的结果,你的负载测试必须是可重复的,并且代表你的实际使用模式。

1. 定义真实的工作负载

  • 索引:数据摄取速率是多少?文档的大小和复杂性如何?你是执行批量索引还是单文档索引?
  • 搜索:典型的查询类型是什么(例如matchtermrange、聚合)?这些查询的复杂性如何?预期的并发度是多少?
  • 数据分布:你的数据如何在索引和分片之间分布?如果可能,使用类似生产环境的数据分布。

2. 建立基线

在进行任何更改之前,运行你选择的基准测试工具以建立基线性能。此基线是衡量优化影响的参考点。

3. 隔离变量

一次只做一个更改。如果你正在测试多个优化,请在每次单独更改后运行基准测试。这有助于你了解哪个特定更改导致了性能提升(或下降)。

4. 一致的环境

确保测试环境在基准测试运行之间尽可能一致。这包括:

  • 硬件:使用具有相同规格的相同节点。
  • 软件:使用相同的Elasticsearch版本、JVM设置和操作系统配置。
  • 网络:保持一致的网络条件。
  • 数据:使用相同的数据集或数据生成方法。

5. 足够的测试持续时间和预热

  • 预热期:在开始测量之前,让集群预热。这涉及运行一些初始负载,以便填充缓存并使JVM稳定。
  • 测试持续时间:运行测试足够长的时间以捕获有意义的平均值并考虑任何瞬态系统行为。短测试可能具有误导性。

6. 监控系统资源

始终监控Elasticsearch节点和运行基准测试工具的任何客户端节点上的系统资源(CPU、RAM、磁盘I/O、网络)。这有助于将性能指标与资源利用率相关联并识别瓶颈。

基准测试的最佳实践

  • 自动化:将基准测试集成到你的CI/CD管道中,以便及早发现回归。
  • 从简单开始:从基本的索引和搜索基准测试开始,然后再转向复杂场景。
  • 了解你的数据:数据的性质(文档大小、字段类型)会显著影响性能。
  • 考虑索引策略:测试不同的refresh_intervaltranslog设置和分片大小。
  • 优化查询:确保你的搜索查询高效。使用profile API分析慢查询。
  • 监控JVM:密切关注垃圾回收日志和堆使用情况。

基准测试你实际会运行的内容

使用与生产工作负载相同类型的数据、映射、查询和并发度对Elasticsearch进行基准测试。从基线开始,更改一个变量,运行足够长的时间以包括预热和稳定状态,并在基准测试报告旁边保留节点指标。这为你提供了可用于调优、容量规划和回归检查的证据。