背景
今天開始寫下Milvus,為了方便,我直接使用的是? milvus-lite 版本,default 情況下,你可能不知道他到底將 db 存儲到什么位置了。啟動 default-server,看下Milvus 的start及存儲邏輯
主邏輯
def start(self):self.config.resolve()_initialize_data_files(self.config.base_data_dir)
-> 看下 resolve 的邏輯:
def resolve(self):self.cleanup_listen_ports()self.resolve_all_listen_ports()self.resolve_storage()for key, value in self.configurable_items.items():if value[1] is None:raise RuntimeError(f'{key} is still not resolved, please try specify one.')# ready to startself.cleanup_listen_ports()self.write_config()self.verbose_configurable_items()
-> 重點是?resolve_storage,繼續看:
def resolve_storage(self):self.base_data_dir = self.configs.get('data_dir', self.get_default_data_dir())self.base_data_dir = abspath(self.base_data_dir)makedirs(self.base_data_dir, exist_ok=True)config_dir = join(self.base_data_dir, 'configs')logs_dir = join(self.base_data_dir, 'logs')storage_dir = join(self.base_data_dir, 'data')for subdir in (config_dir, logs_dir, storage_dir):makedirs(subdir, exist_ok=True)
-> 先看下 base dir:,get_default_data_dir:
def get_default_data_dir(cls):if sys.platform.lower() == 'win32':default_dir = expandvars('%APPDATA%')return join(default_dir, 'milvus.io', 'milvus-server', __version__)default_dir = expandvars('${HOME}')return join(default_dir, '.milvus.io', 'milvus-server', __version__)
看到這里,你應該明白,網上有人說使用Milvus 必須要下載 docker,那不太對,別人是支持 win32的。而且這個源碼還是使用的 2.26 的版本,最新的2.4 更應該支持。
非常清晰。繼續看,日志和配置,晚點在說,先看 data,因為你最關心的,也應該是 data 部分:
看下 data 的 結構:
# data self.set('etcd_data_dir', join(storage_dir, 'etcd.data')) self.set('local_storage_dir', storage_dir) self.set('rocketmq_data_dir', join(storage_dir, 'rocketmq'))
關于這里的配置文件最終會寫在:
中,當然除了上面的咨詢,你也可以看到下面索引的核心default 配置:
HNSW 在前面的文章已經講述過了,默認采用的是 HNSW 可導航分成小世界算法,采用IP丈量距離的方式,M和efCons 是 HNSW 的兩個參數,分別代表本層和向下尋找的節點數。
config = MilvusServerConfig() config.base_data_dir = 'milvus_austin_base_data_dir' server = MilvusServer(config, debug=True) server.start()
你發現這樣設置了無效,原因應該通過上面的代碼很清楚了,
self.base_data_dir = self.configs.get('data_dir', self.get_default_data_dir())
已經限制了他的輸出路徑。
如何修改
要怎么才能使得放在自己的目錄下呢?其實有個函數,你在調用下:
server.set_base_dir(config.base_data_dir)
ok了
你可能會比較疑惑,代碼是?
為什么設置了 沒有被覆蓋?
原因很簡單:
原因很清楚了
當然你也可以直接調用 Milvus 封裝好的 milvus-server --debug 啟動,后面也可以接 --data 參數。但我更喜歡自己看內部的實現。
start邏輯
其實 milvus lite 的 start 就三個作用:
1)加載全局配置,將 data 相關的copy 到指定位置,沒有的話,就創建folder 和文件
2)用進程方式加載 milvus.exe
cmds = [milvus_exe, 'run', 'standalone'] proc_fds = self.proc_fds if self._debug:self.server_proc = subprocess.Popen(cmds, env=envs) else:# pylint: disable=consider-using-withself.server_proc = subprocess.Popen(cmds, stdout=proc_fds['stdout'], stderr=proc_fds['stderr'], env=envs)
3)等待 client 指令
實際上, milvus lite,只是一個外殼,啟動了 milvus exe 執行
與傳統數據庫對比
Milvus Collection 就是 傳統db 的 table
entity 就是 傳統 db 的row
field 就是傳統db 的 column
這個前面講的chroma 很接近。因為在vector db 的世界中都大同小異。
milvus架構
1)接入層(Access Layer):系統的門面,由一組無狀態 proxy 組成。負責客戶端請求并返回結果。
2)協調服務(Coordinator Service):Coordinator,顧名思義,協調者,負責分配任務給執行節點。協調功能有四種,分別為 root coord、data coord、query coord 和 index coord。
3)執行節點(Worker Node):負責執行協調者所下發的任務,實際干活兒的人。執行節點分為三種角色,分別為 data node、query node 和 index node。
4)存儲服務 (Storage): 負責 Milvus 數據的存儲,也就是持久化,分為元數據存儲(meta store)、消息存儲(log broker)和對象存儲(object storage)三個部分。
實際上我們看到 Milvus 這種架構做的非常優秀,適合數據與服務的橫向擴展
接入層和協調服務
root coordinator
主要處理DDL 與? DCL 請求,包括 create 與 delete?collections, partitions, or indexes 等。還有一些與時間同步等請求。
query coodinator
查詢coodinator 有點不太好理解,要多描述下。它主要負責管理查詢節點(Query node)的拓撲結構和負載均衡,以及處理從增長段(Growing segments)到密封段(Sealed segments)的轉換操作。有點生硬,展開描述一下:
?
1.管理查詢節點的拓撲結構
- 拓撲結構管理:Query coord負責維護系統中所有查詢節點的布局和連接關系。這確保了當查詢請求到來時,系統能夠高效地將請求分發到合適的查詢節點上進行處理。
- 節點狀態監控:Query coord還負責監控查詢節點的狀態,包括節點的健康狀況、負載情況等,以便在必要時進行節點間的負載均衡調整。
2. 負載均衡
- 請求分發:Query coord接收來自客戶端的查詢請求,并根據當前查詢節點的負載情況和拓撲結構,智能地將請求分發到合適的節點上進行處理。這有助于提高系統的整體性能和響應時間。
- 動態調整:隨著查詢請求的變化和查詢節點負載的波動,Query coord會動態地調整查詢節點的負載,以確保系統的負載均衡性和穩定性。
3. 從增長段到密封段的轉換
? ? 這算是milvus 的一大特色。其他地方很少聽到這種存儲理念。
- 段的狀態管理:在Milvus中,數據被組織成段(segments),這些段可以是增長段(Growing segments)或密封段(Sealed segments)。增長段用于存儲新的寫入數據,而密封段則用于存儲已經穩定的數據。
- 轉換操作:當增長段的數據量達到一定程度時(默認為512MB),Query coord會觸發從增長段到密封段的轉換操作。這個過程中,Query coord會協調查詢節點和數據節點,將增長段中的數據遷移到密封段,并進行必要的索引構建和優化操作。
data coordinator
顧名思義,就是存data的。包括了索引,數據,及元數據。負責管理數據節點(Data node)和索引節點(Index node)的拓撲結構、維護元數據,并觸發如flush、compact、索引構建等后臺數據操作。以下是這些職責的詳細闡述:
1. 管理數據節點和索引節點的拓撲結構
Data coord負責維護系統中數據節點和索引節點的整體布局和相互連接關系。這種拓撲結構的設計旨在確保數據的均勻分布和高效訪問。隨著系統的擴展或節點的增減,Data coord會相應地調整拓撲結構,以保持系統的平衡和高效。
2. 維護元數據
元數據是描述數據的數據,對于Milvus系統來說,包括但不限于集合(collection)、分區(partition)、段(segment)的定義、屬性以及它們之間的關系。Data coord負責存儲和更新這些元數據,確保它們在系統中的一致性和準確性。這對于數據查詢、數據遷移、數據恢復等操作至關重要。
3. 觸發后臺數據操作
Data coord負責監控系統的狀態,并在適當時機觸發一系列后臺數據操作,以優化數據存儲和查詢性能。這些操作包括:
- Flush操作:當內存中的數據量達到閾值時,Data coord會觸發flush操作,將內存中的數據寫入磁盤上的段中。這有助于減少內存使用,并確保數據的持久化。
- Compact操作:為了優化存儲效率和查詢性能,Data coord會定期或按需觸發compact操作。這個過程中,多個小段(segment)會被合并成一個大段,并刪除其中的重復或無效數據。
- 索引構建:根據用戶配置的索引策略和參數,Data coord會觸發索引構建操作。索引的創建可以顯著加快查詢速度,因為系統可以直接在索引上執行搜索,而無需遍歷整個數據集。
- 其他后臺操作:除了上述操作外,Data coord還可能觸發其他必要的后臺數據操作,如數據修復、數據遷移、數據清理等,以確保系統的穩定性和數據的可靠性。
從整體上說,query coord 就是為了負載均衡及query 等效率。所以從這里看出 milvus 對query 效能是有比較好的設計。當然類似? partition 的設計 chroma 也有。只是segment 在 milvus 中還進行了區分。
Index coordinator
index coord主要負責管理索引節點(Index node)的拓撲結構、構建索引、維護索引元信息,并協調索引相關的操作。以下是Index coord主要職責的詳細闡述:
1. 管理索引節點的拓撲結構
- Index coord負責維護系統中索引節點的布局和連接關系,確保索引數據能夠均勻分布在各個索引節點上,以實現高效的索引構建和查詢。
- 隨著系統的擴展或索引節點的增減,Index coord會相應地調整拓撲結構,以保持系統的平衡和高效。
2. 構建和維護索引
- 當有新的向量數據需要被索引時,Index coord會協調索引節點進行索引構建工作。這包括選擇合適的索引算法、分配索引任務給索引節點,并監控索引構建的進度和結果。
- Index coord還負責維護索引的元信息,如索引的類型、參數、狀態等,以便在查詢時能夠快速地定位到相應的索引數據。
3. 協調索引相關的操作
- Index coord會與其他協調器(如Data coord、Query coord)和執行節點(如Data node、Query node)進行交互,以協調索引相關的操作。例如,在數據遷移或數據修復過程中,Index coord會確保索引數據的一致性和完整性。
- 當索引節點發生故障或性能瓶頸時,Index coord會參與故障恢復或負載均衡的決策過程,以確保系統的穩定性和性能。
4. 支持索引的更新和刪除
- 隨著數據的不斷變化,索引也需要進行相應的更新或刪除操作。Index coord會處理這些請求,并確保索引的更新或刪除操作能夠正確地執行,同時保持索引數據的一致性和準確性。
5. 監控和優化索引性能
- Index coord會監控索引的性能指標,如索引構建速度、查詢響應時間等,并根據監控結果進行相應的優化操作。例如,通過調整索引算法、參數或增加索引節點來提高索引的性能。
milvus 的底層采用的是對象存儲,負責存儲日志的快照文件、標量/向量索引文件以及查詢的中間處理結果。采用 MinIO 作為對象存儲。
當系統啟動后,我們看到各自服務的端口是不一樣的:
[INFO] [indexcoord/service.go:328] [IndexCoord] ["network address"=192.168.3.164] ["network port"=40003]
[INFO] [querynode/service.go:111] [QueryNode] [port=40002]
["Proxy internal server listen on tcp"] [port=19529]
["Proxy server listen on tcp"] [port=19530]
外面使用 19530 訪問,內部 milvus 通過 19529 進行分發到內部處理
存儲結構
其實在最底層,milvus 的存儲是? etcd + MinIO 的形式
我們可以打開etcd文件夾,能窺探到一點內容,當然,網上有專門的軟件打開,我這里直接打開看一看,也能看到一點痕跡:
其實這里每一個概念都可以是一個topic,先簡單介紹下吧,后面再細說。什么是 wal?write-ahead logging,預寫日志系統。這里就體現出milvus的設計理念。盡快可靠的返回,不要阻塞用戶操作,真正需要系統落地的事情,根據wal 的記錄交給milvus后面以async的方式來完成吧。
當用戶向系統中插入數據時, milvus會先把數據攢在內存里,然后當內存數據達到一定規模(默認128M)或者定時器超時(默認1s)后,內存數據再排隊刷入磁盤。當數據真正落盤后它們就能被訪問了。這個設計借鑒了操作系統對流數據的處理方式,在寫磁盤頻率和數據可靠性之間做了一定的平衡。然而隨著我們接觸到越來越多的用戶場景,以上設計漸漸不能滿足需求了。首先,用戶命令的原子性沒得到保證,比如在系統意外退出時,用戶某一次插入的數據可能部分落盤、部分丟失;其次,用戶無法明確知道數據何時可見,即使在客戶端等待1s也未必能保證之前插入數據全部可見;最后,當初設計時沒有考慮數據刪除,同步執行刪除有可能要長時間阻塞用戶線程。為解決上述問題,wal文件機制應運而生,沒記錯的話,應該是 milvus早期0.7 就引入wal文件。
先直觀感受下wal文件:
還是能感受點東西,可以自己體會下,好東西有時候只能意會不能言傳。
其核心思想是把用戶所有的修改操作(插入、刪除)先寫入日志中,然后再應用到系統狀態里。一旦成功寫完日志,即可通知用戶操作成功。由于日志是以尾部追加方式寫入,耗時較短,所以不會長時間阻塞用戶線程。此外為防止意外退出導致數據丟失,系統重啟時還會根據日志重做用戶操作,以保證數據可靠性。這個消息隊列的某些機制很像。時間有限,今天先寫到這里。后面再慢慢梳理