提高 opensearch-java 中的 JSON 解析性能


作为一名开源爱好者,我相信协作的力量可以使开源项目更快、更高效。在这篇博文中,我将分享我的Linagora团队如何与 OpenSearch 社区合作,使用基准测试工具和火焰图识别并解决 OpenSearch Java 客户端中的性能问题,为 Apache James项目做出贡献。


Apache James也称为 Java Apache Mail Enterprise Server,是一个用 Java 编写的开源电子邮件服务器。它实现了常见的电子邮件协议,例如 SMTP、IMAP 或JMAP。Apache James 很容易通过广泛的扩展机制进行定制。它甚至可以轻松用作工具箱来组装您自己的电子邮件服务器!Apache James 提出了一种创新的电子邮件服务器架构:James 是无状态的,依靠 NoSQL 数据库和消息代理进行状态管理。因此,管理 James 就像管理任何现代 Web 应用程序一样简单:不需要分片或协议感知的负载平衡。Apache James 在分布式邮件服务器设置中使用 OpenSearch 来实现与搜索相关的功能。

在操作电子邮件服务器时,性能是一个问题,因为电子邮件是一种广泛使用的应用程序,并且通常被认为是至关重要的。通常,即使是中型部署,每秒处理超过 1,000 个请求也并不罕见。我们使用一系列工具来对 Apache James 的性能进行基准测试。我们的文档解释了我们建议管理员运行的一些基准测试,以识别集群中的性能瓶颈和故障。

Apache James 依靠 OpenSearch 作为其电子邮件数据库的搜索引擎。它依赖opensearch-java client。我们经常使用自定义加特林基准测试来运行性能测试。出于许可原因(作为 Apache 项目,我们必须使用与 Apache License 2.0 兼容的许可证),当我们从 Elasticsearch 7.10 迁移到 OpenSearch 时,我们特别希望对性能进行回归测试。我们意识到 OpenSearch 请求速度较慢,并开始调查原因。

识别性能问题
火焰图是功能强大的可视化工具,可以帮助开发人员分析代码的执行流程并识别瓶颈。通过分析火焰图,我们能够识别导致性能问题的特定代码区域。我们使用 async-profiler 生成火焰图,发现频繁的 SPI 查找导致了性能问题。

这是我们用来诊断这个问题的火焰图。它是使用async-profiler从 Apache James 容器中获取的:

./profiler -d 120 -e itimer -f opensearch_2.4.0.html 1

以下是我们通过火焰图对客户端事件循环的一些观察:

  • 我们花费了大量的资源来执行 SPI 调用,以便找到 JSON 解析器的实现。它不仅消耗CPU(大约占客户端CPU消耗的26%)/堆分配,而且还阻塞HTTP事件循环,这可能是灾难性的:客户端依赖于几个线程并行提交请求,从而严重阻塞线程影响总体延迟和吞吐量。
  • 毫不奇怪,JSON 解析占用了客户端 11% 的 CPU 和 24% 的堆分配。
  • 事件循环繁忙度占用了大约 60% 的事件循环 CPU 和 25% 的堆分配,这看起来很正常。
  • 事件循环 CPU 的 2.3% 是我们与 Reactor 反应式库的绑定,这也是正常的:转换 future 和排队任务需要时间。

为了减少对 SPI 的调用,我们提交了对JsonValueParser.java的[url=https://github.com/opensearch-project/opensearch-java/pull/293]更改[/url]来解决该问题。通过摆脱 SPI 查找,甚至不考虑阻塞操作,JSON 解析速度提高了 50 倍……巨大的胜利!

您可以在此处下载用于诊断此问题的交互式火焰图。


作为一种常见的做法,我们随后运行微基准测试来验证更改,JMH对此会派上用场。它总结了关键指标,执行预热,重复测量,并具有纳秒分辨率!

以下是支持此更改的 JMH 输出:

解决前
Benchmark                Mode  Cnt   Score   Error  Units
JMHFieldBench.jsonBench  avgt    5  71.790 ± 2.125  us/op

解决后
Benchmark                Mode  Cnt  Score   Error  Units
JMHFieldBench.jsonBench  avgt    5  1.418 ± 0.024  us/op


基准代码

package org.apache.james.backends.opensearch;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import org.apache.james.backends.opensearch.json.jackson.JacksonJsonpParser;
import org.junit.Test;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JMHFieldBench {
    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Test
    public void launchBenchmark() throws Exception {
        Options opt = new OptionsBuilder()
            .include(this.getClass().getName() + ".*")
            .mode (Mode.AverageTime)
            .timeUnit(TimeUnit.MICROSECONDS)
            .warmupTime(TimeValue.seconds(5))
            .warmupIterations(3)
            .measurementTime(TimeValue.seconds(2))
            .measurementIterations(5)
            .threads(1)
            .forks(1)
            .shouldFailOnError(true)
            .shouldDoGC(true)
            .build();
        new Runner(opt).run();
    }

    @Benchmark
    public void jsonBench(Blackhole bh) throws IOException {
        final JsonParser parser = OBJECT_MAPPER.createParser(
"[\"a\",\"b\"]");
        try {
            bh.consume(new JacksonJsonpParser(parser).getArrayStream());
        } catch (Exception e) {
           
// do nothing
        } finally {
            parser.close();
        }
    }
}

与 OpenSearch 社区合作
我相信开源项目不仅仅是代码,它们是拥有共同目标的人们的社区,即让技术更容易获得和更高效。通过与 OpenSearch 社区合作,我们摆脱了 SPI 查找并提高了可能在无数服务器上运行的应用程序中的 JSON 解析性能,从而为这一目标做出了贡献。我们共同努力,使 OpenSearch 更快、更高效。对技术及其使用者社区产生积极影响是值得的。本案例研究展示了协作和社区参与如何使开源技术更好地造福于每个人。