得物向量數(shù)據(jù)庫(kù)落地實(shí)踐
一、背景
二、認(rèn)識(shí)向量數(shù)據(jù)庫(kù)
1. 向量數(shù)據(jù)來(lái)源和存儲(chǔ)
2. 向量數(shù)據(jù)庫(kù)是如何工作
三、向量數(shù)據(jù)庫(kù)對(duì)比傳統(tǒng)數(shù)據(jù)庫(kù)
四、如何選擇向量數(shù)據(jù)庫(kù)
1. 向量數(shù)據(jù)庫(kù)比較
2. 選擇流行的索引
3. 綜合比較和選擇
4. 得物選擇Milvus作為向量數(shù)據(jù)庫(kù)
五、Milvus在得物的實(shí)踐
1. 部署架構(gòu)演進(jìn)
2. 獨(dú)立資源池遷移至共享資源池
3. 引入Zilliz
六、向量數(shù)據(jù)庫(kù)運(yùn)維沉淀
1. 索引結(jié)構(gòu)和搜索原理
2. 并不是你想的那樣
3. 錯(cuò)誤處理
七、未來(lái)展望
一、背景
信息通信技術(shù)(ICT)正經(jīng)歷著前所未有的變革浪潮,以大模型和生成式人工智能(GenAI)為代表的技術(shù)突破,正在引發(fā)全球產(chǎn)業(yè)體系的深刻變革,成為驅(qū)動(dòng)企業(yè)技術(shù)架構(gòu)革新和商業(yè)模式轉(zhuǎn)型的關(guān)鍵引擎。
得物是廣受年輕人喜愛(ài)的品質(zhì)生活購(gòu)物社區(qū)。在AI鑒別、圖搜、算法、安全風(fēng)控等場(chǎng)景下都廣泛使用啦GenAI技術(shù)。
向量數(shù)據(jù)庫(kù)作為GenAI的基礎(chǔ)設(shè)施之一,通過(guò)量化的高維空間數(shù)據(jù)結(jié)構(gòu)(如HNSW算法),實(shí)現(xiàn)對(duì)嵌入向量(Embeddings Vector)的高效存儲(chǔ)、索引和最近鄰搜索(ANN),支撐包括多模態(tài)數(shù)據(jù)表征在內(nèi)的復(fù)雜智能應(yīng)用。
二、認(rèn)識(shí)向量數(shù)據(jù)庫(kù)
向量數(shù)據(jù)來(lái)源和存儲(chǔ)
圖片
一般向量數(shù)據(jù)庫(kù)中向量的來(lái)源是將圖片、音頻、視頻、文本等非結(jié)構(gòu)化數(shù)據(jù),將這些非結(jié)構(gòu)化數(shù)據(jù)通過(guò)對(duì)應(yīng)的量化算法計(jì)算出一個(gè)多維度的向量(生產(chǎn)使用一般向量維度會(huì)大于512),并且將向量數(shù)據(jù)持久化在特定的存儲(chǔ)上。
向量數(shù)據(jù)庫(kù)是如何工作
圖片
向量數(shù)據(jù)庫(kù)在查詢(xún)的時(shí)候一般會(huì)將需要查詢(xún)的非結(jié)構(gòu)化數(shù)據(jù)通過(guò)量化,計(jì)算成一個(gè)多維度向量數(shù)據(jù),然后在數(shù)據(jù)庫(kù)中搜索出和查詢(xún)向量相似的數(shù)據(jù)。(需要注意的是這邊查詢(xún)的是相似的數(shù)據(jù)而不是相同的數(shù)據(jù))。
三、向量數(shù)據(jù)庫(kù)對(duì)比傳統(tǒng)數(shù)據(jù)庫(kù)
圖片
圖片
向量數(shù)據(jù)庫(kù)在數(shù)據(jù)結(jié)構(gòu)、檢索方法、擅長(zhǎng)領(lǐng)域與傳統(tǒng)數(shù)據(jù)庫(kù)有很大的不同。
傳統(tǒng)數(shù)據(jù)庫(kù)
結(jié)構(gòu)是處理離散的標(biāo)量數(shù)據(jù)類(lèi)型(例如數(shù)字和字符串),并通過(guò)行和列來(lái)表達(dá)組織數(shù)據(jù)(就是一個(gè)表格)。傳統(tǒng)數(shù)據(jù)庫(kù)主要為了解決結(jié)構(gòu)化數(shù)據(jù)的精確管理和高效查詢(xún)問(wèn)題。并且傳統(tǒng)數(shù)據(jù)庫(kù)通過(guò)B樹(shù)索引、哈希索引等數(shù)據(jù)結(jié)構(gòu),能夠快速定位到精確匹配的記錄。更重要的是,傳統(tǒng)數(shù)據(jù)庫(kù)通過(guò)ACID事務(wù)特性(原子性、一致性、隔離性、持久性)確保了在數(shù)據(jù)中數(shù)據(jù)的絕對(duì)準(zhǔn)確性。
向量數(shù)據(jù)庫(kù)
為了解決非結(jié)構(gòu)化數(shù)據(jù)的語(yǔ)義搜索問(wèn)題,解決如何在海量的高維向量數(shù)據(jù)中,快速找到與查詢(xún)向量最相似的結(jié)果。比如在推薦系統(tǒng)中找到與用戶(hù)喜好相似的物品,或在圖像庫(kù)中檢索出與查詢(xún)圖片最相近的圖片。這類(lèi)問(wèn)題的特點(diǎn)是:
- 查詢(xún)的不是精確匹配,而是相似度排名。
- 數(shù)據(jù)維度極高(通常128-2048維)。
- 數(shù)據(jù)規(guī)模龐大(可能達(dá)到十億級(jí)別)。
傳統(tǒng)數(shù)據(jù)庫(kù)的精確查詢(xún)方式在這種場(chǎng)景下完全失效,因?yàn)椋?/p>
- 無(wú)法為高維向量建立有效的B樹(shù)索引。
- 計(jì)算全量數(shù)據(jù)的精確相似度代價(jià)過(guò)高。
- 無(wú)法支持"相似但不完全相同"的搜索需求。
四、如何選擇向量數(shù)據(jù)庫(kù)
向量數(shù)據(jù)庫(kù)比較
下面我們通過(guò)10個(gè)不同維度來(lái)比較一下不同向量數(shù)據(jù)庫(kù)的區(qū)別:
圖片
從上面表格可以看到:
- 自 2016 年起 ,向量數(shù)據(jù)庫(kù)逐漸嶄露頭角,成為 AI 和大數(shù)據(jù)領(lǐng)域的重要基礎(chǔ)設(shè)施。而到了 2021 年之后 ,隨著深度學(xué)習(xí)、大模型和推薦系統(tǒng)的迅猛發(fā)展,向量數(shù)據(jù)庫(kù)正式邁入爆發(fā)式增長(zhǎng)時(shí)代 ,成為現(xiàn)代數(shù)據(jù)架構(gòu)中不可或缺的核心組件。
- 超過(guò)半數(shù)的向量數(shù)據(jù)庫(kù)均采用分布式架構(gòu)設(shè)計(jì),并且這些支持分布式部署的系統(tǒng)普遍具備彈性擴(kuò)縮容能力,能夠根據(jù)業(yè)務(wù)需求實(shí)現(xiàn)資源的動(dòng)態(tài)調(diào)整。
- 當(dāng)業(yè)務(wù)需要處理億級(jí)甚至更高規(guī)模的向量數(shù)據(jù)時(shí),推薦以下高性能、可擴(kuò)展的向量數(shù)據(jù)庫(kù):Vespa、Milvus/Zilliz、Vald、Qdrant。
- 當(dāng)前主流的向量數(shù)據(jù)庫(kù)普遍采用模塊化、插件式的設(shè)計(jì)理念。其核心引擎大多基于 C/C++ 開(kāi)發(fā),以追求極致的性能表現(xiàn)。與此同時(shí),Go 和 Rust 也正在這一領(lǐng)域嶄露頭角。
- 在向量數(shù)據(jù)庫(kù)領(lǐng)域,NHSW(Hierarchical Navigable Small-World)和 DiskANN 正逐漸成為主流索引方案。其中NHSW主要以?xún)?nèi)存搜索為主,DiskANN主要以磁盤(pán)搜索為主。值得注意的是,Qdrant 在優(yōu)化 NHSW 的基礎(chǔ)上,進(jìn)一步實(shí)現(xiàn)了 基于磁盤(pán)的 NHSW 檢索能力。
選擇流行的索引
在向量數(shù)據(jù)庫(kù)技術(shù)領(lǐng)域,有NHSW 和 DiskANN 作為兩大主流索引方案,各自展現(xiàn)了獨(dú)特的技術(shù)優(yōu)勢(shì)。我們從以下關(guān)鍵維度進(jìn)行專(zhuān)業(yè)對(duì)比分析。
圖片
從上表格我們可以得到,NHSW和DiskANN適用于不同的場(chǎng)景:
- NHSW :以 內(nèi)存優(yōu)先 的設(shè)計(jì)實(shí)現(xiàn)高性能搜索,適合對(duì) 低延遲、高吞吐 要求嚴(yán)格的場(chǎng)景,如實(shí)時(shí)推薦、廣告檢索等。
- DiskANN :以 磁盤(pán)存儲(chǔ)優(yōu)化 為核心,在保證較高召回率的同時(shí) 顯著降低硬件成本 ,適用于大規(guī)模數(shù)據(jù)下的經(jīng)濟(jì)型檢索需求。
隨著數(shù)據(jù)規(guī)模的持續(xù)增長(zhǎng),NHSW 和 DiskANN 的混合部署模式 或?qū)⒊蔀樾袠I(yè)標(biāo)準(zhǔn),讓用戶(hù)能根據(jù)業(yè)務(wù)需求靈活選擇 "極致性能" 或 "最優(yōu)成本" 的檢索策略。
綜合比較和選擇
圖片
從表格中可以得到:
- 如果數(shù)據(jù)流比較小,并且自身對(duì)Redis、PG、ES比較熟悉,就可以選擇Redis、PG、ES。如DBA團(tuán)隊(duì)就比較適合。
- 如果數(shù)據(jù)量比較大,并且前期人力不足可以使用云托管方案。選擇Zilliz、Pinecone、Vespa或者Qdrant,如果后期計(jì)劃從云上遷移到自建可以選擇Zilliz、Vespa或者Qdrant。
得物選擇Milvus作為向量數(shù)據(jù)庫(kù)
我們的需求
社區(qū)圖搜和AI鑒別需要大量的數(shù)據(jù)支持,得物業(yè)務(wù)場(chǎng)景要求能支持十億級(jí)向量數(shù)據(jù)搜索,有如下要求:
- 大數(shù)據(jù)量高性能搜索,RT需要在90ms以?xún)?nèi)。
- 大數(shù)據(jù)量但是性能要求不高時(shí),RT滿(mǎn)足500ms以?xún)?nèi)。
需要支持快速擴(kuò)縮容:
滿(mǎn)足上面2點(diǎn)就已經(jīng)鎖定在Milvus、Qdrant這兩個(gè)向量數(shù)據(jù)庫(kù)。如果從架構(gòu)復(fù)雜度和維護(hù)/學(xué)習(xí)成本的角度考慮,我們應(yīng)該優(yōu)先選擇Qdrant,因?yàn)樗募軜?gòu)相比Milvus沒(méi)有那么復(fù)雜,并且維護(hù)/學(xué)習(xí)成本沒(méi)有Milvus高,重要的Qdrant可以單獨(dú)集群部署,不需要k8s技術(shù)棧的支撐。
Milvus 和 Qdrant 架構(gòu)比較
Milvus架構(gòu)
Milvus部署依賴(lài)許多外部組件,如存儲(chǔ)元信息的ETCD、存儲(chǔ)使用的MinIO、消息存儲(chǔ)Pulasr 等等。
圖片
Qdrant
Qdrant完全獨(dú)立開(kāi)發(fā),支持集群部署,不需要借助ETCD、Pulsar等組件。

選擇Milvus的原因
※ 業(yè)務(wù)發(fā)展需求
業(yè)務(wù)屬于快速發(fā)展階段,數(shù)量的變化導(dǎo)致擴(kuò)縮容頻繁,使用支持k8s的Milvus在擴(kuò)縮容方面會(huì)比Qdrant快的多。
※ 技術(shù)儲(chǔ)備和社區(qū)良好
對(duì)DBA而言,向量數(shù)據(jù)庫(kù)領(lǐng)域需要持續(xù)的知識(shí)更新和技術(shù)支持。從問(wèn)題解決效率來(lái)看,國(guó)內(nèi)技術(shù)社區(qū)對(duì)Milvus的支持體系相較于Qdrant更為完善。
※ 契合得物DBA開(kāi)發(fā)棧
Milvus使用的開(kāi)發(fā)語(yǔ)言是Go,契合DBA團(tuán)隊(duì)技術(shù)棧,在部分運(yùn)維場(chǎng)景下情,通過(guò)二次開(kāi)發(fā)滿(mǎn)足運(yùn)維需求。例如:使用milvus-backup工具做遷移,部分的segment有問(wèn)題需要跳過(guò)。自行修改一下代碼編譯運(yùn)行即可滿(mǎn)足需求。
圖片
五、Milvus在得物的實(shí)踐
部署架構(gòu)演進(jìn)
小試牛刀
初始階段,我們把Milvus部署在K8S上,默認(rèn)使用HNSW索引。架構(gòu)圖如下,Milvus整個(gè)架構(gòu)較為復(fù)雜,外部依賴(lài)的組件多,每個(gè)集群需要部署自己的 ETCD、ZK、消息隊(duì)列模塊,多套集群共享著同一個(gè)存儲(chǔ)。
圖片
存儲(chǔ)拆分,每個(gè)集群獨(dú)立存儲(chǔ)
共享存儲(chǔ)瓶頸導(dǎo)致穩(wěn)定性問(wèn)題凸顯。
隨著業(yè)務(wù)規(guī)模擴(kuò)展,集群數(shù)量呈指數(shù)級(jí)增長(zhǎng),我們觀測(cè)到部分集群節(jié)點(diǎn)出現(xiàn)異常重啟現(xiàn)象,經(jīng)診斷確認(rèn)該問(wèn)題源于底層共享存儲(chǔ)存在性能瓶頸。
圖片
圖片
獨(dú)立資源池遷移至共享資源池
通過(guò)混布的方式提升資源利用率。
前期為了在性能和穩(wěn)定性上更好的服務(wù)業(yè)務(wù),Milvus部署的底層機(jī)器都是獨(dú)立的,目的就是為了和其他應(yīng)用隔離開(kāi),不相互影響。但是隨著集群的越來(lái)越多,并不是所有的集群對(duì)穩(wěn)定性和性能要求那么高,從監(jiān)控上看Milvus集群池的資源使用不超過(guò)10%。為了提高公司資源利用率,我們將獨(dú)立部署的Milvus遷移高共享資源池中,和大數(shù)據(jù)、業(yè)務(wù)應(yīng)用等K8S部署相關(guān)服務(wù)進(jìn)行混合部署。
圖片
DiskANN索引的使用
數(shù)據(jù)量大且搜索QPS小時(shí)選擇DiskANN 作為索引。通過(guò)監(jiān)控發(fā)現(xiàn)有很多集群數(shù)據(jù)量比較大,但是QPS并不是那么高,這時(shí)候就考慮對(duì)這些性能要求不高的集群是否有降本的方案。通過(guò)了解我們默認(rèn)使用的HNSW索引需要將所有數(shù)據(jù)都加載到內(nèi)存中進(jìn)行搜索,第一反應(yīng)就是它的內(nèi)存查詢(xún)和Redis一樣,那是否有類(lèi)似pika的方案內(nèi)存只存少部分?jǐn)?shù)據(jù)大部分?jǐn)?shù)據(jù)存在磁盤(pán)上。這時(shí)候發(fā)現(xiàn)DiskANN就能達(dá)到這樣的效果。
性能壓測(cè)
※ 集群規(guī)格
圖片
QPS
延時(shí)(ms)
新增DiskANN索引后集群架構(gòu)
增加DiskANN后我們需要對(duì)相關(guān)服務(wù)器上掛載 NVME SSD 磁盤(pán),用于在磁盤(pán)上搜索最終數(shù)據(jù)。

DiskANN 加載數(shù)據(jù)過(guò)程
圖片
引入Zilliz
經(jīng)過(guò)大規(guī)模生產(chǎn)驗(yàn)證,Milvus在實(shí)際業(yè)務(wù)場(chǎng)景中展現(xiàn)出卓越的性能表現(xiàn)和穩(wěn)定性,獲得業(yè)務(wù)方的高度認(rèn)可。并且也吸引來(lái)了C端核心業(yè)務(wù)系統(tǒng)的使用。在使用前,我們使用了業(yè)務(wù)真實(shí)流量充分的對(duì)Milvus進(jìn)行了壓測(cè),發(fā)現(xiàn)Milvus在億級(jí)別數(shù)據(jù)量的情況下滿(mǎn)足不了業(yè)務(wù),因此對(duì)于部分核心場(chǎng)景我們使用了Zilliz。
Milvus和Zilliz 壓測(cè)
業(yè)務(wù)的要求是集群返回的RT不能操過(guò)90ms。
使用真實(shí)的業(yè)務(wù)數(shù)據(jù)(億級(jí)別)和業(yè)務(wù)請(qǐng)求對(duì)Milvus進(jìn)行壓測(cè),發(fā)現(xiàn)Milvus并不能滿(mǎn)足業(yè)務(wù)的需求。
類(lèi)型 | QPS | 平均RT(ms) | 客戶(hù)端性能圖 |
Milvus | 110 | 200 |
|
zilliz | 350 | 65 |
|
Milvus RT 200不滿(mǎn)足業(yè)務(wù)需求,并且QPS一直上不去,無(wú)論我們對(duì)QueryNode擴(kuò)容多大,其中還發(fā)生過(guò),將Query擴(kuò)容到60個(gè)后,反而RT上升的問(wèn)題,排查后是因?yàn)橛械腝ueryNode和Proxy交互的時(shí)候網(wǎng)絡(luò)會(huì)抖動(dòng)影響了整體的RT。
從上面可以看出就算業(yè)務(wù)能容忍RT=200ms的,Milvus也需要?jiǎng)?chuàng)建3個(gè)相同的集群提供業(yè)務(wù)訪問(wèn),并且業(yè)務(wù)需要改造代碼實(shí)現(xiàn)多寫(xiě)、多讀的功能,最終還會(huì)發(fā)現(xiàn)3個(gè)集群的成本遠(yuǎn)高于Zilliz。
通過(guò)成本和性能上的考慮,對(duì)于大數(shù)據(jù)量并且性能和穩(wěn)定性要求高場(chǎng)景,我們將選用Zilliz。
遷移方案
對(duì)于不同業(yè)務(wù)場(chǎng)景,我們分別制定了以下3種遷移方案:
方案1:業(yè)務(wù)自行導(dǎo)入數(shù)據(jù)使用

方案2:備份恢復(fù) + 業(yè)務(wù)增量

方案3:全量 + 增量 + 業(yè)務(wù)雙寫(xiě)/回滾

高可用架構(gòu)部署
隨著業(yè)務(wù)關(guān)鍵性持續(xù)提升,Milvus對(duì)應(yīng)的SLA變得越來(lái)越重要。在此背景下,構(gòu)建完善的Milvus高可用架構(gòu)與災(zāi)備體系已成為系統(tǒng)設(shè)計(jì)的核心考量要素。比如:主從、多zone部署,Proxy高可用,Minio高可用,一個(gè)zone完全掛了怎么辦等問(wèn)題?
方案1: 同城多機(jī)房混部
正常訪問(wèn):
- 該方案會(huì)有客戶(hù)端會(huì)有跨機(jī)房訪問(wèn)的情況。
跨機(jī)房訪問(wèn)節(jié)點(diǎn):
客戶(hù)端 -> SLB
SLB -> Proxy
Proxy -> QueryNode
- SLB有高可用
- Proxy有高可用
圖片
當(dāng)部分節(jié)點(diǎn)不可用:
- 當(dāng)zone 1中的proxy 1不可用,不影響整個(gè)訪問(wèn)鏈路,其他Proxy依然可以接受請(qǐng)求。
- 當(dāng) zone 1 中的 QueryNode 1 不可用,會(huì)出現(xiàn)訪問(wèn)報(bào)錯(cuò)的問(wèn)題。需要重建QueryNode1,有可能在 Zone 2 新建QueryNode 5,原本請(qǐng)求QueryNode 1 的流量會(huì)重新指向 QueryNode 5。
圖片
當(dāng) Zone1 不可用:
- 訪問(wèn)會(huì)切換到 Zone 2 的備用SLB中。
- 備用SLB會(huì)訪問(wèn)本機(jī)房的Proxy。
- 由于 QueryNode 1 和 QueryNode 2 已經(jīng)不可用,需要重建QueryNode,新生成 QueryNode 5、QueryNode 6并且加載數(shù)據(jù)提供訪問(wèn)。

方案2: 同城多zone多副本就近訪問(wèn)
正常訪問(wèn):
- 不同zone的客戶(hù)端訪問(wèn)本地的SLB。
- 使用了QueryNode多副本特性,各自zone的QueryNode都加載了所有數(shù)據(jù)。
Proxy -> QueryNode 的訪問(wèn),目前Proxy只能隨機(jī)訪問(wèn)所有zone的QueryNode(這是Milvus的限制)

當(dāng)部分節(jié)點(diǎn)不可用:
- 當(dāng)每個(gè)zone 都有1個(gè)Proxy故障,并不影響業(yè)務(wù)正常訪問(wèn)。
- 由于QueryNode開(kāi)啟了副本,只要每個(gè)zone不相同的QueryNode故障,集群還是能正常運(yùn)行。需要注意的是這時(shí)候需要考慮剩下的QueryNode性能是否滿(mǎn)足需求。所以一般業(yè)務(wù)需要有限流功能,在剩余的QueryNode不滿(mǎn)足需求時(shí),業(yè)務(wù)需要限流,直到其他QueryNode恢復(fù)。

整個(gè)Zone不可用:
- 當(dāng)Zone1整個(gè)不可用,不影響Zone2的訪問(wèn)。

方案3: 同城多zone單獨(dú)部署業(yè)務(wù)交叉訪問(wèn)互相backup
正常訪問(wèn):
- 每個(gè)zone都有單獨(dú)部署的milvus集群。
- 每個(gè)集群的有同時(shí)滿(mǎn)足 業(yè)務(wù)1、業(yè)務(wù)2 訪問(wèn)的數(shù)據(jù)。
- 業(yè)務(wù)訪問(wèn)Proxy的時(shí)候是有交叉訪問(wèn)的情況
- 業(yè)務(wù)改造會(huì)比較多,需要實(shí)現(xiàn)雙寫(xiě)。
圖片
當(dāng)部分節(jié)點(diǎn)不可用:
- 當(dāng)Zone1中的Proxy1部可用,不會(huì)影響Zone1的整個(gè)集群訪問(wèn)。
- 當(dāng)Zone1的QueryNode1不可用,會(huì)影響到線(xiàn)路1、2的正常訪問(wèn),這時(shí)候業(yè)務(wù)需要切換不訪問(wèn)Zone1的SLB。
圖片
當(dāng)整個(gè)zone不可用
- 整個(gè)zone1不可用,由于線(xiàn)路1會(huì)訪問(wèn)到zone1的SLB,因此線(xiàn)路1訪問(wèn)會(huì)報(bào)錯(cuò),業(yè)務(wù)需要將線(xiàn)路1切換成線(xiàn)路2。
圖片
六、向量數(shù)據(jù)庫(kù)運(yùn)維沉淀
索引結(jié)構(gòu)和搜索原理
NHSW 索引
※ 相關(guān)信息

※ 內(nèi)存結(jié)構(gòu)
由于空間問(wèn)題,圖中并沒(méi)有完全按 M=16、ef=200 參數(shù)進(jìn)行畫(huà)圖。
圖片
※ NHSW搜索過(guò)程
現(xiàn)在需要搜索向量N = [....]
第一步:
在第一層隨機(jī)選擇一個(gè)節(jié)點(diǎn),如:3。
圖片
第二步:
- 。通過(guò)節(jié)點(diǎn)3,從第1層轉(zhuǎn)跳到第2層
- 在第2層,通過(guò)節(jié)點(diǎn)3獲取到相鄰的節(jié)點(diǎn):節(jié)點(diǎn)1、節(jié)點(diǎn)2、節(jié)點(diǎn)5、節(jié)點(diǎn)6。
- 將搜索節(jié)點(diǎn)N逐個(gè)和 節(jié)點(diǎn)1、節(jié)點(diǎn)2、節(jié)點(diǎn)5、節(jié)點(diǎn)6 進(jìn)行計(jì)算出各自的距離。并且選擇距離最短的節(jié)點(diǎn)6。
節(jié)點(diǎn)N -> 節(jié)點(diǎn)1:10
節(jié)點(diǎn)N -> 節(jié)點(diǎn)2:6
節(jié)點(diǎn)N -> 節(jié)點(diǎn)5:9
節(jié)點(diǎn)N -> 節(jié)點(diǎn)6:3
節(jié)點(diǎn)N -> 節(jié)點(diǎn)3:4
- 將 節(jié)點(diǎn)1、節(jié)點(diǎn)2、節(jié)點(diǎn)5、節(jié)點(diǎn)6、節(jié)點(diǎn)3 放入結(jié)果候選集中。
第三步:
- 。通過(guò)節(jié)點(diǎn)6,從第二層跳到底3層
- 在第3層,通過(guò)節(jié)點(diǎn)6獲取到相鄰的節(jié)點(diǎn):節(jié)點(diǎn)2、節(jié)點(diǎn)3、節(jié)點(diǎn)6、節(jié)點(diǎn)9,其中 節(jié)點(diǎn)2、節(jié)點(diǎn)3、節(jié)點(diǎn)6 已經(jīng)存在,因此只需要將 節(jié)點(diǎn)9 放入候選結(jié)果集。
- 如果候選結(jié)果集合沒(méi)有滿(mǎn),則繼續(xù)便利 節(jié)點(diǎn)2、節(jié)點(diǎn)3、節(jié)點(diǎn)9 的鄰居,直到 節(jié)點(diǎn)數(shù)=ef=200。

DiskANN 索引
※ 相關(guān)信息

※ 存儲(chǔ)結(jié)構(gòu)和裁剪過(guò)程
由于畫(huà)圖空間問(wèn)題,沒(méi)辦法將 聚類(lèi)數(shù)=10、100/內(nèi)存聚類(lèi)、1w/磁盤(pán)聚類(lèi) 信息畫(huà)全。
圖片
- 初始化隨機(jī)連接:DiskANN算法會(huì)將向量數(shù)據(jù)生成一個(gè)密集的網(wǎng)絡(luò)圖,其中點(diǎn)和點(diǎn)是隨機(jī)鏈接的,并且每個(gè)點(diǎn)大概有500個(gè)鏈接。
- 裁剪冗余鏈接:通過(guò)計(jì)算點(diǎn)和點(diǎn)點(diǎn)距離裁剪掉一些冗余的鏈接,留下質(zhì)量高的鏈接。
- 添加快速導(dǎo)航鏈接:計(jì)算出圖中若干個(gè)中心點(diǎn),并且將這些中心點(diǎn)進(jìn)行鏈接,并且這些鏈接會(huì)跳過(guò)其他點(diǎn),如果圖中黃色鏈接。
- 重復(fù)進(jìn)行裁剪優(yōu)化過(guò)程,達(dá)到最優(yōu)的情況。
- PQ量化操作生成索引:
將向量分成多個(gè)子空間。
獨(dú)立對(duì)每個(gè)子空間進(jìn)行聚類(lèi)操作,并且計(jì)算出多個(gè)質(zhì)心。
將每個(gè)子向量映射到最近的質(zhì)心ID。
※ DiskANN搜索過(guò)程
現(xiàn)在需要搜索向量N = [....]
第一步(內(nèi)存索引中搜索):
將搜索的向量進(jìn)行量化。
將量化后的數(shù)據(jù)在內(nèi)存中索引尋找到離自己較近的質(zhì)心為入口進(jìn)行下一步搜索。
圖片
第二步(內(nèi)存中搜索):
通過(guò)第一個(gè)節(jié)點(diǎn),尋找到它的所有相鄰節(jié)點(diǎn)。
通過(guò)內(nèi)存PQ代碼計(jì)算搜索節(jié)點(diǎn)和相鄰節(jié)點(diǎn)的近似距離。
這些鄰居節(jié)點(diǎn)都是潛在的下次搜索候選口節(jié)點(diǎn)。
圖片
第三步(磁盤(pán)中搜索):
通過(guò)計(jì)算搜索節(jié)點(diǎn)和相鄰節(jié)點(diǎn)的真實(shí)距離,并且得到距離最近的一個(gè)節(jié)點(diǎn)(需要從磁盤(pán)上讀取證實(shí)的節(jié)點(diǎn)數(shù)據(jù)并計(jì)算)。
并且通過(guò)得到的最新節(jié)點(diǎn)獲取到新的相鄰節(jié)點(diǎn)。
圖片
第四步(磁盤(pán)中搜索):
反復(fù)先進(jìn)二、三步驟操作,直到找到足夠數(shù)量的鄰居。
圖片
并不是你想的那樣
querynode 越多越快?
querynode 越多,查詢(xún)?cè)娇?,并發(fā)越高?
※誤區(qū)原因
將querynode看成redis cluster,增加節(jié)點(diǎn)數(shù)能提高查詢(xún)并發(fā),然而并不是。redis cluster 增加節(jié)點(diǎn),數(shù)據(jù)量會(huì)盡可能的打散到每個(gè)節(jié)點(diǎn)中,所以增加節(jié)點(diǎn)和性能提升是相對(duì)成正比。但是milvus不一樣,milvus打散的基本單位是segment,一般segment大小(1G/個(gè)),他的粒度比redis cluster要大。理論上的理想情況是1個(gè)segment對(duì)應(yīng)1個(gè)querynode,但是實(shí)際情況會(huì)收到多因素的干擾,會(huì)導(dǎo)致querynode越多出現(xiàn)不穩(wěn)定的概率越大,如某個(gè)querynode網(wǎng)絡(luò)抖動(dòng)會(huì)影響整體的查詢(xún)RT。
不能提升性能例1:
部分segment數(shù) < querynode數(shù),或部分querynode沒(méi)有任何segment。
圖片
不能提升性能例2:
querynode 過(guò)多,其中1個(gè)querynode RT 高,導(dǎo)致整體客戶(hù)端RT高。

標(biāo)量索引提高性能
在標(biāo)量上創(chuàng)建索引,搜索帶上標(biāo)量過(guò)來(lái)能提高性能?
※誤區(qū)原因
使用傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)的索引查詢(xún)來(lái)理解Milvus的索引查詢(xún),字段上創(chuàng)建了索引能使用到索引掃描進(jìn)行數(shù)據(jù)查詢(xún),比全表掃描快。然而并不是,關(guān)系型數(shù)據(jù)庫(kù)的屬于精確查詢(xún),Milvus屬于近似最近鄰搜索(ANNS),milvus的查詢(xún)是不保證絕對(duì)精確,使用了標(biāo)量索引查詢(xún)反而會(huì)導(dǎo)致數(shù)據(jù)變稀疏查詢(xún)會(huì)變慢。
使用標(biāo)量索引篩選不一定快原因,如下示例:
通過(guò)標(biāo)量搜索后再使用ANNS搜索過(guò)程:
1.Collection A 中的數(shù)據(jù)如下,其中is_delete 是標(biāo)量,其中的值有0和1。
圖片
2.使用ANNS搜索
使用ANNS搜索能直接很快地獲取到滿(mǎn)足條件的數(shù)據(jù)。
圖片
3.再使用 ANNS 搜索獲取最終數(shù)據(jù)。
圖片
思考:
在第二步如果使用ANNS搜索完成之后到底是否需要使用標(biāo)量索引進(jìn)行搜索。
如果需要使用標(biāo)量索引進(jìn)行搜索那邊在ANNS搜索后的結(jié)果集需要額外的進(jìn)行索引構(gòu)建,然后再進(jìn)行過(guò)濾。構(gòu)建構(gòu)建過(guò)程其實(shí)也是需要便利結(jié)果集,那么是不是可以直接在便利的時(shí)候直接進(jìn)行結(jié)果集的篩選。
那么其實(shí)在某種程度上是不是標(biāo)量索引沒(méi)那么好用。
大量單行dml不批量寫(xiě)入能提高數(shù)據(jù)庫(kù)性能
大量單行dml,不使用批量寫(xiě)入操作,能提高數(shù)據(jù)庫(kù)性能。
※誤區(qū)原因
使用傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)為了讓系統(tǒng)盡量少的大事務(wù),減少鎖問(wèn)題并且提高數(shù)據(jù)庫(kù)性能。然而實(shí)際上Milvus如果有很多的小事務(wù)反而會(huì)影響到數(shù)據(jù)庫(kù)的性能。因?yàn)镸ilvus進(jìn)行dml操作會(huì)生成deltalog、insertlog,當(dāng)dml都是小事務(wù)就會(huì)生成大量的相對(duì)較小的deltalog和insertlog文件,deltalog和insertlog在和segment做合并的時(shí)候會(huì)增加打開(kāi)和關(guān)閉文件次數(shù),并且增加做合并次數(shù),導(dǎo)致io一直處于繁忙狀態(tài)。
deltalog 和 insertlog 生成的契機(jī)有2種:
- 當(dāng)數(shù)據(jù)量達(dá)到了一定的閾值會(huì)進(jìn)行生成deltalog 或 insertlog。
- Milvus會(huì)定時(shí)進(jìn)行生成deltalog 或 insertlog。
eltalog、insertlog 和 segment 合并過(guò)程
圖片
人為讓 deltalog、segment 執(zhí)行時(shí)機(jī)可預(yù)測(cè)
如果業(yè)務(wù)對(duì)數(shù)據(jù)是實(shí)現(xiàn)要求不是那么高,建議使用定時(shí)批量的方式對(duì)數(shù)據(jù)進(jìn)行寫(xiě)入,比如可以通過(guò)監(jiān)控獲取到每天的波谷時(shí)間段,在波谷時(shí)間段內(nèi)進(jìn)行集中式數(shù)據(jù)寫(xiě)入。原因是如果不停的在做寫(xiě)入,無(wú)法判斷進(jìn)行合并segment的時(shí)間點(diǎn),要是在高峰期進(jìn)行了合并操作,很有可能會(huì)影響到集群性能。
錯(cuò)誤處理
※ 2.2.6 批量刪除數(shù)據(jù)bug,導(dǎo)致業(yè)務(wù)無(wú)法查詢(xún)
報(bào)錯(cuò):
pymilvus.exceptions.MilvusException: <MilvusException: (code=1, message=syncTimestamp Failed:err: find no available rootcoord, check rootcoord state
, /go/src/github.com/milvus-io/milvus/internal/util/trace/stack_trace.go:51 github.com/milvus-io/milvus/internal/util/trace.StackTrace
/go/src/github.com/milvus-io/milvus/internal/util/grpcclient/client.go:329 github.com/milvus-io/milvus/internal/util/grpcclient.(*ClientBase[...]).ReCall
/go/src/github.com/milvus-io/milvus/internal/distributed/rootcoord/client/client.go:421 github.com/milvus-io/milvus/internal/distributed/rootcoord/client.(*Client).AllocTimestamp
/go/src/github.com/milvus-io/milvus/internal/proxy/timestamp.go:61 github.com/milvus-io/milvus/internal/proxy.(*timestampAllocator).alloc
/go/src/github.com/milvus-io/milvus/internal/proxy/timestamp.go:83 github.com/milvus-io/milvus/internal/proxy.(*timestampAllocator).AllocOne
/go/src/github.com/milvus-io/milvus/internal/proxy/task_scheduler.go:172 github.com/milvus-io/milvus/internal/proxy.(*baseTaskQueue).Enqueue
/go/src/github.com/milvus-io/milvus/internal/proxy/impl.go:2818 github.com/milvus-io/milvus/internal/proxy.(*Proxy).Search
/go/src/github.com/milvus-io/milvus/internal/distributed/proxy/service.go:680 github.com/milvus-io/milvus/internal/distributed/proxy.(*Server).Search
/go/pkg/mod/github.com/milvus-io/milvus-proto/go-api@v0.0.0-20230324025554-5bbe6698c2b0/milvuspb/milvus.pb.go:10560 github.com/milvus-io/milvus-proto/go-api/milvuspb._MilvusService_Search_Handler.func1
/go/src/github.com/milvus-io/milvus/internal/proxy/rate_limit_interceptor.go:47 github.com/milvus-io/milvus/internal/proxy.RateLimitInterceptor.func1
)>解決:將集群升級(jí)到2.2.16,并且讓業(yè)務(wù) 批量刪除和寫(xiě)入數(shù)據(jù)。
※ find no available rootcoord, check rootcoord state
報(bào)錯(cuò):
[2024/09/26 08:19:14.956 +00:00] [ERROR] [grpcclient/client.go:158] ["failed to get client address"] [error="find no available rootcoord, check rootcoord state"] [stack="github.com/milvus-io/milvus/internal/util/grpcclient.(*ClientBase[...]).connect
/go/src/github.com/milvus-io/milvus/internal/util/grpcclient/client.go:158
github.com/milvus-io/milvus/internal/util/grpcclient.(*ClientBase[...]).GetGrpcClient
/go/src/github.com/milvus-io/milvus/internal/util/grpcclient/client.go:131
github.com/milvus-io/milvus/internal/util/grpcclient.(*ClientBase[...]).callOnce
/go/src/github.com/milvus-io/milvus/internal/util/grpcclient/client.go:256
github.com/milvus-io/milvus/internal/util/grpcclient.(*ClientBase[...]).ReCall
/go/src/github.com/milvus-io/milvus/internal/util/grpcclient/client.go:312
github.com/milvus-io/milvus/internal/distributed/rootcoord/client.(*Client).GetComponentStates
/go/src/github.com/milvus-io/milvus/internal/distributed/rootcoord/client/client.go:129
github.com/milvus-io/milvus/internal/util/funcutil.WaitForComponentStates.func1
/go/src/github.com/milvus-io/milvus/internal/util/funcutil/func.go:65
github.com/milvus-io/milvus/internal/util/retry.Do
/go/src/github.com/milvus-io/milvus/internal/util/retry/retry.go:42
github.com/milvus-io/milvus/internal/util/funcutil.WaitForComponentStates
/go/src/github.com/milvus-io/milvus/internal/util/funcutil/func.go:89
github.com/milvus-io/milvus/internal/util/funcutil.WaitForComponentHealthy
/go/src/github.com/milvus-io/milvus/internal/util/funcutil/func.go:104
github.com/milvus-io/milvus/internal/distributed/datanode.(*Server).init
/go/src/github.com/milvus-io/milvus/internal/distributed/datanode/service.go:275
github.com/milvus-io/milvus/internal/distributed/datanode.(*Server).Run
/go/src/github.com/milvus-io/milvus/internal/distributed/datanode/service.go:172
github.com/milvus-io/milvus/cmd/components.(*DataNode).Run
/go/src/github.com/milvus-io/milvus/cmd/components/data_node.go:51
github.com/milvus-io/milvus/cmd/roles.runComponent[...].func1
/go/src/github.com/milvus-io/milvus/cmd/roles/roles.go:102"]問(wèn)題:rootcoord和其他pod通信出現(xiàn)了問(wèn)題。
解決:先重建rootcoord,再依次重建相關(guān)的querynode、indexnode、queryrecord、indexrecord。
※ 頁(yè)面查詢(xún)報(bào)錯(cuò)
(Search 372 failed, reason Timestamp lag too large lag)
[2024/09/26 09:14:13.063 +00:00] [WARN] [retry/retry.go:44] ["retry func failed"] ["retry time"=0] [error="Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_search.go:529] ["QueryNode search result error"] [traceID=62505beaa974c903] [msgID=452812354979102723] [nodeID=372] [reasnotallow="Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_policies.go:132] ["failed to do query with node"] [traceID=62505beaa974c903] [nodeID=372] [dmlChannels="[by-dev-rootcoord-dml_6_442659379752037218v0,by-dev-rootcoord-dml_7_442659379752037218v1]"] [error="code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_policies.go:159] ["retry another query node with round robin"] [traceID=62505beaa974c903] [Nexts="{\"by-dev-rootcoord-dml_6_442659379752037218v0\":-1,\"by-dev-rootcoord-dml_7_442659379752037218v1\":-1}"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_policies.go:60] ["no shard leaders were available"] [traceID=62505beaa974c903] [channel=by-dev-rootcoord-dml_6_442659379752037218v0] [leaders="[<NodeID: 372>]"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_policies.go:119] ["failed to search/query with round-robin policy"] [traceID=62505beaa974c903] [error="Channel: by-dev-rootcoord-dml_7_442659379752037218v1 returns err: code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)Channel: by-dev-rootcoord-dml_6_442659379752037218v0 returns err: code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_search.go:412] ["failed to do search"] [traceID=62505beaa974c903] [Shards="map[by-dev-rootcoord-dml_6_442659379752037218v0:[<NodeID: 372>] by-dev-rootcoord-dml_7_442659379752037218v1:[<NodeID: 372>]]"] [error="code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_search.go:425] ["first search failed, updating shardleader caches and retry search"] [traceID=62505beaa974c903] [error="code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [INFO] [proxy/meta_cache.go:767] ["clearing shard cache for collection"] [collectinotallow=xxx]
[2024/09/26 09:14:13.063 +00:00] [WARN] [retry/retry.go:44] ["retry func failed"] ["retry time"=0] [error="code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)"]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/task_scheduler.go:473] ["Failed to execute task: "] [error="fail to search on all shard leaders, err=All attempts results:\nattempt #1:code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)\nattempt #2:context canceled\n"] [traceID=62505beaa974c903]
[2024/09/26 09:14:13.063 +00:00] [WARN] [proxy/impl.go:2861] ["Search failed to WaitToFinish"] [traceID=62505beaa974c903] [error="fail to search on all shard leaders, err=All attempts results:\nattempt #1:code: UnexpectedError, error: fail to Search, QueryNode ID=372, reasnotallow=Search 372 failed, reason Timestamp lag too large lag(28h44m48.341s) max(24h0m0s) err %!w(<nil>)\nattempt #2:context canceled\n"] [role=proxy] [msgID=452812354979102723] [db=] [collectinotallow=xxx] [partitinotallow="[]"] [dsl=] [len(PlaceholderGroup)=4108] [OutputFields="[id,text,extra]"] [search_params="[{\"key\":\"params\",\"value\":\"{\\\"ef\\\":250}\"},{\"key\":\"anns_field\",\"value\":\"vector\"},{\"key\":\"topk\",\"value\":\"100\"},{\"key\":\"metric_type\",\"value\":\"L2\"},{\"key\":\"round_decimal\",\"value\":\"-1\"}]"] [travel_timestamp=0] [guarantee_timestamp=0]問(wèn)題:pulsar 組件對(duì)應(yīng)相關(guān)pod問(wèn)題導(dǎo)致不進(jìn)行消費(fèi)。
解決:將pulsar 組件相關(guān)pod進(jìn)行重建,查看日志,并且等待消費(fèi)pulsar完成。
※ Query Node 限制內(nèi)存不足
(memory quota exhausted)
報(bào)錯(cuò):
<MilvusException: (code=53, message=deny to write, reason: memory quota exhausted, please allocate more resources, req: /milvus.proto.milvus.MilvusService/Insert)>原因:配置中Query Node配置內(nèi)存上線(xiàn)達(dá)到瓶頸。
解決:增加Query Node配置或者增加QueryNode節(jié)點(diǎn)數(shù)。
※ 底層磁盤(pán)瓶頸導(dǎo)致ETCD訪問(wèn)超時(shí)
報(bào)錯(cuò):
圖片
解決:從架構(gòu)方面上進(jìn)行解決,在集群維度將磁盤(pán)進(jìn)行隔離,每個(gè)集群使用獨(dú)立磁盤(pán)。
七、未來(lái)展望
數(shù)據(jù)遷移閉環(huán)
數(shù)據(jù)遷移閉環(huán):對(duì)于業(yè)務(wù)數(shù)據(jù)加載到向量數(shù)據(jù)庫(kù)的場(chǎng)景,業(yè)務(wù)只關(guān)心數(shù)據(jù)的讀取和使用,不需要關(guān)心數(shù)據(jù)的量化和寫(xiě)入。DBA側(cè)建立數(shù)據(jù)遷移閉環(huán)(下圖綠色部分)。
數(shù)據(jù)準(zhǔn)確性校驗(yàn)
對(duì)于上游數(shù)據(jù)(如MySQL)和下游向量數(shù)據(jù)庫(kù)數(shù)據(jù)庫(kù)一致性校驗(yàn)問(wèn)題,DBA業(yè)將協(xié)同業(yè)務(wù)、Milvus進(jìn)行共建校驗(yàn)工具,保障數(shù)據(jù)的準(zhǔn)確性。
圖片






































