通過Baratine將Lucene庫暴露為微服務
將一個現有的應用或庫暴露為一個web服務,并且無需進行任何代碼改動,這是一種人們渴望已久的概念。Baratine是一個開源的框架,能夠用于創建由松耦合的微服務所構成的平臺。通過使用Baratine,可以通過以下兩個步驟實現這一概念。
- 實現服務端的部分(SOA)
- 隨后實現一個客戶端庫負責通信
通過以上方法,Baratine就能夠將一個現有的庫或應用轉換為一個獨立的web服務。Baratine服務將與現有的庫進行通信,而Baratine客戶端將負責接受由外部發來的請求。
在本文中,我們將探索使用Baratine的一種場景,即將Apache Lucene這個非常流行的開源Java搜索庫暴露為一個高性能的web服務。
Apache基金會是這樣描述Lucene的:“一個完全由Java編寫的高性能、特性完善的文本搜索引擎庫,這一技術適用于幾乎任何需要進行全文搜索的應用,尤其是跨平臺的應用。”
我們在本文中所創建的示例可以成為一份藍圖,它展示了如何將現有的應用或庫轉換為全功能的微服務,它將把Lucene庫轉換為一種高效的搜索微服務,并具備以下特征:
- 表現為一個異步的消息傳遞服務。
- 為Lucene暴露一個公開的WebSocket API。
- 部署至一個獨立托管的服務器。
運行示例
如果你希望在閱讀本文的同時運行這一示例,你可以從GitHub上下載源碼,并按照以下步驟運行:
- 安裝lucene-plugin
- 運行./baratine-run.sh
- 打開瀏覽器并加載 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,那么如何創建一個客戶端與這個服務進行交互呢?
如何創建客戶端
我們必須按照以下方式創建一個客戶端:
- 將服務的URL傳遞給Baratine:
client = new Jamp.BaratineClient(url); - 向ServiceRef發起一次調用,以查找我們的服務
- 保存查找所返回的代理
- 對代理進行方法調用
一些存在時間較長的客戶端,例如某個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
來自: http://www.infoq.com/cn/articles/Building-a-Lucene-Microservice-with-Baratine