貝殼找房基於Milvus的向量搜索實踐(三)

摘要:本系列文章分爲三部分,第一篇主要講基本概念、背景、選型及服務的整體架構;第二篇主要講針對低延時、高吞吐需求,我們對Milvus部署方式的一種定製;本篇主要講實現數據更新、保證數據一致性,以及保證服務穩定及提高資源利用率做的一些事情。

1.數據存儲方案

第二篇中我們解決了部署方案的問題,接下來要考慮的是數據如果存儲。在分佈式部署情況下,Milvus是需要使用Mysql來存儲元數據的[1]。Milvus分佈式部署時,數據只會寫一份,如何實現數據的分佈式使用呢?基本的思路有兩種:1)內部數據複製,典型的例子如elasticsearch[2],kafka[3][4];2)數據存儲在共享存儲上,如NFS,glusterfs,AWS EBS,GCE PD,Azure Disk等,都提供了kubernetes下的支持[5]。兩種思路沒有本質的區分,前者是應用自己實現了數據的存儲及高可用(多副本);缺點是應用複雜度增加;優點是具有更高的靈活性。後者依賴於已有的通用的存儲方案,只需要關注自身的核心功能,複雜度降低了,而且更方便在多種存儲方案下切換。在雲計算技術發展的今天,後者有一定的市場。Milvus選用了共享存儲來存儲數據。爲了實現存儲的統一及高可用,我們把單個Milvus集羣所涉及到的所有數據存儲(mysql數據文件和milvus的存儲),都放到共享存儲中。我們使用了glusterfs做爲共享存儲的具體實現。整體的存儲方案如圖1。

圖1 使用glusterfs存儲數據

爲了解決集羣的自動創建,減少溝通維護成本以及物理資源的最大利用(Milvus是cpu密集型,glusterfs是存儲密集型),我們將glusterfs同Milvus混合部署。我們參考實現了glusterfs在kubernetes下的超融合(Full Hyper-Convergence)部署,並藉助heketi[7]實現了存儲資源的動態分配。另外,在部署過程中,還需要注意的是glusterfs需要一個獨立的磁盤/分區,你也可以使用loop設備[8];在部署過程中,因爲各種原因,不可避免需要重置部署,這時你需要清除髒數據,可以參照以下命令。

# 清除邏輯卷
lvscan | awk 'system("lvremove  -y "$2 )'

# 清除卷分組
vgscan | grep group | awk -F '"' '{system("vgremove "$2)}'

glusterfs在kubernetes下的部署架構如圖2所示,glusterfs服務可以分佈在kubernetes的多個node上,我們可以根據存儲的需求增加結點。

圖2 glusterfs in kubernetes

實現了glusterfs在kubernetes的部署,我們更關心的是glusterfs本身的可用性:1)glusterfs是否可以實現數據的不丟失/高可用;2)glusterfs是否可以存儲大批量數據。

由[9]可知,glusterfs有Distributed volume、Replicated volume、Distributed Replicated volume、Dispersed Glusterfs Volume、Distributed Dispersed Glusterfs Volume 5種類型的卷,其中Distributed volume可以解決數據分佈存儲數據,從而實現大批量數據的存儲,Replicated volume通過數據的冗餘來實現高可用,Distributed Replicated volume同時解決了高可用和大批量數據存儲的問題,Dispersed Glusterfs Volume、Distributed Dispersed Glusterfs Volume是分別對Replicated volume、Distributed Replicated volume的優化,藉助一種前向糾錯碼(erasure code[10])實現數據存儲成本的降低。圖3給出了Distributed Replicated volume類型卷的結構圖。

圖3 Distributed Replicated volume

最後,藉助heketi[7]、以及kubernetes的StorageClass[11]、PVC[12],我們屏蔽掉了以上glusterfs卷創建、擴容、銷燬的細節,比較完美解決了數據存儲的問題。

2.數據更新方案

數據更新分爲實時更新和批量/全量更新兩種,Milvus本身是支持實時更新的,但是數據更新時需要重新創建索引,而索引構建需要消耗大量的CPU資源,從而引發服務整體的穩定性問題。綜合考慮穩定性,以及業務的數據更新場景(絕大多數是T+1更新策略),我們採用瞭如圖4所示的數據更新策略。

我們使用了A、B兩組對等的資源(可以是同機房、跨機房)作爲底層Milvus引擎,在引擎的外層,我們實現了讀寫分離,同一時刻,A、B集羣只會承擔讀、寫角色中的一個。在引擎外層,我們維護了讀寫角色與A、B集羣的對照表;數據更新時,我們操作寫集羣完成數據寫入、索引構建,寫集羣索引構建完成後,切換成角色成讀集羣;數據更新時出現任何問題,不影響讀集羣。另外,在讀寫集羣都有正常數據(數據更新差一天)情況下,如果讀集羣出現問題,寫集羣可以隨時切換成讀集羣,從而在實現數據更新的同時還實現了互備。由於底層資源使用對等的兩份,如何沒有特別的處理,不可避免會造成資源的浪費,後面內容會專門討論解決這個問題的方案。

圖4 T+1數據更新策略

3. 數據一致性保證

解決了數據更新的問題,另一個問題接踵而來:如何保證數據更新時一致性?如何做到以下三點:1)數據量不多不少;2)數據不重複;3)舊數據不會覆蓋新數據。

由於我們的前提是數據全量更新,在業務數據本身不重複的情況下,不會存在數據覆蓋問題,我們重點討論前兩點。

3.1 數據量不多不少

我們總體思路是,明確寫入操作開始和結束(提供專門的api實現),在結束時檢驗數據量。數據全量寫入開始時,我們清空數據,在數據全量寫入結束時,判斷數據寫入的實際數量與預期是否一致,如果一致,我們可以確認數據數量是沒有問題的。數據寫入操作可以併發進行,以保證整體的寫入吞吐量,但是需要使用方保證,結束寫操作需要在所有寫入操作之後。另外,爲了兼顧數據一致性、引擎穩定性以及服務整體可用,可以設定一致性錯誤容忍度(比如可以容忍多少比例的數據量差異)。

3.2 數據不重複

我們假設,寫入Milvus的請求返回成功,數據寫入成功;請求返回失敗,數據寫入失敗。

我們寫入Milvus時,通過同步阻塞來實現數據不重複。具體地,寫入時,我們設定寫入超時時間大於引擎內部寫入請求的處理時間,也就是留出足夠時間來讓引擎返回成功/失敗(即感知到引擎因爲各種問題引起的失敗);如果失敗,我們會執行一次刪除操作(刪除可能寫入的指定數據),並進行重試(如果重試指定次數還未成功,會由數據量校驗來決定是否全量更新成功)。

除了以上方案,還有兩種可選的方案:

  1. 外部維護一個數據是否已經寫入的標識,數據寫入前進行判斷,如果已經存在,就不再寫入。
  2. Milvus自身支持upset(如果不存在就插入,如果存在就更新)操作。

方案1在實現同步阻塞方案效果的基礎上,還兼顧了使用方與向量服務之間的可能網絡異常(寫入成功,但是沒有返回給業務方,業務方重試,導致數據寫入重複;Milvus在0.8.0下不能去重);但是,增加了額外的開銷,系統的複雜度也隨之增加。

方案2是一個更優秀的方案,把去重的工作外部透明瞭。當然,這個依賴於Milvus的版本迭代[13]

圖5展示了數據T+1全量更新的步驟:

  1. 全量寫開始 - 刪除Milvus中舊數據,清除內外id映射數據,擴容Milvus寫實例。

  2. 批量寫 - 向Milvus寫實例批量寫入數據,失敗重試。

  3. 結束寫 - 檢驗數據量是否符合預期。

  4. 觸發異步建索引 - 調用Milvus建索引接口(數據量大時建索引接口可能會阻塞)。

  5. 異步等待 - 調用Milvus建索引接口返回(超時/完成),循環判斷是否建索引成功(可以根據showCollectionInfo接口的返回判斷)。

  6. 引擎預熱 - 讓引擎把數據加載到內存中;多partition時需要遍歷所有的partition才能保證所有數據都加載。

  7. 引擎切換 - A、B引擎集羣角色互換,並把對應關係持久化;對原有的讀集羣縮容。

圖5 數據全量更新流程

4.存活檢測

在Milvus0.8.0使用過程中,多次出現cpu指令異常,導致Milvus服務退出的情況;但是,由於Milvus沒有暴露存活檢測的接口,Milvus Pod[14](在kubernetes下,可以認爲是一個服務)還被認爲可用,還會有流量被負載均衡到,從而引發外部使用報錯。

解決方式很直接,我們需要給Milvus0.8.0增加存活檢測的接口,並且在kubernets下配置上對Milvus的檢測。由[15]可知,kubernetes有readinessProbe、livenessProbe兩者存活檢測的手段,前者用於檢測服務是否正常啓動,後者用於檢測服務正式在正常運行,如果不正常,會有相應的重啓策略。

readinessProbe、livenessProbe的具體實現有exec、httpGet、tcpSocket三種;exec定時到指定容器中執行一個shell命令;httpGet定時請求容器暴露的http接口;tcpSocket是定時請求容器暴露的socket端口;三者根據指定格式的返回結果來判斷服務是否正常,根據Probe配置來決定是否重啓。具體的配置可以參考[15]

有了kubernetes的支持,我們剩下需要做的就是如何判斷Milvus是否正常;幸運的是,Milvus雖然沒有暴露kubernetes指定格式的Probe接口,但是它提供的server_status接口可以判斷服務是否正常運行。接下來,我們需要做的,就是如何包裝下這個接口,返回kubernetes指定的格式。

最直接、簡單的方案是exec。我們給原生Milvus0.8.0版本的docker鏡像增加了執行python腳本功能的能力,並把以下python腳本打包到鏡像中,最後exec配置定時調用以下腳本。我們使用這個思路初步解決了問題,但是,在後續的測試驗證過程中發現,當同一臺機器上存在多個Milvus實例時,服務空轉時就消耗了不少的cpu資源。我們由[16]可知,exec最終調用了docker的exec api[17],docker exec api在執行shell命令外,它還做了不少額外工作,從而導致對資源的消耗[18]

from milvus import Milvus, IndexType, MetricType, Status
client = Milvus(host='localhost', port='19530')
try:
 status,msg= client.server_status(timeout=10)
except Exception as e:
 print('1')
else:

 if status.OK():
  print('0')
 else:
  print('2')

爲了解決exec的問題,我們採用了圖6的方案。基於以上的分析,我們把python腳本包裝成一個http服務器,在容器啓動時,將http服務器啓動爲一個常駐的進程,然後我們採用httpGet方案解決檢測的問題。經過實踐檢驗,該方案對性能和資源佔用基本沒有影響。

圖6 httpGet存活檢測方案

5.資源伸縮

考慮到資源的充分利用(我們重點考慮cpu資源),我們有必要在不使用時,對資源進行回收。對資源的回收有手動和自動兩方案,整體思路見圖7。

圖7 資源伸縮

5.1 手動

我們可以使用kubernetes的客戶端工具kubectl來更改服務的副本數、cpu/內存佔用;也可以通過kubernetes的sdk,把相應功能做爲kubernetes管理工具集成到自已的應用中,從而實現資源的個性化調節。

5.2 自動

HPA(Horizontal Pod Autoscaler)[19]是kubernetes下支持的一種資源自動伸縮方案(以pod爲單位),它參照監控數據提供的cpu資源利用率,根據配置的具體規則,來實現pod數自動調整。

6.參考文獻

  1. https://github.com/milvus-io/milvus
  2. https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html
  3. http://kafka.apache.org/documentation/#min.insync.replicas
  4. http://kafka.apache.org/documentation/#replication
  5. https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner
  6. https://github.com/gluster/gluster-kubernetes/blob/master/docs/setup-guide.md
  7. https://github.com/heketi/heketi
  8. https://en.wikipedia.org/wiki/Loop_device
  9. https://docs.gluster.org/en/latest/Quick-Start-Guide/Architecture/
  10. https://en.wikipedia.org/wiki/Erasure_code
  11. https://kubernetes.io/docs/concepts/storage/storage-classes/#glusterfs
  12. https://kubernetes.io/docs/concepts/storage/persistent-volumes/
  13. https://github.com/milvus-io/milvus/issues/3093
  14. https://kubernetes.io/docs/concepts/workloads/pods/
  15. https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
  16. https://github.com/kubernetes/kubernetes/blob/18099e1ef7283d9ab09c45c5a4a90e26fdce1161/pkg/kubelet/dockershim/exec.go#L63
  17. https://docs.docker.com/engine/reference/commandline/exec/
  18. https://github.com/docker/for-linux/issues/466
  19. https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

作者簡介

往期精彩

    基於Milvus的向量搜索實踐(二)

    基於Milvus的向量搜索實踐(一)



最後,一起來了解 Milvus 吧!



   歡迎加入 Milvus 社區


github.com/milvus-io/milvus | 源碼
milvus.io | 官網
milvusio.slack.com | Slack 社區
zhihu.com/org/zilliz-11| 知乎
zilliz.blog.csdn.net | CSDN 博客
space.bilibili.com/478166626 | Bilibili


本文分享自微信公衆號 - ZILLIZ(Zilliztech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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