将一个现有的应用或库暴露为一个web服务,并且无需进行任何代码改动,这是一种人们渴望已久的概念。Baratine是一个开源的框架,能够用于创建由松耦合的微服务所构成的平台。通过使用Baratine,可以通过以下两个步骤实现这一概念。
通过以上方法,Baratine就能够将一个现有的库或应用转换为一个独立的web服务。Baratine服务将与现有的库进行通信,而Baratine客户端将负责接受由外部发来的请求。
在本文中,我们将探索使用Baratine的一种场景,即将Apache Lucene这个非常流行的开源Java搜索库暴露为一个高性能的web服务。
Apache基金会是这样描述Lucene的:“一个完全由Java编写的高性能、特性完善的文本搜索引擎库,这一技术适用于几乎任何需要进行全文搜索的应用,尤其是跨平台的应用。”
我们在本文中所创建的示例可以成为一份蓝图,它展示了如何将现有的应用或库转换为全功能的微服务,它将把Lucene库转换为一种高效的搜索微服务,并具备以下特征:
如果你希望在阅读本文的同时运行这一示例,你可以从GitHub上下载源码,并按照以下步骤运行:
本示例包含用Java实现的Lucene服务,以及一个JavaScript客户端,可用于浏览器或node.js。本项目还将通过AngularJS框架与后端服务进行交互。这就意味着,只需使用Baratine和一个前端框架,就能够轻易地创建一个高性能的微服务。我们所做的工作是通过Baratine服务发布一个Lucene的HTTP或WebSocket API。
完成这一项目需要实现两个主要的任务:
任务1:发布一个基于HTTP/WebSocket的客户端API。任务2:实现Lucene的同步库与Baratine异步服务的对接。
主要的工作是通过以下JavaScript与Java文件完成的:
让我们看看怎样实现这两个主要任务:
LuceneFacade是所发布的HTTP/WebSocket的API,用于完成第一个任务,它的具体实现使用了LuceneFacadeImpl。这个已发布的API是异步的,它将Baratine中返回的结果Result作为callback方法中的内容,以实现对数据的处理。整个实现是单线程无阻塞的,这就免除了方法同步的需求。
LuceneReaderImpl负责实现搜索查询服务,它在Lucene的同步库与Baratine的异步API之间起到了桥梁作用。由于Lucene的搜索是多线程阻塞式的,因此LuceneReaderImpl利用了Baratine的多工作线程的支持(@Workers)。多工作线程支持类似于数据库的连接池,LuceneReader的使用者所面对的仅是一个异步服务,而无需了解它是由一个多线程服务所实现的。
LuceneWriterImpl负责实现索引更新服务。这是一个单线程的服务,它将对Lucene的写入请求进行批量化,以提高效率。随着负载的提高,LuceneWriter将变得更加高效,因为它的批量大小会自动进行增长,我们稍后将对此进行分析。
LuceneWriterImpl的实现能够展现出将一个方法调用封装为它本身自有的服务是多么简单,不仅如何,它还能够防止对这些方法的调用阻塞其他客户端的请求。Lucene并没有提供异步的API,但由于Baratine的服务是异步的,我们允许在持续调用这些方法的期间进行处理。下图展示了这个新架构的一个高层次概要:
(点击放大图像)
客户端API是该服务的Java接口,在本例中即代表LuceneFacade.java接口。每个服务都具备一个URL语法的地址。可以通过纯粹的进程内方法调用的方式对服务中的方法进行调用。Baratine的JavaScript库将负责管理细节部分,为协议提供一个方法接口,通过HTTP或WebSocket进行JSON风格的方法调用。
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,那么如何创建一个客户端与这个服务进行交互呢?
我们必须按照以下方式创建一个客户端:
一些存在时间较长的客户端,例如某个Java应用服务器或是Node.JS服务可以通过消息共享一个单线程,并且保证线程安全的连接,因为协议本身是异步的。一些快速的请求,例如对内存缓存进行查询的请求可能会比较早发送的慢请求更快完成,从而打乱了返回的次序。举例来说,一个Lucene搜索可能会调用某个MySQL数据库。使用单一的连接将进一步改进效率,因为多个调用可以进行批量处理,从而改进了TCP的性能。
对于这个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客户端调用,如同一个使用Lucene的web应用一般。与JavaScript客户端一样,该Java客户端通常也是一个存在时间较长的连接。由于客户端本身是线程安全的,因此可以在一个多线程的应用中高效地使用它。
@Inject @Lookup("public://lucene/service") LuceneFacade _lucene;
当某个Java应用调用了Baratine服务,例如这个Lucene服务时,它通常会通过一种同步的阻塞式调用,以连接到某个服务API的同步版本的方法上。而Baratine客户端代理将作为同步的客户端与异步的Baratine服务之间的桥梁。这个Lucene门面的同步版本如下所示:
public interface LuceneFacadeSync extends LuceneFacade { List<LuceneEntry> search (String collection, String query, int limit); }
如以下代码所示,客户端的调用看上去类似一个纯粹的Java方法调用。由于ServiceClient是线程安全的,并且可用于多个Baratine服务,因此它可以实现为一个单例对象。实际上,由于Baratine的内部的批量方法与消息传递机制,在多个线程之间使用一个单一的客户端是一种更高效的作法。
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服务端需要完成两项任务:一是发布客户端API,二是在Lucene的多线程阻塞式实现与Baratine的异步架构之间起到桥梁作用。该示例中使用了三个Baratine服务以实现整个服务:
对于Lucene服务端来说,读写操作被分为两个不同的服务,因为读与写的行为是完全不同的,因此对应的服务也经过了自定义,从而有效地支持两者的不同行为。
写操作最好使用一个单一的writer线程,它能够在高负载时提高效率,因为它能够将多个新操作合并为一个单一的提交。
而Lucene的读操作最好使用多个reader线程。Lucene的搜索操作有可能会在数据库查询时阻塞,使得线程不可用。利用多线程的方式,一次新的搜索可以由一个独立的线程完成。请注意,由于Lucene可能会使用一个迟缓的阻塞式服务,所以才有使用多线程的必要。如果Lucene本身是基于内存的或是异步的,那么单一的reader线程将更为高效,因为它能够更好地利用CPU的缓存。
Lucene服务的实现代码定义在五个主要的文件中,我们将简单地进行分析。
客户端API是由LuceneFacade这个Baratine服务实现的,它的主要工作是将请求转发给Reader和Writer服务。当我们使用多个服务对Lucene服务器进行分区时(我们将在之后的文章中介绍这一点),这个门面将承担更大的责任。对于这个示例来说,应当关注于客户端API的简洁性,保证服务端正常运行,能够使得客户端更简单。
由于Lucene库被实现为一个单例对象,因此reader和writer的相关服务将共享一个相同的LuceneIndexBean实例。这种设计是按照Lucene自身的设计而产生的,我们只是通过Baratine与现有的架构打交道,而不是强迫Lucene去遵循Baratine的架构。如果我们是从头开始打造一个Baratine服务,而不是去适应一个现有的库,那么我们很可能会选择一种不同的架构。当reader与writer服务初始化时,将注入这个共享的LuceneIndexBean单例对象。
由于我们始终是在利用底层的Lucene库,因此可以简单地实现LuceneFacade接口,并添加一些辅助方法,以符合我们的特定索引功能的需求(例如对特殊字符进行转义、使用另一种数据存储机制、限制提交的大小等等。)
Writer服务将通过门面获取请求,并更新Lucene中的索引。它将通过一个单一的工作线程将更新写入Lucene中,直到所有请求都完成。如果没有更多的请求,它就会调用Lucene的提交方法以完成写入操作。在负载较重的场景中,请求的数量将会提升,批量处理的大小也将提高以提升性能。
由于Lucene的搜索操作在等待较慢的数据库时会产生阻塞,并且Lucene是多线程的,因此reader的实现使用了一个 @Worker 注解,以请求为服务分配多个线程。当为该服务使用了 @Workers(20) 这个注解时,我们就提供了一个线程池的执行者,它能够持续地为读取服务分配线程。由于这些线程只在需要时才会分配,因此多个工作线程的消耗并不大。多个工作线程的方式能够让Lucene服务一次性应对多个请求。
一般来说,多工作线程这种特性应当仅用于通过某个网关服务连接外部的阻塞式依赖的场景,例如某个数据库连接或一个REST调用。为Baratine设计的服务应当使用一个单一的工作线程,因为它应当被设计为异步的无阻塞式服务。
我们所做的一切是将Lucene API实现为一系列的Baratine服务。在Baratine中,每个 服务 都对应一个唯一的URL,并且通过一个单线程、单归属及单写入的契约进行操作(在Lucene reader中使用的多工作线程桥接服务是一个例外,它的作用是让Baratine能够使用外部的库)。对于某个特定服务URL的请求将进入该服务的队列Inbox中,这种方式能够保证请求将按照所传入的次序进行处理。
核心的Baratine构建块如下图所示:
根据上面这张图,我们可以对于Baratine服务进行一下总结:
在Baratine中,我们无需为方法调用添加同步功能。因为每个服务都是由唯一的线程处理的,因此另一个线程不可能干扰更新中的数据。因此我们就可以使用POJO对象进行类的设计了。
有些读者可以已经注意到了,我们的示例与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