十分鐘看懂!基於gRPC的Faiss server實踐,細

Faiss簡介

Faiss是Facebook AI團隊開源的針對聚類和相似性搜索的開源庫,爲稠密向量提供高效相似度搜索服務,支持十億級別向量的搜索,是目前最爲成熟的近似近鄰搜索庫之一。Faiss提供了多種索引類型如L2距離,向量內積等,詳細介紹可參考Faiss Indexes,我們可以針對不同大小的向量集和聚類算法選擇合適的索引類型。

在mx推薦服務中,目前主要藉助Faiss做相似性召回和打分。我們將不同的算法產出item和user的embedding數據,加載到Faiss的索引中,便可實現諸如item-based/user-based等算法。同時,也可以根據業務場景需要,利用Faiss給相應的結果進行打分排序。

服務選型

最初,MX的Faiss server是根據一個開源的用flask框架開發的web server(faiss-web-service)改進而來的,爲了儘快推上線做實驗,並沒有進行詳細地設計和優化,因此線上表現不算太好,平均響應時間大概爲6ms左右,另外擴展性較差,在添加或刪除索引時在很多方面受到框架限制。經充分調研和技術選型,最終決定用RPC框架進行重構。

目前開源的RPC框架有很多,例如Thrift,Dubbo,gRPC,rpcx等,由於mx推薦服務是用java語言開發的,而Faiss目前只支持C++和Python兩種語言,考慮到多語言支持的需求,只有gRPC和Thrift滿足要求。在調研中發現(參考《流行的rpc框架benchmark》),在處理10ms級業務時,在吞吐和延遲方面,gRPC較Thrift有一定的優勢,故最終選擇了gRPC框架。

需求分析

1、多類型向量加載

不同算法產出的向量以算法名+item類型+產出的時間戳(版本號)的命名方式存儲在s3上,例如S3上鍵爲deepwalk/movie/index.20190821_043002_64的文件是用deepwalk算法產出的movie類型的item的向量。Faiss server需要能加載各個類型的向量構建索引,並根據請求中的算法類型和item類型從對應的索引中進行相似性搜索。

2、多類型索引試驗

Faiss提供了豐富的索引類型,我們可以將同一類型的向量加載成不同類型的索引來進行小流量試驗,由此來找出效果最好的一組向量類型與索引類型的映射,這裏的效果包括推薦結果的指標,Faiss server內存佔用率,CPU使用率以及響應時間等。

3、索引方便配置

因爲每個索引的線上表現並不相同,隨着試驗的進行,有些索引需要被下掉,新的算法產出的向量也需要及時做試驗,所以Faiss server需要能夠靈活地配置索引。

4、索引版本控制

我們在產出item向量的同時也會產出user的向量,推薦系統可以用user向量去召回同一向量空間下的item,但是user向量存儲在pika裏,產出也會滯後於item向量,也有可能因爲某些原因user向量的的寫入出現問題,導致推薦系統取出的是舊向量,而Faiss server已經更新成新的索引,因此Faiss server需要能加載同一類型多個版本的向量,這樣推薦系統就可以通過指定版本來獲取同一向量空間裏與user向量近鄰的item。

5、索引熱更新

不同算法產出的向量每天會不定時的進行更新,Faiss server除了在服務啓動時加載最新向量構建索引外,還需要及時用最新的向量更新索引,同時也要能夠正常響應請求。

設計與實現

1. Faiss Server

1.1 組織結構

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

上圖爲Faiss server的組織結構圖。爲了滿足需求1,2,4,我們將Faiss索引封裝在FaissHandler中。FaissHandler包含了5個字段,algorithm_type, category和index_type唯一指定了一個index,由此可以方便支持多向量類型和多索引類型,而index_dict保存了真正的Faiss索引,以向量(索引)版本爲key,Faiss索引爲value,由此可以方便地支持指定版本的最近鄰搜索服務,另外還有一個latest_version字段,用來保存index_dict中最新的版本號,當不指定向量版本進行搜索時將會使用最新版本的索引搜索。

我們可以在HandlerCollection裏配置當前需要使用的FaissHandler,索引的更新程序可以通過遍歷collection逐個更新索引。另外也滿足了需求3,添加、更改或刪除都只需要修改一行代碼即可,非常方便。而gRPC服務只需要將請求中的參數進行組裝,然後路由到對應的handler就行,剩下的操作都由handler完成。

1.2 接口設計

Faiss server需要提供一個搜索最近鄰item的RPC接口search,search接口的請求中需要指明具體使用哪個索引,以及目標item的id或者向量,請求體的主要字段如下:

message SearcRequest {
    string algorithmType = 1;
    string category = 2;
    int32 num = 3;
    string indexType = 4;
    repeated string itemId = 5;
    repeated FloatArray vector = 6;
}

SearchRequest中前四個字段是必須的,因爲依賴這四個字段來查找handler,而itemId和vector至少需要一個存在。itemId和vector都用repeated修飾,是爲了支持多item,多vector搜索。

message SearchResponse {
    message Str2FloatMap {    
        map<string, float> innerMap = 1;
    }
    map<string, Str2FloatMap> similarItems = 1;
} 

response很簡單,只有一個map,key爲目標item或vector(SearchRequest中的),value則爲最近鄰結果和對一個的分數。

1.3 search流程

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

2. 索引更新

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

上圖爲索引更新的架構圖,celery beat每隔2分鐘創建一個更新索引的task,要求更新HandlerCollection中的所有handler對應的索引,worker接收到task之後從AWS S3上下載對應的向量文件,加載向量構建索引,然後將索引序列化到文件,Faiss server提供gRPC接口來接收celery的通知(調用),接到通知後直接加載索引文件更新索引。向量的下載和索引的構建都是非常耗時的操作,交由celery完成能夠節省server的資源,保證server穩定高效地運行。詳細的索引更新流程如下圖:

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

上圖講述了詳細的更新流程,但是爲了流程的流暢性和可讀性隱藏了一些細節。更新操作並非是單線程一路走下來的,因爲我們有很多索引,爲了儘快更新索引提供服務,在遍歷handler collection時,會爲每個handler創建一個檢查version的celery task,然後celery worker會併發地去執行這些task,針對每一個task,如果真的需要更新,同樣創建一個download vector的task併發執行,但是這裏有一個問題,就是celery beat每隔2分鐘創建一條更新索引的task,但是下載向量是非常耗時的操作,因爲向量文件可能很大,如果上一次的更新操作還處於download vector執行中,而這一次也走到了download vector這一步,這樣就造成了同時有多個worker在下載相同的向量,這樣不僅造成了worker資源的浪費,還造成了網絡IO的浪費,所以,在真正下載向量之前,會先嚐試獲取一個針對具體向量的鎖,如果無法獲得鎖,說明有worker正在執行download vector task,當前worker直接return,結束任務,等待接收其他task。下載完向量之後也是併發的加載索引和通知server更新。

部署

Faiss server是用python語言開發的,由於python具有全局解釋器鎖無法有效地利用多核CPU,所以我們採用了單機多服務實例的模式進行部署。

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

線上表現

在上線之前進行了充分的壓力測試,結果表明單機QPS比之前高了2倍以上,響應時間降低了大約67%。新服務上線前後在newrelic上的響應時間對比如下圖:

十分鐘看懂!基於gRPC的Faiss server實踐,細

 

總結

基於gRPC的Faiss server是針對業務需求高度定製化的服務,有效地解決了原有服務所存在的各種問題,具有高效率,高可擴展性等特點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章