通过 Baratine 将 Lucene 库暴露为微服务

阅读数:1640 2016 年 2 月 24 日

话题:Java语言 & 开发架构

将一个现有的应用或库暴露为一个 web 服务,并且无需进行任何代码改动,这是一种人们渴望已久的概念。Baratine 是一个开源的框架,能够用于创建由松耦合的微服务所构成的平台。通过使用 Baratine,可以通过以下两个步骤实现这一概念。

  1. 实现服务端的部分(SOA)
  2. 随后实现一个客户端库负责通信

通过以上方法,Baratine 就能够将一个现有的库或应用转换为一个独立的 web 服务。Baratine 服务将与现有的库进行通信,而 Baratine 客户端将负责接受由外部发来的请求。

在本文中,我们将探索使用 Baratine 的一种场景,即将 Apache Lucene 这个非常流行的开源 Java 搜索库暴露为一个高性能的 web 服务。

Apache 基金会是这样描述 Lucene 的:“一个完全由 Java 编写的高性能、特性完善的文本搜索引擎库,这一技术适用于几乎任何需要进行全文搜索的应用,尤其是跨平台的应用。”

我们在本文中所创建的示例可以成为一份蓝图,它展示了如何将现有的应用或库转换为全功能的微服务,它将把 Lucene 库转换为一种高效的搜索微服务,并具备以下特征:

  • 表现为一个异步的消息传递服务。
  • 为 Lucene 暴露一个公开的 WebSocket API。
  • 部署至一个独立托管的服务器。

运行示例

如果你希望在阅读本文的同时运行这一示例,你可以从 GitHub 上下载源码,并按照以下步骤运行:

  1. 安装 lucene-plugin
  2. 运行./baratine-run.sh
  3. 打开浏览器并加载http://localhost:8085这个网址

项目的内容及概述

本示例包含用 Java 实现的 Lucene 服务,以及一个 JavaScript 客户端,可用于浏览器或 node.js。本项目还将通过 AngularJS 框架与后端服务进行交互。这就意味着,只需使用 Baratine 和一个前端框架,就能够轻易地创建一个高性能的微服务。我们所做的工作是通过 Baratine 服务发布一个 Lucene 的 HTTP 或 WebSocket API。

完成这一项目需要实现两个主要的任务:

任务 1:发布一个基于 HTTP/WebSocket 的客户端 API。

任务 2:实现 Lucene 的同步库与 Baratine 异步服务的对接。

主要的工作是通过以下 JavaScript 与 Java 文件完成的:

服务:

  • 公开的 API 服务:负责将对 Lucene 库的方法请求以代理的方式转发。
  • Lucene 读服务写服务:负责读取及写入搜索索引,并在 Lucene 的同步阻塞式 API 与 Baratine 的异步及消息传递 API 之间进行桥接。

客户端:

  • Baratine Lucene 客户端:正如我们的示例代码所示,负责暴露 Lucene 的客户端方法的功能,以用于在浏览器中显示搜索结果。
  • Baratine JavaScript 客户端:通过 Baratine 分发的协议库,通过 HTTP 或 WebSocket 协议,以及 Baratine 中的JAMP协议,与服务进行通信。

让我们看看怎样实现这两个主要任务:

任务 1 —— Baratine 服务:LuceneFacade

LuceneFacade 是所发布的 HTTP/WebSocket 的 API,用于完成第一个任务,它的具体实现使用了 LuceneFacadeImpl。这个已发布的 API 是异步的,它将 Baratine 中返回的结果 Result 作为 callback 方法中的内容,以实现对数据的处理。整个实现是单线程无阻塞的,这就免除了方法同步的需求。

任务 2A —— Baratine 服务:LuceneReaderImpl

LuceneReaderImpl 负责实现搜索查询服务,它在 Lucene 的同步库与 Baratine 的异步 API 之间起到了桥梁作用。由于 Lucene 的搜索是多线程阻塞式的,因此 LuceneReaderImpl 利用了 Baratine 的多工作线程的支持(@Workers)。多工作线程支持类似于数据库的连接池,LuceneReader 的使用者所面对的仅是一个异步服务,而无需了解它是由一个多线程服务所实现的。

任务 2B —— Baratine 服务:LuceneWriterImpl

LuceneWriterImpl 负责实现索引更新服务。这是一个单线程的服务,它将对 Lucene 的写入请求进行批量化,以提高效率。随着负载的提高,LuceneWriter 将变得更加高效,因为它的批量大小会自动进行增长,我们稍后将对此进行分析。

LuceneWriterImpl 的实现能够展现出将一个方法调用封装为它本身自有的服务是多么简单,不仅如何,它还能够防止对这些方法的调用阻塞其他客户端的请求。Lucene 并没有提供异步的 API,但由于 Baratine 的服务是异步的,我们允许在持续调用这些方法的期间进行处理。下图展示了这个新架构的一个高层次概要:

(点击放大图像)

HTTP/WebSocket 客户端 API

客户端 API 是该服务的 Java 接口,在本例中即代表 LuceneFacade.java 接口。每个服务都具备一个 URL 语法的地址。可以通过纯粹的进程内方法调用的方式对服务中的方法进行调用。Baratine 的 JavaScript 库将负责管理细节部分,为协议提供一个方法接口,通过 HTTP 或 WebSocket 进行 JSON 风格的方法调用。

Lucene 的 API 方法:

void indexFile(String collection, String path, Result result) throws LuceneException;

void indexText(String collection, String id, String text,Result result) throws LuceneException;

void indexMap(String collection, String id, Map map, Result result) throws LuceneException;

void search(String collection, String query, int limit, Result> result) throws LuceneException;

void delete(String collection, String id, Result result) throws LuceneException;

void clear(String collection, Result result) throws LuceneException;

客户端能够直接调用这些方法。

我们已经知道每个服务对应一个 URL,那么如何创建一个客户端与这个服务进行交互呢?

如何创建客户端

我们必须按照以下方式创建一个客户端:

  1. 将服务的 URL 传递给 Baratine:

    client = new Jamp.BaratineClient(url);
  2. 向 ServiceRef 发起一次调用,以查找我们的服务
  3. 保存查找所返回的代理
  4. 对代理进行方法调用

一些存在时间较长的客户端,例如某个 Java 应用服务器或是 Node.JS 服务可以通过消息共享一个单线程,并且保证线程安全的连接,因为协议本身是异步的。一些快速的请求,例如对内存缓存进行查询的请求可能会比较早发送的慢请求更快完成,从而打乱了返回的次序。举例来说,一个 Lucene 搜索可能会调用某个 MySQL 数据库。使用单一的连接将进一步改进效率,因为多个调用可以进行批量处理,从而改进了 TCP 的性能。

JavaScript 客户端

对于这个 Lucene 示例来说,API 将提供对文本文档进行搜索、以及将新的文档添加到搜索引擎等方法。

Baratine 将通过类实现接口,因此我们就能够直接调用那些打算暴露的方法的接口。方法的调用将返回一个 callback,并返回搜索结果或通知索引操作已结束。由于 Baratine 是异步的,因此对于搜索或文件索引的调用也是无阻塞的

Baratine 支持在客户端与被调用的服务之间建立一个 websocket 连接,这就允许通过一个单一的 TCP 连接进行全双工通信,websocket 正是为了给 web 服务带来原生桌面应用般的响应度。

Lucene 的 JavaScript 客户端将遵循 Java API 的功能,以下代码来自于 lucene.js 文件,以展示如何将 Baratine 调用转换为对应的 JavaScript。

首先要新建一个 Jamp.BaratineClient 对象,传入服务器的 HTTP URL,以建立一个新的连接。该 URL 是“http://localhost:8085/s/lucene”。通常来说,这个客户端将存在较长时间,以用于发送多次请求。

搜索

复制代码
{
this.client.query(“/service”,
“search”, [coll, query, limit], onResult);
}

搜索代码非常直观,它将请求通过代理直接传递给后端。

索引

复制代码
{
this.client.send(“/service”,
“indexText”, [coll, extId, text]);
}

对文本的索引操作被实现为一种无阻塞的方法。

服务将通过 Baratine 进行暴露,方法是创建一个由 Baratine 托管的 Bean。

Java 客户端

服务也可以从 Java 客户端调用,如同一个使用 Lucene 的 web 应用一般。与 JavaScript 客户端一样,该 Java 客户端通常也是一个存在时间较长的连接。由于客户端本身是线程安全的,因此可以在一个多线程的应用中高效地使用它。

异步 Java 客户端

复制代码
@Inject
@Lookup("public://lucene/service")
LuceneFacade _lucene;

当某个 Java 应用调用了 Baratine 服务,例如这个 Lucene 服务时,它通常会通过一种同步的阻塞式调用,以连接到某个服务 API 的同步版本的方法上。而 Baratine 客户端代理将作为同步的客户端与异步的 Baratine 服务之间的桥梁。这个 Lucene 门面的同步版本如下所示:

LuceneFacadeSync

复制代码
public interface LuceneFacadeSync extends LuceneFacade
{
List<LuceneEntry> search (String collection, String query, int limit);
}

如以下代码所示,客户端的调用看上去类似一个纯粹的 Java 方法调用。由于 ServiceClient 是线程安全的,并且可用于多个 Baratine 服务,因此它可以实现为一个单例对象。实际上,由于 Baratine 的内部的批量方法与消息传递机制,在多个线程之间使用一个单一的客户端是一种更高效的作法。

同步的 Java 客户端

复制代码
ServiceClient client = ServiceClient.newClient(url).build();
LuceneFacadeSync lucene;
lucene = client.lookup(“remote:///service”).as(LuceneFacadeSync.class);
List<LuceneEntry> result = lucene.search(collection, query, limit);

如你所见,客户端的创建非常直观,因为 Baratine 的灵活架构允许进行高效的 API 协议设计。

Lucene 服务端的实现

Lucene 服务端需要完成两项任务:一是发布客户端 API,二是在 Lucene 的多线程阻塞式实现与 Baratine 的异步架构之间起到桥梁作用。该示例中使用了三个 Baratine 服务以实现整个服务:

  • 客户端 API 的门面服务
  • 用于搜索的 Reader 服务
  • 用于更新索引的 Writer 服务

对于 Lucene 服务端来说,读写操作被分为两个不同的服务,因为读与写的行为是完全不同的,因此对应的服务也经过了自定义,从而有效地支持两者的不同行为。

写操作最好使用一个单一的 writer 线程,它能够在高负载时提高效率,因为它能够将多个新操作合并为一个单一的提交。

而 Lucene 的读操作最好使用多个 reader 线程。Lucene 的搜索操作有可能会在数据库查询时阻塞,使得线程不可用。利用多线程的方式,一次新的搜索可以由一个独立的线程完成。请注意,由于 Lucene 可能会使用一个迟缓的阻塞式服务,所以才有使用多线程的必要。如果 Lucene 本身是基于内存的或是异步的,那么单一的 reader 线程将更为高效,因为它能够更好地利用 CPU 的缓存。

Lucene 服务的实现代码定义在五个主要的文件中,我们将简单地进行分析。

LuceneFacadeImpl.java

客户端 API 是由 LuceneFacade 这个 Baratine 服务实现的,它的主要工作是将请求转发给 Reader 和 Writer 服务。当我们使用多个服务对 Lucene 服务器进行分区时(我们将在之后的文章中介绍这一点),这个门面将承担更大的责任。对于这个示例来说,应当关注于客户端 API 的简洁性,保证服务端正常运行,能够使得客户端更简单。

LuceneIndexBean.java

由于 Lucene 库被实现为一个单例对象,因此 reader 和 writer 的相关服务将共享一个相同的 LuceneIndexBean 实例。这种设计是按照 Lucene 自身的设计而产生的,我们只是通过 Baratine 与现有的架构打交道,而不是强迫 Lucene 去遵循 Baratine 的架构。如果我们是从头开始打造一个 Baratine 服务,而不是去适应一个现有的库,那么我们很可能会选择一种不同的架构。当 reader 与 writer 服务初始化时,将注入这个共享的 LuceneIndexBean 单例对象。

由于我们始终是在利用底层的 Lucene 库,因此可以简单地实现 LuceneFacade 接口,并添加一些辅助方法,以符合我们的特定索引功能的需求(例如对特殊字符进行转义、使用另一种数据存储机制、限制提交的大小等等。)

LuceneWriterImpl.java

Writer 服务将通过门面获取请求,并更新 Lucene 中的索引。它将通过一个单一的工作线程将更新写入 Lucene 中,直到所有请求都完成。如果没有更多的请求,它就会调用 Lucene 的提交方法以完成写入操作。在负载较重的场景中,请求的数量将会提升,批量处理的大小也将提高以提升性能。

LuceneReaderImpl.java

由于 Lucene 的搜索操作在等待较慢的数据库时会产生阻塞,并且 Lucene 是多线程的,因此 reader 的实现使用了一个@Worker注解,以请求为服务分配多个线程。当为该服务使用了@Workers(20)这个注解时,我们就提供了一个线程池的执行者,它能够持续地为读取服务分配线程。由于这些线程只在需要时才会分配,因此多个工作线程的消耗并不大。多个工作线程的方式能够让 Lucene 服务一次性应对多个请求。

一般来说,多工作线程这种特性应当仅用于通过某个网关服务连接外部的阻塞式依赖的场景,例如某个数据库连接或一个 REST 调用。为 Baratine 设计的服务应当使用一个单一的工作线程,因为它应当被设计为异步的无阻塞式服务。

Baratine 的架构

我们所做的一切是将 Lucene API 实现为一系列的 Baratine 服务。在 Baratine 中,每个服务都对应一个唯一的 URL,并且通过一个单线程、单归属及单写入的契约进行操作(在 Lucene reader 中使用的多工作线程桥接服务是一个例外,它的作用是让 Baratine 能够使用外部的库)。对于某个特定服务 URL 的请求将进入该服务的队列 Inbox 中,这种方式能够保证请求将按照所传入的次序进行处理。

核心的 Baratine 构建块如下图所示:

根据上面这张图,我们可以对于 Baratine 服务进行一下总结:

  • 每个 Baratine 服务对应一个唯一的 URL
  • 每个 Baratine 服务都对应一个单一的线程,它负责操作服务的数据
  • 请求将进入服务的 Inbox 中,并以批量的方式进行处理

在 Baratine 中,我们无需为方法调用添加同步功能。因为每个服务都是由唯一的线程处理的,因此另一个线程不可能干扰更新中的数据。因此我们就可以使用 POJO 对象进行类的设计了。

性能及与 Apache Solr 的对比

有些读者可以已经注意到了,我们的示例与 Apache Solr 有些类似,后者也为 Lucene 库提供了一个服务端。与 Solr 进行比较是很有意义的,因为人们熟悉它,而且能够直接进行比较。

我们对于 Apache Solr 与多种客户端进行了基准测试。对于读操作来说,Baratine 比起 Solr 的性能大约能提高 20% 左右。而在混合读写操作的基准测试中,我们已证实 Baratine 比 Solr 快上 3 倍。

测试是在以下规格的硬件中执行的:

Intel(R) Core(TM) 2 Quad CPU

Q6700 @ 2.66GHz

内存 4G,速度: 667 MHz

硬盘: ST3250410AS

java version “1.8.0_51"OS Linux deb-0 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u2 (2015-07-17) x86_64 GNU/Linux

基准测试的结果如下图所示:

正如结果所示,Baratine 搜索(读)操作在与 Apache Solr 的平行比较中胜出一筹。

在混合读 / 写请求的场景下,Baratine 明显优胜。

总结

我们在这个示例中对 Lucene 的改造其实也适用于其他任何类库或应用。通过将一个 Baratine 服务包装为这个类库的一个门面,我们就能够将任何类库(例如 java.util)转化为异步的服务。

Baratine 中包含的许多原则都反映在Reactive Manifesto中。响应式应用具有弹性、可响应性、适应性,并且是由消息驱动的。这也是物联网趋势提出的需求,因为同一个应用可能会面对几万台设备的连接。这些原则是开发者很渴望,但在实践中还非常难以实现的。Baratine 通过明确的 POJO 层面上的抽象定义了一种数据与线程的封装层次,以应对这些困难。如此一来,Baratine 就是一个响应式平台上的一种 SOA 实现,它让开发者能够继续使用已经熟悉的面向对象风格进行编程。我们相信,如果新的 web 应用需要与现有的系统进行集成,那么许多应用都会采取这些原则进行开发。这样,正如我们在本文中所展示的一样,Baratine 非常适合于他们的需求。

随着你向应用中添加更多的功能,Baratine 的价值也在不断地提高。举例来说,如果我们决定对于所执行的查询进行实时分析,我们就可以在一个 Baratine 节点中部署一个简单的 POJO 类,用于获取统计信息,并将信息进行中断。通过现有的 BFS(Baratine 文件系统),它能够以接近实时的方式提供这些信息,也可以进行预先计算并将结果批量发送至外部数据源以使用。无论采用哪种方式,Baratine 统一而灵活的架构都能够以对应当前特定任务的方式设计系统,而不会对将来可能的发展进行限制。由于服务通常能够改进 web 应用的质量,一个概念验证只需数分钟就能够完成从白板上的 API 进入部署状态。

Baratine 目前还处在 Beta(0.10)版本,按照其路线图的计划,在 2016 年第一季度将发布一个能够在生产环境中使用的版本。在本文的示例中,我们才仅仅了解了如何通过 Baratine 实现一个基于 Lucene 的微服务的能力而已。

敬请期待本系列的第二部分,我们将探讨如何通过 Baratine 让这个 Lucene 微服务实现分片与伸缩性!

关于作者

Sean Wiley是 Caucho Technology 的技术传教士与高级销售工程师,他领导着销售与工程团队促进技术的适应性。他的工作包括为客户提供技术培训、文档以及实现细节。在加入 Caucho 之前,Sean 在 Cisco 担任 IT 分析师,也在 Scripps Institution of Oceanography 担任过数据库程序员。他具有位于圣地亚哥的加州大学的计算机科学本科学位。

查看英文原文:Exposing the Lucene Library as a Microservice with Baratine