特型搜索服務在線架構

jopen 9年前發布 | 11K 次閱讀 架構 軟件架構
 

什么是特型搜索

搜索返回的多條結果中,包括自自然搜索結果和特型搜索結果。

自然搜索結果是針對用戶請求(query),返回互聯網上相關性較高的內容。是否滿足用戶需求取決于互聯網內容及相關性算法。

特型搜索則是針對query,線下先對內容進行挖掘、整合(線下的方式有多種,其中 知識圖譜 是一個方向),線上直接返回滿足用戶需求的結果。比如當搜索"冰與火之歌"時,排在前面的百科、小說、影視等部分就屬于特型搜索結果。

特型搜索服務模型

除了和傳統搜索一樣有著嚴格的性能要求外,特型搜索服務還有兩個比較大的特點:

1. 開放:一次用戶請求的結果是由多個不同的服務來完成的,而且不同的服務往往由不同的小組負責研究。

2. 收斂:如果一次請求返回了多個特型結果,需要對結果進行聚合再返回。

所以特型搜索服務模型就是在葉子節點開放、非葉子結點收斂的一種樹行搜索結構:

特型搜索服務在線架構

架構實現

針對該服務模型,極端一點的做法可以讓各個service自由發揮,然后通過rpc進行訪問就可以了。但這樣做顯然是比較浪費和容易引起混亂的做法:

1. 接口標準:首先是接口問題,上游服務對下游服務進行進行聚合的時候,如果不能識別對方的返回結果,那"后來人"只能罵娘了。

2. 流量分發:下游服務雖然可以獨立開發,但仍需要向上游服務申請流量,也就是流量分發的功能,這其實是可以統一處理的。

3. 服務調用:如果service之間都是通過rpc調用,當調用層次比較深的時候,在對性能要求十分嚴格的環境下,是不可能滿足要求的。

4. 組件開發:一些通用的組件,比如日志、緩存、內存詞典等,如果讓每個服務都去開發,是非常浪費的。

以下對"接口標準", "流量分發", "服務調用"展開說明一下。

接口標準

接口標準是為了降低服務調用和聚合的成本。服務的接口是比較簡單的,用protobuf service的描述類似:

service ServiceAPI {
    rpc query(message.Request) returns (message.Response);
};

其中Request表示用戶的一次請求信息,對各服務都是一樣的。Response稍微復雜一些,因為每個service的結果是不一樣的,必須提供擴展機制。當然用protobuf是比較容易做到的:

message Response {  //common fields:  ... ...  repeated Result = 10;
};
message Result {  //common fields:  ... ...  extensions 128 to 512;
};

當然也可以不用extensions, 而是在Result里面加入自定義Message,這樣可以在Result里面看到所有的message(如果某些因素(比如歷史遺留問題)要求你這么做的話),我們采用的就是后者。

流量分發

流量分發是將用戶請求發送到服務的過程。要解決幾個問題:

  • 服務怎么知道這個請求是自己處理?還是轉發再聚合?
    是自己處理還是聚合其他服務,這是業務分析的時候就可以確定的,比如A服務就是要對"天氣"相關的請求出結果,而B服務就是要對不同服務結果進行排序。為 此,在架構上我們提供了兩個基類分別滿足這兩種應用場景 - AtomicService 和 AggregatedService.
  • 如果是轉發,怎么知道轉發給誰?是全量轉發還是部分轉發?
    轉發其實是AggregatedService的職責,我們的轉發機制也是集成在這個基類里面的。主要提供了"服務注冊"和"流量分發"功能。
    服務注冊可以理解為是"依賴倒置"pattern的一種應用,上游服務提供注冊接口,由下游服務發起注冊。這樣可以使新開發服務對已有服務的影響最小。你 可能會問"不是要在上游服務做聚合嗎"? 實際情況是很多服務其實是相對比較獨立的,聚合的情況其實不會太多,也就是說一次請求很多情況只有一個AtomicService完成響應就夠了。
    流量分發也有不同的做法,一種做法是全量轉發,由下游服務自己決定是否/如何處理。這樣對下游服務來說是最好的,但是缺點是需要服務節點能承受全流量的壓 力。 另一種做法是在入口處先對query做一定分析,然后將服務id放入請求中,作為后續轉發的一個依據。這種做法的缺陷是所有服務需要在一個地方完成 query解析,也有單點帶來的維護等問題。綜合比較我們采用的是后者。

服務調用

服務調用要解決的是服務加載和服務發現的問題。如果每個服務都是單獨的進程,可以直接加載到rpc server中,向上注冊的時候同時聲明自己的server地址及端口就可以了,現成的rpc實現有 grpc 。但總有一些原因(比如性能要求、或者因為部署機制跟不上),要求service能在一個進程里面跑,同一個進程的service可以通過函數call而不是rpc調用,那將service放入一個容器(container)中,是一個不錯的選擇。

在我們的實現中,service是加載進一個container的,請求通過container進行轉發。當向上游service注冊時,需要 聲明自己在哪個container(這樣做的一個問題是當service切換container時,需要修改上游。更好的做法可以考慮注冊時只聲明服務 名,service和container的關系通過ZK之類的東西進行管理。我們沒這樣做是因為目前為止service和container的關系是穩定 的)。這樣container就可以發現待調用service是在container內還是外,如果是內部則通過函數call調用,外部則用rpc。

并不是任何service都可以加載到相同的container中,這對service的隔離是一個很大的限制,實際使用中我們會綜合考慮服務的穩定性、開發、維護等因素。

特型搜索服務在線架構

其他

在線服務往往會對流量進行抽樣然后進行小流量實驗,每一個實驗其實就是一個不同的service,流量分發時除了service id,可能還會考慮到抽樣id,原理大致差不多。一些通用組件的開發這里先不做描述了。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!