电脑怎么做最新系统下载网站,渠道网络大厦,长沙谷歌优化,中国万网域名注册作者#xff1a;来自 Elastic Piotr Przybyl 掌握高级 Elasticsearch 集成测试#xff1a;更快、更智能、更优化。 在上一篇关于集成测试的文章中#xff0c;我们介绍了如何通过改变数据初始化策略来缩短依赖于真实 Elasticsearch 的集成测试的执行时间。在本期中#xff0…作者来自 Elastic Piotr Przybyl 掌握高级 Elasticsearch 集成测试更快、更智能、更优化。 在上一篇关于集成测试的文章中我们介绍了如何通过改变数据初始化策略来缩短依赖于真实 Elasticsearch 的集成测试的执行时间。在本期中我们将进一步缩短测试套件的持续时间这次我们将通过将高级技术应用于运行 Elasticsearch 的 Docker 容器和 Elasticsearch 本身。 请注意下面描述的技术通常可以精挑细选你可以选择最适合你特定情况的技术。 这里有“龙”权衡取舍
在我们深入探讨各种性能优化方法之前必须明白一个道理并非所有优化都应该无条件应用。尽管优化通常能带来提升但它们也可能让整体设置变得更难理解尤其是对于不熟悉的人来说。换句话说在接下来的章节中我们不会修改测试本身而是重新设计其周围的 “基础架构代码”。这些改动可能会让代码对经验不足的团队成员来说更加晦涩。文中介绍的技术并非高深莫测但在使用时仍需谨慎并且有一定经验会更好。 快照
当我们停止演示代码时我们仍在使用每个测试的数据初始化 Elasticsearch。这种方法有一些优势特别是如果我们的数据集在测试用例之间有所不同例如我们有时会索引略有不同的文档。但是如果我们所有的测试用例都可以依赖相同的数据集我们可以使用快照和恢复方法。
了解快照和恢复在 Elasticsearch 中的工作方式很有帮助官方文档对此进行了解释。
在我们的方法中我们不是通过 CLI 或 DevOps 方法处理这个问题而是将其集成到测试周围的设置代码中。这确保了在开发人员机器以及 CI/CD 中顺利执行测试。
这个想法很简单我们不是删除索引并在每次测试之前从头开始重新创建它们而是
在容器的本地文件系统中创建快照如果它尚不存在因为这在以后会变得必要。在每次测试之前恢复快照。 准备快照位置
需要注意的一件重要事情 —— 这使得 Elasticsearch 与许多关系数据库不同 —— 在发送创建快照的请求之前我们首先需要注册一个可以存储快照的位置即所谓的存储库。有许多可用的存储选项这对于云部署非常方便在我们的例子中将它们保存在容器内的本地目录中就足够了。
注意 此处使用的 /tmp/... 位置仅适用于易变集成测试切勿在生产环境中使用。在生产环境中请始终将快照存储在安全可靠的位置以进行备份。 为了避免将备份存储在不安全的位置我们首先将其添加到我们的测试中
static final String REPO_LOCATION /tmp/bad_backup_location;
接下来我们配置 ElasticsearchContainer 以确保它可以使用此位置作为备份位置
static ElasticsearchContainer elasticsearch new ElasticsearchContainer(ELASTICSEARCH_IMAGE)// other instructions.withEnv(path.repo, REPO_LOCATION); 更改设置
现在我们准备将以下逻辑附加到我们的 BeforeAll 方法中
if (!snapshotExists()) {setupDataInContainer();createSnapshotInContainer();
}
我们的 BeforeEach 方法应该以以下内容开头
restoreSnapshot();
可以通过验证 REPO_LOCATION 目录是否存在并包含一些文件来检查快照是否存在
static boolean snapshotExists() throws IOException, InterruptedException {ExecResult execResult elasticsearch.execInContainer(sh, -c, [ -d \ REPO_LOCATION \ ] [ \$(ls -A REPO_LOCATION )\ ]);return execResult.getExitCode() 0;
}
setupDataInContainer() 方法略有变化它不再在 BeforeEach 中调用我们在需要时按需执行它并且可以删除 DELETE books 请求因为不再需要它。
要创建快照我们首先需要注册一个快照位置然后在那里存储任意数量的快照尽管我们只保留一个因为测试不需要更多
static void createSnapshotInContainer() throws IOException, InterruptedException {// Register REPO_LOCATION for backupsExecResult result elasticsearch.execInContainer(curl, https://localhost:9200/_snapshot/init_backup, -u, elastic:changeme,--cacert, /usr/share/elasticsearch/config/certs/http_ca.crt,-H, Content-Type: application/json,-X, PUT,-d, {type:fs,settings:{location:%s}}.formatted(REPO_LOCATION));assert result.getExitCode() 0 result.getStdout().contains(\acknowledged\:true);// Create the snapshot and wait for completionresult elasticsearch.execInContainer(curl, https://localhost:9200/_snapshot/init_backup/snapshot_1?wait_for_completiontrue, -u, elastic:changeme,--cacert, /usr/share/elasticsearch/config/certs/http_ca.crt,-H, Content-Type: application/json,-X, PUT,-d, {indices: [books]});assert result.getExitCode() 0;
}
一旦创建快照我们可以在每次测试之前恢复它如下所示
static void restoreSnapshot() {Container.ExecResult execResult;try {execResult elasticsearch.execInContainer(/bin/sh, -c, curl --cacert /usr/share/elasticsearch/config/certs/http_ca.crt -s -u elastic:changeme -X DELETE https://localhost:9200/books \ \curl --cacert /usr/share/elasticsearch/config/certs/http_ca.crt -s -u elastic:changeme -X POST https://localhost:9200/_snapshot/init_backup/snapshot_1/_restore?wait_for_completiontrue);} catch (Exception e) {throw new RuntimeException(e);}if (execResult.getExitCode() ! 0) {throw new RuntimeException(Error when restoring backup: [%s] [%s].formatted(execResult.getStdout(), execResult.getStderr()));}
}
请注意以下几点
在恢复索引之前索引不能存在因此我们必须先将其删除。如果你需要删除多个索引则可以在单个 curl 调用中执行此操作例如 “https://localhost:9200/indexA,indexB”。要在容器中链接多个命令你无需将它们包装在单独的 execInContainer 调用中运行简单的脚本可以提高可读性并减少一些网络往返。
在示例项目中此技术将我的构建时间缩短至 26 秒。虽然乍一看这似乎不是一个显着的收益但该方法是一种通用技术可以在切换到 _bulk 摄取在上一篇文章中讨论之前甚至代替它应用。换句话说你可以以任何方式在 BeforeAll 中为测试准备数据然后对其进行快照以在 BeforeEach 中使用。如果你想最大限度地提高效率甚至可以使用 elasticsearch.copyFileFromContainer(...) 将快照复制回测试机器使其作为一种缓存形式仅在你需要更新数据集例如测试新功能时才会清除。有关完整示例请查看标签 snapshots。 将数据保存在 RAM 中
有时我们的测试用例明显包含大量数据这可能会对性能产生负面影响尤其是在底层存储速度较慢的情况下。如果你的测试需要读取和写入大量数据而 SSD 甚至硬盘速度非常慢你可以指示容器将数据保存在 RAM 中 - 前提是你有足够的可用内存。
这本质上是一行代码需要在容器定义中添加 .withTmpFs(Map.of(/usr/share/elasticsearch/data, rw))。容器设置将如下所示
static ElasticsearchContainer elasticsearch new ElasticsearchContainer(ELASTICSEARCH_IMAGE)// other instructions.withTmpFs(Map.of(/usr/share/elasticsearch/data, rw));
存储速度越慢性能提升就越显著因为 Elasticsearch 现在将写入和读取 RAM 中的临时文件系统。
注意 顾名思义这是一个临时文件系统也就是说它不是持久性的。因此此解决方案仅适用于测试。请勿在生产中使用它因为它可能会导致数据丢失。 要评估此解决方案可以在多大程度上提高硬件性能 你可以尝试标签 tmpfs。 更多工作相同时间
产品代码库的大小在活跃开发阶段增长最快。然后当它进入维护阶段如果适用时通常只涉及错误修复。但是测试库的大小会不断增长因为功能和错误都需要通过测试来覆盖以防止回归。理想情况下错误修复应始终伴随着测试以防止错误再次出现。这意味着即使开发不是特别活跃测试的数量也会不断增加。本节中描述的方法提供了有关如何在不显着增加测试套件持续时间的情况下管理不断增长的测试库的提示前提是有足够的资源来实现并行化。
为简单起见我们假设示例中的测试用例数量增加了一倍而不是编写额外的测试我们将复制现有的测试用例来进行此演示。
在最简单的方法中我们可以向 BookSearcherIntTest 类添加三个 Test 方法。然后我们可以以一种有点非正统的方式使用 Java 的一个分析器Java Flight Recorder 来观察 CPU 和内存消耗。由于我们已将其添加到 POM 中因此在运行测试后我们可以在主目录中打开 recording-1.jfr。结果在 Environment - Processes 中可能如下所示 如你所见在一个类中运行六个测试使所需时间增加了一倍。此外上面的 CPU 使用率图表中的主要颜色是……根本没有颜色因为 CPU 利用率在高峰时刻几乎达不到 20%。当你为使用时间付费时无论是向云提供商付费还是以你自己的挂钟时间来获得有意义的反馈未充分利用 CPU 是一种浪费。
你使用的 CPU 很可能有多个核心。这里的优化是将工作负载分成两部分这应该大约将持续时间减半。为了实现这一点我们将新添加的测试移到另一个名为 BookSearcherAnotherIntTest 的类中并指示 Maven 使用 -DforkCount2 运行两个分支进行测试。完整命令变为
./mvnw test -Dtest*IntTest* -DforkCount2 在这里CPU 的利用率更高。
不应将重点放在确切的数字上来解释此示例。相反重要的是不仅适用于 Java 的总体趋势
检查你的 CPU 在测试期间是否得到正确利用。如果没有请尝试尽可能多地并行化你的测试尽管其他资源有时可能会限制你。请记住不同的环境可能需要不同的并行化因素例如Maven 中的 -DforkCountN。最好避免在构建脚本中硬编码这些因素而是根据项目和环境对其进行调整如果只运行一个测试类则可以跳过开发人员机器developer machines的这一步骤。对于功能较弱的 CI 环境较低的数字可能就足够了。对于功能更强大的 CI 设置较高的数字可能效果很好。
对于 Java重要的是避免使用一个大类而是尽可能将测试分成较小的类。不同的并行化技术和参数适用于其他技术堆栈但总体目标仍然是充分利用你的硬件资源。
为了进一步完善请避免在测试类中重复设置代码。将测试本身与基础架构/设置代码分开。例如应将配置元素如镜像版本声明放在一个地方。在 Java 版 Testcontainers 中我们可以使用或稍微重新利用继承来确保在测试之前加载并执行包含基础架构代码的类。结构如下所示
class CommonTestBase {// Containers declarations, startup, and data handling code here
}class BookSearcherIntTest extends CommonTestBase {// Integration tests, ideally for one service
}class AnotherServiceIntTest extends CommonTestBase {// More integration tests, ideally for another service
}
如需完整的演示请再次参考 GitHub 上的示例项目。 重用 - 仅启动一次
本文中介绍的最后一种技术对于开发人员机器eveloper machines特别有用。它可能不适用于传统的 CI例如内部托管的 Jenkins并且对于临时 CI 环境如基于云的 CI其中构建机器是一次性使用的每次构建后都会退役通常是不必要的。此技术依赖于 Testcontainers 的预览功能称为重用。
通常测试套件完成后会自动清理容器。这种默认行为非常方便尤其是在长期运行的 CI 中因为它可以确保无论测试结果如何都不会剩余容器。但是在某些情况下我们可以让容器在测试之间保持运行以便后续测试不会浪费时间再次启动它。这种方法对于在较长时间内有时是几天开发功能或修复错误其中重复运行相同的测试类的开发人员特别有益。 如何启用重用
启用重用是一个两步过程 1. 在声明容器时将其标记为可重用
static ElasticsearchContainer elasticsearch new ElasticsearchContainer(ELASTICSEARCH_IMAGE)// other instructions.withReuse(true); 2. 选择在合适的环境中启用重用功能例如在你的开发机器上。在开发人员工作站上执行此操作的最简单、最持久的方法是确保 $HOME 目录中的配置文件具有正确的内容。在 ~/.testcontainers.properties 中包含以下行
testcontainers.reuse.enabletrue
就这样首次使用时测试不会更快因为容器仍需要启动。但是在初始测试之后
运行 docker ps 将显示容器仍在运行这现在是一项功能而不是错误。后续测试将更快。
注意 一旦启用重用手动停止容器就成为你的责任。 利用快照或初始化数据实现重用
重用功能与仅将初始化数据文件复制到容器一次或使用快照等技术结合使用效果特别好。启用重用后无需为后续测试重新创建快照从而节省更多时间。所有优化部分都开始落实到位。 重复使用分叉容器
虽然重复使用在许多情况下效果很好但在第二次运行期间将重复使用与多个分叉相结合时会出现问题。这可能会导致与容器或 Elasticsearch 处于不正确状态相关的错误或乱码输出。如果你希望同时使用这两项改进例如在提交 PR 之前在功能强大的工作站上运行许多集成测试则需要进行额外的调整。 问题
问题可能表现为以下错误
co.elastic.clients.elasticsearch._types.ElasticsearchException: [es/esql.query] failed: [illegal_index_shard_state_exception] CurrentState[RECOVERING] operations only allowed when shard state is one of [POST_RECOVERY, STARTED]
发生这种情况的原因在于 Testcontainers 如何识别可重复使用的容器。
当两个分支都启动且没有 Elasticsearch 容器运行时每个分支都会初始化自己的容器。 但是重新启动后每个分支都会查找可重复使用的容器并找到一个。 由于所有容器看起来与 Testcontainers 相同因此两个分支可能会选择同一个容器。 这会导致竞争条件其中多个分支尝试使用同一个 Elasticsearch 实例。 例如一个分支可能正在恢复快照而另一个分支正在尝试执行相同操作从而导致上述错误。 解决方案
为了解决这个问题我们需要引入容器之间的差异并确保分支根据这些差异确定性地选择容器。 步骤 1更新 pom.xml
修改 pom.xml 中的 Surefire 配置以包含以下内容
environmentVariablesforkfork_${surefire.forkNumber}/fork
/environmentVariables
这会为每个 fork 添加一个唯一标识符 (fork_${surefire.forkNumber}) 作为环境变量。 第 2 步修改容器声明
调整代码中的 Elasticsearch 容器声明以包含基于 fork 标识符的标签
static ElasticsearchContainer elasticsearch new ElasticsearchContainer(ELASTICSEARCH_IMAGE)// other settings.withLabel(fork, System.getenv().getOrDefault(fork, fork_1)); 效果
这些更改确保每个分支都会创建并使用自己的容器。由于标签独特容器略有不同允许测试容器将它们确定性地分配给特定分支。
这种方法消除了竞争条件因为没有两个分支会尝试重复使用同一个容器。重要的是容器内的 Elasticsearch 功能保持不变并且可以在分支之间动态分配测试而不会影响结果。 它真的值得吗
正如本文开头所警告的那样这里介绍的改进应谨慎应用因为它们会使我们的测试设置代码变得不那么直观。有什么好处
我们以在我的计算机上大约 25 秒的三个集成测试开始本文。在应用所有改进并将实际测试数量增加一倍至六个之后我笔记本电脑上的执行时间下降到 8 秒。测试数量增加了一倍构建时间缩短了三分之二。由你来决定它是否适合你的情况。;-) 它不止于此
这个关于使用真实 Elasticsearch 进行测试的迷你系列到此结束。在第一部分中我们讨论了何时模拟 Elasticsearch 索引是有意义的以及何时进行集成测试是一个更好的主意。在第二部分中我们解决了导致集成测试变慢的最常见错误。第三部分更加努力使集成测试运行得更快只需几秒钟而不是几分钟。
还有更多方法可以优化你的体验并降低使用 Elasticsearch 进行系统集成测试相关的成本。不要犹豫探索这些可能性并尝试你的技术堆栈。
如果你的案例涉及上述任何技术或者你有任何疑问请随时通过我们的讨论论坛或社区 Slack 频道与我们联系。
想要获得 Elastic 认证了解下一次 Elasticsearch 工程师培训何时开始
Elasticsearch 包含新功能可帮助你为你的用例构建最佳搜索解决方案。深入了解我们的示例笔记本以了解更多信息开始免费云试用或立即在你的本地机器上试用 Elastic。 原文Advanced integration tests with real Elasticsearch - Elasticsearch Labs