使用真实 Elasticsearch 进行更快的集成测试

news2024/11/18 11:15:22

作者:来自 Elastic Piotr Przybyl

了解如何使用各种数据初始化和性能改进技术加快 Elasticsearch 的自动化集成测试速度。

在本系列的第 1 部分中,我们探讨了如何编写集成测试,让我们能够在真实的 Elasticsearch 环境中测试软件,并非难事。本文将演示各种数据初始化和性能改进的技术。

不同的目的,不同的特点

一旦测试基础设施设置完毕,并且项目已经使用集成测试框架进行至少一个测试(例如我们在演示项目中使用 Testcontainers),添加更多测试就变得很容易,因为它不需要模拟。例如,如果你需要验证 1776 年获取的书籍数量是否正确,你只需添加一个测试,如下所示:

@Test
void shouldFetchTheNumberOfBooksPublishedInGivenYear() {
    var systemUnderTest = new BookSearcher(client);
    int books = systemUnderTest.numberOfBooksPublishedInYear(1776);
    Assertions.assertEquals(2, books, "there were 2 books published in 1776 in the dataset");
}

只要用于初始化 Elasticsearch 的数据集已包含相关数据,这就足够了。创建此类测试的成本很低,维护它们几乎毫不费力(因为它主要涉及更新 Docker 镜像版本)。

没有软件是独立存在的

如今,我们编写的每一个软件都与其他系统相连。虽然使用模拟的测试非常适合验证我们正在构建的系统的行为,但集成测试让我们确信整个解决方案能够按预期运行并将继续如此。这可能会让我们忍不住添加越来越多的集成测试。

集成测试有其成本

然而,集成测试并非免费。由于其性质 —— 超越了仅在内存中的设置 —— 它们往往更慢,从而浪费我们的执行时间。

平衡收益(集成测试带来的信心)与成本(测试执行时间和计费,通常直接转化为云供应商的发票)至关重要。我们可以让它们运行得更快,而不是因为测试速度慢而限制测试数量。这样,我们可以在添加更多测试的同时保持相同的执行时间。本文的其余部分将重点介绍如何实现这一点。

让我们重新回顾一下我们迄今为止使用的示例,因为它非常慢并且需要优化。对于本次和后续实验,我假设 Elasticsearch Docker 映像已被提取,因此不会影响时间。另外,请注意,这不是一个合适的基准,而是一个一般准则。

利用 Elasticsearch 的测试也可以从性能提升中受益

Elasticsearch 经常被选为搜索解决方案是因为其高性能表现。开发人员通常会非常谨慎地优化生产代码,尤其是在关键路径上。然而,测试通常被视为次要,导致测试运行缓慢,以至于很少有人愿意运行测试。但情况并不一定要如此。通过一些简单的技术调整和方法上的改变,集成测试可以运行得更快。

让我们从当前的集成测试套件开始。该测试套件按预期运行,但仅运行三个测试时,通过执行 time ./mvnw test '-Dtest=*IntTest*' 需要耗时五分半钟 —— 每个测试大约 90 秒。请注意,你的结果可能会因硬件、网络速度等因素而有所不同。

如果可以,请批量处理

在集成测试套件中,许多性能问题源于数据初始化效率低下。虽然某些流程在生产流程中可能是自然的或可接受的(例如,数据在用户输入时到达),但这些流程可能不是测试的最佳选择,因为我们需要快速批量导入数据。

我们示例中的数据集约为 50 MiB,包含近 81,000 条有效记录。如果我们单独处理和索引每条记录,我们最终会发出 81,000 个请求,只是为了为每个测试准备数据。

而不是像在主分支中那样使用简单的循环逐个索引文档:

boolean hasNext = false;
do {
    try {
        Book book = it.nextValue();
        client.index(i -> i.index("books").document(book));
        hasNext = it.hasNextValue();
    } catch (JsonParseException | InvalidFormatException e) {
        // ignore malformed data
    }
} while (hasNext);

我们应该使用批处理方法,例如使用 BulkIngester。这允许并发索引请求,每个请求发送多个文档,从而大大减少请求数量:

try (BulkIngester<?> ingester = BulkIngester.of(bi -> bi
    .client(client)
    .maxConcurrentRequests(20)
    .maxOperations(5000))) {

    boolean hasNext = true;
    while (hasNext) {
        try {
            Book book = it.nextValue();
            ingester.add(BulkOperation.of(b -> b
                .index(i -> i
                    .index("books")
                    .document(book))));
            hasNext = it.hasNextValue();
        } catch (JsonParseException | InvalidFormatException e) {
            // ignore malformed data
        }
    }
}

这一简单的改变将整体测试时间缩短至 3 分 40 秒左右,即每次测试大约 73 秒。虽然这是一个不错的改进,但我们可以进一步改进。

保持本地化

我们在上一步中通过限制网络往返缩短了测试时长。在不改变测试本身的情况下,我们是否可以消除更多的网络调用?

让我们回顾一下当前的情况:

  • 在每次测试之前,我们都会反复从远程位置获取测试数据。
  • 在获取数据时,我们会将其批量发送到 Elasticsearch 容器。

我们可以通过将数据尽可能靠近 Elasticsearch 容器来提高性能。还有什么比容器本身更近呢?

将数据批量导入 Elasticsearch 的一种方法是 _bulk REST API,我们可以使用 curl 调用它。此方法允许我们发送以换行符分隔的 JSON 格式编写的大型有效负载(例如,来自文件)。格式如下所示:

action_and_meta_data\n
optional_source\n
action_and_meta_data\n
optional_source\n
....
action_and_meta_data\n
optional_source\n

确保最后一行为空。

在我们的例子中,文件可能如下所示:

{"index":{"_index":"books"}}
{"title":"...","description":"...","year":...,"publisher":"...","ratings":...}
{"index":{"_index":"books"}}
{"title":"Whispers of the Wicked Saints","description":"Julia ...","author":"Veronica Haddon","year":2005,"publisher":"iUniverse","ratings":3.72}

理想情况下,我们可以将这些测试数据存储在一个文件中,并将其包含在存储库中,例如 src/test/resources/。如果这不可行,我们可以使用简单的脚本或程序从原始数据生成文件。例如,请查看演示存储库中的 CSV2JSONConverter.java。

一旦我们在本地有了这样的文件(这样我们就消除了获取数据的网络调用),我们就可以解决另一点,即:将文件从运行测试的机器复制到运行 Elasticsearch 的容器中。这很容易,我们可以在定义容器时使用单个方法调用 withCopyToContainer 来做到这一点。所以更改后它看起来像这样:

@Container
ElasticsearchContainer elasticsearch =
    new ElasticsearchContainer(ELASTICSEARCH_IMAGE)
        .withCopyToContainer(MountableFile.forHostPath("src/test/resources/books.ndjson"), "/tmp/books.ndjson");

最后一步是从容器内部发出请求,将数据发送到 Elasticsearch。我们可以通过在容器内运行 curl 来使用 curl 和 _bulk 端点执行此操作。虽然这可以在 CLI 中使用 docker exec 完成,但在我们的 @BeforeEach 中,它变成了 elasticsearch.execInContainer,如下所示:

ExecResult result = elasticsearch.execInContainer(
    "curl", "https://localhost:9200/_bulk?refresh=true", "-u", "elastic:changeme",
    "--cacert", "/usr/share/elasticsearch/config/certs/http_ca.crt",
    "-X", "POST",
    "-H", "Content-Type: application/x-ndjson",
    "--data-binary", "@/tmp/books.ndjson"
);
assert result.getExitCode() == 0;

从顶部开始,我们以这种方式向 _bulk 端点发出 POST 请求(并等待刷新完成),使用默认密码以用户 elastic 进行身份验证,接受自动生成的自签名证书(这意味着我们不必禁用 SSL/TLL),有效负载是 /tmp/books.ndjson 文件的内容,该文件在启动时复制到容器中。这样,我们减少了频繁网络调用的需要。假设 books.ndjson 文件已经存在于运行测试的机器上,则总持续时间减少到 58 秒。

少(通常)即是多

在上一步中,我们减少了测试中与网络相关的延迟。现在,让我们解决 CPU 使用率问题。

依赖 @Testcontainers 和 @Container 注释并没有错。但关键是要了解它们的工作原理:当你使用 @Container 注释实例字段时,Testcontainers 将为每个测试启动一个新容器。由于容器启动不是免费的(它需要时间和资源),所以我们要为每个测试支付这笔费用。

在某些情况下(例如,测试系统启动行为时),为每个测试启动一个新容器是必要的,但在我们的例子中不是。我们不必为每个测试启动一个新容器,而是为所有测试保留相同的容器和 Elasticsearch 实例,只要我们在每次测试之前正确重置容器的状态即可。

首先,将容器设为静态字段。接下来,在创建 books 索引(通过定义映射)并用文档填充它之前,如果现有索引来自之前的测试,请删除它。

因此,setupDataInContainer() 应该以类似以下内容开头:

ExecResult result = elasticsearch.execInContainer(
   "curl", "https://localhost:9200/books", "-u", "elastic:changeme",
   "--cacert", "/usr/share/elasticsearch/config/certs/http_ca.crt",
   "-X", "DELETE"
);
// we don't check the result, because the index might not have existed

// now we create the index and give it a precise mapping, just like for production
result = elasticsearch.execInContainer(
   "curl", "https://localhost:9200/books", "-u", "elastic:changeme",
   "--cacert", "/usr/share/elasticsearch/config/certs/http_ca.crt",
   "-X", "PUT",
   "-H", "Content-Type: application/json",
   "-d", """
       {
         "mappings": {
           "properties": {
             "title": { "type": "text" },
             "description": { "type": "text" },
             "author": { "type": "text" },
             "year": { "type": "short" },
             "publisher": { "type": "text" },
             "ratings": { "type": "half_float" }
           }
         }
       }
       """
);
assert result.getExitCode() == 0;

如你所见,我们可以使用 curl 在容器内执行几乎任何命令。这种方法有两个显著的​​优势:

  • 速度:如果有效负载(如 books.ndjson)已经在容器内,我们就不需要重复复制相同的数据,从而大大缩短了执行时间。
  • 语言独立性:由于 curl 命令与测试的编程语言无关,因此它们更容易理解和维护,即使对于那些可能更熟悉其他技术堆栈的人来说也是如此。

虽然使用原始 curl 调用对于生产代码来说并不理想,但它是测试设置的有效解决方案。尤其是与单个容器启动结合使用时,这种方法将我的测试执行时间缩短到大约 30 秒。

还值得注意的是,在演示项目(分支 data-init)中,目前只有三个集成测试,大约一半的总持续时间花在容器启动上。初始预热后,每个测试大约需要 3 秒钟。因此,再添加三个测试不会使总时间增加一倍至 30 秒,而只会增加大约 9-10 秒。可以在 IDE 中观察到测试执行时间(包括数据初始化):

总结

在这篇文章中,我展示了使用 Elasticsearch 进行集成测试的几项改进:

  • 集成测试可以在不改变测试本身的情况下运行得更快 —— 只需重新考虑数据初始化和容器生命周期管理。
  • Elasticsearch 应该只启动一次,而不是每次测试都启动一次。
  • 当数据尽可能接近 Elasticsearch 并高效传输时,数据初始化效率最高。
  • 虽然减少测试数据集大小是一种明显的优化(这里没有介绍),但有时并不切实际。因此,我们专注于展示技术方法。

总体而言,我们显著缩短了测试套件的持续时间 —— 从 5.5 分钟缩短到 30 秒左右 —— 降低了成本并加快了反馈循环。

在下一篇文章中,我们将探索更先进的技术,以进一步减少 Elasticsearch 集成测试的执行时间。

如果你的案例使用了上述技术之一,或者你在我们的讨论论坛和社区 Slack 频道上有任何疑问,请告诉我们。

准备好自己尝试一下了吗?开始免费试用。

想要获得 Elastic 认证吗?了解下一期 Elasticsearch 工程师培训何时举行!

原文:Faster integration tests with real Elasticsearch - Search Labs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2242781.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据分布之指数分布(sample database classicmodels _No.10)

数据分布之指数分布&#xff08;sample database classicmodels _No.10&#xff09; 准备工作&#xff0c;可以去下载 classicmodels 数据库具体如下 点击&#xff1a;classicmodels 也可以去 下面我的博客资源下载 https://download.csdn.net/download/tomxjc/88685970 文章…

RPC-健康检测机制

什么是健康检测&#xff1f; 在真实环境中服务提供方是以一个集群的方式提供服务&#xff0c;这对于服务调用方来说&#xff0c;就是一个接口会有多个服务提供方同时提供服务&#xff0c;调用方在每次发起请求的时候都可以拿到一个可用的连接。 健康检测&#xff0c;能帮助从连…

Flink_DataStreamAPI_执行环境

DataStreamAPI_执行环境 1创建执行环境1.1getExecutionEnvironment1.2createLocalEnvironment1.3createRemoteEnvironment 2执行模式&#xff08;Execution Mode&#xff09;3触发程序执行 Flink程序可以在各种上下文环境中运行&#xff1a;我们可以在本地JVM中执行程序&#x…

Cyberchef配合Wireshark提取并解析HTTP/TLS流量数据包中的文件

本文将介绍一种手动的轻量级的方式&#xff0c;还原HTTP/TLS协议中传输的文件&#xff0c;为流量数据包中的文件分析提供帮助。 如果捕获的数据包中存在非文本类文件&#xff0c;例如png,jpg等图片文件&#xff0c;或者word&#xff0c;Excel等office文件异或是其他类型的二进…

Golang云原生项目:—实现ping操作

熟悉报文结构 ICMP校验和算法&#xff1a; 报文内容&#xff0c;相邻两个字节拼接到一起组成一个16bit数&#xff0c;将这些数累加求和若长度为奇数&#xff0c;则将剩余一个字节&#xff0c;也累加求和得出总和之后&#xff0c;将和值的高16位与低16位不断求和&#xff0c;直…

基于STM32 HAL库的FFT计算与数学运算:幅值、频率、均方根、平均值、最大值、最小值、峰峰值与标准差

一、用STM32进行FFT计算与数学运算的过程 1. 信号采集 首先&#xff0c;我们需要使用STM32的ADC模块来采集模拟信号&#xff0c;比如三相交流电。ADC将模拟信号&#xff08;如电压或电流&#xff09;转换为数字信号&#xff0c;供后续处理。 采样数量&#xff1a;FFT的计算通…

关于Github报错Verify your two-factor authentication (2FA) settings的解决方案

如果我们在使用GitHub出现2FA验证问题&#xff1a;Verify your two-factor authentication (2FA) settings&#xff0c;那么可以参考下面的解决方法解决问题。 当然&#xff0c;如果有国外的手机号直接使用验证码接收就可以&#xff0c;问题是不支持中国手机啊。那么怎么办呢&…

【机器学习chp2】贝叶斯最优分类器、概率密度函数的参数估计、朴素贝叶斯分类器、高斯判别分析。万字超详细分析总结与思考

前言&#xff0c;请先看。 本文的《一》《二》属于两个单独的知识点&#xff1a;共轭先验和Laplace平滑&#xff0c;主要因为他们在本文的后续部分经常使用&#xff0c;又因为他们是本人的知识盲点&#xff0c;所以先对这两个知识进行了分析&#xff0c;后续内容按照标题中的顺…

游戏引擎学习第16天

视频参考:https://www.bilibili.com/video/BV1mEUCY8EiC/ 这些字幕讨论了编译器警告的概念以及如何在编译过程中启用和处理警告。以下是字幕的内容摘要&#xff1a; 警告的定义&#xff1a;警告是编译器用来告诉你某些地方可能存在问题&#xff0c;尽管编译器不强制要求你修复…

01.防火墙概述

防火墙概述 防火墙概述1. 防火墙的分类2. Linux 防火墙的基本认识3. netfilter 中五个勾子函数和报文流向 防火墙概述 防火墙&#xff08; FireWall &#xff09;&#xff1a;隔离功能&#xff0c;工作在网络或主机边缘&#xff0c;对进出网络或主机的数据包基于一定的 规则检…

express 从0-1如何创建一个项目 注册接口

内容参考&#xff1a; windos下安装mysql express 使用mysql 一、创建一个空项目 二、创建一个包管理工具 npm init -y三、安装需要的插件及app.js的部分实现 npm i express 安装express 框架 npm i cors 安装cors 用于跨域 npm install mysql2 安装mysql数据库 npm i b…

Shell基础(4)

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团…

(长期更新)《零基础入门 ArcGIS(ArcMap) 》实验一(下)----空间数据的编辑与处理(超超超详细!!!)

续上篇博客&#xff08;长期更新&#xff09;《零基础入门 ArcGIS(ArcMap) 》实验一&#xff08;上&#xff09;----空间数据的编辑与处理&#xff08;超超超详细&#xff01;&#xff01;&#xff01;&#xff09;-CSDN博客 继续更新 本篇博客内容为道路拓扑检查与修正&#x…

Python防检测之鼠标移动轨迹算法

一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序&#xff0c;它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言&#xff0c;原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势&#xff1a; 模拟…

3D编辑器教程:如何实现3D模型多材质定制效果?

想要实现下图这样的产品DIY定制效果&#xff0c;该如何实现&#xff1f; 可以使用51建模网线上3D编辑器的材质替换功能&#xff0c;为产品3D模型每个部位添加多套材质贴图&#xff0c;从而让3D模型在展示时实现DIY定制效果。 具体操作流程如下&#xff1a; 第1步&#xff1a;上…

Qt按钮类-->day09

按钮基类 QAbstractButton 标题与图标 // 参数text的内容显示到按钮上 void QAbstractButton::setText(const QString &text); // 得到按钮上显示的文本内容, 函数的返回就是 QString QAbstractButton::text() const;// 得到按钮设置的图标 QIcon icon() const; // 给按钮…

Cellebrite VS IOS18Rebooting

Cellebrite VS IOS18Rebooting我们想分享一些有关 iOS 18 重启“功能”的信息。在过去一周左右的时间里&#xff0c;人们对 iOS 18 中一项新的未记录功能产生了极大关注&#xff0c;该功能会导致设备在一段时间不活动后重新启动。 这意味着&#xff0c;如果设备在一定时间不活…

【Linux】:进程信号(详谈信号捕捉 OS 运行)

✨ 来去都是自由风&#xff0c;该相逢的人总会相逢 &#x1f30f; &#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞…

视觉SLAM相机——单目相机、双目相机、深度相机

一、单目相机 只使用一个摄像头进行SLAM的做法称为单目SLAM&#xff0c;这种传感器的结构特别简单&#xff0c;成本特别低&#xff0c;单目相机的数据&#xff1a;照片。照片本质上是拍摄某个场景在相机的成像平面上留下的一个投影。它以二维的形式记录了三维的世界。这个过程中…

MongoDB在现代Web开发中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 MongoDB在现代Web开发中的应用 MongoDB在现代Web开发中的应用 MongoDB在现代Web开发中的应用 引言 MongoDB 概述 定义与原理 发展…