ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Quarkus 与 Elasticsearch 集成实战:云原生搜索服务快速入门

Quarkus 与 Elasticsearch 集成实战:云原生搜索服务快速入门 1. 为什么我会盯上 Quarkus 和 Elasticsearch 的组合第一次看到elasticsearch-quickstart这个示例项目名的时候我的直觉是又是一个跑通就完事的官方 Demo。但真正把代码拉下来跑了一遍之后我发现它其实解决了一个很实际的问题——在云原生场景下如何让一个 Java 服务以极低的内存占用和极快的启动速度去对接 Elasticsearch 做全文检索。传统 Spring Boot 应用打包出来动辄七八十兆启动要好几秒JVM 预热之后内存占用轻松上 G。如果只是做一个搜索接口这个成本实在太高。Quarkus 的定位就是把这个成本压下来编译期做尽可能多的事情运行期只留必要的逻辑。配合 GraalVM 做原生镜像启动时间可以压到几十毫秒级别内存占用也能降到几十兆。这个 quickstart 适合谁看我认为有三类人值得花时间研究一是正在做微服务拆分、手里有一堆小服务需要瘦身的后端开发者二是想给搜索功能找一个轻量级宿主框架的工程师三是单纯想了解 Quarkus 这套编译期增强思路到底怎么落地的人。它不复杂但麻雀虽小五脏俱全REST 层、数据访问层、配置管理、原生镜像适配该有的都有。我打算按我自己实际跑这个项目的顺序来写先讲整体设计思路再拆核心细节然后是完整的实操过程最后把我踩过的坑和排查方法整理出来。你跟着走一遍基本就能把这个 quickstart 改造成自己的东西。2. 整体设计与思路拆解2.1 为什么是 Quarkus 而不是 Spring Boot先说选型逻辑。Elasticsearch 的 Java 客户端本身不重重的是宿主框架。Spring Boot 的自动配置机制在启动时要扫描大量类路径、解析条件注解、构建 Bean 定义这些工作在开发阶段很方便但在生产环境、尤其是容器化部署时就是纯粹的浪费——配置又不会在运行时变。Quarkus 的核心思路是把能提前做的事全部挪到构建期。依赖注入的绑定关系、REST 路由的映射、配置项的解析这些在编译阶段就确定下来运行期直接查表。这带来的直接好处是启动快、内存低副作用是构建慢一点、对反射和动态代理的支持需要显式声明。对于elasticsearch-quickstart这种场景我认为 Quarkus 是更合适的选择。搜索服务通常是 IO 密集型业务逻辑不复杂启动速度和资源占用比开发便利性更重要。而且这个 quickstart 用的是 REST 客户端而不是完整的 Spring Data 抽象本身就没有太多需要自动配置的东西。2.2 这个 quickstart 的分层结构项目本身的分层很清晰我把它拆成四层来看资源层Resource暴露 REST 接口处理 HTTP 请求和响应对应 JAX-RS 的注解体系。服务层Service封装业务逻辑比如索引的创建、文档的增删改查。客户端层ClientElasticsearch 的 REST 客户端封装负责和 ES 集群通信。配置层Config连接地址、超时、索引名等外部化配置。这个分层不是强制的但我觉得它合理。资源层只做参数校验和响应组装服务层管业务客户端层管通信职责边界清楚。很多人在写 quickstart 的时候喜欢把逻辑全塞进 Resource跑起来是没问题但一旦要加缓存、加重试、加事务就会乱成一团。2.3 依赖选型的考量quickstart 里用到的关键依赖我逐个说一下为什么这么选依赖作用选型理由quarkus-rest-client声明式 HTTP 客户端比手写 HttpClient 简洁支持响应式elasticsearch-javaES 官方 Java 客户端官方维护API 稳定支持类型安全quarkus-arcCDI 依赖注入Quarkus 内置编译期完成注入quarkus-jacksonJSON 序列化和 REST 层无缝集成这里有个细节值得说Elasticsearch 官方客户端从 8.x 开始换成了新的co.elastic.clients包和老的RestHighLevelClient是两套 API。quickstart 用的是新客户端好处是类型安全、支持 lambda 风格的查询构建坏处是网上大部分教程还是老 API遇到问题搜到的答案可能对不上。我建议直接用新 API别回头。提示新客户端依赖 Jackson 做序列化如果你的项目里同时引入了其他 JSON 库注意版本冲突尤其是jackson-databind的版本要统一。3. 核心细节解析与实操要点3.1 客户端初始化别在每次请求里 new 一个这是我最想强调的一点。Elasticsearch 客户端的创建成本不低它内部维护连接池、线程池、节点发现逻辑。如果你在每次请求里都new一个性能会惨不忍睹而且连接泄漏是迟早的事。正确的做法是把它做成单例交给 CDI 管理。在 Quarkus 里可以这样写ApplicationScoped public class ElasticsearchClientProducer { ConfigProperty(name elasticsearch.host) String host; ConfigProperty(name elasticsearch.port, defaultValue 9200) int port; private ElasticsearchClient client; PostConstruct void init() { RestClient restClient RestClient.builder( new HttpHost(host, port)).build(); ElasticsearchTransport transport new RestClientTransport( restClient, new JacksonJsonpMapper()); this.client new ElasticsearchClient(transport); } Produces ApplicationScoped public ElasticsearchClient client() { return client; } }ApplicationScoped保证整个应用只有一个实例PostConstruct在 Bean 创建时初始化连接。这样客户端在应用启动时就建好了后续所有请求复用同一个。3.2 索引操作的幂等性处理quickstart 里有个容易被忽略的点创建索引。如果索引已经存在直接创建会报错。生产环境里服务重启是常态所以创建索引的逻辑必须幂等。我的做法是先判断存在性再决定是否创建public void createIndexIfNotExists(String indexName) throws IOException { Boolean exists client.indices() .exists(e - e.index(indexName)) .value(); if (!exists) { client.indices().create(c - c.index(indexName)); } }这里用到了新客户端的 lambda 风格构建器e - e.index(indexName)这种写法比老的CreateIndexRequest直观很多。exists返回的是一个包装对象.value()取布尔值。注意判断存在性和创建索引之间存在竞态条件。如果多个实例同时启动可能都判断为不存在然后同时创建其中一个会失败。生产环境要么用分布式锁要么捕获resource_already_exists_exception异常忽略掉。3.3 文档的增删改查文档操作是搜索服务的核心。quickstart 里演示了基本的 CRUD我补充几个实操中容易出问题的细节。写入文档时索引名和文档 ID 的指定方式IndexResponse response client.index(i - i .index(products) .id(product.getId()) .document(product) );如果不指定 IDES 会自动生成一个。指定 ID 的好处是幂等——同样的 ID 重复写入会覆盖而不是新增。但要注意覆盖是整体替换不是字段合并。查询文档时新客户端的返回类型是泛型化的GetResponseProduct response client.get(g - g .index(products) .id(id), Product.class); if (response.found()) { Product product response.source(); }found()判断文档是否存在source()取反序列化后的对象。这里有个坑如果索引的 mapping 和你的实体类字段对不上反序列化会静默丢字段不会报错。我建议在开发阶段打开客户端的日志确认字段映射正确。3.4 配置管理别把连接信息写死在代码里Quarkus 的配置体系基于 MicroProfile Config支持多种来源application.properties、环境变量、系统属性。优先级是系统属性 环境变量 配置文件。application.properties里的典型配置elasticsearch.hostlocalhost elasticsearch.port9200 elasticsearch.index.productsproducts elasticsearch.timeout5000在代码里用ConfigProperty注入。这样做的好处是容器化部署时可以通过环境变量覆盖不用重新打包。提示配置项命名建议用点分层级比如elasticsearch.index.products比esProductsIndex可读性好得多。Quarkus 会自动做驼峰转换两种写法都能注入但配置文件里保持统一风格。4. 实操过程与核心环节实现4.1 环境准备与项目初始化我实际跑这个 quickstart 的环境是JDK 17、Maven 3.9、Elasticsearch 8.x 单节点。JDK 版本很重要Quarkus 3.x 最低要求 JDK 17用 JDK 11 会直接编译失败。初始化项目有两种方式。一种是用 Quarkus 的 Maven 插件生成骨架mvn io.quarkus.platform:quarkus-maven-plugin:3.x:create \ -DprojectGroupIdcom.example \ -DprojectArtifactIdelasticsearch-quickstart \ -Dextensionsrest-client,rest-jackson另一种是直接拉官方 quickstart 仓库。我建议用第一种因为生成的骨架版本和你的 Quarkus 版本匹配不会出现依赖冲突。生成之后目录结构是这样的elasticsearch-quickstart/ ├── src/main/java/com/example/ │ ├── GreetingResource.java │ └── ... ├── src/main/resources/ │ └── application.properties ├── src/test/java/ └── pom.xml4.2 添加 Elasticsearch 依赖在pom.xml里加上官方客户端dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version8.x.x/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency版本号要和你的 ES 服务端匹配。客户端和服务端大版本必须一致小版本可以不同。比如服务端是 8.11客户端用 8.10 或 8.12 都行但用 7.x 的客户端连 8.x 的服务端会报兼容性错误。4.3 编写客户端生产者前面 3.1 已经给了代码这里补充一个细节连接超时和 socket 超时的设置。RestClient restClient RestClient.builder(new HttpHost(host, port)) .setRequestConfigCallback(builder - builder .setConnectTimeout(5000) .setSocketTimeout(60000)) .build();connectTimeout是建立 TCP 连接的超时socketTimeout是等待响应的超时。搜索请求可能比较慢socketTimeout要给足我一般设 60 秒。connectTimeout可以短一点5 秒足够连不上就快速失败。4.4 实现 REST 接口资源层的代码大概长这样Path(/products) Produces(MediaType.APPLICATION_JSON) Consumes(MediaType.APPLICATION_JSON) public class ProductResource { Inject ProductService productService; POST public Response create(Product product) throws IOException { productService.index(product); return Response.status(Response.Status.CREATED).build(); } GET Path(/{id}) public Response getById(PathParam(id) String id) throws IOException { Product product productService.findById(id); if (product null) { return Response.status(Response.Status.NOT_FOUND).build(); } return Response.ok(product).build(); } GET Path(/search) public ListProduct search(QueryParam(q) String keyword) throws IOException { return productService.search(keyword); } }注意Inject注入的是服务层不是直接注入客户端。这样资源层不关心 ES 的细节测试的时候也容易 mock。4.5 实现搜索逻辑搜索是重点我详细说一下查询构建。最简单的全文搜索public ListProduct search(String keyword) throws IOException { SearchResponseProduct response client.search(s - s .index(products) .query(q - q .multiMatch(m - m .fields(name, description) .query(keyword))), Product.class); return response.hits().hits().stream() .map(Hit::source) .collect(Collectors.toList()); }multiMatch会在多个字段里搜索关键词fields指定字段列表。ES 默认会对搜索结果做相关性排序_score高的排前面。如果要加分页.from(0) .size(20)from是起始偏移size是返回条数。注意from size不能超过index.max_result_window默认 10000超过会报错。深度分页要用search_after这个 quickstart 里没涉及但生产环境迟早会遇到。4.6 打包与运行开发模式直接跑./mvnw quarkus:dev这个模式支持热重载改代码保存后自动重新编译调试很方便。生产打包./mvnw package会生成一个quarkus-run.jar这是 Quarkus 特有的可执行 jar内部结构经过优化启动比普通 fat jar 快。如果要原生镜像./mvnw package -Pnative这个需要本地装了 GraalVM或者用容器构建。原生镜像的启动速度是 JVM 模式的几十倍但构建时间长而且反射、动态代理相关的代码需要额外配置。quickstart 里的代码基本都能直接编译成原生镜像因为 Quarkus 的扩展已经处理好了大部分适配。注意原生镜像模式下Jackson 的反序列化需要注册反射信息。如果你的实体类字段没有对应的 getter/setter可能会反序列化失败。Quarkus 的 Jackson 扩展会自动扫描但自定义的序列化器需要手动注册。5. 常见问题与排查技巧实录5.1 连接被拒绝先查端口再查绑定地址最常见的报错是Connection refused。排查顺序确认 ES 服务在跑curl localhost:9200确认端口对默认 9200有些部署会改确认绑定地址ES 默认只绑localhost容器里跑的话要改成0.0.0.0第三步是最容易踩的坑。本地开发时 ES 和 Quarkus 在同一台机器localhost没问题一旦 ES 跑在容器里Quarkus 跑在宿主机localhost就指向宿主机自己了连不上容器。这时候要用容器的 IP 或者服务名。5.2 版本不匹配客户端和服务端大版本必须一致报错信息通常是UnsupportedProductException或者响应解析失败。ES 客户端会在握手时检查服务端版本大版本不一致直接拒绝。解决办法就是对齐版本。查服务端版本curl localhost:9200返回的 JSON 里有version.number字段。然后改pom.xml里的客户端版本。5.3 索引 mapping 冲突字段类型对不上如果你先写入了一条price是数字的文档再写入一条price是字符串的ES 会报mapper_parsing_exception。因为 ES 的 mapping 一旦确定就不能改字段类型。解决办法是提前定义好 mapping或者在开发阶段直接删索引重建curl -X DELETE localhost:9200/products生产环境不能这么干要用 reindex 或者别名切换。5.4 中文搜索分词问题默认的分词器对中文是按字切分的搜索手机会匹配到所有包含手或机的文档召回率高但准确率低。要装中文分词插件然后在 mapping 里指定分词器。这个 quickstart 没涉及但如果你要做中文搜索这是绕不过去的。我的建议是先用默认分词器跑通流程再根据实际效果决定要不要上插件。5.5 常见问题速查表现象可能原因排查方向Connection refused服务未启动/端口错/绑定地址错curl 测试连通性UnsupportedProductException客户端服务端版本不匹配对齐大版本号mapper_parsing_exception字段类型冲突检查 mapping必要时重建索引反序列化丢字段字段名不匹配/缺 getter打开客户端日志核对字段原生镜像启动报错反射信息缺失检查是否需要 RegisterForReflection搜索无结果分词问题/字段名错用 _analyze API 测试分词5.6 我踩过的几个坑第一个坑是忘记关客户端。应用关闭时如果不调用client.close()连接池不会释放在容器里反复重启会积累大量 TIME_WAIT 连接。解决办法是加PreDestroy钩子PreDestroy void cleanup() throws IOException { if (client ! null) { client._transport().close(); } }第二个坑是在循环里调用 ES。我一开始写批量导入的时候for 循环里一条条 index一千条数据跑了几十秒。后来改成bulk批量接口同样的数据一秒不到。ES 的 bulk API 一次可以提交几千条性能差距是数量级的。第三个坑是忽略了 refresh 机制。ES 写入文档后默认 1 秒才可见这叫 refresh interval。如果你写完立刻查可能查不到。测试的时候可以用refreshwait_for参数强制刷新但生产环境别这么干会影响性能。6. 从 quickstart 到生产可用的改造方向quickstart 跑通只是起点真要上生产还有几件事要做。连接池和重试。官方客户端自带节点发现和故障转移但重试策略需要配置。我一般会设置最大重试次数和重试间隔避免网络抖动导致请求直接失败。健康检查。Quarkus 有quarkus-smallrye-health扩展可以自定义健康检查探针定期 ping 一下 ES不健康时让负载均衡摘掉这个实例。指标监控。客户端的请求耗时、成功率、连接池状态都值得监控。Quarkus 的 Micrometer 扩展可以很方便地接入。配置外置。把 ES 地址、索引名、超时参数全部放到环境变量里不同环境用不同配置代码不用改。异常处理。ES 的异常类型很多ElasticsearchException是基类下面有ConnectionException、ResponseException等。建议在服务层统一捕获转换成业务异常别让底层异常直接透到 REST 层。安全加固。生产环境的 ES 一般会开认证和 TLS客户端要配置用户名密码和证书。这部分 quickstart 没涉及但实际部署时是必须的。我个人在实际操作中的体会是Quarkus 这套东西的学习曲线主要在编译期思维的转变上。习惯了 Spring 的运行期魔法之后一开始会不习惯什么都得显式声明。但一旦适应了你会发现代码更可控启动更快内存更省尤其是做微服务的时候这种优势会随着服务数量增加而放大。Elasticsearch 客户端本身不难难的是把索引设计、查询优化、集群运维这些周边事情做好那是另一个话题了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表